Skip to content

Commit 83a3c9b

Browse files
authored
Add config for columns with formula (#18)
* Add config for columns with formula * Apply Code Coverage Badge --------- Co-authored-by: edocsss <[email protected]>
1 parent d5a8e0e commit 83a3c9b

File tree

6 files changed

+230
-47
lines changed

6 files changed

+230
-47
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

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

internal/common/set.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package common
2+
3+
type Set[T comparable] struct {
4+
values map[T]struct{}
5+
}
6+
7+
func (s *Set[T]) Contains(v T) bool {
8+
_, ok := s.values[v]
9+
return ok
10+
}
11+
12+
func NewSet[T comparable](values []T) *Set[T] {
13+
s := &Set[T]{
14+
values: make(map[T]struct{}, len(values)),
15+
}
16+
for _, v := range values {
17+
s.values[v] = struct{}{}
18+
}
19+
return s
20+
}

internal/google/store/row.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,30 @@ type GoogleSheetRowStoreConfig struct {
1717
// The column ordering will be used for arranging the real columns in Google Sheet.
1818
// Changing the column ordering in this config but not in Google Sheet will result in unexpected behaviour.
1919
Columns []string
20+
21+
// ColumnsWithFormula defines the list of column names containing a Google Sheet formula.
22+
// Note that only string fields can have a formula.
23+
ColumnsWithFormula []string
2024
}
2125

2226
func (c GoogleSheetRowStoreConfig) validate() error {
2327
if len(c.Columns) == 0 {
2428
return errors.New("columns must have at least one column")
2529
}
2630
if len(c.Columns) > maxColumn {
27-
return errors.New("you can only have up to 1000 columns")
31+
return fmt.Errorf("you can only have up to %d columns", maxColumn)
2832
}
2933
return nil
3034
}
3135

3236
// GoogleSheetRowStore encapsulates row store functionality on top of a Google Sheet.
3337
type GoogleSheetRowStore struct {
34-
wrapper sheetsWrapper
35-
spreadsheetID string
36-
sheetName string
37-
colsMapping common.ColsMapping
38-
config GoogleSheetRowStoreConfig
38+
wrapper sheetsWrapper
39+
spreadsheetID string
40+
sheetName string
41+
colsMapping common.ColsMapping
42+
colsWithFormula *common.Set[string]
43+
config GoogleSheetRowStoreConfig
3944
}
4045

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

156161
config = injectTimestampCol(config)
157-
158162
store := &GoogleSheetRowStore{
159-
wrapper: wrapper,
160-
spreadsheetID: spreadsheetID,
161-
sheetName: sheetName,
162-
colsMapping: common.GenerateColumnMapping(config.Columns),
163-
config: config,
163+
wrapper: wrapper,
164+
spreadsheetID: spreadsheetID,
165+
sheetName: sheetName,
166+
colsMapping: common.GenerateColumnMapping(config.Columns),
167+
colsWithFormula: common.NewSet(config.ColumnsWithFormula),
168+
config: config,
164169
}
165170

166171
_ = ensureSheets(store.wrapper, store.spreadsheetID, store.sheetName)

internal/google/store/row_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,62 @@ func TestGoogleSheetRowStore_Integration_EdgeCases(t *testing.T) {
150150
assert.NotNil(t, err)
151151
}
152152

153+
type formulaWriteModel struct {
154+
Value string `json:"value" db:"value"`
155+
}
156+
157+
type formulaReadModel struct {
158+
Value int `json:"value" db:"value"`
159+
}
160+
161+
func TestGoogleSheetRowStore_Formula(t *testing.T) {
162+
spreadsheetID, authJSON, shouldRun := getIntegrationTestInfo()
163+
if !shouldRun {
164+
t.Skip("integration test should be run only in GitHub Actions")
165+
}
166+
sheetName := fmt.Sprintf("integration_row_%d", common.CurrentTimeMs())
167+
168+
googleAuth, err := auth.NewServiceFromJSON([]byte(authJSON), auth.GoogleSheetsReadWrite, auth.ServiceConfig{})
169+
if err != nil {
170+
t.Fatalf("error when instantiating google auth: %s", err)
171+
}
172+
173+
db := NewGoogleSheetRowStore(
174+
googleAuth,
175+
spreadsheetID,
176+
sheetName,
177+
GoogleSheetRowStoreConfig{
178+
Columns: []string{"value"},
179+
ColumnsWithFormula: []string{"value"},
180+
},
181+
)
182+
defer func() {
183+
time.Sleep(time.Second)
184+
deleteSheet(t, db.wrapper, spreadsheetID, []string{db.sheetName})
185+
_ = db.Close(context.Background())
186+
}()
187+
188+
var out []formulaReadModel
189+
190+
time.Sleep(time.Second)
191+
err = db.Insert(formulaWriteModel{Value: "=ROW()-1"}).Exec(context.Background())
192+
assert.Nil(t, err)
193+
194+
time.Sleep(time.Second)
195+
err = db.Select(&out).Exec(context.Background())
196+
assert.Nil(t, err)
197+
assert.ElementsMatch(t, []formulaReadModel{{Value: 1}}, out)
198+
199+
time.Sleep(time.Second)
200+
err = db.Update(map[string]interface{}{"value": "=ROW()"}).Exec(context.Background())
201+
assert.Nil(t, err)
202+
203+
time.Sleep(time.Second)
204+
err = db.Select(&out).Exec(context.Background())
205+
assert.Nil(t, err)
206+
assert.ElementsMatch(t, []formulaReadModel{{Value: 2}}, out)
207+
}
208+
153209
func TestInjectTimestampCol(t *testing.T) {
154210
result := injectTimestampCol(GoogleSheetRowStoreConfig{Columns: []string{"col1", "col2"}})
155211
assert.Equal(t, GoogleSheetRowStoreConfig{Columns: []string{rowIdxCol, "col1", "col2"}}, result)

internal/google/store/stmt.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,13 @@ func (s *GoogleSheetInsertStmt) convertRowToSlice(row interface{}) ([]interface{
399399
result := make([]interface{}, len(s.store.colsMapping))
400400
result[0] = rowIdxFormula
401401

402-
for key, value := range output {
403-
if colIdx, ok := s.store.colsMapping[key]; ok {
404-
escapedValue := common.EscapeValue(value)
405-
if err := common.CheckIEEE754SafeInteger(escapedValue); err != nil {
402+
for col, value := range output {
403+
if colIdx, ok := s.store.colsMapping[col]; ok {
404+
escapedValue, err := escapeValue(col, value, s.store.colsWithFormula)
405+
if err != nil {
406+
return nil, err
407+
}
408+
if err = common.CheckIEEE754SafeInteger(escapedValue); err != nil {
406409
return nil, err
407410
}
408411
result[colIdx.Idx] = escapedValue
@@ -501,8 +504,11 @@ func (s *GoogleSheetUpdateStmt) generateBatchUpdateRequests(rowIndices []int64)
501504
return nil, fmt.Errorf("failed to update, unknown column name provided: %s", col)
502505
}
503506

504-
escapedValue := common.EscapeValue(value)
505-
if err := common.CheckIEEE754SafeInteger(escapedValue); err != nil {
507+
escapedValue, err := escapeValue(col, value, s.store.colsWithFormula)
508+
if err != nil {
509+
return nil, err
510+
}
511+
if err = common.CheckIEEE754SafeInteger(escapedValue); err != nil {
506512
return nil, err
507513
}
508514

@@ -657,3 +663,19 @@ func ridWhereClauseInterceptor(where string) string {
657663
}
658664
return fmt.Sprintf(rowWhereNonEmptyConditionTemplate, where)
659665
}
666+
667+
func escapeValue(
668+
col string,
669+
value any,
670+
colsWithFormula *common.Set[string],
671+
) (any, error) {
672+
if !colsWithFormula.Contains(col) {
673+
return common.EscapeValue(value), nil
674+
}
675+
676+
_, ok := value.(string)
677+
if !ok {
678+
return nil, fmt.Errorf("value of column %s is not a string, but expected to contain formula", col)
679+
}
680+
return value, nil
681+
}

0 commit comments

Comments
 (0)