Skip to content

Commit

Permalink
Add config for columns with formula (#18)
Browse files Browse the repository at this point in the history
* Add config for columns with formula

* Apply Code Coverage Badge

---------

Co-authored-by: edocsss <[email protected]>
  • Loading branch information
edocsss and edocsss authored Jan 25, 2025
1 parent d5a8e0e commit 83a3c9b
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 47 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

![Unit Test](https://github.com/FreeLeh/GoFreeDB/actions/workflows/unit_test.yml/badge.svg)
![Integration Test](https://github.com/FreeLeh/GoFreeDB/actions/workflows/full_test.yml/badge.svg)
![Coverage](https://img.shields.io/badge/Coverage-82.6%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-82.8%25-brightgreen)
[![Go Report Card](https://goreportcard.com/badge/github.com/FreeLeh/GoFreeDB)](https://goreportcard.com/report/github.com/FreeLeh/GoFreeDB)
[![Go Reference](https://pkg.go.dev/badge/github.com/FreeLeh/GoFreeDB.svg)](https://pkg.go.dev/github.com/FreeLeh/GoFreeDB)

Expand Down
20 changes: 20 additions & 0 deletions internal/common/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package common

type Set[T comparable] struct {
values map[T]struct{}
}

func (s *Set[T]) Contains(v T) bool {
_, ok := s.values[v]
return ok
}

func NewSet[T comparable](values []T) *Set[T] {
s := &Set[T]{
values: make(map[T]struct{}, len(values)),
}
for _, v := range values {
s.values[v] = struct{}{}
}
return s
}
29 changes: 17 additions & 12 deletions internal/google/store/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ type GoogleSheetRowStoreConfig struct {
// The column ordering will be used for arranging the real columns in Google Sheet.
// Changing the column ordering in this config but not in Google Sheet will result in unexpected behaviour.
Columns []string

// ColumnsWithFormula defines the list of column names containing a Google Sheet formula.
// Note that only string fields can have a formula.
ColumnsWithFormula []string
}

func (c GoogleSheetRowStoreConfig) validate() error {
if len(c.Columns) == 0 {
return errors.New("columns must have at least one column")
}
if len(c.Columns) > maxColumn {
return errors.New("you can only have up to 1000 columns")
return fmt.Errorf("you can only have up to %d columns", maxColumn)
}
return nil
}

// GoogleSheetRowStore encapsulates row store functionality on top of a Google Sheet.
type GoogleSheetRowStore struct {
wrapper sheetsWrapper
spreadsheetID string
sheetName string
colsMapping common.ColsMapping
config GoogleSheetRowStoreConfig
wrapper sheetsWrapper
spreadsheetID string
sheetName string
colsMapping common.ColsMapping
colsWithFormula *common.Set[string]
config GoogleSheetRowStoreConfig
}

// Select specifies which columns to return from the Google Sheet when querying and the output variable
Expand Down Expand Up @@ -154,13 +159,13 @@ func NewGoogleSheetRowStore(
}

config = injectTimestampCol(config)

store := &GoogleSheetRowStore{
wrapper: wrapper,
spreadsheetID: spreadsheetID,
sheetName: sheetName,
colsMapping: common.GenerateColumnMapping(config.Columns),
config: config,
wrapper: wrapper,
spreadsheetID: spreadsheetID,
sheetName: sheetName,
colsMapping: common.GenerateColumnMapping(config.Columns),
colsWithFormula: common.NewSet(config.ColumnsWithFormula),
config: config,
}

_ = ensureSheets(store.wrapper, store.spreadsheetID, store.sheetName)
Expand Down
56 changes: 56 additions & 0 deletions internal/google/store/row_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,62 @@ func TestGoogleSheetRowStore_Integration_EdgeCases(t *testing.T) {
assert.NotNil(t, err)
}

type formulaWriteModel struct {
Value string `json:"value" db:"value"`
}

type formulaReadModel struct {
Value int `json:"value" db:"value"`
}

func TestGoogleSheetRowStore_Formula(t *testing.T) {
spreadsheetID, authJSON, shouldRun := getIntegrationTestInfo()
if !shouldRun {
t.Skip("integration test should be run only in GitHub Actions")
}
sheetName := fmt.Sprintf("integration_row_%d", common.CurrentTimeMs())

googleAuth, err := auth.NewServiceFromJSON([]byte(authJSON), auth.GoogleSheetsReadWrite, auth.ServiceConfig{})
if err != nil {
t.Fatalf("error when instantiating google auth: %s", err)
}

db := NewGoogleSheetRowStore(
googleAuth,
spreadsheetID,
sheetName,
GoogleSheetRowStoreConfig{
Columns: []string{"value"},
ColumnsWithFormula: []string{"value"},
},
)
defer func() {
time.Sleep(time.Second)
deleteSheet(t, db.wrapper, spreadsheetID, []string{db.sheetName})
_ = db.Close(context.Background())
}()

var out []formulaReadModel

time.Sleep(time.Second)
err = db.Insert(formulaWriteModel{Value: "=ROW()-1"}).Exec(context.Background())
assert.Nil(t, err)

time.Sleep(time.Second)
err = db.Select(&out).Exec(context.Background())
assert.Nil(t, err)
assert.ElementsMatch(t, []formulaReadModel{{Value: 1}}, out)

time.Sleep(time.Second)
err = db.Update(map[string]interface{}{"value": "=ROW()"}).Exec(context.Background())
assert.Nil(t, err)

time.Sleep(time.Second)
err = db.Select(&out).Exec(context.Background())
assert.Nil(t, err)
assert.ElementsMatch(t, []formulaReadModel{{Value: 2}}, out)
}

func TestInjectTimestampCol(t *testing.T) {
result := injectTimestampCol(GoogleSheetRowStoreConfig{Columns: []string{"col1", "col2"}})
assert.Equal(t, GoogleSheetRowStoreConfig{Columns: []string{rowIdxCol, "col1", "col2"}}, result)
Expand Down
34 changes: 28 additions & 6 deletions internal/google/store/stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,13 @@ func (s *GoogleSheetInsertStmt) convertRowToSlice(row interface{}) ([]interface{
result := make([]interface{}, len(s.store.colsMapping))
result[0] = rowIdxFormula

for key, value := range output {
if colIdx, ok := s.store.colsMapping[key]; ok {
escapedValue := common.EscapeValue(value)
if err := common.CheckIEEE754SafeInteger(escapedValue); err != nil {
for col, value := range output {
if colIdx, ok := s.store.colsMapping[col]; ok {
escapedValue, err := escapeValue(col, value, s.store.colsWithFormula)
if err != nil {
return nil, err
}
if err = common.CheckIEEE754SafeInteger(escapedValue); err != nil {
return nil, err
}
result[colIdx.Idx] = escapedValue
Expand Down Expand Up @@ -501,8 +504,11 @@ func (s *GoogleSheetUpdateStmt) generateBatchUpdateRequests(rowIndices []int64)
return nil, fmt.Errorf("failed to update, unknown column name provided: %s", col)
}

escapedValue := common.EscapeValue(value)
if err := common.CheckIEEE754SafeInteger(escapedValue); err != nil {
escapedValue, err := escapeValue(col, value, s.store.colsWithFormula)
if err != nil {
return nil, err
}
if err = common.CheckIEEE754SafeInteger(escapedValue); err != nil {
return nil, err
}

Expand Down Expand Up @@ -657,3 +663,19 @@ func ridWhereClauseInterceptor(where string) string {
}
return fmt.Sprintf(rowWhereNonEmptyConditionTemplate, where)
}

func escapeValue(
col string,
value any,
colsWithFormula *common.Set[string],
) (any, error) {
if !colsWithFormula.Contains(col) {
return common.EscapeValue(value), nil
}

_, ok := value.(string)
if !ok {
return nil, fmt.Errorf("value of column %s is not a string, but expected to contain formula", col)
}
return value, nil
}
Loading

0 comments on commit 83a3c9b

Please sign in to comment.