Skip to content

Commit

Permalink
Add the '_rid is not null' condition to all statements with WHERE clause
Browse files Browse the repository at this point in the history
  • Loading branch information
edocsss committed Aug 29, 2022
1 parent fc5fae3 commit 39783d9
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 50 deletions.
18 changes: 5 additions & 13 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ const (
kvGetDefaultQueryTemplate = "=VLOOKUP(\"%s\", %s, 2, FALSE)"
kvFindKeyA1RangeQueryTemplate = "=MATCH(\"%s\", %s, 0)"

rowGetIndicesQueryTemplate = "=JOIN(\",\", ARRAYFORMULA(QUERY({%s, ROW(%s)}, \"%s\")))"
rwoCountQueryTemplate = "=COUNT(QUERY(%s, \"%s\"))"
rowUpdateModifyWhereNonEmptyTemplate = "%s IS NOT NULL AND %s"
rowUpdateModifyWhereEmptyTemplate = "%s IS NOT NULL"

naValue = "#N/A"
errorValue = "#ERROR!"
rowIdxCol = "_rid"
Expand All @@ -63,6 +58,11 @@ var (
defaultRowFullTableRange = "A2:" + generateColumnName(maxColumn-1)
rowDeleteRangeTemplate = "A%d:" + generateColumnName(maxColumn-1) + "%d"

// The first condition `_rid IS NOT NULL` is necessary to ensure we are just updating rows that are non-empty.
// This is required for UPDATE without WHERE clause (otherwise it will see every row as update target).
rowWhereNonEmptyConditionTemplate = rowIdxCol + " is not null AND %s"
rowWhereEmptyConditionTemplate = rowIdxCol + " is not null"

lastColIdxName = "Col" + strconv.FormatInt(int64(maxColumn+1), 10)

googleSheetSelectStmtStringKeyword = regexp.MustCompile("^(date|datetime|timeofday)")
Expand Down Expand Up @@ -103,14 +103,6 @@ func (m colsMapping) NameMap() map[string]string {
return result
}

func (m colsMapping) ColIdxNameMap() map[string]string {
result := make(map[string]string, 0)
for col, val := range m {
result[col] = "Col" + strconv.FormatInt(int64(val.idx+1), 10)
}
return result
}

// OrderBy defines the type of column ordering used for GoogleSheetRowStore.Select().
type OrderBy string

Expand Down
61 changes: 37 additions & 24 deletions stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import (
"github.com/FreeLeh/GoFreeDB/internal/google/sheets"
)

type whereInterceptorFunc func(where string) string

type queryBuilder struct {
replacer *strings.Replacer
columns []string
where string
whereArgs []interface{}
orderBy []string
limit uint64
offset uint64
replacer *strings.Replacer
columns []string
where string
whereArgs []interface{}
whereInterceptor whereInterceptorFunc
orderBy []string
limit uint64
offset uint64
}

func (q *queryBuilder) Where(condition string, args ...interface{}) *queryBuilder {
Expand Down Expand Up @@ -83,16 +86,17 @@ func (q *queryBuilder) writeCols(stmt *strings.Builder) error {
}

func (q *queryBuilder) writeWhere(stmt *strings.Builder) error {
if len(q.where) == 0 {
return nil
where := q.where
if q.whereInterceptor != nil {
where = q.whereInterceptor(q.where)
}

nArgs := strings.Count(q.where, "?")
nArgs := strings.Count(where, "?")
if nArgs != len(q.whereArgs) {
return fmt.Errorf("number of arguments required in the 'where' clause (%d) is not the same as the number of provided arguments (%d)", nArgs, len(q.whereArgs))
}

where := q.replacer.Replace(q.where)
where = q.replacer.Replace(where)
tokens := strings.Split(where, "?")

result := make([]string, 0)
Expand Down Expand Up @@ -188,13 +192,21 @@ func (q *queryBuilder) writeLimit(stmt *strings.Builder) error {
return nil
}

func newQueryBuilder(colReplacements map[string]string, colSelected []string) *queryBuilder {
func newQueryBuilder(
colReplacements map[string]string,
whereInterceptor whereInterceptorFunc,
colSelected []string,
) *queryBuilder {
replacements := make([]string, 0, 2*len(colReplacements))
for col, repl := range colReplacements {
replacements = append(replacements, col, repl)
}

return &queryBuilder{replacer: strings.NewReplacer(replacements...), columns: colSelected}
return &queryBuilder{
replacer: strings.NewReplacer(replacements...),
columns: colSelected,
whereInterceptor: whereInterceptor,
}
}

// GoogleSheetSelectStmt encapsulates information required to query the row store.
Expand Down Expand Up @@ -326,7 +338,7 @@ func newGoogleSheetSelectStmt(store *GoogleSheetRowStore, output interface{}, co
return &GoogleSheetSelectStmt{
store: store,
columns: columns,
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), columns),
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), ridWhereClauseInterceptor, columns),
output: output,
}
}
Expand Down Expand Up @@ -413,13 +425,7 @@ type GoogleSheetUpdateStmt struct {
// It works just like the GoogleSheetSelectStmt.Where() method.
// Please read GoogleSheetSelectStmt.Where() for more details.
func (s *GoogleSheetUpdateStmt) Where(condition string, args ...interface{}) *GoogleSheetUpdateStmt {
// The first condition `_rid IS NOT NULL` is necessary to ensure we are just updating rows that are non-empty.
// This is required for UPDATE without WHERE clause (otherwise it will see every row as update target).
if condition == "" {
s.queryBuilder.Where(fmt.Sprintf(rowUpdateModifyWhereEmptyTemplate, rowIdxCol), args...)
} else {
s.queryBuilder.Where(fmt.Sprintf(rowUpdateModifyWhereNonEmptyTemplate, rowIdxCol, condition), args...)
}
s.queryBuilder.Where(condition, args...)
return s
}

Expand Down Expand Up @@ -478,7 +484,7 @@ func newGoogleSheetUpdateStmt(store *GoogleSheetRowStore, colToValue map[string]
return &GoogleSheetUpdateStmt{
store: store,
colToValue: colToValue,
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), []string{rowIdxCol}),
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), ridWhereClauseInterceptor, []string{rowIdxCol}),
}
}

