Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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