From 2fb07cad19cae61cf13eea5b939a9f9a82bf9b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Thu, 8 Apr 2021 23:45:15 -0300 Subject: [PATCH 01/10] Add first broken version of the kbuilder tool --- kbuilder/kbuilder.go | 140 ++++++++++++++++++++++++++++++++++++++ kbuilder/kbuilder_test.go | 38 +++++++++++ 2 files changed, 178 insertions(+) create mode 100644 kbuilder/kbuilder.go create mode 100644 kbuilder/kbuilder_test.go diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go new file mode 100644 index 0000000..f5b8708 --- /dev/null +++ b/kbuilder/kbuilder.go @@ -0,0 +1,140 @@ +package kbuilder + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +type Builder struct { + driver string +} + +func New(driver string) Builder { + return Builder{ + driver: driver, + } +} + +func (_ *Builder) Build(query Query) (sqlQuery string, params []interface{}, _ error) { + var b strings.Builder + // TODO: Actually build the Select from the struct: + b.WriteString(fmt.Sprint("SELECT", query.Select)) + + b.WriteString(" FROM " + query.From) + + if len(query.Where) > 0 { + b.WriteString(" WHERE " + query.Where.build()) + } + + if query.OrderBy.fields != "" { + b.WriteString(" ORDER BY " + query.OrderBy.fields) + if query.OrderBy.desc { + b.WriteString(" DESC") + } + } + + if query.Limit > 0 { + b.WriteString(" LIMIT " + strconv.Itoa(query.Limit)) + } + + if query.Offset > 0 { + b.WriteString(" OFFSET " + strconv.Itoa(query.Limit)) + } + + return b.String(), []interface{}{}, nil +} + +type Query struct { + // Select expects a struct using the `ksql` tags + Select interface{} + + // From expects the FROM clause from an SQL query, e.g. `users JOIN posts USING(post_id)` + From string + + // Where expects a list of WhereQuery instances built + // by the public Where() function. + Where WhereQueries + + Limit int + Offset int + OrderBy OrderByQuery +} + +type WhereQuery struct { + // Accepts any SQL boolean expression + // This expression may optionally contain + // string formatting directives %s and only %s. + // + // For each of these directives we expect a new param + // on the params list below. + // + // In the resulting query each %s will be properly replaced + // by placeholders according to the database driver, e.g. `$1` + // for postgres or `?` for sqlite3. + cond string + params []interface{} +} + +type WhereQueries []WhereQuery + +func (w WhereQueries) build() string { + // TODO: Implement this + return "" +} + +func (w WhereQueries) Where(cond string, params ...interface{}) WhereQueries { + return append(w, WhereQuery{ + cond: cond, + params: params, + }) +} + +func (w WhereQueries) WhereIf(cond string, param interface{}) WhereQueries { + if param == nil || reflect.ValueOf(param).IsNil() { + return w + } + + return append(w, WhereQuery{ + cond: cond, + params: []interface{}{param}, + }) +} + +func Where(cond string, params ...interface{}) WhereQueries { + return WhereQueries{{ + cond: cond, + params: params, + }} +} + +func WhereIf(cond string, param interface{}) WhereQueries { + if param == nil || reflect.ValueOf(param).IsNil() { + return WhereQueries{} + } + + return WhereQueries{{ + cond: cond, + params: []interface{}{param}, + }} +} + +type OrderByQuery struct { + fields string + desc bool +} + +func (o OrderByQuery) Desc() OrderByQuery { + return OrderByQuery{ + fields: o.fields, + desc: true, + } +} + +func OrderBy(fields string) OrderByQuery { + return OrderByQuery{ + fields: fields, + desc: false, + } +} diff --git a/kbuilder/kbuilder_test.go b/kbuilder/kbuilder_test.go new file mode 100644 index 0000000..098a9ec --- /dev/null +++ b/kbuilder/kbuilder_test.go @@ -0,0 +1,38 @@ +package kbuilder_test + +import ( + "testing" + + "github.com/tj/assert" + "github.com/vingarcia/ksql/kbuilder" +) + +type User struct { + Name string `ksql:"name"` + Age string `ksql:"name"` +} + +func TestBuilder(t *testing.T) { + t.Run("should build queries correctly", func(t *testing.T) { + b := kbuilder.New("postgres") + + var user User + var nullableField *int + query, params, err := b.Build(kbuilder.Query{ + Select: &user, + From: "users", + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullableField), + + OrderBy: kbuilder.OrderBy("id").Desc(), + Offset: 100, + Limit: 10, + }) + + assert.Equal(t, nil, err) + assert.Equal(t, `SELECT * FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10 OFFSET 100`, query) + assert.Equal(t, []interface{}{42, "%ending"}, params) + }) +} From 186dde8afe3b59e66133d994986c0dad51b32ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 10 Apr 2021 22:09:18 -0300 Subject: [PATCH 02/10] Finish MVP of the kbuilder package --- dialect.go | 21 ++++++++-- kbuilder/kbuilder.go | 82 ++++++++++++++++++++++++++++++++------- kbuilder/kbuilder_test.go | 7 ++-- ksql.go | 16 ++++---- ksql_test.go | 13 +++++-- structs/structs.go | 4 ++ 6 files changed, 111 insertions(+), 32 deletions(-) diff --git a/dialect.go b/dialect.go index 5522fb8..32c2e21 100644 --- a/dialect.go +++ b/dialect.go @@ -1,8 +1,13 @@ package ksql -import "strconv" +import ( + "fmt" + "strconv" +) -type dialect interface { +// Dialect is used to represent the different ways +// of writing SQL queries used by each SQL driver. +type Dialect interface { Escape(str string) string Placeholder(idx int) string } @@ -27,9 +32,17 @@ func (sqlite3Dialect) Placeholder(idx int) string { return "?" } -func getDriverDialect(driver string) dialect { - return map[string]dialect{ +// GetDriverDialect instantiantes the dialect for the +// provided driver string, if the drive is not supported +// it returns an error +func GetDriverDialect(driver string) (Dialect, error) { + dialect, found := map[string]Dialect{ "postgres": &postgresDialect{}, "sqlite3": &sqlite3Dialect{}, }[driver] + if !found { + return nil, fmt.Errorf("unsupported driver `%s`", driver) + } + + return dialect, nil } diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go index f5b8708..009edac 100644 --- a/kbuilder/kbuilder.go +++ b/kbuilder/kbuilder.go @@ -5,27 +5,43 @@ import ( "reflect" "strconv" "strings" + + "github.com/pkg/errors" + "github.com/vingarcia/ksql" + "github.com/vingarcia/ksql/structs" ) type Builder struct { - driver string + dialect ksql.Dialect } -func New(driver string) Builder { +func New(driver string) (Builder, error) { + dialect, err := ksql.GetDriverDialect(driver) return Builder{ - driver: driver, - } + dialect: dialect, + }, err } -func (_ *Builder) Build(query Query) (sqlQuery string, params []interface{}, _ error) { +func (builder *Builder) Build(query Query) (sqlQuery string, params []interface{}, _ error) { var b strings.Builder - // TODO: Actually build the Select from the struct: - b.WriteString(fmt.Sprint("SELECT", query.Select)) + + switch v := query.Select.(type) { + case string: + b.WriteString("SELECT " + v) + default: + selectQuery, err := buildSelectQuery(v, builder.dialect) + if err != nil { + return "", nil, errors.Wrap(err, "error reading the Select field") + } + b.WriteString("SELECT " + selectQuery) + } b.WriteString(" FROM " + query.From) if len(query.Where) > 0 { - b.WriteString(" WHERE " + query.Where.build()) + var whereQuery string + whereQuery, params = query.Where.build(builder.dialect) + b.WriteString(" WHERE " + whereQuery) } if query.OrderBy.fields != "" { @@ -40,10 +56,10 @@ func (_ *Builder) Build(query Query) (sqlQuery string, params []interface{}, _ e } if query.Offset > 0 { - b.WriteString(" OFFSET " + strconv.Itoa(query.Limit)) + b.WriteString(" OFFSET " + strconv.Itoa(query.Offset)) } - return b.String(), []interface{}{}, nil + return b.String(), params, nil } type Query struct { @@ -79,9 +95,19 @@ type WhereQuery struct { type WhereQueries []WhereQuery -func (w WhereQueries) build() string { - // TODO: Implement this - return "" +func (w WhereQueries) build(dialect ksql.Dialect) (query string, params []interface{}) { + var conds []string + for _, whereQuery := range w { + var placeholders []interface{} + for i := range whereQuery.params { + placeholders = append(placeholders, dialect.Placeholder(len(params)+i)) + } + + conds = append(conds, fmt.Sprintf(whereQuery.cond, placeholders...)) + params = append(params, whereQuery.params...) + } + + return strings.Join(conds, " AND "), params } func (w WhereQueries) Where(cond string, params ...interface{}) WhereQueries { @@ -138,3 +164,33 @@ func OrderBy(fields string) OrderByQuery { desc: false, } } + +var cachedSelectQueries = map[reflect.Type]string{} + +// Builds the select query using cached info so that its efficient +func buildSelectQuery(obj interface{}, dialect ksql.Dialect) (string, error) { + t := reflect.TypeOf(obj) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return "", fmt.Errorf("expected to receive a pointer to struct, but got: %T", obj) + } + + if query, found := cachedSelectQueries[t]; found { + return query, nil + } + + info := structs.GetTagInfo(t) + + var escapedNames []string + for i := 0; i < info.NumFields(); i++ { + name := info.ByIndex(i).Name + escapedNames = append(escapedNames, dialect.Escape(name)) + } + + query := strings.Join(escapedNames, ", ") + cachedSelectQueries[t] = query + return query, nil +} diff --git a/kbuilder/kbuilder_test.go b/kbuilder/kbuilder_test.go index 098a9ec..0988da7 100644 --- a/kbuilder/kbuilder_test.go +++ b/kbuilder/kbuilder_test.go @@ -9,12 +9,13 @@ import ( type User struct { Name string `ksql:"name"` - Age string `ksql:"name"` + Age string `ksql:"age"` } func TestBuilder(t *testing.T) { t.Run("should build queries correctly", func(t *testing.T) { - b := kbuilder.New("postgres") + b, err := kbuilder.New("postgres") + assert.Equal(t, nil, err) var user User var nullableField *int @@ -32,7 +33,7 @@ func TestBuilder(t *testing.T) { }) assert.Equal(t, nil, err) - assert.Equal(t, `SELECT * FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10 OFFSET 100`, query) + assert.Equal(t, `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10 OFFSET 100`, query) assert.Equal(t, []interface{}{42, "%ending"}, params) }) } diff --git a/ksql.go b/ksql.go index e426514..f8504fe 100644 --- a/ksql.go +++ b/ksql.go @@ -16,7 +16,7 @@ import ( // the KissSQL interface `SQLProvider`. type DB struct { driver string - dialect dialect + dialect Dialect tableName string db sqlProvider @@ -75,9 +75,9 @@ func New( db.SetMaxOpenConns(config.MaxOpenConns) - dialect := getDriverDialect(dbDriver) - if dialect == nil { - return DB{}, fmt.Errorf("unsupported driver `%s`", dbDriver) + dialect, err := GetDriverDialect(dbDriver) + if err != nil { + return DB{}, err } if len(config.IDColumns) == 0 { @@ -562,7 +562,7 @@ func (c DB) Update( } func buildInsertQuery( - dialect dialect, + dialect Dialect, tableName string, record interface{}, idFieldNames ...string, @@ -619,7 +619,7 @@ func buildInsertQuery( } func buildUpdateQuery( - dialect dialect, + dialect Dialect, tableName string, record interface{}, idFieldNames ...string, @@ -809,7 +809,7 @@ func scanRows(rows *sql.Rows, record interface{}) error { } func buildSingleKeyDeleteQuery( - dialect dialect, + dialect Dialect, table string, idName string, idMaps []map[string]interface{}, @@ -829,7 +829,7 @@ func buildSingleKeyDeleteQuery( } func buildCompositeKeyDeleteQuery( - dialect dialect, + dialect Dialect, table string, idNames []string, idMaps []map[string]interface{}, diff --git a/ksql_test.go b/ksql_test.go index 903acff..583fece 100644 --- a/ksql_test.go +++ b/ksql_test.go @@ -1389,9 +1389,14 @@ func newTestDB(db *sql.DB, driver string, tableName string, ids ...string) DB { ids = []string{"id"} } + dialect, err := GetDriverDialect(driver) + if err != nil { + panic(err) + } + return DB{ driver: driver, - dialect: getDriverDialect(driver), + dialect: dialect, db: db, tableName: tableName, @@ -1422,7 +1427,7 @@ func shiftErrSlice(errs *[]error) error { return err } -func getUsersByID(dbi sqlProvider, dialect dialect, resultsPtr *[]User, ids ...uint) error { +func getUsersByID(dbi sqlProvider, dialect Dialect, resultsPtr *[]User, ids ...uint) error { db := dbi.(*sql.DB) placeholders := make([]string, len(ids)) @@ -1464,7 +1469,7 @@ func getUsersByID(dbi sqlProvider, dialect dialect, resultsPtr *[]User, ids ...u return nil } -func getUserByID(dbi sqlProvider, dialect dialect, result *User, id uint) error { +func getUserByID(dbi sqlProvider, dialect Dialect, result *User, id uint) error { db := dbi.(*sql.DB) row := db.QueryRow(`SELECT id, name, age, address FROM users WHERE id=`+dialect.Placeholder(0), id) @@ -1485,7 +1490,7 @@ func getUserByID(dbi sqlProvider, dialect dialect, result *User, id uint) error return json.Unmarshal(rawAddr, &result.Address) } -func getUserByName(dbi sqlProvider, dialect dialect, result *User, name string) error { +func getUserByName(dbi sqlProvider, dialect Dialect, result *User, name string) error { db := dbi.(*sql.DB) row := db.QueryRow(`SELECT id, name, age, address FROM users WHERE name=`+dialect.Placeholder(0), name) diff --git a/structs/structs.go b/structs/structs.go index 7c7941d..c85e0b7 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -42,6 +42,10 @@ func (s structInfo) Add(field fieldInfo) { s.byName[field.Name] = &field } +func (s structInfo) NumFields() int { + return len(s.byIndex) +} + // This cache is kept as a pkg variable // because the total number of types on a program // should be finite. So keeping a single cache here From 4c6556af883b5deecf5b08f3e54ab20a4f12caa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 10 Apr 2021 22:51:29 -0300 Subject: [PATCH 03/10] Add more tests to the kbuilder package --- kbuilder/kbuilder.go | 4 ++ kbuilder/kbuilder_test.go | 145 ++++++++++++++++++++++++++++++++------ 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go index 009edac..78998d4 100644 --- a/kbuilder/kbuilder.go +++ b/kbuilder/kbuilder.go @@ -44,6 +44,10 @@ func (builder *Builder) Build(query Query) (sqlQuery string, params []interface{ b.WriteString(" WHERE " + whereQuery) } + if strings.TrimSpace(query.From) == "" { + return "", nil, fmt.Errorf("the From field is mandatory for every query") + } + if query.OrderBy.fields != "" { b.WriteString(" ORDER BY " + query.OrderBy.fields) if query.OrderBy.desc { diff --git a/kbuilder/kbuilder_test.go b/kbuilder/kbuilder_test.go index 0988da7..23623b0 100644 --- a/kbuilder/kbuilder_test.go +++ b/kbuilder/kbuilder_test.go @@ -1,6 +1,7 @@ package kbuilder_test import ( + "fmt" "testing" "github.com/tj/assert" @@ -12,28 +13,132 @@ type User struct { Age string `ksql:"age"` } +var nullField *int + func TestBuilder(t *testing.T) { - t.Run("should build queries correctly", func(t *testing.T) { - b, err := kbuilder.New("postgres") - assert.Equal(t, nil, err) - var user User - var nullableField *int - query, params, err := b.Build(kbuilder.Query{ - Select: &user, - From: "users", - Where: kbuilder. - Where("foo < %s", 42). - Where("bar LIKE %s", "%ending"). - WhereIf("foobar = %s", nullableField), + tests := []struct { + desc string + query kbuilder.Query + expectedQuery string + expectedParams []interface{} + expectedErr bool + }{ + { + desc: "should build queries correctly", + query: kbuilder.Query{ + Select: &User{}, + From: "users", + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullField), - OrderBy: kbuilder.OrderBy("id").Desc(), - Offset: 100, - Limit: 10, + OrderBy: kbuilder.OrderBy("id").Desc(), + Offset: 100, + Limit: 10, + }, + expectedQuery: `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10 OFFSET 100`, + expectedParams: []interface{}{42, "%ending"}, + }, + { + desc: "should build queries omitting the OFFSET", + query: kbuilder.Query{ + Select: &User{}, + From: "users", + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullField), + + OrderBy: kbuilder.OrderBy("id").Desc(), + Limit: 10, + }, + expectedQuery: `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10`, + expectedParams: []interface{}{42, "%ending"}, + }, + { + desc: "should build queries omitting the LIMIT", + query: kbuilder.Query{ + Select: &User{}, + From: "users", + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullField), + + OrderBy: kbuilder.OrderBy("id").Desc(), + Offset: 100, + }, + expectedQuery: `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC OFFSET 100`, + expectedParams: []interface{}{42, "%ending"}, + }, + { + desc: "should build queries omitting the ORDER BY clause", + query: kbuilder.Query{ + Select: &User{}, + From: "users", + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullField), + + Offset: 100, + Limit: 10, + }, + expectedQuery: `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 LIMIT 10 OFFSET 100`, + expectedParams: []interface{}{42, "%ending"}, + }, + { + desc: "should build queries omitting the WHERE clause", + query: kbuilder.Query{ + Select: &User{}, + From: "users", + + OrderBy: kbuilder.OrderBy("id").Desc(), + Offset: 100, + Limit: 10, + }, + expectedQuery: `SELECT "name", "age" FROM users ORDER BY id DESC LIMIT 10 OFFSET 100`, + }, + + /* * * * * Testing error cases: * * * * */ + { + desc: "should report error if the FROM clause is missing", + query: kbuilder.Query{ + Select: &User{}, + Where: kbuilder. + Where("foo < %s", 42). + Where("bar LIKE %s", "%ending"). + WhereIf("foobar = %s", nullField), + + OrderBy: kbuilder.OrderBy("id").Desc(), + Offset: 100, + Limit: 10, + }, + + expectedErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + b, err := kbuilder.New("postgres") + assert.Equal(t, nil, err) + + query, params, err := b.Build(test.query) + + expectError(t, test.expectedErr, err) + assert.Equal(t, test.expectedQuery, query) + assert.Equal(t, test.expectedParams, params) }) - - assert.Equal(t, nil, err) - assert.Equal(t, `SELECT "name", "age" FROM users WHERE foo < $1 AND bar LIKE $2 ORDER BY id DESC LIMIT 10 OFFSET 100`, query) - assert.Equal(t, []interface{}{42, "%ending"}, params) - }) + } +} + +func expectError(t *testing.T, expect bool, err error) { + if expect { + assert.Equal(t, true, err != nil, "expected an error, but got nothing") + } else { + assert.Equal(t, false, err != nil, fmt.Sprintf("unexpected error %s", err)) + } } From 99ed52b59197046dcd93dd215c951d87bac8b378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 10 Apr 2021 22:55:04 -0300 Subject: [PATCH 04/10] Add a very small README for the kbuilder package --- kbuilder/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 kbuilder/README.md diff --git a/kbuilder/README.md b/kbuilder/README.md new file mode 100644 index 0000000..24f5361 --- /dev/null +++ b/kbuilder/README.md @@ -0,0 +1,10 @@ +# Welcome to the KISS Query Builder + +This is the Keep It Stupid Simple query builder created to work +either in conjunction or separated from the ksql package. + +## TODO List + +- Support Insert and Update operations +- Improve support to JOINs by adding the `tablename` tag to the structs +- Add error check for when the Select, Insert and Update attrs are all empty From d2c88ed71b970abaee06cd7cf3c7dd008f3a4d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sat, 24 Apr 2021 12:15:59 -0300 Subject: [PATCH 05/10] Improve a comment and a test --- kbuilder/kbuilder.go | 4 +++- kbuilder/kbuilder_test.go | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go index 78998d4..b17b808 100644 --- a/kbuilder/kbuilder.go +++ b/kbuilder/kbuilder.go @@ -67,7 +67,9 @@ func (builder *Builder) Build(query Query) (sqlQuery string, params []interface{ } type Query struct { - // Select expects a struct using the `ksql` tags + // Select expects either a struct using the `ksql` tags + // or a string listing the column names using SQL syntax, + // e.g.: `id, username, address` Select interface{} // From expects the FROM clause from an SQL query, e.g. `users JOIN posts USING(post_id)` diff --git a/kbuilder/kbuilder_test.go b/kbuilder/kbuilder_test.go index 23623b0..bbf4503 100644 --- a/kbuilder/kbuilder_test.go +++ b/kbuilder/kbuilder_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" "github.com/tj/assert" "github.com/vingarcia/ksql/kbuilder" ) @@ -137,8 +138,8 @@ func TestBuilder(t *testing.T) { func expectError(t *testing.T, expect bool, err error) { if expect { - assert.Equal(t, true, err != nil, "expected an error, but got nothing") + require.Equal(t, true, err != nil, "expected an error, but got nothing") } else { - assert.Equal(t, false, err != nil, fmt.Sprintf("unexpected error %s", err)) + require.Equal(t, false, err != nil, fmt.Sprintf("unexpected error %s", err)) } } From ba6727b14a2e4e3b410dc344f7f68b7ed096d5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Tue, 17 Aug 2021 12:08:37 -0300 Subject: [PATCH 06/10] Improve the list of methods on the kbuilder.Query struct Add the queryBuilder interface Make the kbuilder.Query implement this interface Add kbuilder.Query.Build() helper method for facilitating the usage if the person prefers no to inject the kbuilder.Builder struct. --- kbuilder/kbuilder.go | 104 ++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go index e809ddc..ed188e4 100644 --- a/kbuilder/kbuilder.go +++ b/kbuilder/kbuilder.go @@ -21,6 +21,10 @@ type Builder struct { dialect ksql.Dialect } +type queryBuilder interface { + BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) +} + // New creates a new Builder container. func New(driver string) (Builder, error) { dialect, err := ksql.GetDriverDialect(driver) @@ -31,48 +35,8 @@ func New(driver string) (Builder, error) { // Build receives a query builder struct, injects it with the configurations // build the query according to its arguments. -func (builder *Builder) Build(query Query) (sqlQuery string, params []interface{}, _ error) { - var b strings.Builder - - switch v := query.Select.(type) { - case string: - b.WriteString("SELECT " + v) - default: - selectQuery, err := buildSelectQuery(v, builder.dialect) - if err != nil { - return "", nil, errors.Wrap(err, "error reading the Select field") - } - b.WriteString("SELECT " + selectQuery) - } - - b.WriteString(" FROM " + query.From) - - if len(query.Where) > 0 { - var whereQuery string - whereQuery, params = query.Where.build(builder.dialect) - b.WriteString(" WHERE " + whereQuery) - } - - if strings.TrimSpace(query.From) == "" { - return "", nil, fmt.Errorf("the From field is mandatory for every query") - } - - if query.OrderBy.fields != "" { - b.WriteString(" ORDER BY " + query.OrderBy.fields) - if query.OrderBy.desc { - b.WriteString(" DESC") - } - } - - if query.Limit > 0 { - b.WriteString(" LIMIT " + strconv.Itoa(query.Limit)) - } - - if query.Offset > 0 { - b.WriteString(" OFFSET " + strconv.Itoa(query.Offset)) - } - - return b.String(), params, nil +func (builder *Builder) Build(query queryBuilder) (sqlQuery string, params []interface{}, _ error) { + return query.BuildQuery(builder.dialect) } // Query is is the struct template for building SELECT queries. @@ -94,6 +58,62 @@ type Query struct { OrderBy OrderByQuery } +// Build is a utility function for finding the dialect based on the driver and +// then calling BuildQuery(dialect) +func (q Query) Build(driver string) (sqlQuery string, params []interface{}, _ error) { + dialect, err := ksql.GetDriverDialect(driver) + if err != nil { + return "", nil, err + } + + return q.BuildQuery(dialect) +} + +// BuildQuery implements the QueryBuilder interface +func (q Query) BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) { + var b strings.Builder + + switch v := q.Select.(type) { + case string: + b.WriteString("SELECT " + v) + default: + selectQuery, err := buildSelectQuery(v, dialect) + if err != nil { + return "", nil, errors.Wrap(err, "error reading the Select field") + } + b.WriteString("SELECT " + selectQuery) + } + + b.WriteString(" FROM " + q.From) + + if len(q.Where) > 0 { + var whereQuery string + whereQuery, params = q.Where.build(dialect) + b.WriteString(" WHERE " + whereQuery) + } + + if strings.TrimSpace(q.From) == "" { + return "", nil, fmt.Errorf("the From field is mandatory for every query") + } + + if q.OrderBy.fields != "" { + b.WriteString(" ORDER BY " + q.OrderBy.fields) + if q.OrderBy.desc { + b.WriteString(" DESC") + } + } + + if q.Limit > 0 { + b.WriteString(" LIMIT " + strconv.Itoa(q.Limit)) + } + + if q.Offset > 0 { + b.WriteString(" OFFSET " + strconv.Itoa(q.Offset)) + } + + return b.String(), params, nil +} + // WhereQuery represents a single condition in a WHERE expression. type WhereQuery struct { // Accepts any SQL boolean expression From 22fa8fdfa492f46168854199e869d524eb20ec0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Fri, 3 Sep 2021 11:08:52 -0300 Subject: [PATCH 07/10] Improve the description of the Provider interface --- README.md | 2 +- contracts.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2fe2de1..f791f58 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The current interface is as follows and we plan on keeping it with as little functions as possible, so don't expect many additions: ```go -// Provider describes the public behavior of this ORM +// Provider describes the ksql public behavior type Provider interface { Insert(ctx context.Context, table Table, record interface{}) error Update(ctx context.Context, table Table, record interface{}) error diff --git a/contracts.go b/contracts.go index 0599770..3325bce 100644 --- a/contracts.go +++ b/contracts.go @@ -14,7 +14,7 @@ var ErrRecordNotFound error = errors.Wrap(sql.ErrNoRows, "ksql: the query return // ErrAbortIteration ... var ErrAbortIteration error = fmt.Errorf("ksql: abort iteration, should only be used inside QueryChunks function") -// Provider describes the public behavior of this ORM +// Provider describes the ksql public behavior type Provider interface { Insert(ctx context.Context, table Table, record interface{}) error Update(ctx context.Context, table Table, record interface{}) error From 6935bddf291d982e5943d7490afa755c0c8d256c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Tue, 7 Sep 2021 12:01:34 -0300 Subject: [PATCH 08/10] Add the Insert struct to the kbuilder package --- kbuilder/insert.go | 105 +++++++++ kbuilder/insert_test.go | 92 ++++++++ kbuilder/kbuilder.go | 213 ------------------ kbuilder/query.go | 218 +++++++++++++++++++ kbuilder/{kbuilder_test.go => query_test.go} | 5 +- 5 files changed, 417 insertions(+), 216 deletions(-) create mode 100644 kbuilder/insert.go create mode 100644 kbuilder/insert_test.go create mode 100644 kbuilder/query.go rename kbuilder/{kbuilder_test.go => query_test.go} (98%) diff --git a/kbuilder/insert.go b/kbuilder/insert.go new file mode 100644 index 0000000..d65c483 --- /dev/null +++ b/kbuilder/insert.go @@ -0,0 +1,105 @@ +package kbuilder + +import ( + "fmt" + "reflect" + "strings" + + "github.com/vingarcia/ksql" + "github.com/vingarcia/ksql/kstructs" +) + +// Insert is the struct template for building INSERT queries +type Insert struct { + // Into expects a table name, e.g. "users" + Into string + + // Data expected either a single record annotated with `ksql` tags + // or a list of records annotated likewise. + Data interface{} +} + +// Build is a utility function for finding the dialect based on the driver and +// then calling BuildQuery(dialect) +func (i Insert) Build(driver string) (sqlQuery string, params []interface{}, _ error) { + dialect, err := ksql.GetDriverDialect(driver) + if err != nil { + return "", nil, err + } + + return i.BuildQuery(dialect) +} + +// BuildQuery implements the queryBuilder interface +func (i Insert) BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) { + var b strings.Builder + b.WriteString("INSERT INTO " + dialect.Escape(i.Into)) + + if i.Into == "" { + return "", nil, fmt.Errorf( + "expected the Into attr to contain the tablename, but got an empty string instead", + ) + } + + if i.Data == nil { + return "", nil, fmt.Errorf( + "expected the Data attr to contain a struct or a list of structs, but got `%v`", + i.Data, + ) + } + + v := reflect.ValueOf(i.Data) + t := v.Type() + if t.Kind() != reflect.Slice { + // Convert it to a slice of a single element: + v = reflect.Append(reflect.MakeSlice(reflect.SliceOf(t), 0, 1), v) + } else { + t = t.Elem() + } + + if v.Len() == 0 { + return "", nil, fmt.Errorf( + "can't create an insertion query from an empty list of values", + ) + } + + isPtr := false + if t.Kind() == reflect.Ptr { + isPtr = true + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return "", nil, fmt.Errorf("expected Data attr to be a struct or slice of structs but got: %v", t) + } + + info := kstructs.GetTagInfo(t) + + b.WriteString(" (") + var escapedNames []string + for i := 0; i < info.NumFields(); i++ { + name := info.ByIndex(i).Name + escapedNames = append(escapedNames, dialect.Escape(name)) + } + b.WriteString(strings.Join(escapedNames, ", ")) + b.WriteString(") VALUES ") + + params = []interface{}{} + values := []string{} + for i := 0; i < v.Len(); i++ { + record := v.Index(i) + if isPtr { + record = record.Elem() + } + + placeholders := []string{} + for j := 0; j < info.NumFields(); j++ { + placeholders = append(placeholders, dialect.Placeholder(len(params))) + params = append(params, record.Field(j).Interface()) + } + values = append(values, "("+strings.Join(placeholders, ", ")+")") + } + b.WriteString(strings.Join(values, ", ")) + + return b.String(), params, nil +} diff --git a/kbuilder/insert_test.go b/kbuilder/insert_test.go new file mode 100644 index 0000000..745e8bf --- /dev/null +++ b/kbuilder/insert_test.go @@ -0,0 +1,92 @@ +package kbuilder_test + +import ( + "testing" + + "github.com/tj/assert" + "github.com/vingarcia/ksql/kbuilder" +) + +func TestInsertQuery(t *testing.T) { + tests := []struct { + desc string + query kbuilder.Insert + expectedQuery string + expectedParams []interface{} + expectedErr bool + }{ + { + desc: "should build queries witha single record correctly", + query: kbuilder.Insert{ + Into: "users", + Data: &User{ + Name: "foo", + Age: 42, + }, + }, + expectedQuery: `INSERT INTO "users" ("name", "age") VALUES ($1, $2)`, + expectedParams: []interface{}{"foo", 42}, + }, + { + desc: "should build queries with multiple records correctly", + query: kbuilder.Insert{ + Into: "users", + Data: []User{ + { + Name: "foo", + Age: 42, + }, + { + Name: "bar", + Age: 43, + }, + }, + }, + expectedQuery: `INSERT INTO "users" ("name", "age") VALUES ($1, $2), ($3, $4)`, + expectedParams: []interface{}{"foo", 42, "bar", 43}, + }, + + /* * * * * Testing error cases: * * * * */ + { + desc: "should report error if the `Data` attribute is missing", + query: kbuilder.Insert{ + Into: "users", + }, + + expectedErr: true, + }, + { + desc: "should report error if the `Into` attribute is missing", + query: kbuilder.Insert{ + Data: &User{ + Name: "foo", + Age: 42, + }, + }, + + expectedErr: true, + }, + { + desc: "should report error Data contains an empty list", + query: kbuilder.Insert{ + Into: "users", + Data: []User{}, + }, + + expectedErr: true, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + b, err := kbuilder.New("postgres") + assert.Equal(t, nil, err) + + query, params, err := b.Build(test.query) + + expectError(t, test.expectedErr, err) + assert.Equal(t, test.expectedQuery, query) + assert.Equal(t, test.expectedParams, params) + }) + } +} diff --git a/kbuilder/kbuilder.go b/kbuilder/kbuilder.go index ed188e4..0cf84de 100644 --- a/kbuilder/kbuilder.go +++ b/kbuilder/kbuilder.go @@ -1,14 +1,7 @@ package kbuilder import ( - "fmt" - "reflect" - "strconv" - "strings" - - "github.com/pkg/errors" "github.com/vingarcia/ksql" - "github.com/vingarcia/ksql/kstructs" ) // Builder is the basic container for injecting @@ -38,209 +31,3 @@ func New(driver string) (Builder, error) { func (builder *Builder) Build(query queryBuilder) (sqlQuery string, params []interface{}, _ error) { return query.BuildQuery(builder.dialect) } - -// Query is is the struct template for building SELECT queries. -type Query struct { - // Select expects either a struct using the `ksql` tags - // or a string listing the column names using SQL syntax, - // e.g.: `id, username, address` - Select interface{} - - // From expects the FROM clause from an SQL query, e.g. `users JOIN posts USING(post_id)` - From string - - // Where expects a list of WhereQuery instances built - // by the public Where() function. - Where WhereQueries - - Limit int - Offset int - OrderBy OrderByQuery -} - -// Build is a utility function for finding the dialect based on the driver and -// then calling BuildQuery(dialect) -func (q Query) Build(driver string) (sqlQuery string, params []interface{}, _ error) { - dialect, err := ksql.GetDriverDialect(driver) - if err != nil { - return "", nil, err - } - - return q.BuildQuery(dialect) -} - -// BuildQuery implements the QueryBuilder interface -func (q Query) BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) { - var b strings.Builder - - switch v := q.Select.(type) { - case string: - b.WriteString("SELECT " + v) - default: - selectQuery, err := buildSelectQuery(v, dialect) - if err != nil { - return "", nil, errors.Wrap(err, "error reading the Select field") - } - b.WriteString("SELECT " + selectQuery) - } - - b.WriteString(" FROM " + q.From) - - if len(q.Where) > 0 { - var whereQuery string - whereQuery, params = q.Where.build(dialect) - b.WriteString(" WHERE " + whereQuery) - } - - if strings.TrimSpace(q.From) == "" { - return "", nil, fmt.Errorf("the From field is mandatory for every query") - } - - if q.OrderBy.fields != "" { - b.WriteString(" ORDER BY " + q.OrderBy.fields) - if q.OrderBy.desc { - b.WriteString(" DESC") - } - } - - if q.Limit > 0 { - b.WriteString(" LIMIT " + strconv.Itoa(q.Limit)) - } - - if q.Offset > 0 { - b.WriteString(" OFFSET " + strconv.Itoa(q.Offset)) - } - - return b.String(), params, nil -} - -// WhereQuery represents a single condition in a WHERE expression. -type WhereQuery struct { - // Accepts any SQL boolean expression - // This expression may optionally contain - // string formatting directives %s and only %s. - // - // For each of these directives we expect a new param - // on the params list below. - // - // In the resulting query each %s will be properly replaced - // by placeholders according to the database driver, e.g. `$1` - // for postgres or `?` for sqlite3. - cond string - params []interface{} -} - -// WhereQueries is the helper for creating complex WHERE queries -// in a dynamic way. -type WhereQueries []WhereQuery - -func (w WhereQueries) build(dialect ksql.Dialect) (query string, params []interface{}) { - var conds []string - for _, whereQuery := range w { - var placeholders []interface{} - for i := range whereQuery.params { - placeholders = append(placeholders, dialect.Placeholder(len(params)+i)) - } - - conds = append(conds, fmt.Sprintf(whereQuery.cond, placeholders...)) - params = append(params, whereQuery.params...) - } - - return strings.Join(conds, " AND "), params -} - -// Where adds a new bollean condition to an existing -// WhereQueries helper. -func (w WhereQueries) Where(cond string, params ...interface{}) WhereQueries { - return append(w, WhereQuery{ - cond: cond, - params: params, - }) -} - -// WhereIf condionally adds a new boolean expression to the WhereQueries helper. -func (w WhereQueries) WhereIf(cond string, param interface{}) WhereQueries { - if param == nil || reflect.ValueOf(param).IsNil() { - return w - } - - return append(w, WhereQuery{ - cond: cond, - params: []interface{}{param}, - }) -} - -// Where adds a new bollean condition to an existing -// WhereQueries helper. -func Where(cond string, params ...interface{}) WhereQueries { - return WhereQueries{{ - cond: cond, - params: params, - }} -} - -// WhereIf condionally adds a new boolean expression to the WhereQueries helper -func WhereIf(cond string, param interface{}) WhereQueries { - if param == nil || reflect.ValueOf(param).IsNil() { - return WhereQueries{} - } - - return WhereQueries{{ - cond: cond, - params: []interface{}{param}, - }} -} - -// OrderByQuery represents the ORDER BY part of the query -type OrderByQuery struct { - fields string - desc bool -} - -// Desc is a setter function for configuring the -// ORDER BY part of the query as DESC -func (o OrderByQuery) Desc() OrderByQuery { - return OrderByQuery{ - fields: o.fields, - desc: true, - } -} - -// OrderBy is a helper for building the ORDER BY -// part of the query. -func OrderBy(fields string) OrderByQuery { - return OrderByQuery{ - fields: fields, - desc: false, - } -} - -var cachedSelectQueries = map[reflect.Type]string{} - -// Builds the select query using cached info so that its efficient -func buildSelectQuery(obj interface{}, dialect ksql.Dialect) (string, error) { - t := reflect.TypeOf(obj) - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - return "", fmt.Errorf("expected to receive a pointer to struct, but got: %T", obj) - } - - if query, found := cachedSelectQueries[t]; found { - return query, nil - } - - info := kstructs.GetTagInfo(t) - - var escapedNames []string - for i := 0; i < info.NumFields(); i++ { - name := info.ByIndex(i).Name - escapedNames = append(escapedNames, dialect.Escape(name)) - } - - query := strings.Join(escapedNames, ", ") - cachedSelectQueries[t] = query - return query, nil -} diff --git a/kbuilder/query.go b/kbuilder/query.go new file mode 100644 index 0000000..2fc4d28 --- /dev/null +++ b/kbuilder/query.go @@ -0,0 +1,218 @@ +package kbuilder + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/vingarcia/ksql" + "github.com/vingarcia/ksql/kstructs" +) + +// Query is is the struct template for building SELECT queries. +type Query struct { + // Select expects either a struct using the `ksql` tags + // or a string listing the column names using SQL syntax, + // e.g.: `id, username, address` + Select interface{} + + // From expects the FROM clause from an SQL query, e.g. `users JOIN posts USING(post_id)` + From string + + // Where expects a list of WhereQuery instances built + // by the public Where() function. + Where WhereQueries + + Limit int + Offset int + OrderBy OrderByQuery +} + +// Build is a utility function for finding the dialect based on the driver and +// then calling BuildQuery(dialect) +func (q Query) Build(driver string) (sqlQuery string, params []interface{}, _ error) { + dialect, err := ksql.GetDriverDialect(driver) + if err != nil { + return "", nil, err + } + + return q.BuildQuery(dialect) +} + +// BuildQuery implements the queryBuilder interface +func (q Query) BuildQuery(dialect ksql.Dialect) (sqlQuery string, params []interface{}, _ error) { + var b strings.Builder + + switch v := q.Select.(type) { + case string: + b.WriteString("SELECT " + v) + default: + selectQuery, err := buildSelectQuery(v, dialect) + if err != nil { + return "", nil, errors.Wrap(err, "error reading the Select field") + } + b.WriteString("SELECT " + selectQuery) + } + + b.WriteString(" FROM " + q.From) + + if len(q.Where) > 0 { + var whereQuery string + whereQuery, params = q.Where.build(dialect) + b.WriteString(" WHERE " + whereQuery) + } + + if strings.TrimSpace(q.From) == "" { + return "", nil, fmt.Errorf("the From field is mandatory for every query") + } + + if q.OrderBy.fields != "" { + b.WriteString(" ORDER BY " + q.OrderBy.fields) + if q.OrderBy.desc { + b.WriteString(" DESC") + } + } + + if q.Limit > 0 { + b.WriteString(" LIMIT " + strconv.Itoa(q.Limit)) + } + + if q.Offset > 0 { + b.WriteString(" OFFSET " + strconv.Itoa(q.Offset)) + } + + return b.String(), params, nil +} + +// WhereQuery represents a single condition in a WHERE expression. +type WhereQuery struct { + // Accepts any SQL boolean expression + // This expression may optionally contain + // string formatting directives %s and only %s. + // + // For each of these directives we expect a new param + // on the params list below. + // + // In the resulting query each %s will be properly replaced + // by placeholders according to the database driver, e.g. `$1` + // for postgres or `?` for sqlite3. + cond string + params []interface{} +} + +// WhereQueries is the helper for creating complex WHERE queries +// in a dynamic way. +type WhereQueries []WhereQuery + +func (w WhereQueries) build(dialect ksql.Dialect) (query string, params []interface{}) { + var conds []string + for _, whereQuery := range w { + var placeholders []interface{} + for i := range whereQuery.params { + placeholders = append(placeholders, dialect.Placeholder(len(params)+i)) + } + + conds = append(conds, fmt.Sprintf(whereQuery.cond, placeholders...)) + params = append(params, whereQuery.params...) + } + + return strings.Join(conds, " AND "), params +} + +// Where adds a new bollean condition to an existing +// WhereQueries helper. +func (w WhereQueries) Where(cond string, params ...interface{}) WhereQueries { + return append(w, WhereQuery{ + cond: cond, + params: params, + }) +} + +// WhereIf condionally adds a new boolean expression to the WhereQueries helper. +func (w WhereQueries) WhereIf(cond string, param interface{}) WhereQueries { + if param == nil || reflect.ValueOf(param).IsNil() { + return w + } + + return append(w, WhereQuery{ + cond: cond, + params: []interface{}{param}, + }) +} + +// Where adds a new bollean condition to an existing +// WhereQueries helper. +func Where(cond string, params ...interface{}) WhereQueries { + return WhereQueries{{ + cond: cond, + params: params, + }} +} + +// WhereIf condionally adds a new boolean expression to the WhereQueries helper +func WhereIf(cond string, param interface{}) WhereQueries { + if param == nil || reflect.ValueOf(param).IsNil() { + return WhereQueries{} + } + + return WhereQueries{{ + cond: cond, + params: []interface{}{param}, + }} +} + +// OrderByQuery represents the ORDER BY part of the query +type OrderByQuery struct { + fields string + desc bool +} + +// Desc is a setter function for configuring the +// ORDER BY part of the query as DESC +func (o OrderByQuery) Desc() OrderByQuery { + return OrderByQuery{ + fields: o.fields, + desc: true, + } +} + +// OrderBy is a helper for building the ORDER BY +// part of the query. +func OrderBy(fields string) OrderByQuery { + return OrderByQuery{ + fields: fields, + desc: false, + } +} + +var cachedSelectQueries = map[reflect.Type]string{} + +// Builds the select query using cached info so that its efficient +func buildSelectQuery(obj interface{}, dialect ksql.Dialect) (string, error) { + t := reflect.TypeOf(obj) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return "", fmt.Errorf("expected to receive a pointer to struct, but got: %T", obj) + } + + if query, found := cachedSelectQueries[t]; found { + return query, nil + } + + info := kstructs.GetTagInfo(t) + + var escapedNames []string + for i := 0; i < info.NumFields(); i++ { + name := info.ByIndex(i).Name + escapedNames = append(escapedNames, dialect.Escape(name)) + } + + query := strings.Join(escapedNames, ", ") + cachedSelectQueries[t] = query + return query, nil +} diff --git a/kbuilder/kbuilder_test.go b/kbuilder/query_test.go similarity index 98% rename from kbuilder/kbuilder_test.go rename to kbuilder/query_test.go index bbf4503..d737d3c 100644 --- a/kbuilder/kbuilder_test.go +++ b/kbuilder/query_test.go @@ -11,13 +11,12 @@ import ( type User struct { Name string `ksql:"name"` - Age string `ksql:"age"` + Age int `ksql:"age"` } var nullField *int -func TestBuilder(t *testing.T) { - +func TestSelectQuery(t *testing.T) { tests := []struct { desc string query kbuilder.Query From 61f21409a097501e5cb7c2031207ce926bc7edb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Thu, 16 Sep 2021 12:22:31 -0300 Subject: [PATCH 09/10] Improve panic message when ksql.Mock methods are called but unimplemented --- mocks.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/mocks.go b/mocks.go index 97911cd..e8b251a 100644 --- a/mocks.go +++ b/mocks.go @@ -1,6 +1,9 @@ package ksql -import "context" +import ( + "context" + "fmt" +) var _ Provider = Mock{} @@ -20,40 +23,64 @@ type Mock struct { // Insert ... func (m Mock) Insert(ctx context.Context, table Table, record interface{}) error { + if m.InsertFn == nil { + panic(fmt.Errorf("Mock.Insert(ctx, %v, %v) called but the ksql.Mock.InsertFn() is not set", table, record)) + } return m.InsertFn(ctx, table, record) } // Update ... func (m Mock) Update(ctx context.Context, table Table, record interface{}) error { + if m.UpdateFn == nil { + panic(fmt.Errorf("Mock.Update(ctx, %v, %v) called but the ksql.Mock.UpdateFn() is not set", table, record)) + } return m.UpdateFn(ctx, table, record) } // Delete ... func (m Mock) Delete(ctx context.Context, table Table, ids ...interface{}) error { + if m.DeleteFn == nil { + panic(fmt.Errorf("Mock.Delete(ctx, %v, %v) called but the ksql.Mock.DeleteFn() is not set", table, ids)) + } return m.DeleteFn(ctx, table, ids...) } // Query ... func (m Mock) Query(ctx context.Context, records interface{}, query string, params ...interface{}) error { + if m.QueryFn == nil { + panic(fmt.Errorf("Mock.Query(ctx, %v, %s, %v) called but the ksql.Mock.QueryFn() is not set", records, query, params)) + } return m.QueryFn(ctx, records, query, params...) } // QueryOne ... func (m Mock) QueryOne(ctx context.Context, record interface{}, query string, params ...interface{}) error { + if m.QueryOneFn == nil { + panic(fmt.Errorf("Mock.QueryOne(ctx, %v, %s, %v) called but the ksql.Mock.QueryOneFn() is not set", record, query, params)) + } return m.QueryOneFn(ctx, record, query, params...) } // QueryChunks ... func (m Mock) QueryChunks(ctx context.Context, parser ChunkParser) error { + if m.QueryChunksFn == nil { + panic(fmt.Errorf("Mock.QueryChunks(ctx, %v) called but the ksql.Mock.QueryChunksFn() is not set", parser)) + } return m.QueryChunksFn(ctx, parser) } // Exec ... func (m Mock) Exec(ctx context.Context, query string, params ...interface{}) error { + if m.ExecFn == nil { + panic(fmt.Errorf("Mock.Exec(ctx, %s, %v) called but the ksql.Mock.ExecFn() is not set", query, params)) + } return m.ExecFn(ctx, query, params...) } // Transaction ... func (m Mock) Transaction(ctx context.Context, fn func(db Provider) error) error { + if m.TransactionFn == nil { + return fn(m) + } return m.TransactionFn(ctx, fn) } From a503c218ba2e0fdd517dbffbd15942ce0b4e7857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Fri, 17 Sep 2021 22:06:06 -0300 Subject: [PATCH 10/10] Improve kbuilder README --- kbuilder/README.md | 6 +++++- kbuilder/insert_test.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/kbuilder/README.md b/kbuilder/README.md index 24f5361..de6385d 100644 --- a/kbuilder/README.md +++ b/kbuilder/README.md @@ -3,8 +3,12 @@ This is the Keep It Stupid Simple query builder created to work either in conjunction or separated from the ksql package. +This package was started after ksql and while the ksql is already +in a usable state I still don't recommend using this one since this +being actively implemented and might change without further warning. + ## TODO List -- Support Insert and Update operations +- Add support to Update and Delete operations - Improve support to JOINs by adding the `tablename` tag to the structs - Add error check for when the Select, Insert and Update attrs are all empty diff --git a/kbuilder/insert_test.go b/kbuilder/insert_test.go index 745e8bf..49f55a5 100644 --- a/kbuilder/insert_test.go +++ b/kbuilder/insert_test.go @@ -67,7 +67,7 @@ func TestInsertQuery(t *testing.T) { expectedErr: true, }, { - desc: "should report error Data contains an empty list", + desc: "should report error if `Data` contains an empty list", query: kbuilder.Insert{ Into: "users", Data: []User{},