Expand Down Expand Up @@ -521,7 +527,7 @@ func (s *GoogleSheetDeleteStmt) Exec(ctx context.Context) error {
func newGoogleSheetDeleteStmt(store *GoogleSheetRowStore) *GoogleSheetDeleteStmt {
return &GoogleSheetDeleteStmt{
store: store,
queryBuilder: newQueryBuilder(store.colsMapping.ColIdxNameMap(), []string{lastColIdxName}),
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), ridWhereClauseInterceptor, []string{rowIdxCol}),
}
}

Expand Down Expand Up @@ -566,7 +572,7 @@ func newGoogleSheetCountStmt(store *GoogleSheetRowStore) *GoogleSheetCountStmt {
countClause := fmt.Sprintf("COUNT(%s)", rowIdxCol)
return &GoogleSheetCountStmt{
store: store,
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), []string{countClause}),
queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), ridWhereClauseInterceptor, []string{countClause}),
}
}

Expand Down Expand Up @@ -606,3 +612,10 @@ func generateRowA1Ranges(sheetName string, indices []int64) []string {
}
return locations
}

func ridWhereClauseInterceptor(where string) string {
if where == "" {
return rowWhereEmptyConditionTemplate
}
return fmt.Sprintf(rowWhereNonEmptyConditionTemplate, where)
}
26 changes: 13 additions & 13 deletions stmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,30 @@ func TestGenerateQuery(t *testing.T) {
colsMapping := colsMapping{rowIdxCol: {"A", 0}, "col1": {"B", 1}, "col2": {"C", 2}}

t.Run("successful_basic", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
result, err := builder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C", result)
assert.Equal(t, "select B, C where A is not null", result)
})

t.Run("unsuccessful_basic_wrong_column", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2", "col3"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2", "col3"})
result, err := builder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C, col3", result)
assert.Equal(t, "select B, C, col3 where A is not null", result)
})

t.Run("successful_with_where", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
builder.Where("(col1 > ? AND col2 <= ?) OR (col1 != ? AND col2 == ?)", 100, true, "value", 3.14)

result, err := builder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C where (B > 100 AND C <= true ) OR (B != 'value' AND C == 3.14 )", result)
assert.Equal(t, "select B, C where A is not null AND (B > 100 AND C <= true ) OR (B != 'value' AND C == 3.14 )", result)
})

t.Run("unsuccessful_with_where_wrong_arg_count", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
builder.Where("(col1 > ? AND col2 <= ?) OR (col1 != ? AND col2 == ?)", 100, true)

result, err := builder.Generate()
Expand All @@ -51,7 +51,7 @@ func TestGenerateQuery(t *testing.T) {
})

t.Run("unsuccessful_with_where_unsupported_arg_type", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
builder.Where("(col1 > ? AND col2 <= ?) OR (col1 != ? AND col2 == ?)", 100, true, nil, []string{})

result, err := builder.Generate()
Expand All @@ -60,21 +60,21 @@ func TestGenerateQuery(t *testing.T) {
})

t.Run("successful_with_limit_offset", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
builder.Limit(10).Offset(100)

result, err := builder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C offset 100 limit 10", result)
assert.Equal(t, "select B, C where A is not null offset 100 limit 10", result)
})

t.Run("successful_with_order_by", func(t *testing.T) {
builder := newQueryBuilder(colsMapping.NameMap(), []string{"col1", "col2"})
builder := newQueryBuilder(colsMapping.NameMap(), ridWhereClauseInterceptor, []string{"col1", "col2"})
builder.OrderBy([]ColumnOrderBy{{Column: "col2", OrderBy: OrderByDesc}, {Column: "col1", OrderBy: OrderByAsc}})

result, err := builder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C order by C DESC, B ASC", result)
assert.Equal(t, "select B, C where A is not null order by C DESC, B ASC", result)
})
}

Expand All @@ -87,7 +87,7 @@ func TestSelectStmt_AllColumns(t *testing.T) {

result, err := stmt.queryBuilder.Generate()
assert.Nil(t, err)
assert.Equal(t, "select B, C", result)
assert.Equal(t, "select B, C where A is not null", result)
}

func TestSelectStmt_Exec(t *testing.T) {
Expand Down

0 comments on commit 39783d9

Please sign in to comment.