From f14642a736b6f44ed86d432e37ade74dc98001f6 Mon Sep 17 00:00:00 2001 From: what Date: Sat, 22 Mar 2025 23:02:05 +0800 Subject: [PATCH] fork github.com/doug-martin --- .gitignore | 4 + README.md | 2 + database.go | 627 ++++++ database_example_test.go | 146 ++ database_test.go | 759 ++++++++ db.go | 74 + db_example_test.go | 255 +++ db_test.go | 61 + delete_dataset.go | 246 +++ delete_dataset_example_test.go | 312 +++ delete_dataset_test.go | 511 +++++ dialect/mysql/mysql.go | 87 + dialect/mysql/mysql_dialect_test.go | 143 ++ dialect/mysql/mysql_test.go | 509 +++++ dialect/postgres/postgres.go | 14 + dialect/postgres/postgres_test.go | 499 +++++ dialect/sqlite3/sqlite3.go | 76 + dialect/sqlite3/sqlite3_dialect_test.go | 158 ++ dialect/sqlite3/sqlite3_test.go | 469 +++++ dialect/sqlserver/sqlserver.go | 99 + dialect/sqlserver/sqlserver_dialect_test.go | 60 + dialect/sqlserver/sqlserver_test.go | 485 +++++ engine/engine.go | 89 + engine/engine_config.go | 399 ++++ engine/engine_config_test.go | 383 ++++ exec/query_executor.go | 253 +++ exec/query_executor_internal_test.go | 1247 ++++++++++++ exec/query_factory.go | 36 + exec/scanner.go | 168 ++ exec/scanner_internal_test.go | 69 + exp/alias.go | 60 + exp/alias_test.go | 69 + exp/bitwise.go | 89 + exp/bitwise_test.go | 84 + exp/bool.go | 185 ++ exp/case.go | 78 + exp/case_test.go | 99 + exp/cast.go | 57 + exp/cast_test.go | 81 + exp/col.go | 85 + exp/compound.go | 19 + exp/conflict.go | 89 + exp/cte.go | 23 + exp/delete_clauses.go | 177 ++ exp/delete_clauses_test.go | 256 +++ exp/exp.go | 734 +++++++ exp/exp_list.go | 67 + exp/exp_map.go | 164 ++ exp/exp_map_test.go | 320 ++++ exp/func.go | 89 + exp/func_test.go | 81 + exp/ident.go | 216 +++ exp/ident_test.go | 247 +++ exp/insert.go | 163 ++ exp/insert_clauses.go | 205 ++ exp/insert_clauses_test.go | 242 +++ exp/insert_test.go | 383 ++++ exp/join.go | 140 ++ exp/lateral.go | 25 + exp/lateral_test.go | 36 + exp/literal.go | 85 + exp/literal_test.go | 87 + exp/lock.go | 48 + exp/order.go | 52 + exp/range.go | 62 + exp/record.go | 68 + exp/select_clauses.go | 379 ++++ exp/select_clauses_test.go | 578 ++++++ exp/truncate.go | 11 + exp/truncate_clauses.go | 50 + exp/truncate_clauses_test.go | 68 + exp/update.go | 73 + exp/update_clauses.go | 216 +++ exp/update_clauses_test.go | 298 +++ exp/update_test.go | 182 ++ exp/window.go | 90 + exp/window_func.go | 124 ++ exp/window_func_test.go | 107 ++ exp/window_test.go | 84 + expressions.go | 333 ++++ expressions_example_test.go | 1902 +++++++++++++++++++ expressions_test.go | 190 ++ go.mod | 24 + go.sum | 63 + insert_dataset.go | 273 +++ insert_dataset_example_test.go | 779 ++++++++ insert_dataset_test.go | 618 ++++++ internal/errors/error.go | 19 + internal/sb/sql_builder.go | 101 + internal/tag/tags.go | 51 + internal/util/column_map.go | 135 ++ internal/util/reflect.go | 217 +++ internal/util/reflect_test.go | 1346 +++++++++++++ internal/util/value_slice.go | 33 + issues_test.go | 496 +++++ mocks/SQLDialect.go | 52 + prepared.go | 48 + select_dataset.go | 701 +++++++ select_dataset_example_test.go | 1685 ++++++++++++++++ select_dataset_test.go | 1623 ++++++++++++++++ sql_dialect.go | 103 + sql_dialect_example_test.go | 23 + sql_dialect_internal_test.go | 91 + sqlgen/base_test.go | 39 + sqlgen/common_sql_generator.go | 155 ++ sqlgen/common_sql_generator_test.go | 339 ++++ sqlgen/delete_sql_generator.go | 72 + sqlgen/delete_sql_generator_test.go | 233 +++ sqlgen/expression_sql_generator.go | 736 +++++++ sqlgen/expression_sql_generator_test.go | 1670 ++++++++++++++++ sqlgen/insert_sql_generator.go | 203 ++ sqlgen/insert_sql_generator_test.go | 468 +++++ sqlgen/mocks/DeleteSQLGenerator.go | 31 + sqlgen/mocks/InsertSQLGenerator.go | 31 + sqlgen/mocks/SelectSQLGenerator.go | 31 + sqlgen/mocks/TruncateSQLGenerator.go | 31 + sqlgen/mocks/UpdateSQLGenerator.go | 31 + sqlgen/select_sql_generator.go | 266 +++ sqlgen/select_sql_generator_test.go | 615 ++++++ sqlgen/sql_dialect_options.go | 607 ++++++ sqlgen/sql_dialect_options_test.go | 49 + sqlgen/sqlgen.go | 15 + sqlgen/truncate_sql_generator.go | 64 + sqlgen/truncate_sql_generator_test.go | 121 ++ sqlgen/update_sql_generator.go | 112 ++ sqlgen/update_sql_generator_test.go | 259 +++ truncate_dataset.go | 171 ++ truncate_dataset_test.go | 340 ++++ update_dataset.go | 246 +++ update_dataset_example_test.go | 722 +++++++ update_dataset_test.go | 527 +++++ 131 files changed, 34555 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 database.go create mode 100644 database_example_test.go create mode 100644 database_test.go create mode 100644 db.go create mode 100644 db_example_test.go create mode 100644 db_test.go create mode 100644 delete_dataset.go create mode 100644 delete_dataset_example_test.go create mode 100644 delete_dataset_test.go create mode 100644 dialect/mysql/mysql.go create mode 100644 dialect/mysql/mysql_dialect_test.go create mode 100644 dialect/mysql/mysql_test.go create mode 100644 dialect/postgres/postgres.go create mode 100644 dialect/postgres/postgres_test.go create mode 100644 dialect/sqlite3/sqlite3.go create mode 100644 dialect/sqlite3/sqlite3_dialect_test.go create mode 100644 dialect/sqlite3/sqlite3_test.go create mode 100644 dialect/sqlserver/sqlserver.go create mode 100644 dialect/sqlserver/sqlserver_dialect_test.go create mode 100644 dialect/sqlserver/sqlserver_test.go create mode 100644 engine/engine.go create mode 100644 engine/engine_config.go create mode 100644 engine/engine_config_test.go create mode 100644 exec/query_executor.go create mode 100644 exec/query_executor_internal_test.go create mode 100644 exec/query_factory.go create mode 100644 exec/scanner.go create mode 100644 exec/scanner_internal_test.go create mode 100644 exp/alias.go create mode 100644 exp/alias_test.go create mode 100644 exp/bitwise.go create mode 100644 exp/bitwise_test.go create mode 100644 exp/bool.go create mode 100644 exp/case.go create mode 100644 exp/case_test.go create mode 100644 exp/cast.go create mode 100644 exp/cast_test.go create mode 100644 exp/col.go create mode 100644 exp/compound.go create mode 100644 exp/conflict.go create mode 100644 exp/cte.go create mode 100644 exp/delete_clauses.go create mode 100644 exp/delete_clauses_test.go create mode 100644 exp/exp.go create mode 100644 exp/exp_list.go create mode 100644 exp/exp_map.go create mode 100644 exp/exp_map_test.go create mode 100644 exp/func.go create mode 100644 exp/func_test.go create mode 100644 exp/ident.go create mode 100644 exp/ident_test.go create mode 100644 exp/insert.go create mode 100644 exp/insert_clauses.go create mode 100644 exp/insert_clauses_test.go create mode 100644 exp/insert_test.go create mode 100644 exp/join.go create mode 100644 exp/lateral.go create mode 100644 exp/lateral_test.go create mode 100644 exp/literal.go create mode 100644 exp/literal_test.go create mode 100644 exp/lock.go create mode 100644 exp/order.go create mode 100644 exp/range.go create mode 100644 exp/record.go create mode 100644 exp/select_clauses.go create mode 100644 exp/select_clauses_test.go create mode 100644 exp/truncate.go create mode 100644 exp/truncate_clauses.go create mode 100644 exp/truncate_clauses_test.go create mode 100644 exp/update.go create mode 100644 exp/update_clauses.go create mode 100644 exp/update_clauses_test.go create mode 100644 exp/update_test.go create mode 100644 exp/window.go create mode 100644 exp/window_func.go create mode 100644 exp/window_func_test.go create mode 100644 exp/window_test.go create mode 100644 expressions.go create mode 100644 expressions_example_test.go create mode 100644 expressions_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 insert_dataset.go create mode 100644 insert_dataset_example_test.go create mode 100644 insert_dataset_test.go create mode 100644 internal/errors/error.go create mode 100644 internal/sb/sql_builder.go create mode 100644 internal/tag/tags.go create mode 100644 internal/util/column_map.go create mode 100644 internal/util/reflect.go create mode 100644 internal/util/reflect_test.go create mode 100644 internal/util/value_slice.go create mode 100644 issues_test.go create mode 100644 mocks/SQLDialect.go create mode 100644 prepared.go create mode 100644 select_dataset.go create mode 100644 select_dataset_example_test.go create mode 100644 select_dataset_test.go create mode 100644 sql_dialect.go create mode 100644 sql_dialect_example_test.go create mode 100644 sql_dialect_internal_test.go create mode 100644 sqlgen/base_test.go create mode 100644 sqlgen/common_sql_generator.go create mode 100644 sqlgen/common_sql_generator_test.go create mode 100644 sqlgen/delete_sql_generator.go create mode 100644 sqlgen/delete_sql_generator_test.go create mode 100644 sqlgen/expression_sql_generator.go create mode 100644 sqlgen/expression_sql_generator_test.go create mode 100644 sqlgen/insert_sql_generator.go create mode 100644 sqlgen/insert_sql_generator_test.go create mode 100644 sqlgen/mocks/DeleteSQLGenerator.go create mode 100644 sqlgen/mocks/InsertSQLGenerator.go create mode 100644 sqlgen/mocks/SelectSQLGenerator.go create mode 100644 sqlgen/mocks/TruncateSQLGenerator.go create mode 100644 sqlgen/mocks/UpdateSQLGenerator.go create mode 100644 sqlgen/select_sql_generator.go create mode 100644 sqlgen/select_sql_generator_test.go create mode 100644 sqlgen/sql_dialect_options.go create mode 100644 sqlgen/sql_dialect_options_test.go create mode 100644 sqlgen/sqlgen.go create mode 100644 sqlgen/truncate_sql_generator.go create mode 100644 sqlgen/truncate_sql_generator_test.go create mode 100644 sqlgen/update_sql_generator.go create mode 100644 sqlgen/update_sql_generator_test.go create mode 100644 truncate_dataset.go create mode 100644 truncate_dataset_test.go create mode 100644 update_dataset.go create mode 100644 update_dataset_example_test.go create mode 100644 update_dataset_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d884bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +src +*.iml +coverage.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e9c2e0 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## Credits +https://github.com/doug-martin/goqu \ No newline at end of file diff --git a/database.go b/database.go new file mode 100644 index 0000000..ed9d832 --- /dev/null +++ b/database.go @@ -0,0 +1,627 @@ +package db + +import ( + "context" + "database/sql" + "sync" + + "git.fsdpf.net/go/db/v2/exec" +) + +type ( + Logger interface { + Printf(format string, v ...interface{}) + } + // Interface for sql.DB, an interface is used so you can use with other + // libraries such as sqlx instead of the native sql.DB + SQLDatabase interface { + Begin() (*sql.Tx, error) + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + } + // This struct is the wrapper for a Db. The struct delegates most calls to either an Exec instance or to the Db + // passed into the constructor. + Database struct { + logger Logger + dialect string + //nolint:stylecheck // keep for backwards compatibility + Db SQLDatabase + qf exec.QueryFactory + qfOnce sync.Once + } +) + +func newDatabase(dialect string, db SQLDatabase) *Database { + return &Database{ + logger: nil, + dialect: dialect, + Db: db, + qf: nil, + qfOnce: sync.Once{}, + } +} + +// returns this databases dialect +func (d *Database) Dialect() string { + return d.dialect +} + +// Starts a new Transaction. +func (d *Database) Begin() (*TxDatabase, error) { + sqlTx, err := d.Db.Begin() + if err != nil { + return nil, err + } + tx := NewTx(d.dialect, sqlTx) + tx.Logger(d.logger) + return tx, nil +} + +// Starts a new Transaction. See sql.DB#BeginTx for option description +func (d *Database) BeginTx(ctx context.Context, opts *sql.TxOptions) (*TxDatabase, error) { + sqlTx, err := d.Db.BeginTx(ctx, opts) + if err != nil { + return nil, err + } + tx := NewTx(d.dialect, sqlTx) + tx.Logger(d.logger) + return tx, nil +} + +// WithTx starts a new transaction and executes it in Wrap method +func (d *Database) WithTx(fn func(*TxDatabase) error) error { + tx, err := d.Begin() + if err != nil { + return err + } + return tx.Wrap(func() error { return fn(tx) }) +} + +// Creates a new Dataset that uses the correct adapter and supports queries. +// +// var ids []uint32 +// if err := db.From("items").Where(db.I("id").Gt(10)).Pluck("id", &ids); err != nil { +// panic(err.Error()) +// } +// fmt.Printf("%+v", ids) +// +// from...: Sources for you dataset, could be table names (strings), a db.Literal or another db.Dataset +func (d *Database) From(from ...interface{}) *SelectDataset { + return newDataset(d.dialect, d.queryFactory()).From(from...) +} + +func (d *Database) Select(cols ...interface{}) *SelectDataset { + return newDataset(d.dialect, d.queryFactory()).Select(cols...) +} + +func (d *Database) Update(table interface{}) *UpdateDataset { + return newUpdateDataset(d.dialect, d.queryFactory()).Table(table) +} + +func (d *Database) Insert(table interface{}) *InsertDataset { + return newInsertDataset(d.dialect, d.queryFactory()).Into(table) +} + +func (d *Database) Delete(table interface{}) *DeleteDataset { + return newDeleteDataset(d.dialect, d.queryFactory()).From(table) +} + +func (d *Database) Truncate(table ...interface{}) *TruncateDataset { + return newTruncateDataset(d.dialect, d.queryFactory()).Table(table...) +} + +// Sets the logger for to use when logging queries +func (d *Database) Logger(logger Logger) { + d.logger = logger +} + +// Logs a given operation with the specified sql and arguments +func (d *Database) Trace(op, sqlString string, args ...interface{}) { + if d.logger != nil { + if sqlString != "" { + if len(args) != 0 { + d.logger.Printf("[db] %s [query:=`%s` args:=%+v]", op, sqlString, args) + } else { + d.logger.Printf("[db] %s [query:=`%s`]", op, sqlString) + } + } else { + d.logger.Printf("[db] %s", op) + } + } +} + +// Uses the db to Execute the query with arguments and return the sql.Result +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) { + return d.ExecContext(context.Background(), query, args...) +} + +// Uses the db to Execute the query with arguments and return the sql.Result +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + d.Trace("EXEC", query, args...) + return d.Db.ExecContext(ctx, query, args...) +} + +// Can be used to prepare a query. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, args, err := db.From("items").Where(db.I("id").Gt(10)).ToSQL(true) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// stmt, err := db.Prepare(sql) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer stmt.Close() +// rows, err := stmt.Query(args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer rows.Close() +// for rows.Next(){ +// //scan your rows +// } +// if rows.Err() != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// +// query: The SQL statement to prepare. +func (d *Database) Prepare(query string) (*sql.Stmt, error) { + return d.PrepareContext(context.Background(), query) +} + +// Can be used to prepare a query. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, args, err := db.From("items").Where(db.I("id").Gt(10)).ToSQL(true) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// stmt, err := db.Prepare(sql) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer stmt.Close() +// rows, err := stmt.QueryContext(ctx, args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer rows.Close() +// for rows.Next(){ +// //scan your rows +// } +// if rows.Err() != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// +// query: The SQL statement to prepare. +func (d *Database) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + d.Trace("PREPARE", query) + return d.Db.PrepareContext(ctx, query) +} + +// Used to query for multiple rows. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, err := db.From("items").Where(db.I("id").Gt(10)).ToSQL() +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// rows, err := stmt.Query(args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer rows.Close() +// for rows.Next(){ +// //scan your rows +// } +// if rows.Err() != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) { + return d.QueryContext(context.Background(), query, args...) +} + +// Used to query for multiple rows. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, err := db.From("items").Where(db.I("id").Gt(10)).ToSQL() +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// rows, err := stmt.QueryContext(ctx, args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// defer rows.Close() +// for rows.Next(){ +// //scan your rows +// } +// if rows.Err() != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + d.Trace("QUERY", query, args...) + return d.Db.QueryContext(ctx, query, args...) +} + +// Used to query for a single row. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, err := db.From("items").Where(db.I("id").Gt(10)).Limit(1).ToSQL() +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// rows, err := stmt.QueryRow(args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// //scan your row +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row { + return d.QueryRowContext(context.Background(), query, args...) +} + +// Used to query for a single row. +// +// You can use this in tandem with a dataset by doing the following. +// +// sql, err := db.From("items").Where(db.I("id").Gt(10)).Limit(1).ToSQL() +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// rows, err := stmt.QueryRowContext(ctx, args) +// if err != nil{ +// panic(err.Error()) //you could gracefully handle the error also +// } +// //scan your row +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + d.Trace("QUERY ROW", query, args...) + return d.Db.QueryRowContext(ctx, query, args...) +} + +func (d *Database) queryFactory() exec.QueryFactory { + d.qfOnce.Do(func() { + d.qf = exec.NewQueryFactory(d) + }) + return d.qf +} + +// Queries the database using the supplied query, and args and uses CrudExec.ScanStructs to scan the results into a +// slice of structs +// +// i: A pointer to a slice of structs +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanStructs(i interface{}, query string, args ...interface{}) error { + return d.ScanStructsContext(context.Background(), i, query, args...) +} + +// Queries the database using the supplied context, query, and args and uses CrudExec.ScanStructsContext to scan the +// results into a slice of structs +// +// i: A pointer to a slice of structs +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanStructsContext(ctx context.Context, i interface{}, query string, args ...interface{}) error { + return d.queryFactory().FromSQL(query, args...).ScanStructsContext(ctx, i) +} + +// Queries the database using the supplied query, and args and uses CrudExec.ScanStruct to scan the results into a +// struct +// +// i: A pointer to a struct +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanStruct(i interface{}, query string, args ...interface{}) (bool, error) { + return d.ScanStructContext(context.Background(), i, query, args...) +} + +// Queries the database using the supplied context, query, and args and uses CrudExec.ScanStructContext to scan the +// results into a struct +// +// i: A pointer to a struct +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanStructContext(ctx context.Context, i interface{}, query string, args ...interface{}) (bool, error) { + return d.queryFactory().FromSQL(query, args...).ScanStructContext(ctx, i) +} + +// Queries the database using the supplied query, and args and uses CrudExec.ScanVals to scan the results into a slice +// of primitive values +// +// i: A pointer to a slice of primitive values +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanVals(i interface{}, query string, args ...interface{}) error { + return d.ScanValsContext(context.Background(), i, query, args...) +} + +// Queries the database using the supplied context, query, and args and uses CrudExec.ScanValsContext to scan the +// results into a slice of primitive values +// +// i: A pointer to a slice of primitive values +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanValsContext(ctx context.Context, i interface{}, query string, args ...interface{}) error { + return d.queryFactory().FromSQL(query, args...).ScanValsContext(ctx, i) +} + +// Queries the database using the supplied query, and args and uses CrudExec.ScanVal to scan the results into a +// primitive value +// +// i: A pointer to a primitive value +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanVal(i interface{}, query string, args ...interface{}) (bool, error) { + return d.ScanValContext(context.Background(), i, query, args...) +} + +// Queries the database using the supplied context, query, and args and uses CrudExec.ScanValContext to scan the +// results into a primitive value +// +// i: A pointer to a primitive value +// +// query: The SQL to execute +// +// args...: for any placeholder parameters in the query +func (d *Database) ScanValContext(ctx context.Context, i interface{}, query string, args ...interface{}) (bool, error) { + return d.queryFactory().FromSQL(query, args...).ScanValContext(ctx, i) +} + +// A wrapper around a sql.Tx and works the same way as Database +type ( + // Interface for sql.Tx, an interface is used so you can use with other + // libraries such as sqlx instead of the native sql.DB + SQLTx interface { + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row + Commit() error + Rollback() error + } + TxDatabase struct { + logger Logger + dialect string + Tx SQLTx + qf exec.QueryFactory + qfOnce sync.Once + } +) + +// Creates a new TxDatabase +func NewTx(dialect string, tx SQLTx) *TxDatabase { + return &TxDatabase{dialect: dialect, Tx: tx} +} + +// returns this databases dialect +func (td *TxDatabase) Dialect() string { + return td.dialect +} + +// Creates a new Dataset for querying a Database. +func (td *TxDatabase) From(cols ...interface{}) *SelectDataset { + return newDataset(td.dialect, td.queryFactory()).From(cols...) +} + +func (td *TxDatabase) Select(cols ...interface{}) *SelectDataset { + return newDataset(td.dialect, td.queryFactory()).Select(cols...) +} + +func (td *TxDatabase) Update(table interface{}) *UpdateDataset { + return newUpdateDataset(td.dialect, td.queryFactory()).Table(table) +} + +func (td *TxDatabase) Insert(table interface{}) *InsertDataset { + return newInsertDataset(td.dialect, td.queryFactory()).Into(table) +} + +func (td *TxDatabase) Delete(table interface{}) *DeleteDataset { + return newDeleteDataset(td.dialect, td.queryFactory()).From(table) +} + +func (td *TxDatabase) Truncate(table ...interface{}) *TruncateDataset { + return newTruncateDataset(td.dialect, td.queryFactory()).Table(table...) +} + +// Sets the logger +func (td *TxDatabase) Logger(logger Logger) { + td.logger = logger +} + +func (td *TxDatabase) Trace(op, sqlString string, args ...interface{}) { + if td.logger != nil { + if sqlString != "" { + if len(args) != 0 { + td.logger.Printf("[db - transaction] %s [query:=`%s` args:=%+v] ", op, sqlString, args) + } else { + td.logger.Printf("[db - transaction] %s [query:=`%s`] ", op, sqlString) + } + } else { + td.logger.Printf("[db - transaction] %s", op) + } + } +} + +// See Database#Exec +func (td *TxDatabase) Exec(query string, args ...interface{}) (sql.Result, error) { + return td.ExecContext(context.Background(), query, args...) +} + +// See Database#ExecContext +func (td *TxDatabase) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + td.Trace("EXEC", query, args...) + return td.Tx.ExecContext(ctx, query, args...) +} + +// See Database#Prepare +func (td *TxDatabase) Prepare(query string) (*sql.Stmt, error) { + return td.PrepareContext(context.Background(), query) +} + +// See Database#PrepareContext +func (td *TxDatabase) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + td.Trace("PREPARE", query) + return td.Tx.PrepareContext(ctx, query) +} + +// See Database#Query +func (td *TxDatabase) Query(query string, args ...interface{}) (*sql.Rows, error) { + return td.QueryContext(context.Background(), query, args...) +} + +// See Database#QueryContext +func (td *TxDatabase) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + td.Trace("QUERY", query, args...) + return td.Tx.QueryContext(ctx, query, args...) +} + +// See Database#QueryRow +func (td *TxDatabase) QueryRow(query string, args ...interface{}) *sql.Row { + return td.QueryRowContext(context.Background(), query, args...) +} + +// See Database#QueryRowContext +func (td *TxDatabase) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + td.Trace("QUERY ROW", query, args...) + return td.Tx.QueryRowContext(ctx, query, args...) +} + +func (td *TxDatabase) queryFactory() exec.QueryFactory { + td.qfOnce.Do(func() { + td.qf = exec.NewQueryFactory(td) + }) + return td.qf +} + +// See Database#ScanStructs +func (td *TxDatabase) ScanStructs(i interface{}, query string, args ...interface{}) error { + return td.ScanStructsContext(context.Background(), i, query, args...) +} + +// See Database#ScanStructsContext +func (td *TxDatabase) ScanStructsContext(ctx context.Context, i interface{}, query string, args ...interface{}) error { + return td.queryFactory().FromSQL(query, args...).ScanStructsContext(ctx, i) +} + +// See Database#ScanStruct +func (td *TxDatabase) ScanStruct(i interface{}, query string, args ...interface{}) (bool, error) { + return td.ScanStructContext(context.Background(), i, query, args...) +} + +// See Database#ScanStructContext +func (td *TxDatabase) ScanStructContext(ctx context.Context, i interface{}, query string, args ...interface{}) (bool, error) { + return td.queryFactory().FromSQL(query, args...).ScanStructContext(ctx, i) +} + +// See Database#ScanVals +func (td *TxDatabase) ScanVals(i interface{}, query string, args ...interface{}) error { + return td.ScanValsContext(context.Background(), i, query, args...) +} + +// See Database#ScanValsContext +func (td *TxDatabase) ScanValsContext(ctx context.Context, i interface{}, query string, args ...interface{}) error { + return td.queryFactory().FromSQL(query, args...).ScanValsContext(ctx, i) +} + +// See Database#ScanVal +func (td *TxDatabase) ScanVal(i interface{}, query string, args ...interface{}) (bool, error) { + return td.ScanValContext(context.Background(), i, query, args...) +} + +// See Database#ScanValContext +func (td *TxDatabase) ScanValContext(ctx context.Context, i interface{}, query string, args ...interface{}) (bool, error) { + return td.queryFactory().FromSQL(query, args...).ScanValContext(ctx, i) +} + +// COMMIT the transaction +func (td *TxDatabase) Commit() error { + td.Trace("COMMIT", "") + return td.Tx.Commit() +} + +// ROLLBACK the transaction +func (td *TxDatabase) Rollback() error { + td.Trace("ROLLBACK", "") + return td.Tx.Rollback() +} + +// A helper method that will automatically COMMIT or ROLLBACK once the supplied function is done executing +// +// tx, err := db.Begin() +// if err != nil{ +// panic(err.Error()) // you could gracefully handle the error also +// } +// if err := tx.Wrap(func() error{ +// if _, err := tx.From("test").Insert(Record{"a":1, "b": "b"}).Exec(){ +// // this error will be the return error from the Wrap call +// return err +// } +// return nil +// }); err != nil{ +// panic(err.Error()) // you could gracefully handle the error also +// } +func (td *TxDatabase) Wrap(fn func() error) (err error) { + defer func() { + if p := recover(); p != nil { + _ = td.Rollback() + panic(p) + } + if err != nil { + if rollbackErr := td.Rollback(); rollbackErr != nil { + err = rollbackErr + } + } else { + if commitErr := td.Commit(); commitErr != nil { + err = commitErr + } + } + }() + return fn() +} diff --git a/database_example_test.go b/database_example_test.go new file mode 100644 index 0000000..06254e9 --- /dev/null +++ b/database_example_test.go @@ -0,0 +1,146 @@ +package db_test + +import ( + "context" + "database/sql" + "fmt" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" +) + +func ExampleDatabase_Begin() { + db := getDB() + + tx, err := db.Begin() + if err != nil { + fmt.Println("Error starting transaction", err.Error()) + } + + // use tx.From to get a dataset that will execute within this transaction + update := tx.Update("test_user"). + Set(dbv2.Record{"last_name": "Ucon"}). + Where(dbv2.Ex{"last_name": "Yukon"}). + Returning("id"). + Executor() + + var ids []int64 + if err := update.ScanVals(&ids); err != nil { + if rErr := tx.Rollback(); rErr != nil { + fmt.Println("An error occurred while issuing ROLLBACK\n\t", rErr.Error()) + } else { + fmt.Println("An error occurred while updating users ROLLBACK transaction\n\t", err.Error()) + } + return + } + if err := tx.Commit(); err != nil { + fmt.Println("An error occurred while issuing COMMIT\n\t", err.Error()) + } else { + fmt.Printf("Updated users in transaction [ids:=%+v]", ids) + } + // Output: + // Updated users in transaction [ids:=[1 2 3]] +} + +func ExampleDatabase_BeginTx() { + db := getDB() + + ctx := context.Background() + tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) + if err != nil { + fmt.Println("Error starting transaction", err.Error()) + } + + // use tx.From to get a dataset that will execute within this transaction + update := tx.Update("test_user"). + Set(dbv2.Record{"last_name": "Ucon"}). + Where(dbv2.Ex{"last_name": "Yukon"}). + Returning("id"). + Executor() + + var ids []int64 + if err := update.ScanVals(&ids); err != nil { + if rErr := tx.Rollback(); rErr != nil { + fmt.Println("An error occurred while issuing ROLLBACK\n\t", rErr.Error()) + } else { + fmt.Println("An error occurred while updating users ROLLBACK transaction\n\t", err.Error()) + } + return + } + if err := tx.Commit(); err != nil { + fmt.Println("An error occurred while issuing COMMIT\n\t", err.Error()) + } else { + fmt.Printf("Updated users in transaction [ids:=%+v]", ids) + } + // Output: + // Updated users in transaction [ids:=[1 2 3]] +} + +func ExampleDatabase_WithTx() { + db := getDB() + var ids []int64 + if err := db.WithTx(func(tx *dbv2.TxDatabase) error { + // use tx.From to get a dataset that will execute within this transaction + update := tx.Update("test_user"). + Where(dbv2.Ex{"last_name": "Yukon"}). + Returning("id"). + Set(dbv2.Record{"last_name": "Ucon"}). + Executor() + + return update.ScanVals(&ids) + }); err != nil { + fmt.Println("An error occurred in transaction\n\t", err.Error()) + } else { + fmt.Printf("Updated users in transaction [ids:=%+v]", ids) + } + // Output: + // Updated users in transaction [ids:=[1 2 3]] +} + +func ExampleDatabase_Dialect() { + db := getDB() + + fmt.Println(db.Dialect()) + + // Output: + // postgres +} + +func ExampleDatabase_Exec() { + db := getDB() + + _, err := db.Exec(`DROP TABLE "user_role"; DROP TABLE "test_user"`) + if err != nil { + fmt.Println("Error occurred while dropping tables", err.Error()) + } + fmt.Println("Dropped tables user_role and test_user") + // Output: + // Dropped tables user_role and test_user +} + +func ExampleDatabase_ExecContext() { + db := getDB() + d := time.Now().Add(50 * time.Millisecond) + ctx, cancel := context.WithDeadline(context.Background(), d) + defer cancel() + _, err := db.ExecContext(ctx, `DROP TABLE "user_role"; DROP TABLE "test_user"`) + if err != nil { + fmt.Println("Error occurred while dropping tables", err.Error()) + } + fmt.Println("Dropped tables user_role and test_user") + // Output: + // Dropped tables user_role and test_user +} + +func ExampleDatabase_From() { + db := getDB() + var names []string + + if err := db.From("test_user").Select("first_name").ScanVals(&names); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Println("Fetched Users names:", names) + } + // Output: + // Fetched Users names: [Bob Sally Vinita John] +} diff --git a/database_test.go b/database_test.go new file mode 100644 index 0000000..b7048b2 --- /dev/null +++ b/database_test.go @@ -0,0 +1,759 @@ +package db_test + +import ( + "context" + "fmt" + "sync" + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/internal/errors" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" +) + +type testActionItem struct { + Address string `db:"address"` + Name string `db:"name"` +} + +type dbTestMockLogger struct { + Messages []string +} + +func (dtml *dbTestMockLogger) Printf(format string, v ...interface{}) { + dtml.Messages = append(dtml.Messages, fmt.Sprintf(format, v...)) +} + +func (dtml *dbTestMockLogger) Reset() { + dtml.Messages = dtml.Messages[0:0] +} + +type databaseSuite struct { + suite.Suite +} + +func (ds *databaseSuite) TestLogger() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectExec(`SELECT \* FROM "items" WHERE "id" = ?`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(0, 0)) + + db := dbv2.New("db-mock", mDB) + logger := new(dbTestMockLogger) + db.Logger(logger) + var items []testActionItem + ds.NoError(db.ScanStructs(&items, `SELECT * FROM "items"`)) + _, err = db.Exec(`SELECT * FROM "items" WHERE "id" = ?`, 1) + ds.NoError(err) + db.Trace("TEST", "") + ds.Equal([]string{ + "[db] QUERY [query:=`SELECT * FROM \"items\"`]", + "[db] EXEC [query:=`SELECT * FROM \"items\" WHERE \"id\" = ?` args:=[1]]", + "[db] TEST", + }, logger.Messages) +} + +func (ds *databaseSuite) TestScanStructs() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT "test" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("db-mock", mDB) + var items []testActionItem + ds.NoError(db.ScanStructs(&items, `SELECT * FROM "items"`)) + ds.Len(items, 2) + ds.Equal("111 Test Addr", items[0].Address) + ds.Equal("Test1", items[0].Name) + + ds.Equal("211 Test Addr", items[1].Address) + ds.Equal("Test2", items[1].Name) + + items = items[0:0] + ds.EqualError(db.ScanStructs(items, `SELECT * FROM "items"`), + "db: type must be a pointer to a slice when scanning into structs") + ds.EqualError(db.ScanStructs(&testActionItem{}, `SELECT * FROM "items"`), + "db: type must be a pointer to a slice when scanning into structs") + ds.EqualError(db.ScanStructs(&items, `SELECT "test" FROM "items"`), + `db: unable to find corresponding field to column "test" returned by query`) +} + +func (ds *databaseSuite) TestScanStruct() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + mock.ExpectQuery(`SELECT "test" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("mock", mDB) + var item testActionItem + found, err := db.ScanStruct(&item, `SELECT * FROM "items" LIMIT 1`) + ds.NoError(err) + ds.True(found) + ds.Equal("111 Test Addr", item.Address) + ds.Equal("Test1", item.Name) + + _, err = db.ScanStruct(item, `SELECT * FROM "items" LIMIT 1`) + ds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.ScanStruct([]testActionItem{}, `SELECT * FROM "items" LIMIT 1`) + ds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.ScanStruct(&item, `SELECT "test" FROM "items" LIMIT 1`) + ds.EqualError(err, `db: unable to find corresponding field to column "test" returned by query`) +} + +func (ds *databaseSuite) TestScanVals() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + + db := dbv2.New("mock", mDB) + var ids []uint32 + ds.NoError(db.ScanVals(&ids, `SELECT "id" FROM "items"`)) + ds.Len(ids, 5) + + ds.EqualError(db.ScanVals([]uint32{}, `SELECT "id" FROM "items"`), + "db: type must be a pointer to a slice when scanning into vals") + ds.EqualError(db.ScanVals(testActionItem{}, `SELECT "id" FROM "items"`), + "db: type must be a pointer to a slice when scanning into vals") +} + +func (ds *databaseSuite) TestScanVal() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("10")) + + db := dbv2.New("mock", mDB) + var id int64 + found, err := db.ScanVal(&id, `SELECT "id" FROM "items"`) + ds.NoError(err) + ds.Equal(int64(10), id) + ds.True(found) + + found, err = db.ScanVal([]int64{}, `SELECT "id" FROM "items"`) + ds.False(found) + ds.EqualError(err, "db: type must be a pointer when scanning into val") + found, err = db.ScanVal(10, `SELECT "id" FROM "items"`) + ds.False(found) + ds.EqualError(err, "db: type must be a pointer when scanning into val") +} + +func (ds *databaseSuite) TestExec() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectExec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE \("name" IS NULL\)`). + WithArgs(). + WillReturnResult(sqlmock.NewResult(0, 0)) + + mock.ExpectExec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE \("name" IS NULL\)`). + WithArgs(). + WillReturnError(errors.New("mock error")) + + db := dbv2.New("mock", mDB) + _, err = db.Exec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE ("name" IS NULL)`) + ds.NoError(err) + _, err = db.Exec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE ("name" IS NULL)`) + ds.EqualError(err, "db: mock error") +} + +func (ds *databaseSuite) TestQuery() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnError(errors.New("mock error")) + + db := dbv2.New("mock", mDB) + _, err = db.Query(`SELECT * FROM "items"`) //nolint:rowserrcheck // not checking row scan + ds.NoError(err, "dbv2 - mock error") + + _, err = db.Query(`SELECT * FROM "items"`) //nolint:rowserrcheck // not checking row scan + ds.EqualError(err, "db: mock error") +} + +func (ds *databaseSuite) TestQueryRow() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnError(errors.New("mock error")) + + db := dbv2.New("mock", mDB) + rows := db.QueryRow(`SELECT * FROM "items"`) + var address string + var name string + ds.NoError(rows.Scan(&address, &name)) + + rows = db.QueryRow(`SELECT * FROM "items"`) + ds.EqualError(rows.Scan(&address, &name), "db: mock error") +} + +func (ds *databaseSuite) TestPrepare() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectPrepare("SELECT \\* FROM test WHERE id = \\?") + db := dbv2.New("mock", mDB) + stmt, err := db.Prepare("SELECT * FROM test WHERE id = ?") + ds.NoError(err) + ds.NotNil(stmt) +} + +func (ds *databaseSuite) TestBegin() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectBegin() + mock.ExpectBegin().WillReturnError(errors.New("transaction error")) + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + ds.NoError(err) + ds.Equal("mock", tx.Dialect()) + + _, err = db.Begin() + ds.EqualError(err, "db: transaction error") +} + +func (ds *databaseSuite) TestBeginTx() { + ctx := context.Background() + mDB, mock, err := sqlmock.New() + ds.NoError(err) + mock.ExpectBegin() + mock.ExpectBegin().WillReturnError(errors.New("transaction error")) + db := dbv2.New("mock", mDB) + tx, err := db.BeginTx(ctx, nil) + ds.NoError(err) + ds.Equal("mock", tx.Dialect()) + + _, err = db.BeginTx(ctx, nil) + ds.EqualError(err, "db: transaction error") +} + +func (ds *databaseSuite) TestWithTx() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + + db := dbv2.New("mock", mDB) + + cases := []struct { + expectf func(sqlmock.Sqlmock) + f func(*dbv2.TxDatabase) error + wantErr bool + errStr string + }{ + { + expectf: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectCommit() + }, + f: func(_ *dbv2.TxDatabase) error { return nil }, + wantErr: false, + }, + { + expectf: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin().WillReturnError(errors.New("transaction begin error")) + }, + f: func(_ *dbv2.TxDatabase) error { return nil }, + wantErr: true, + errStr: "db: transaction begin error", + }, + { + expectf: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectRollback() + }, + f: func(_ *dbv2.TxDatabase) error { return errors.New("transaction error") }, + wantErr: true, + errStr: "db: transaction error", + }, + { + expectf: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectRollback().WillReturnError(errors.New("transaction rollback error")) + }, + f: func(_ *dbv2.TxDatabase) error { return errors.New("something wrong") }, + wantErr: true, + errStr: "db: transaction rollback error", + }, + { + expectf: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectCommit().WillReturnError(errors.New("commit error")) + }, + f: func(_ *dbv2.TxDatabase) error { return nil }, + wantErr: true, + errStr: "db: commit error", + }, + } + for _, c := range cases { + c.expectf(mock) + err := db.WithTx(c.f) + if c.wantErr { + ds.EqualError(err, c.errStr) + } else { + ds.NoError(err) + } + } +} + +func (ds *databaseSuite) TestRollbackOnPanic() { + mDB, mock, err := sqlmock.New() + + defer func() { + p := recover() + if p == nil { + ds.Fail("there should be a panic") + } + ds.Require().Equal("a problem has happened", p.(string)) + ds.Require().NoError(mock.ExpectationsWereMet()) + }() + + ds.NoError(err) + + mock.ExpectBegin() + mock.ExpectRollback() + + db := dbv2.New("mock", mDB) + _ = db.WithTx(func(_ *dbv2.TxDatabase) error { + panic("a problem has happened") + }) +} + +func (ds *databaseSuite) TestDataRace() { + mDB, mock, err := sqlmock.New() + ds.NoError(err) + db := dbv2.New("mock", mDB) + + const concurrency = 10 + + for i := 0; i < concurrency; i++ { + mock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + } + + wg := sync.WaitGroup{} + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + sql := db.From("items").Limit(1) + var item testActionItem + found, err := sql.ScanStruct(&item) + ds.NoError(err) + ds.True(found) + ds.Equal(item.Address, "111 Test Addr") + ds.Equal(item.Name, "Test1") + }() + } + + wg.Wait() +} + +func TestDatabaseSuite(t *testing.T) { + suite.Run(t, new(databaseSuite)) +} + +type txdatabaseSuite struct { + suite.Suite +} + +func (tds *txdatabaseSuite) TestLogger() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectExec(`SELECT \* FROM "items" WHERE "id" = ?`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectCommit() + + tx, err := dbv2.New("db-mock", mDB).Begin() + tds.NoError(err) + logger := new(dbTestMockLogger) + tx.Logger(logger) + var items []testActionItem + tds.NoError(tx.ScanStructs(&items, `SELECT * FROM "items"`)) + _, err = tx.Exec(`SELECT * FROM "items" WHERE "id" = ?`, 1) + tds.NoError(err) + tds.NoError(tx.Commit()) + tds.Equal([]string{ + "[dbv2 - transaction] QUERY [query:=`SELECT * FROM \"items\"`] ", + "[dbv2 - transaction] EXEC [query:=`SELECT * FROM \"items\" WHERE \"id\" = ?` args:=[1]] ", + "[dbv2 - transaction] COMMIT", + }, logger.Messages) +} + +func (tds *txdatabaseSuite) TestLogger_FromDb() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectExec(`SELECT \* FROM "items" WHERE "id" = ?`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectCommit() + + db := dbv2.New("db-mock", mDB) + logger := new(dbTestMockLogger) + db.Logger(logger) + tx, err := db.Begin() + tds.NoError(err) + + var items []testActionItem + tds.NoError(tx.ScanStructs(&items, `SELECT * FROM "items"`)) + _, err = tx.Exec(`SELECT * FROM "items" WHERE "id" = ?`, 1) + tds.NoError(err) + tds.NoError(tx.Commit()) + tds.Equal([]string{ + "[dbv2 - transaction] QUERY [query:=`SELECT * FROM \"items\"`] ", + "[dbv2 - transaction] EXEC [query:=`SELECT * FROM \"items\" WHERE \"id\" = ?` args:=[1]] ", + "[dbv2 - transaction] COMMIT", + }, logger.Messages) +} + +func (tds *txdatabaseSuite) TestCommit() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestRollback() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectRollback() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + tds.NoError(tx.Rollback()) +} + +func (tds *txdatabaseSuite) TestFrom() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + tds.NotNil(dbv2.From("test")) + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestScanStructs() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + mock.ExpectQuery(`SELECT "test" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + var items []testActionItem + tds.NoError(tx.ScanStructs(&items, `SELECT * FROM "items"`)) + tds.Len(items, 2) + tds.Equal("111 Test Addr", items[0].Address) + tds.Equal("Test1", items[0].Name) + + tds.Equal("211 Test Addr", items[1].Address) + tds.Equal("Test2", items[1].Name) + + items = items[0:0] + tds.EqualError(tx.ScanStructs(items, `SELECT * FROM "items"`), + "db: type must be a pointer to a slice when scanning into structs") + tds.EqualError(tx.ScanStructs(&testActionItem{}, `SELECT * FROM "items"`), + "db: type must be a pointer to a slice when scanning into structs") + tds.EqualError(tx.ScanStructs(&items, `SELECT "test" FROM "items"`), + `db: unable to find corresponding field to column "test" returned by query`) + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestScanStruct() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + mock.ExpectQuery(`SELECT "test" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + var item testActionItem + found, err := tx.ScanStruct(&item, `SELECT * FROM "items" LIMIT 1`) + tds.NoError(err) + tds.True(found) + tds.Equal("111 Test Addr", item.Address) + tds.Equal("Test1", item.Name) + + _, err = tx.ScanStruct(item, `SELECT * FROM "items" LIMIT 1`) + tds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = tx.ScanStruct([]testActionItem{}, `SELECT * FROM "items" LIMIT 1`) + tds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = tx.ScanStruct(&item, `SELECT "test" FROM "items" LIMIT 1`) + tds.EqualError(err, `db: unable to find corresponding field to column "test" returned by query`) + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestScanVals() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + var ids []uint32 + tds.NoError(tx.ScanVals(&ids, `SELECT "id" FROM "items"`)) + tds.Len(ids, 5) + + tds.EqualError(tx.ScanVals([]uint32{}, `SELECT "id" FROM "items"`), + "db: type must be a pointer to a slice when scanning into vals") + tds.EqualError(tx.ScanVals(testActionItem{}, `SELECT "id" FROM "items"`), + "db: type must be a pointer to a slice when scanning into vals") + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestScanVal() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("10")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + var id int64 + found, err := tx.ScanVal(&id, `SELECT "id" FROM "items"`) + tds.NoError(err) + tds.Equal(int64(10), id) + tds.True(found) + + found, err = tx.ScanVal([]int64{}, `SELECT "id" FROM "items"`) + tds.False(found) + tds.EqualError(err, "db: type must be a pointer when scanning into val") + found, err = tx.ScanVal(10, `SELECT "id" FROM "items"`) + tds.False(found) + tds.EqualError(err, "db: type must be a pointer when scanning into val") + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestExec() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectExec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE \("name" IS NULL\)`). + WithArgs(). + WillReturnResult(sqlmock.NewResult(0, 0)) + + mock.ExpectExec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE \("name" IS NULL\)`). + WithArgs(). + WillReturnError(errors.New("mock error")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + _, err = tx.Exec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE ("name" IS NULL)`) + tds.NoError(err) + _, err = tx.Exec(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE ("name" IS NULL)`) + tds.EqualError(err, "db: mock error") + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestQuery() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnError(errors.New("mock error")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + _, err = tx.Query(`SELECT * FROM "items"`) //nolint:rowserrcheck // not checking row scan + tds.NoError(err, "dbv2 - mock error") + + _, err = tx.Query(`SELECT * FROM "items"`) //nolint:rowserrcheck // not checking row scan + tds.EqualError(err, "db: mock error") + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestQueryRow() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnError(errors.New("mock error")) + mock.ExpectCommit() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + rows := tx.QueryRow(`SELECT * FROM "items"`) + var address string + var name string + tds.NoError(rows.Scan(&address, &name)) + + rows = tx.QueryRow(`SELECT * FROM "items"`) + tds.EqualError(rows.Scan(&address, &name), "db: mock error") + tds.NoError(tx.Commit()) +} + +func (tds *txdatabaseSuite) TestWrap() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + mock.ExpectCommit() + mock.ExpectBegin() + mock.ExpectRollback() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + tds.NoError(tx.Wrap(func() error { + return nil + })) + tx, err = db.Begin() + tds.NoError(err) + tds.EqualError(tx.Wrap(func() error { + return errors.New("tx error") + }), "db: tx error") +} + +func (tds *txdatabaseSuite) TestDataRace() { + mDB, mock, err := sqlmock.New() + tds.NoError(err) + mock.ExpectBegin() + db := dbv2.New("mock", mDB) + tx, err := db.Begin() + tds.NoError(err) + + const concurrency = 10 + + for i := 0; i < concurrency; i++ { + mock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + } + + wg := sync.WaitGroup{} + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + sql := tx.From("items").Limit(1) + var item testActionItem + found, err := sql.ScanStruct(&item) + tds.NoError(err) + tds.True(found) + tds.Equal(item.Address, "111 Test Addr") + tds.Equal(item.Name, "Test1") + }() + } + + wg.Wait() + mock.ExpectCommit() + tds.NoError(tx.Commit()) +} + +func TestTxDatabaseSuite(t *testing.T) { + suite.Run(t, new(txdatabaseSuite)) +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..ca58feb --- /dev/null +++ b/db.go @@ -0,0 +1,74 @@ +package db + +import ( + "time" + + "git.fsdpf.net/go/db/v2/internal/util" + "git.fsdpf.net/go/db/v2/sqlgen" +) + +type DialectWrapper struct { + dialect string +} + +// Creates a new DialectWrapper to create db.Datasets or db.Databases with the specified dialect. +func Dialect(dialect string) DialectWrapper { + return DialectWrapper{dialect: dialect} +} + +// Create a new dataset for creating SELECT sql statements +func (dw DialectWrapper) From(table ...interface{}) *SelectDataset { + return From(table...).WithDialect(dw.dialect) +} + +// Create a new dataset for creating SELECT sql statements +func (dw DialectWrapper) Select(cols ...interface{}) *SelectDataset { + return newDataset(dw.dialect, nil).Select(cols...) +} + +// Create a new dataset for creating UPDATE sql statements +func (dw DialectWrapper) Update(table interface{}) *UpdateDataset { + return Update(table).WithDialect(dw.dialect) +} + +// Create a new dataset for creating INSERT sql statements +func (dw DialectWrapper) Insert(table interface{}) *InsertDataset { + return Insert(table).WithDialect(dw.dialect) +} + +// Create a new dataset for creating DELETE sql statements +func (dw DialectWrapper) Delete(table interface{}) *DeleteDataset { + return Delete(table).WithDialect(dw.dialect) +} + +// Create a new dataset for creating TRUNCATE sql statements +func (dw DialectWrapper) Truncate(table ...interface{}) *TruncateDataset { + return Truncate(table...).WithDialect(dw.dialect) +} + +func (dw DialectWrapper) DB(db SQLDatabase) *Database { + return newDatabase(dw.dialect, db) +} + +func New(dialect string, db SQLDatabase) *Database { + return newDatabase(dialect, db) +} + +// Set the behavior when encountering struct fields that do not have a db tag. +// By default this is false; if set to true any field without a db tag will not +// be targeted by Select or Scan operations. +func SetIgnoreUntaggedFields(ignore bool) { + util.SetIgnoreUntaggedFields(ignore) +} + +// Set the column rename function. This is used for struct fields that do not have a db tag to specify the column name +// By default all struct fields that do not have a db tag will be converted lowercase +func SetColumnRenameFunction(renameFunc func(string) string) { + util.SetColumnRenameFunction(renameFunc) +} + +// Set the location to use when interpolating time.Time instances. See https://golang.org/pkg/time/#LoadLocation +// NOTE: This has no effect when using prepared statements. +func SetTimeLocation(loc *time.Location) { + sqlgen.SetTimeLocation(loc) +} diff --git a/db_example_test.go b/db_example_test.go new file mode 100644 index 0000000..213f046 --- /dev/null +++ b/db_example_test.go @@ -0,0 +1,255 @@ +package db_test + +import ( + "fmt" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + _ "git.fsdpf.net/go/db/v2/dialect/mysql" + _ "git.fsdpf.net/go/db/v2/dialect/postgres" + _ "git.fsdpf.net/go/db/v2/dialect/sqlite3" + "github.com/DATA-DOG/go-sqlmock" +) + +// Creating a mysql dataset. Be sure to import the mysql adapter. +func ExampleDialect_datasetMysql() { + // import _ "git.fsdpf.net/go/db/v2/dialect/mysql" + + d := dbv2.Dialect("mysql") + ds := d.From("test").Where(dbv2.Ex{ + "foo": "bar", + "baz": []int64{1, 2, 3}, + }).Limit(10) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM `test` WHERE ((`baz` IN (1, 2, 3)) AND (`foo` = 'bar')) LIMIT 10 [] + // SELECT * FROM `test` WHERE ((`baz` IN (?, ?, ?)) AND (`foo` = ?)) LIMIT ? [1 2 3 bar 10] +} + +// Creating a mysql database. Be sure to import the mysql adapter. +func ExampleDialect_dbMysql() { + // import _ "git.fsdpf.net/go/db/v2/dialect/mysql" + + type item struct { + ID int64 `db:"id"` + Address string `db:"address"` + Name string `db:"name"` + } + + // set up a mock db this would normally be + // db, err := sql.Open("mysql", dbURI) + // if err != nil { + // panic(err.Error()) + // } + mDB, mock, _ := sqlmock.New() + + d := dbv2.Dialect("mysql") + + db := d.DB(mDB) + + // use the db.From to get a dataset to execute queries + ds := db.From("items").Where(dbv2.C("id").Eq(1)) + + // set up mock for example purposes + mock.ExpectQuery("SELECT `address`, `id`, `name` FROM `items` WHERE \\(`id` = 1\\) LIMIT 1"). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + var it item + found, err := ds.ScanStruct(&it) + fmt.Println(it, found, err) + + // set up mock for example purposes + mock.ExpectQuery("SELECT `address`, `id`, `name` FROM `items` WHERE \\(`id` = \\?\\) LIMIT \\?"). + WithArgs(1, 1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + + found, err = ds.Prepared(true).ScanStruct(&it) + fmt.Println(it, found, err) + + // Output: + // {1 111 Test Addr Test1} true + // {1 111 Test Addr Test1} true +} + +// Creating a mysql dataset. Be sure to import the postgres adapter +func ExampleDialect_datasetPostgres() { + // import _ "git.fsdpf.net/go/db/v2/dialect/postgres" + + d := dbv2.Dialect("postgres") + ds := d.From("test").Where(dbv2.Ex{ + "foo": "bar", + "baz": []int64{1, 2, 3}, + }).Limit(10) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("baz" IN (1, 2, 3)) AND ("foo" = 'bar')) LIMIT 10 [] + // SELECT * FROM "test" WHERE (("baz" IN ($1, $2, $3)) AND ("foo" = $4)) LIMIT $5 [1 2 3 bar 10] +} + +// Creating a postgres dataset. Be sure to import the postgres adapter +func ExampleDialect_dbPostgres() { + // import _ "git.fsdpf.net/go/db/v2/dialect/postgres" + + type item struct { + ID int64 `db:"id"` + Address string `db:"address"` + Name string `db:"name"` + } + + // set up a mock db this would normally be + // db, err := sql.Open("postgres", dbURI) + // if err != nil { + // panic(err.Error()) + // } + mDB, mock, _ := sqlmock.New() + + d := dbv2.Dialect("postgres") + + db := d.DB(mDB) + + // use the db.From to get a dataset to execute queries + ds := db.From("items").Where(dbv2.C("id").Eq(1)) + + // set up mock for example purposes + mock.ExpectQuery(`SELECT "address", "id", "name" FROM "items" WHERE \("id" = 1\) LIMIT 1`). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + var it item + found, err := ds.ScanStruct(&it) + fmt.Println(it, found, err) + + // set up mock for example purposes + mock.ExpectQuery(`SELECT "address", "id", "name" FROM "items" WHERE \("id" = \$1\) LIMIT \$2`). + WithArgs(1, 1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + + found, err = ds.Prepared(true).ScanStruct(&it) + fmt.Println(it, found, err) + + // Output: + // {1 111 Test Addr Test1} true + // {1 111 Test Addr Test1} true +} + +// Creating a mysql dataset. Be sure to import the sqlite3 adapter +func ExampleDialect_datasetSqlite3() { + // import _ "git.fsdpf.net/go/db/v2/dialect/sqlite3" + + d := dbv2.Dialect("sqlite3") + ds := d.From("test").Where(dbv2.Ex{ + "foo": "bar", + "baz": []int64{1, 2, 3}, + }).Limit(10) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM `test` WHERE ((`baz` IN (1, 2, 3)) AND (`foo` = 'bar')) LIMIT 10 [] + // SELECT * FROM `test` WHERE ((`baz` IN (?, ?, ?)) AND (`foo` = ?)) LIMIT ? [1 2 3 bar 10] +} + +// Creating a sqlite3 database. Be sure to import the sqlite3 adapter +func ExampleDialect_dbSqlite3() { + // import _ "git.fsdpf.net/go/db/v2/dialect/sqlite3" + type item struct { + ID int64 `db:"id"` + Address string `db:"address"` + Name string `db:"name"` + } + + // set up a mock db this would normally be + // db, err := sql.Open("sqlite3", dbURI) + // if err != nil { + // panic(err.Error()) + // } + mDB, mock, _ := sqlmock.New() + + d := dbv2.Dialect("sqlite3") + + db := d.DB(mDB) + + // use the db.From to get a dataset to execute queries + ds := db.From("items").Where(dbv2.C("id").Eq(1)) + + // set up mock for example purposes + mock.ExpectQuery("SELECT `address`, `id`, `name` FROM `items` WHERE \\(`id` = 1\\) LIMIT 1"). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + var it item + found, err := ds.ScanStruct(&it) + fmt.Println(it, found, err) + + // set up mock for example purposes + mock.ExpectQuery("SELECT `address`, `id`, `name` FROM `items` WHERE \\(`id` = \\?\\) LIMIT \\?"). + WithArgs(1, 1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "address", "name"}). + FromCSVString("1, 111 Test Addr,Test1"), + ) + + found, err = ds.Prepared(true).ScanStruct(&it) + fmt.Println(it, found, err) + + // Output: + // {1 111 Test Addr Test1} true + // {1 111 Test Addr Test1} true +} + +func ExampleSetTimeLocation() { + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + panic(err) + } + + created, err := time.Parse(time.RFC3339, "2019-10-01T15:01:00Z") + if err != nil { + panic(err) + } + + // use original time with tz info + dbv2.SetTimeLocation(loc) + ds := dbv2.Insert("test").Rows(dbv2.Record{ + "address": "111 Address", + "name": "Bob Yukon", + "created": created, + }) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + + // convert time to UTC + dbv2.SetTimeLocation(time.UTC) + sql, _, _ = ds.ToSQL() + fmt.Println(sql) + + // Output: + // INSERT INTO "test" ("address", "created", "name") VALUES ('111 Address', '2019-10-01T23:01:00+08:00', 'Bob Yukon') + // INSERT INTO "test" ("address", "created", "name") VALUES ('111 Address', '2019-10-01T15:01:00Z', 'Bob Yukon') +} diff --git a/db_test.go b/db_test.go new file mode 100644 index 0000000..b48da49 --- /dev/null +++ b/db_test.go @@ -0,0 +1,61 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" +) + +type ( + dialectWrapperSuite struct { + suite.Suite + } +) + +func (dws *dialectWrapperSuite) SetupSuite() { + testDialect := dbv2.DefaultDialectOptions() + // override to some value to ensure correct dialect is set + dbv2.RegisterDialect("test", testDialect) +} + +func (dws *dialectWrapperSuite) TearDownSuite() { + dbv2.DeregisterDialect("test") +} + +func (dws *dialectWrapperSuite) TestFrom() { + dw := dbv2.Dialect("test") + dws.Equal(dbv2.From("table").WithDialect("test"), dw.From("table")) +} + +func (dws *dialectWrapperSuite) TestSelect() { + dw := dbv2.Dialect("test") + dws.Equal(dbv2.Select("col").WithDialect("test"), dw.Select("col")) +} + +func (dws *dialectWrapperSuite) TestInsert() { + dw := dbv2.Dialect("test") + dws.Equal(dbv2.Insert("table").WithDialect("test"), dw.Insert("table")) +} + +func (dws *dialectWrapperSuite) TestDelete() { + dw := dbv2.Dialect("test") + dws.Equal(dbv2.Delete("table").WithDialect("test"), dw.Delete("table")) +} + +func (dws *dialectWrapperSuite) TestTruncate() { + dw := dbv2.Dialect("test") + dws.Equal(dbv2.Truncate("table").WithDialect("test"), dw.Truncate("table")) +} + +func (dws *dialectWrapperSuite) TestDB() { + mDB, _, err := sqlmock.New() + dws.Require().NoError(err) + dw := dbv2.Dialect("test") + dws.Equal(dbv2.New("test", mDB), dw.DB(mDB)) +} + +func TestDialectWrapper(t *testing.T) { + suite.Run(t, new(dialectWrapperSuite)) +} diff --git a/delete_dataset.go b/delete_dataset.go new file mode 100644 index 0000000..5685371 --- /dev/null +++ b/delete_dataset.go @@ -0,0 +1,246 @@ +package db + +import ( + "git.fsdpf.net/go/db/v2/exec" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +var ErrBadFromArgument = errors.New("unsupported DeleteDataset#From argument, a string or identifier expression is required") + +type DeleteDataset struct { + dialect SQLDialect + clauses exp.DeleteClauses + isPrepared prepared + queryFactory exec.QueryFactory + err error +} + +// used internally by database to create a database with a specific adapter +func newDeleteDataset(d string, queryFactory exec.QueryFactory) *DeleteDataset { + return &DeleteDataset{ + clauses: exp.NewDeleteClauses(), + dialect: GetDialect(d), + queryFactory: queryFactory, + isPrepared: preparedNoPreference, + err: nil, + } +} + +func Delete(table interface{}) *DeleteDataset { + return newDeleteDataset("default", nil).From(table) +} + +func (dd *DeleteDataset) Expression() exp.Expression { + return dd +} + +// Clones the dataset +func (dd *DeleteDataset) Clone() exp.Expression { + return dd.copy(dd.clauses) +} + +// Set the parameter interpolation behavior. See examples +// +// prepared: If true the dataset WILL NOT interpolate the parameters. +func (dd *DeleteDataset) Prepared(prepared bool) *DeleteDataset { + ret := dd.copy(dd.clauses) + ret.isPrepared = preparedFromBool(prepared) + return ret +} + +// Returns true if Prepared(true) has been called on this dataset +func (dd *DeleteDataset) IsPrepared() bool { + return dd.isPrepared.Bool() +} + +// Sets the adapter used to serialize values and create the SQL statement +func (dd *DeleteDataset) WithDialect(dl string) *DeleteDataset { + ds := dd.copy(dd.GetClauses()) + ds.dialect = GetDialect(dl) + return ds +} + +// Returns the current SQLDialect on the dataset +func (dd *DeleteDataset) Dialect() SQLDialect { + return dd.dialect +} + +// Set the dialect for this dataset. +func (dd *DeleteDataset) SetDialect(dialect SQLDialect) *DeleteDataset { + cd := dd.copy(dd.GetClauses()) + cd.dialect = dialect + return cd +} + +// Returns the current clauses on the dataset. +func (dd *DeleteDataset) GetClauses() exp.DeleteClauses { + return dd.clauses +} + +// used interally to copy the dataset +func (dd *DeleteDataset) copy(clauses exp.DeleteClauses) *DeleteDataset { + return &DeleteDataset{ + dialect: dd.dialect, + clauses: clauses, + isPrepared: dd.isPrepared, + queryFactory: dd.queryFactory, + err: dd.err, + } +} + +// Creates a WITH clause for a common table expression (CTE). +// +// The name will be available to SELECT from in the associated query; and can optionally +// contain a list of column names "name(col1, col2, col3)". +// +// The name will refer to the results of the specified subquery. +func (dd *DeleteDataset) With(name string, subquery exp.Expression) *DeleteDataset { + return dd.copy(dd.clauses.CommonTablesAppend(exp.NewCommonTableExpression(false, name, subquery))) +} + +// Creates a WITH RECURSIVE clause for a common table expression (CTE) +// +// The name will be available to SELECT from in the associated query; and must +// contain a list of column names "name(col1, col2, col3)" for a recursive clause. +// +// The name will refer to the results of the specified subquery. The subquery for +// a recursive query will always end with a UNION or UNION ALL with a clause that +// refers to the CTE by name. +func (dd *DeleteDataset) WithRecursive(name string, subquery exp.Expression) *DeleteDataset { + return dd.copy(dd.clauses.CommonTablesAppend(exp.NewCommonTableExpression(true, name, subquery))) +} + +// Adds a FROM clause. This return a new dataset with the original sources replaced. See examples. +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Dataset: Will be added as a sub select. If the Dataset is not aliased it will automatically be aliased +// LiteralExpression: (See Literal) Will use the literal SQL +func (dd *DeleteDataset) From(table interface{}) *DeleteDataset { + switch t := table.(type) { + case exp.IdentifierExpression: + return dd.copy(dd.clauses.SetFrom(t)) + case string: + return dd.copy(dd.clauses.SetFrom(exp.ParseIdentifier(t))) + default: + panic(ErrBadFromArgument) + } +} + +// Adds a WHERE clause. See examples. +func (dd *DeleteDataset) Where(expressions ...exp.Expression) *DeleteDataset { + return dd.copy(dd.clauses.WhereAppend(expressions...)) +} + +// Removes the WHERE clause. See examples. +func (dd *DeleteDataset) ClearWhere() *DeleteDataset { + return dd.copy(dd.clauses.ClearWhere()) +} + +// Adds a ORDER clause. If the ORDER is currently set it replaces it. See examples. +func (dd *DeleteDataset) Order(order ...exp.OrderedExpression) *DeleteDataset { + return dd.copy(dd.clauses.SetOrder(order...)) +} + +// Adds a more columns to the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (dd *DeleteDataset) OrderAppend(order ...exp.OrderedExpression) *DeleteDataset { + return dd.copy(dd.clauses.OrderAppend(order...)) +} + +// Adds a more columns to the beginning of the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (dd *DeleteDataset) OrderPrepend(order ...exp.OrderedExpression) *DeleteDataset { + return dd.copy(dd.clauses.OrderPrepend(order...)) +} + +// Removes the ORDER BY clause. See examples. +func (dd *DeleteDataset) ClearOrder() *DeleteDataset { + return dd.copy(dd.clauses.ClearOrder()) +} + +// Adds a LIMIT clause. If the LIMIT is currently set it replaces it. See examples. +func (dd *DeleteDataset) Limit(limit uint) *DeleteDataset { + if limit > 0 { + return dd.copy(dd.clauses.SetLimit(limit)) + } + return dd.copy(dd.clauses.ClearLimit()) +} + +// Adds a LIMIT ALL clause. If the LIMIT is currently set it replaces it. See examples. +func (dd *DeleteDataset) LimitAll() *DeleteDataset { + return dd.copy(dd.clauses.SetLimit(L("ALL"))) +} + +// Removes the LIMIT clause. +func (dd *DeleteDataset) ClearLimit() *DeleteDataset { + return dd.copy(dd.clauses.ClearLimit()) +} + +// Adds a RETURNING clause to the dataset if the adapter supports it. +func (dd *DeleteDataset) Returning(returning ...interface{}) *DeleteDataset { + return dd.copy(dd.clauses.SetReturning(exp.NewColumnListExpression(returning...))) +} + +// Get any error that has been set or nil if no error has been set. +func (dd *DeleteDataset) Error() error { + return dd.err +} + +// Set an error on the dataset if one has not already been set. This error will be returned by a future call to Error +// or as part of ToSQL. This can be used by end users to record errors while building up queries without having to +// track those separately. +func (dd *DeleteDataset) SetError(err error) *DeleteDataset { + if dd.err == nil { + dd.err = err + } + + return dd +} + +// Generates a DELETE sql statement, if Prepared has been called with true then the parameters will not be interpolated. +// See examples. +// +// Errors: +// - There is an error generating the SQL +func (dd *DeleteDataset) ToSQL() (sql string, params []interface{}, err error) { + return dd.deleteSQLBuilder().ToSQL() +} + +// Appends this Dataset's DELETE statement to the SQLBuilder +// This is used internally when using deletes in CTEs +func (dd *DeleteDataset) AppendSQL(b sb.SQLBuilder) { + if dd.err != nil { + b.SetError(dd.err) + return + } + dd.dialect.ToDeleteSQL(b, dd.GetClauses()) +} + +func (dd *DeleteDataset) GetAs() exp.IdentifierExpression { + return nil +} + +func (dd *DeleteDataset) ReturnsColumns() bool { + return dd.clauses.HasReturning() +} + +// Creates an QueryExecutor to execute the query. +// +// db.Delete("test").Exec() +// +// See Dataset#ToUpdateSQL for arguments +func (dd *DeleteDataset) Executor() exec.QueryExecutor { + return dd.queryFactory.FromSQLBuilder(dd.deleteSQLBuilder()) +} + +func (dd *DeleteDataset) deleteSQLBuilder() sb.SQLBuilder { + buf := sb.NewSQLBuilder(dd.isPrepared.Bool()) + if dd.err != nil { + return buf.SetError(dd.err) + } + dd.dialect.ToDeleteSQL(buf, dd.clauses) + return buf +} diff --git a/delete_dataset_example_test.go b/delete_dataset_example_test.go new file mode 100644 index 0000000..7960485 --- /dev/null +++ b/delete_dataset_example_test.go @@ -0,0 +1,312 @@ +package db_test + +import ( + "fmt" + + dbv2 "git.fsdpf.net/go/db/v2" + _ "git.fsdpf.net/go/db/v2/dialect/mysql" +) + +func ExampleDelete() { + ds := dbv2.Delete("items") + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + // Output: + // DELETE FROM "items" [] +} + +func ExampleDeleteDataset_Executor() { + _db := getDB() + + de := _db.Delete("test_user"). + Where(dbv2.Ex{"first_name": "Bob"}). + Executor() + if r, err := de.Exec(); err != nil { + fmt.Println(err.Error()) + } else { + c, _ := r.RowsAffected() + fmt.Printf("Deleted %d users", c) + } + + // Output: + // Deleted 1 users +} + +func ExampleDeleteDataset_Executor_returning() { + _db := getDB() + + de := _db.Delete("test_user"). + Where(dbv2.C("last_name").Eq("Yukon")). + Returning(dbv2.C("id")). + Executor() + + var ids []int64 + if err := de.ScanVals(&ids); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("Deleted users [ids:=%+v]", ids) + } + + // Output: + // Deleted users [ids:=[1 2 3]] +} + +func ExampleDeleteDataset_With() { + sql, _, _ := dbv2.Delete("test"). + With("check_vals(val)", dbv2.From().Select(dbv2.L("123"))). + Where(dbv2.C("val").Eq(dbv2.From("check_vals").Select("val"))). + ToSQL() + fmt.Println(sql) + + // Output: + // WITH check_vals(val) AS (SELECT 123) DELETE FROM "test" WHERE ("val" IN (SELECT "val" FROM "check_vals")) +} + +func ExampleDeleteDataset_WithRecursive() { + sql, _, _ := dbv2.Delete("nums"). + WithRecursive("nums(x)", + dbv2.From().Select(dbv2.L("1")). + UnionAll(dbv2.From("nums"). + Select(dbv2.L("x+1")).Where(dbv2.C("x").Lt(5)))). + ToSQL() + fmt.Println(sql) + // Output: + // WITH RECURSIVE nums(x) AS (SELECT 1 UNION ALL (SELECT x+1 FROM "nums" WHERE ("x" < 5))) DELETE FROM "nums" +} + +func ExampleDeleteDataset_Where() { + // By default everything is anded together + sql, _, _ := dbv2.Delete("test").Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use ExOr to get ORed expressions together + sql, _, _ = dbv2.Delete("test").Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use Or with Ex to Or multiple Ex maps together + sql, _, _ = dbv2.Delete("test").Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql) + // By default everything is anded together + sql, _, _ = dbv2.Delete("test").Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql) + // You can use a combination of Ors and Ands + sql, _, _ = dbv2.Delete("test").Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM "test" WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // DELETE FROM "test" WHERE (("a" > 10) OR ("b" < 10) OR ("c" IS NULL) OR ("d" IN ('a', 'b', 'c'))) + // DELETE FROM "test" WHERE ((("a" > 10) AND ("b" < 10)) OR (("c" IS NULL) AND ("d" IN ('a', 'b', 'c')))) + // DELETE FROM "test" WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // DELETE FROM "test" WHERE (("a" > 10) OR (("b" < 10) AND ("c" IS NULL))) +} + +func ExampleDeleteDataset_Where_prepared() { + // By default everything is anded together + sql, args, _ := dbv2.Delete("test").Prepared(true).Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use ExOr to get ORed expressions together + sql, args, _ = dbv2.Delete("test").Prepared(true).Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use Or with Ex to Or multiple Ex maps together + sql, args, _ = dbv2.Delete("test").Prepared(true).Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql, args) + // By default everything is anded together + sql, args, _ = dbv2.Delete("test").Prepared(true).Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql, args) + // You can use a combination of Ors and Ands + sql, args, _ = dbv2.Delete("test").Prepared(true).Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql, args) + // Output: + // DELETE FROM "test" WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [10 10 a b c] + // DELETE FROM "test" WHERE (("a" > ?) OR ("b" < ?) OR ("c" IS NULL) OR ("d" IN (?, ?, ?))) [10 10 a b c] + // DELETE FROM "test" WHERE ((("a" > ?) AND ("b" < ?)) OR (("c" IS NULL) AND ("d" IN (?, ?, ?)))) [10 10 a b c] + // DELETE FROM "test" WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [10 10 a b c] + // DELETE FROM "test" WHERE (("a" > ?) OR (("b" < ?) AND ("c" IS NULL))) [10 10] +} + +func ExampleDeleteDataset_ClearWhere() { + ds := dbv2.Delete("test").Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ) + sql, _, _ := ds.ClearWhere().ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM "test" +} + +func ExampleDeleteDataset_Limit() { + ds := dbv2.Dialect("mysql").Delete("test").Limit(10) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM `test` LIMIT 10 +} + +func ExampleDeleteDataset_LimitAll() { + // Using mysql dialect because it supports limit on delete + ds := dbv2.Dialect("mysql").Delete("test").LimitAll() + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM `test` LIMIT ALL +} + +func ExampleDeleteDataset_ClearLimit() { + // Using mysql dialect because it supports limit on delete + ds := dbv2.Dialect("mysql").Delete("test").Limit(10) + sql, _, _ := ds.ClearLimit().ToSQL() + fmt.Println(sql) + // Output: + // DELETE `test` FROM `test` +} + +func ExampleDeleteDataset_Order() { + // use mysql dialect because it supports order by on deletes + ds := dbv2.Dialect("mysql").Delete("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM `test` ORDER BY `a` ASC +} + +func ExampleDeleteDataset_OrderAppend() { + // use mysql dialect because it supports order by on deletes + ds := dbv2.Dialect("mysql").Delete("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.OrderAppend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM `test` ORDER BY `a` ASC, `b` DESC NULLS LAST +} + +func ExampleDeleteDataset_OrderPrepend() { + // use mysql dialect because it supports order by on deletes + ds := dbv2.Dialect("mysql").Delete("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.OrderPrepend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM `test` ORDER BY `b` DESC NULLS LAST, `a` ASC +} + +func ExampleDeleteDataset_ClearOrder() { + ds := dbv2.Delete("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ClearOrder().ToSQL() + fmt.Println(sql) + // Output: + // DELETE FROM "test" +} + +func ExampleDeleteDataset_ToSQL() { + sql, args, _ := dbv2.Delete("items").ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.Delete("items"). + Where(dbv2.Ex{"id": dbv2.Op{"gt": 10}}). + ToSQL() + fmt.Println(sql, args) + + // Output: + // DELETE FROM "items" [] + // DELETE FROM "items" WHERE ("id" > 10) [] +} + +func ExampleDeleteDataset_Prepared() { + sql, args, _ := dbv2.Delete("items").Prepared(true).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.Delete("items"). + Prepared(true). + Where(dbv2.Ex{"id": dbv2.Op{"gt": 10}}). + ToSQL() + fmt.Println(sql, args) + + // Output: + // DELETE FROM "items" [] + // DELETE FROM "items" WHERE ("id" > ?) [10] +} + +func ExampleDeleteDataset_Returning() { + ds := dbv2.Delete("items") + sql, args, _ := ds.Returning("id").ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Returning("id").Where(dbv2.C("id").IsNotNull()).ToSQL() + fmt.Println(sql, args) + + // Output: + // DELETE FROM "items" RETURNING "id" [] + // DELETE FROM "items" WHERE ("id" IS NOT NULL) RETURNING "id" [] +} diff --git a/delete_dataset_test.go b/delete_dataset_test.go new file mode 100644 index 0000000..4b2e4de --- /dev/null +++ b/delete_dataset_test.go @@ -0,0 +1,511 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/mocks" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ( + deleteTestCase struct { + ds *dbv2.DeleteDataset + clauses exp.DeleteClauses + } + deleteDatasetSuite struct { + suite.Suite + } +) + +func (dds *deleteDatasetSuite) assertCases(cases ...deleteTestCase) { + for _, s := range cases { + dds.Equal(s.clauses, s.ds.GetClauses()) + } +} + +func (dds *deleteDatasetSuite) SetupSuite() { + noReturn := dbv2.DefaultDialectOptions() + noReturn.SupportsReturn = false + dbv2.RegisterDialect("no-return", noReturn) + + limitOnDelete := dbv2.DefaultDialectOptions() + limitOnDelete.SupportsLimitOnDelete = true + dbv2.RegisterDialect("limit-on-delete", limitOnDelete) + + orderOnDelete := dbv2.DefaultDialectOptions() + orderOnDelete.SupportsOrderByOnDelete = true + dbv2.RegisterDialect("order-on-delete", orderOnDelete) +} + +func (dds *deleteDatasetSuite) TearDownSuite() { + dbv2.DeregisterDialect("no-return") + dbv2.DeregisterDialect("limit-on-delete") + dbv2.DeregisterDialect("order-on-delete") +} + +func (dds *deleteDatasetSuite) TestDelete() { + ds := dbv2.Delete("test") + dds.IsType(&dbv2.DeleteDataset{}, ds) + dds.Implements((*exp.Expression)(nil), ds) + dds.Implements((*exp.AppendableExpression)(nil), ds) +} + +func (dds *deleteDatasetSuite) TestClone() { + ds := dbv2.Delete("test") + dds.Equal(ds.Clone(), ds) +} + +func (dds *deleteDatasetSuite) TestExpression() { + ds := dbv2.Delete("test") + dds.Equal(ds.Expression(), ds) +} + +func (dds *deleteDatasetSuite) TestDialect() { + ds := dbv2.Delete("test") + dds.NotNil(ds.Dialect()) +} + +func (dds *deleteDatasetSuite) TestWithDialect() { + ds := dbv2.Delete("test") + md := new(mocks.SQLDialect) + ds = ds.SetDialect(md) + + dialect := dbv2.GetDialect("default") + dialectDs := ds.WithDialect("default") + dds.Equal(md, ds.Dialect()) + dds.Equal(dialect, dialectDs.Dialect()) +} + +func (dds *deleteDatasetSuite) TestPrepared() { + ds := dbv2.Delete("test") + preparedDs := ds.Prepared(true) + dds.True(preparedDs.IsPrepared()) + dds.False(ds.IsPrepared()) + // should apply the prepared to any datasets created from the root + dds.True(preparedDs.Where(dbv2.Ex{"a": 1}).IsPrepared()) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + // should be prepared by default + ds = dbv2.Delete("test") + dds.True(ds.IsPrepared()) +} + +func (dds *deleteDatasetSuite) TestGetClauses() { + ds := dbv2.Delete("test") + ce := exp.NewDeleteClauses().SetFrom(dbv2.I("test")) + dds.Equal(ce, ds.GetClauses()) +} + +func (dds *deleteDatasetSuite) TestWith() { + from := dbv2.From("cte") + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.With("test-cte", from), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestWithRecursive() { + from := dbv2.From("cte") + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.WithRecursive("test-cte", from), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(true, "test-cte", from)), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestFrom_withIdentifier() { + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.From("items2"), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items2")), + }, + deleteTestCase{ + ds: bd.From(dbv2.C("items2")), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items2")), + }, + deleteTestCase{ + ds: bd.From(dbv2.T("items2")), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.T("items2")), + }, + deleteTestCase{ + ds: bd.From("schema.table"), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.I("schema.table")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) + + dds.PanicsWithValue(dbv2.ErrBadFromArgument, func() { + dbv2.Delete("test").From(true) + }) +} + +func (dds *deleteDatasetSuite) TestWhere() { + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.Where(dbv2.Ex{"a": 1}), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}), + }, + deleteTestCase{ + ds: bd.Where(dbv2.Ex{"a": 1}).Where(dbv2.C("b").Eq("c")), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}). + WhereAppend(dbv2.C("b").Eq("c")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestClearWhere() { + bd := dbv2.Delete("items").Where(dbv2.Ex{"a": 1}) + dds.assertCases( + deleteTestCase{ + ds: bd.ClearWhere(), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}), + }, + ) +} + +func (dds *deleteDatasetSuite) TestOrder() { + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.Order(dbv2.C("a").Asc()), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc()), + }, + deleteTestCase{ + ds: bd.Order(dbv2.C("a").Asc()).Order(dbv2.C("b").Desc()), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("b").Desc()), + }, + deleteTestCase{ + ds: bd.Order(dbv2.C("a").Asc(), dbv2.C("b").Desc()), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc(), dbv2.C("b").Desc()), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestOrderAppend() { + bd := dbv2.Delete("items").Order(dbv2.C("a").Asc()) + dds.assertCases( + deleteTestCase{ + ds: bd.OrderAppend(dbv2.C("b").Desc()), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc(), dbv2.C("b").Desc()), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (dds *deleteDatasetSuite) TestOrderPrepend() { + bd := dbv2.Delete("items").Order(dbv2.C("a").Asc()) + dds.assertCases( + deleteTestCase{ + ds: bd.OrderPrepend(dbv2.C("b").Desc()), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("b").Desc(), dbv2.C("a").Asc()), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (dds *deleteDatasetSuite) TestClearOrder() { + bd := dbv2.Delete("items").Order(dbv2.C("a").Asc()) + dds.assertCases( + deleteTestCase{ + ds: bd.ClearOrder(), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (dds *deleteDatasetSuite) TestLimit() { + bd := dbv2.Delete("test") + dds.assertCases( + deleteTestCase{ + ds: bd.Limit(10), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("test")). + SetLimit(uint(10)), + }, + deleteTestCase{ + ds: bd.Limit(0), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")), + }, + deleteTestCase{ + ds: bd.Limit(10).Limit(2), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("test")). + SetLimit(uint(2)), + }, + deleteTestCase{ + ds: bd.Limit(10).Limit(0), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestLimitAll() { + bd := dbv2.Delete("test") + dds.assertCases( + deleteTestCase{ + ds: bd.LimitAll(), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("test")). + SetLimit(dbv2.L("ALL")), + }, + deleteTestCase{ + ds: bd.Limit(10).LimitAll(), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("test")). + SetLimit(dbv2.L("ALL")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestClearLimit() { + bd := dbv2.Delete("test").Limit(10) + dds.assertCases( + deleteTestCase{ + ds: bd.ClearLimit(), + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("test")).SetLimit(uint(10)), + }, + ) +} + +func (dds *deleteDatasetSuite) TestReturning() { + bd := dbv2.Delete("items") + dds.assertCases( + deleteTestCase{ + ds: bd.Returning("a"), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("a")), + }, + deleteTestCase{ + ds: bd.Returning(), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + deleteTestCase{ + ds: bd.Returning(nil), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + deleteTestCase{ + ds: bd.Returning("a").Returning("b"), + clauses: exp.NewDeleteClauses(). + SetFrom(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("b")), + }, + deleteTestCase{ + ds: bd, + clauses: exp.NewDeleteClauses().SetFrom(dbv2.C("items")), + }, + ) +} + +func (dds *deleteDatasetSuite) TestReturnsColumns() { + ds := dbv2.Delete("test") + dds.False(ds.ReturnsColumns()) + dds.True(ds.Returning("foo", "bar").ReturnsColumns()) +} + +func (dds *deleteDatasetSuite) TestToSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.Delete("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToDeleteSQL", sqlB, c).Return(nil).Once() + + sql, args, err := ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Nil(err) + md.AssertExpectations(dds.T()) +} + +func (dds *deleteDatasetSuite) TestToSQL_Prepared() { + md := new(mocks.SQLDialect) + ds := dbv2.Delete("test").Prepared(true).SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(true) + md.On("ToDeleteSQL", sqlB, c).Return(nil).Once() + + sql, args, err := ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Nil(err) + md.AssertExpectations(dds.T()) +} + +func (dds *deleteDatasetSuite) TestToSQL_WithError() { + md := new(mocks.SQLDialect) + ds := dbv2.Delete("test").SetDialect(md) + c := ds.GetClauses() + ee := errors.New("expected error") + sqlB := sb.NewSQLBuilder(false) + md.On("ToDeleteSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(ee) + }).Once() + + sql, args, err := ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Equal(ee, err) + md.AssertExpectations(dds.T()) +} + +func (dds *deleteDatasetSuite) TestExecutor() { + mDB, _, err := sqlmock.New() + dds.NoError(err) + + ds := dbv2.New("mock", mDB).Delete("items").Where(dbv2.Ex{"id": dbv2.Op{"gt": 10}}) + + dsql, args, err := ds.Executor().ToSQL() + dds.NoError(err) + dds.Empty(args) + dds.Equal(`DELETE FROM "items" WHERE ("id" > 10)`, dsql) + + dsql, args, err = ds.Prepared(true).Executor().ToSQL() + dds.NoError(err) + dds.Equal([]interface{}{int64(10)}, args) + dds.Equal(`DELETE FROM "items" WHERE ("id" > ?)`, dsql) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + dsql, args, err = ds.Executor().ToSQL() + dds.NoError(err) + dds.Equal([]interface{}{int64(10)}, args) + dds.Equal(`DELETE FROM "items" WHERE ("id" > ?)`, dsql) +} + +func (dds *deleteDatasetSuite) TestSetError() { + err1 := errors.New("error #1") + err2 := errors.New("error #2") + err3 := errors.New("error #3") + + // Verify initial error set/get works properly + md := new(mocks.SQLDialect) + ds := dbv2.Delete("test").SetDialect(md) + ds = ds.SetError(err1) + dds.Equal(err1, ds.Error()) + sql, args, err := ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Equal(err1, err) + + // Repeated SetError calls on Dataset should not overwrite the original error + ds = ds.SetError(err2) + dds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Equal(err1, err) + + // Builder functions should not lose the error + ds = ds.ClearLimit() + dds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Equal(err1, err) + + // Deeper errors inside SQL generation should still return original error + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToDeleteSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(err3) + }).Once() + + sql, args, err = ds.ToSQL() + dds.Empty(sql) + dds.Empty(args) + dds.Equal(err1, err) +} + +func TestDeleteDataset(t *testing.T) { + suite.Run(t, new(deleteDatasetSuite)) +} diff --git a/dialect/mysql/mysql.go b/dialect/mysql/mysql.go new file mode 100644 index 0000000..69c97ea --- /dev/null +++ b/dialect/mysql/mysql.go @@ -0,0 +1,87 @@ +package mysql + +import ( + "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" +) + +func DialectOptions() *db.SQLDialectOptions { + opts := db.DefaultDialectOptions() + + opts.SupportsReturn = false + opts.SupportsOrderByOnUpdate = true + opts.SupportsLimitOnUpdate = true + opts.SupportsLimitOnDelete = true + opts.SupportsOrderByOnDelete = true + opts.SupportsConflictUpdateWhere = false + opts.SupportsInsertIgnoreSyntax = true + opts.SupportsConflictTarget = false + opts.SupportsWithCTE = false + opts.SupportsWithCTERecursive = false + opts.SupportsDistinctOn = false + opts.SupportsWindowFunction = false + opts.SupportsDeleteTableHint = true + + opts.UseFromClauseForMultipleUpdateTables = false + + opts.PlaceHolderFragment = []byte("?") + opts.IncludePlaceholderNum = false + opts.QuoteRune = '`' + opts.DefaultValuesFragment = []byte("") + opts.True = []byte("1") + opts.False = []byte("0") + opts.TimeFormat = "2006-01-02 15:04:05" + opts.BooleanOperatorLookup = map[exp.BooleanOperation][]byte{ + exp.EqOp: []byte("="), + exp.NeqOp: []byte("!="), + exp.GtOp: []byte(">"), + exp.GteOp: []byte(">="), + exp.LtOp: []byte("<"), + exp.LteOp: []byte("<="), + exp.InOp: []byte("IN"), + exp.NotInOp: []byte("NOT IN"), + exp.IsOp: []byte("IS"), + exp.IsNotOp: []byte("IS NOT"), + exp.LikeOp: []byte("LIKE BINARY"), + exp.NotLikeOp: []byte("NOT LIKE BINARY"), + exp.ILikeOp: []byte("LIKE"), + exp.NotILikeOp: []byte("NOT LIKE"), + exp.RegexpLikeOp: []byte("REGEXP BINARY"), + exp.RegexpNotLikeOp: []byte("NOT REGEXP BINARY"), + exp.RegexpILikeOp: []byte("REGEXP"), + exp.RegexpNotILikeOp: []byte("NOT REGEXP"), + } + opts.BitwiseOperatorLookup = map[exp.BitwiseOperation][]byte{ + exp.BitwiseInversionOp: []byte("~"), + exp.BitwiseOrOp: []byte("|"), + exp.BitwiseAndOp: []byte("&"), + exp.BitwiseXorOp: []byte("^"), + exp.BitwiseLeftShiftOp: []byte("<<"), + exp.BitwiseRightShiftOp: []byte(">>"), + } + opts.EscapedRunes = map[rune][]byte{ + '\'': []byte("\\'"), + '"': []byte("\\\""), + '\\': []byte("\\\\"), + '\n': []byte("\\n"), + '\r': []byte("\\r"), + 0: []byte("\\x00"), + 0x1a: []byte("\\x1a"), + } + opts.InsertIgnoreClause = []byte("INSERT IGNORE INTO") + opts.ConflictFragment = []byte("") + opts.ConflictDoUpdateFragment = []byte(" ON DUPLICATE KEY UPDATE ") + opts.ConflictDoNothingFragment = []byte("") + return opts +} + +func DialectOptionsV8() *db.SQLDialectOptions { + opts := DialectOptions() + opts.SupportsWindowFunction = true + return opts +} + +func init() { + db.RegisterDialect("mysql", DialectOptions()) + db.RegisterDialect("mysql8", DialectOptionsV8()) +} diff --git a/dialect/mysql/mysql_dialect_test.go b/dialect/mysql/mysql_dialect_test.go new file mode 100644 index 0000000..2787f89 --- /dev/null +++ b/dialect/mysql/mysql_dialect_test.go @@ -0,0 +1,143 @@ +package mysql_test + +import ( + "regexp" + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type ( + mysqlDialectSuite struct { + suite.Suite + } + sqlTestCase struct { + ds exp.SQLExpression + sql string + err string + isPrepared bool + args []interface{} + } +) + +func (mds *mysqlDialectSuite) GetDs(table string) *dbv2.SelectDataset { + return dbv2.Dialect("mysql").From(table) +} + +func (mds *mysqlDialectSuite) assertSQL(cases ...sqlTestCase) { + for i, c := range cases { + actualSQL, actualArgs, err := c.ds.ToSQL() + if c.err == "" { + mds.NoError(err, "test case %d failed", i) + } else { + mds.EqualError(err, c.err, "test case %d failed", i) + } + mds.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + mds.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + mds.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (mds *mysqlDialectSuite) TestIdentifiers() { + ds := mds.GetDs("test") + mds.assertSQL( + sqlTestCase{ds: ds.Select( + "a", + dbv2.I("a.b.c"), + dbv2.I("c.d"), + dbv2.C("test").As("test"), + ), sql: "SELECT `a`, `a`.`b`.`c`, `c`.`d`, `test` AS `test` FROM `test`"}, + ) +} + +func (mds *mysqlDialectSuite) TestLiteralString() { + ds := mds.GetDs("test") + col := dbv2.C("a") + mds.assertSQL( + sqlTestCase{ds: ds.Where(col.Eq("test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test')"}, + sqlTestCase{ds: ds.Where(col.Eq("test'test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\'test')"}, + sqlTestCase{ds: ds.Where(col.Eq(`test"test`)), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\\"test')"}, + sqlTestCase{ds: ds.Where(col.Eq(`test\test`)), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\\\test')"}, + sqlTestCase{ds: ds.Where(col.Eq("test\ntest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\ntest')"}, + sqlTestCase{ds: ds.Where(col.Eq("test\rtest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\rtest')"}, + sqlTestCase{ds: ds.Where(col.Eq("test\x00test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\x00test')"}, + sqlTestCase{ds: ds.Where(col.Eq("test\x1atest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')"}, + ) +} + +func (mds *mysqlDialectSuite) TestLiteralBytes() { + col := dbv2.C("a") + ds := mds.GetDs("test") + mds.assertSQL( + sqlTestCase{ds: ds.Where(col.Eq([]byte("test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte("test'test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\'test')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte(`test"test`))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\\"test')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte(`test\test`))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\\\test')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte("test\ntest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\ntest')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte("test\rtest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\rtest')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte("test\x00test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\x00test')"}, + sqlTestCase{ds: ds.Where(col.Eq([]byte("test\x1atest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\x1atest')"}, + ) +} + +func (mds *mysqlDialectSuite) TestBooleanOperations() { + col := dbv2.C("a") + ds := mds.GetDs("test") + mds.assertSQL( + sqlTestCase{ds: ds.Where(col.Eq(true)), sql: "SELECT * FROM `test` WHERE (`a` IS TRUE)"}, + sqlTestCase{ds: ds.Where(col.Eq(false)), sql: "SELECT * FROM `test` WHERE (`a` IS FALSE)"}, + sqlTestCase{ds: ds.Where(col.Is(true)), sql: "SELECT * FROM `test` WHERE (`a` IS TRUE)"}, + sqlTestCase{ds: ds.Where(col.Is(false)), sql: "SELECT * FROM `test` WHERE (`a` IS FALSE)"}, + sqlTestCase{ds: ds.Where(col.IsTrue()), sql: "SELECT * FROM `test` WHERE (`a` IS TRUE)"}, + sqlTestCase{ds: ds.Where(col.IsFalse()), sql: "SELECT * FROM `test` WHERE (`a` IS FALSE)"}, + sqlTestCase{ds: ds.Where(col.Neq(true)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT TRUE)"}, + sqlTestCase{ds: ds.Where(col.Neq(false)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT FALSE)"}, + sqlTestCase{ds: ds.Where(col.IsNot(true)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT TRUE)"}, + sqlTestCase{ds: ds.Where(col.IsNot(false)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT FALSE)"}, + sqlTestCase{ds: ds.Where(col.IsNotTrue()), sql: "SELECT * FROM `test` WHERE (`a` IS NOT TRUE)"}, + sqlTestCase{ds: ds.Where(col.IsNotFalse()), sql: "SELECT * FROM `test` WHERE (`a` IS NOT FALSE)"}, + sqlTestCase{ds: ds.Where(col.Like("a%")), sql: "SELECT * FROM `test` WHERE (`a` LIKE BINARY 'a%')"}, + sqlTestCase{ds: ds.Where(col.NotLike("a%")), sql: "SELECT * FROM `test` WHERE (`a` NOT LIKE BINARY 'a%')"}, + sqlTestCase{ds: ds.Where(col.ILike("a%")), sql: "SELECT * FROM `test` WHERE (`a` LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(col.NotILike("a%")), sql: "SELECT * FROM `test` WHERE (`a` NOT LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(col.Like(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` REGEXP BINARY '[ab]')"}, + sqlTestCase{ds: ds.Where(col.NotLike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` NOT REGEXP BINARY '[ab]')"}, + sqlTestCase{ds: ds.Where(col.ILike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` REGEXP '[ab]')"}, + sqlTestCase{ds: ds.Where(col.NotILike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` NOT REGEXP '[ab]')"}, + ) +} + +func (mds *mysqlDialectSuite) TestBitwiseOperations() { + col := dbv2.C("a") + ds := mds.GetDs("test") + mds.assertSQL( + sqlTestCase{ds: ds.Where(col.BitwiseInversion()), sql: "SELECT * FROM `test` WHERE (~ `a`)"}, + sqlTestCase{ds: ds.Where(col.BitwiseAnd(1)), sql: "SELECT * FROM `test` WHERE (`a` & 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseOr(1)), sql: "SELECT * FROM `test` WHERE (`a` | 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseXor(1)), sql: "SELECT * FROM `test` WHERE (`a` ^ 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseLeftShift(1)), sql: "SELECT * FROM `test` WHERE (`a` << 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseRightShift(1)), sql: "SELECT * FROM `test` WHERE (`a` >> 1)"}, + ) +} + +func (mds *mysqlDialectSuite) TestUpdateSQL() { + ds := mds.GetDs("test").Update() + mds.assertSQL( + sqlTestCase{ + ds: ds. + Set(dbv2.Record{"foo": "bar"}). + From("test_2"). + Where(dbv2.I("test.id").Eq(dbv2.I("test_2.test_id"))), + sql: "UPDATE `test`,`test_2` SET `foo`='bar' WHERE (`test`.`id` = `test_2`.`test_id`)", + }, + ) +} + +func TestDatasetAdapterSuite(t *testing.T) { + suite.Run(t, new(mysqlDialectSuite)) +} diff --git a/dialect/mysql/mysql_test.go b/dialect/mysql/mysql_test.go new file mode 100644 index 0000000..7c003f2 --- /dev/null +++ b/dialect/mysql/mysql_test.go @@ -0,0 +1,509 @@ +package mysql_test + +import ( + "database/sql" + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/dialect/mysql" + _ "github.com/go-sql-driver/mysql" + "github.com/stretchr/testify/suite" +) + +const ( + dropTable = "DROP TABLE IF EXISTS `entry`;" + createTable = "CREATE TABLE `entry` (" + + "`id` INT NOT NULL AUTO_INCREMENT ," + + "`int` INT NOT NULL UNIQUE," + + "`float` FLOAT NOT NULL ," + + "`string` VARCHAR(255) NOT NULL ," + + "`time` DATETIME NOT NULL ," + + "`bool` TINYINT NOT NULL ," + + "`bytes` BLOB NOT NULL ," + + "PRIMARY KEY (`id`) );" + insertDefaultReords = "INSERT INTO `entry` (`int`, `float`, `string`, `time`, `bool`, `bytes`) VALUES" + + "(0, 0.000000, '0.000000', '2015-02-22 18:19:55', TRUE, '0.000000')," + + "(1, 0.100000, '0.100000', '2015-02-22 19:19:55', FALSE, '0.100000')," + + "(2, 0.200000, '0.200000', '2015-02-22 20:19:55', TRUE, '0.200000')," + + "(3, 0.300000, '0.300000', '2015-02-22 21:19:55', FALSE, '0.300000')," + + "(4, 0.400000, '0.400000', '2015-02-22 22:19:55', TRUE, '0.400000')," + + "(5, 0.500000, '0.500000', '2015-02-22 23:19:55', FALSE, '0.500000')," + + "(6, 0.600000, '0.600000', '2015-02-23 00:19:55', TRUE, '0.600000')," + + "(7, 0.700000, '0.700000', '2015-02-23 01:19:55', FALSE, '0.700000')," + + "(8, 0.800000, '0.800000', '2015-02-23 02:19:55', TRUE, '0.800000')," + + "(9, 0.900000, '0.900000', '2015-02-23 03:19:55', FALSE, '0.900000');" +) + +const defaultDBURI = "root@/dbv2mysql?parseTime=true" + +type ( + mysqlTest struct { + suite.Suite + db *dbv2.Database + } + entry struct { + ID uint32 `db:"id" ff:"skipinsert,skipupdate"` + Int int `db:"int"` + Float float64 `db:"float"` + String string `db:"string"` + Time time.Time `db:"time"` + Bool bool `db:"bool"` + Bytes []byte `db:"bytes"` + } + entryTestCase struct { + ds *dbv2.SelectDataset + len int + check func(entry entry, index int) + err string + } +) + +func (mt *mysqlTest) SetupSuite() { + dbURI := os.Getenv("MYSQL_URI") + if dbURI == "" { + dbURI = defaultDBURI + } + db, err := sql.Open("mysql", dbURI) + if err != nil { + panic(err.Error()) + } + mt.db = dbv2.New("mysql", db) +} + +func (mt *mysqlTest) assertSQL(cases ...sqlTestCase) { + for i, c := range cases { + actualSQL, actualArgs, err := c.ds.ToSQL() + if c.err == "" { + mt.NoError(err, "test case %d failed", i) + } else { + mt.EqualError(err, c.err, "test case %d failed", i) + } + mt.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + mt.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + mt.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (mt *mysqlTest) assertEntries(cases ...entryTestCase) { + for i, c := range cases { + var entries []entry + err := c.ds.ScanStructs(&entries) + if c.err == "" { + mt.NoError(err, "test case %d failed", i) + } else { + mt.EqualError(err, c.err, "test case %d failed", i) + } + mt.Len(entries, c.len) + for index, entry := range entries { + c.check(entry, index) + } + } +} + +func (mt *mysqlTest) SetupTest() { + if _, err := mt.db.Exec(dropTable); err != nil { + panic(err) + } + if _, err := mt.db.Exec(createTable); err != nil { + panic(err) + } + if _, err := mt.db.Exec(insertDefaultReords); err != nil { + panic(err) + } +} + +func (mt *mysqlTest) TestToSQL() { + ds := mt.db.From("entry") + mt.assertSQL( + sqlTestCase{ds: ds.Select("id", "float", "string", "time", "bool"), sql: "SELECT `id`, `float`, `string`, `time`, `bool` FROM `entry`"}, + sqlTestCase{ds: ds.Where(dbv2.C("int").Eq(10)), sql: "SELECT * FROM `entry` WHERE (`int` = 10)"}, + sqlTestCase{ + ds: ds.Prepared(true).Where(dbv2.L("? = ?", dbv2.C("int"), 10)), + sql: "SELECT * FROM `entry` WHERE `int` = ?", args: []interface{}{int64(10)}, + }, + ) +} + +func (mt *mysqlTest) TestQuery() { + ds := mt.db.From("entry") + floatVal := float64(0) + baseDate, err := time.Parse( + "2006-01-02 15:04:05", + "2015-02-22 18:19:55", + ) + mt.NoError(err) + mt.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + mt.Equal(uint32(index+1), entry.ID) + mt.Equal(index, entry.Int) + mt.Equal(f, fmt.Sprintf("%f", entry.Float)) + mt.Equal(f, entry.String) + mt.Equal([]byte(f), entry.Bytes) + mt.Equal(index%2 == 0, entry.Bool) + mt.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + mt.True(entry.Int >= 3) + mt.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + mt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + mt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + mt.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + mt.Fail("Should not have returned any records") + }}, + ) +} + +func (mt *mysqlTest) TestQuery_Prepared() { + ds := mt.db.From("entry").Prepared(true) + floatVal := float64(0) + baseDate, err := time.Parse( + "2006-01-02 15:04:05", + "2015-02-22 18:19:55", + ) + mt.NoError(err) + mt.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + mt.Equal(uint32(index+1), entry.ID) + mt.Equal(index, entry.Int) + mt.Equal(f, fmt.Sprintf("%f", entry.Float)) + mt.Equal(f, entry.String) + mt.Equal([]byte(f), entry.Bytes) + mt.Equal(index%2 == 0, entry.Bool) + mt.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + mt.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + mt.True(entry.Int >= 3) + mt.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + mt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + mt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + mt.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + mt.Fail("Should not have returned any records") + }}, + ) +} + +func (mt *mysqlTest) TestQuery_ValueExpressions() { + type wrappedEntry struct { + entry + BoolValue bool `db:"bool_value"` + } + expectedDate, err := time.Parse("2006-01-02 15:04:05", "2015-02-22 19:19:55") + mt.NoError(err) + ds := mt.db.From("entry").Select(dbv2.Star(), dbv2.V(true).As("bool_value")).Where(dbv2.Ex{"int": 1}) + var we wrappedEntry + found, err := ds.ScanStruct(&we) + mt.NoError(err) + mt.True(found) + mt.Equal(wrappedEntry{ + entry{2, 1, 0.100000, "0.100000", expectedDate, false, []byte("0.100000")}, + true, + }, we) +} + +func (mt *mysqlTest) TestCount() { + ds := mt.db.From("entry") + count, err := ds.Count() + mt.NoError(err) + mt.Equal(int64(10), count) + count, err = ds.Where(dbv2.C("int").Gt(4)).Count() + mt.NoError(err) + mt.Equal(int64(5), count) + count, err = ds.Where(dbv2.C("int").Gte(4)).Count() + mt.NoError(err) + mt.Equal(int64(6), count) + count, err = ds.Where(dbv2.C("string").Like("0.1%")).Count() + mt.NoError(err) + mt.Equal(int64(1), count) + count, err = ds.Where(dbv2.C("string").IsNull()).Count() + mt.NoError(err) + mt.Equal(int64(0), count) +} + +func (mt *mysqlTest) TestInsert() { + ds := mt.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Executor().Exec() + mt.NoError(err) + + var insertedEntry entry + found, err := ds.Where(dbv2.C("int").Eq(10)).ScanStruct(&insertedEntry) + mt.NoError(err) + mt.True(found) + mt.True(insertedEntry.ID > 0) + + entries := []entry{ + {Int: 11, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")}, + {Int: 12, Float: 1.200000, String: "1.200000", Time: now, Bool: true, Bytes: []byte("1.200000")}, + {Int: 13, Float: 1.300000, String: "1.300000", Time: now, Bool: false, Bytes: []byte("1.300000")}, + {Int: 14, Float: 1.400000, String: "1.400000", Time: now, Bool: true, Bytes: []byte("1.400000")}, + } + _, err = ds.Insert().Rows(entries).Executor().Exec() + mt.NoError(err) + + var newEntries []entry + mt.NoError(ds.Where(dbv2.C("int").In([]uint32{11, 12, 13, 14})).ScanStructs(&newEntries)) + mt.Len(newEntries, 4) + for i, e := range newEntries { + mt.Equal(entries[i].Int, e.Int) + mt.Equal(entries[i].Float, e.Float) + mt.Equal(entries[i].String, e.String) + mt.Equal(entries[i].Time.UTC().Format(mysql.DialectOptions().TimeFormat), e.Time.Format(mysql.DialectOptions().TimeFormat)) + mt.Equal(entries[i].Bool, e.Bool) + mt.Equal(entries[i].Bytes, e.Bytes) + } + + _, err = ds.Insert().Rows( + entry{Int: 15, Float: 1.500000, String: "1.500000", Time: now, Bool: false, Bytes: []byte("1.500000")}, + entry{Int: 16, Float: 1.600000, String: "1.600000", Time: now, Bool: true, Bytes: []byte("1.600000")}, + entry{Int: 17, Float: 1.700000, String: "1.700000", Time: now, Bool: false, Bytes: []byte("1.700000")}, + entry{Int: 18, Float: 1.800000, String: "1.800000", Time: now, Bool: true, Bytes: []byte("1.800000")}, + ).Executor().Exec() + mt.NoError(err) + + newEntries = newEntries[0:0] + mt.NoError(ds.Where(dbv2.C("int").In([]uint32{15, 16, 17, 18})).ScanStructs(&newEntries)) + mt.Len(newEntries, 4) +} + +func (mt *mysqlTest) TestInsertReturning() { + ds := mt.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Returning(dbv2.Star()).Executor().ScanStruct(&e) + mt.Error(err) +} + +func (mt *mysqlTest) TestUpdate() { + ds := mt.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + mt.NoError(err) + mt.True(found) + e.Int = 11 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Update().Set(e).Executor().Exec() + mt.NoError(err) + + count, err := ds.Where(dbv2.C("int").Eq(11)).Count() + mt.NoError(err) + mt.Equal(int64(1), count) +} + +func (mt *mysqlTest) TestUpdateReturning() { + ds := mt.db.From("entry") + var id uint32 + _, err := ds.Where(dbv2.C("int").Eq(11)). + Update(). + Set(dbv2.Record{"int": 9}). + Returning("id"). + Executor().ScanVal(&id) + mt.Error(err) + mt.EqualError(err, "dbv2: dialect does not support RETURNING clause [dialect=mysql]") +} + +func (mt *mysqlTest) TestDelete() { + ds := mt.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + mt.NoError(err) + mt.True(found) + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Executor().Exec() + mt.NoError(err) + + count, err := ds.Count() + mt.NoError(err) + mt.Equal(int64(9), count) + + var id uint32 + found, err = ds.Where(dbv2.C("id").Eq(e.ID)).ScanVal(&id) + mt.NoError(err) + mt.False(found) + + e = entry{} + found, err = ds.Where(dbv2.C("int").Eq(8)).Select("id").ScanStruct(&e) + mt.NoError(err) + mt.True(found) + mt.NotEqual(0, e.ID) + + id = 0 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Returning("id").Executor().ScanVal(&id) + mt.EqualError(err, "dbv2: dialect does not support RETURNING clause [dialect=mysql]") +} + +func (mt *mysqlTest) TestInsertIgnore() { + ds := mt.db.From("entry") + now := time.Now() + + // insert one + entries := []entry{ + {Int: 8, Float: 6.100000, String: "6.100000", Time: now, Bytes: []byte("6.100000")}, + {Int: 9, Float: 7.200000, String: "7.200000", Time: now, Bytes: []byte("7.200000")}, + {Int: 10, Float: 7.200000, String: "7.200000", Time: now, Bytes: []byte("7.200000")}, + } + _, err := ds.Insert().Rows(entries).OnConflict(dbv2.DoNothing()).Executor().Exec() + mt.NoError(err) + + count, err := ds.Count() + mt.NoError(err) + mt.Equal(count, int64(11)) +} + +func (mt *mysqlTest) TestInsert_OnConflict() { + ds := mt.db.From("entry") + now := time.Now() + + // insert + e := entry{Int: 10, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")} + _, err := ds.Insert().Rows(e).OnConflict(dbv2.DoNothing()).Executor().Exec() + mt.NoError(err) + + // duplicate + e = entry{Int: 10, Float: 2.100000, String: "2.100000", Time: now.Add(time.Hour * 100), Bool: false, Bytes: []byte("2.100000")} + _, err = ds.Insert().Rows(e).OnConflict(dbv2.DoNothing()).Executor().Exec() + mt.NoError(err) + + // update + var entryActual entry + e2 := entry{Int: 10, String: "2.000000"} + _, err = ds.Insert(). + Rows(e2). + OnConflict(dbv2.DoUpdate("int", dbv2.Record{"string": "upsert"})). + Executor().Exec() + mt.NoError(err) + _, err = ds.Where(dbv2.C("int").Eq(10)).ScanStruct(&entryActual) + mt.NoError(err) + mt.Equal("upsert", entryActual.String) + + // update where should error + entries := []entry{ + {Int: 8, Float: 6.100000, String: "6.100000", Time: now, Bytes: []byte("6.100000")}, + {Int: 9, Float: 7.200000, String: "7.200000", Time: now, Bytes: []byte("7.200000")}, + } + _, err = ds.Insert(). + Rows(entries). + OnConflict(dbv2.DoUpdate("int", dbv2.Record{"string": "upsert"}).Where(dbv2.C("int").Eq(9))). + Executor().Exec() + mt.EqualError(err, "dbv2: dialect does not support upsert with where clause [dialect=mysql]") +} + +func (mt *mysqlTest) TestWindowFunction() { + var version string + ok, err := mt.db.Select(dbv2.Func("version")).ScanVal(&version) + mt.NoError(err) + mt.True(ok) + + fields := strings.Split(version, ".") + mt.True(len(fields) > 0) + major, err := strconv.Atoi(fields[0]) + mt.NoError(err) + if major < 8 { + //nolint:forbidigo + fmt.Printf("SKIPPING MYSQL WINDOW FUNCTION TEST BECAUSE VERSION IS < 8 [mysql_version:=%d]\n", major) + return + } + + ds := mt.db.From("entry"). + Select("int", dbv2.ROW_NUMBER().OverName(dbv2.I("w")).As("id")). + Window(dbv2.W("w").OrderBy(dbv2.I("int").Desc())) + + var entries []entry + mt.NoError(ds.WithDialect("mysql8").ScanStructs(&entries)) + + mt.Equal([]entry{ + {Int: 9, ID: 1}, + {Int: 8, ID: 2}, + {Int: 7, ID: 3}, + {Int: 6, ID: 4}, + {Int: 5, ID: 5}, + {Int: 4, ID: 6}, + {Int: 3, ID: 7}, + {Int: 2, ID: 8}, + {Int: 1, ID: 9}, + {Int: 0, ID: 10}, + }, entries) + + mt.Error(ds.WithDialect("mysql").ScanStructs(&entries), "dbv2: adapter does not support window function clause") +} + +func (mt *mysqlTest) TestInsertFromSelect() { + ds := mt.db.From("entry") + + subquery := dbv2.Select( + dbv2.V(11), + dbv2.V(11), + dbv2.C("float"), + dbv2.C("string"), + dbv2.C("time"), + dbv2.C("bool"), + dbv2.C("bytes"), + ).From(dbv2.T("entry")).Where(dbv2.C("int").Eq(9)) + + query := ds.Insert().Cols().FromQuery(subquery) + _, _, err := query.ToSQL() + + mt.NoError(err) + _, err = query.Executor().Exec() + mt.NoError(err) +} + +func TestMysqlSuite(t *testing.T) { + suite.Run(t, new(mysqlTest)) +} diff --git a/dialect/postgres/postgres.go b/dialect/postgres/postgres.go new file mode 100644 index 0000000..92e9d77 --- /dev/null +++ b/dialect/postgres/postgres.go @@ -0,0 +1,14 @@ +package postgres + +import "git.fsdpf.net/go/db/v2" + +func DialectOptions() *db.SQLDialectOptions { + do := db.DefaultDialectOptions() + do.PlaceHolderFragment = []byte("$") + do.IncludePlaceholderNum = true + return do +} + +func init() { + db.RegisterDialect("postgres", DialectOptions()) +} diff --git a/dialect/postgres/postgres_test.go b/dialect/postgres/postgres_test.go new file mode 100644 index 0000000..dd91f35 --- /dev/null +++ b/dialect/postgres/postgres_test.go @@ -0,0 +1,499 @@ +package postgres_test + +import ( + "database/sql" + "fmt" + "os" + "testing" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + + "github.com/lib/pq" + "github.com/stretchr/testify/suite" +) + +const schema = ` + DROP TABLE IF EXISTS "entry"; + CREATE TABLE "entry" ( + "id" SERIAL PRIMARY KEY NOT NULL, + "int" INT NOT NULL UNIQUE, + "float" NUMERIC NOT NULL , + "string" VARCHAR(45) NOT NULL , + "time" TIMESTAMP NOT NULL , + "bool" BOOL NOT NULL , + "bytes" VARCHAR(45) NOT NULL); + INSERT INTO "entry" ("int", "float", "string", "time", "bool", "bytes") VALUES + (0, 0.000000, '0.000000', '2015-02-22T18:19:55.000000000-00:00', TRUE, '0.000000'), + (1, 0.100000, '0.100000', '2015-02-22T19:19:55.000000000-00:00', FALSE, '0.100000'), + (2, 0.200000, '0.200000', '2015-02-22T20:19:55.000000000-00:00', TRUE, '0.200000'), + (3, 0.300000, '0.300000', '2015-02-22T21:19:55.000000000-00:00', FALSE, '0.300000'), + (4, 0.400000, '0.400000', '2015-02-22T22:19:55.000000000-00:00', TRUE, '0.400000'), + (5, 0.500000, '0.500000', '2015-02-22T23:19:55.000000000-00:00', FALSE, '0.500000'), + (6, 0.600000, '0.600000', '2015-02-23T00:19:55.000000000-00:00', TRUE, '0.600000'), + (7, 0.700000, '0.700000', '2015-02-23T01:19:55.000000000-00:00', FALSE, '0.700000'), + (8, 0.800000, '0.800000', '2015-02-23T02:19:55.000000000-00:00', TRUE, '0.800000'), + (9, 0.900000, '0.900000', '2015-02-23T03:19:55.000000000-00:00', FALSE, '0.900000'); + ` + +const defaultDBURI = "postgres://postgres:@localhost:5435/dbv2postgres?sslmode=disable" + +type ( + postgresTest struct { + suite.Suite + db *dbv2.Database + } + entry struct { + ID uint32 `db:"id" ff:"skipinsert,skipupdate"` + Int int `db:"int"` + Float float64 `db:"float"` + String string `db:"string"` + Time time.Time `db:"time"` + Bool bool `db:"bool"` + Bytes []byte `db:"bytes"` + } + entryTestCase struct { + ds *dbv2.SelectDataset + len int + check func(entry entry, index int) + err string + } +) + +func (pt *postgresTest) assertEntries(cases ...entryTestCase) { + for i, c := range cases { + var entries []entry + err := c.ds.ScanStructs(&entries) + if c.err == "" { + pt.NoError(err, "test case %d failed", i) + } else { + pt.EqualError(err, c.err, "test case %d failed", i) + } + pt.Len(entries, c.len) + for index, entry := range entries { + c.check(entry, index) + } + } +} + +func (pt *postgresTest) SetupSuite() { + dbURI := os.Getenv("PG_URI") + if dbURI == "" { + dbURI = defaultDBURI + } + uri, err := pq.ParseURL(dbURI) + if err != nil { + panic(err) + } + db, err := sql.Open("postgres", uri) + if err != nil { + panic(err) + } + pt.db = dbv2.New("postgres", db) +} + +func (pt *postgresTest) SetupTest() { + if _, err := pt.db.Exec(schema); err != nil { + panic(err) + } +} + +func (pt *postgresTest) TestToSQL() { + ds := pt.db.From("entry") + s, _, err := ds.Select("id", "float", "string", "time", "bool").ToSQL() + pt.NoError(err) + pt.Equal(`SELECT "id", "float", "string", "time", "bool" FROM "entry"`, s) + + s, _, err = ds.Where(dbv2.C("int").Eq(10)).ToSQL() + pt.NoError(err) + pt.Equal(`SELECT * FROM "entry" WHERE ("int" = 10)`, s) + + s, args, err := ds.Prepared(true).Where(dbv2.L("? = ?", dbv2.C("int"), 10)).ToSQL() + pt.NoError(err) + pt.Equal([]interface{}{int64(10)}, args) + pt.Equal(`SELECT * FROM "entry" WHERE "int" = $1`, s) +} + +func (pt *postgresTest) TestQuery() { + ds := pt.db.From("entry") + floatVal := float64(0) + baseDate, err := time.Parse(time.RFC3339Nano, "2015-02-22T18:19:55.000000000-00:00") + pt.NoError(err) + baseDate = baseDate.UTC() + pt.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + pt.Equal(uint32(index+1), entry.ID) + pt.Equal(index, entry.Int) + pt.Equal(f, fmt.Sprintf("%f", entry.Float)) + pt.Equal(f, entry.String) + pt.Equal([]byte(f), entry.Bytes) + pt.Equal(index%2 == 0, entry.Bool) + pt.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + pt.True(entry.Int >= 3) + pt.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + pt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + pt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + pt.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + pt.Fail("Should not have returned any records") + }}, + ) +} + +func (pt *postgresTest) TestQuery_Prepared() { + ds := pt.db.From("entry").Prepared(true) + floatVal := float64(0) + baseDate, err := time.Parse(time.RFC3339Nano, "2015-02-22T18:19:55.000000000-00:00") + pt.NoError(err) + baseDate = baseDate.UTC() + pt.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + pt.Equal(uint32(index+1), entry.ID) + pt.Equal(index, entry.Int) + pt.Equal(f, fmt.Sprintf("%f", entry.Float)) + pt.Equal(f, entry.String) + pt.Equal([]byte(f), entry.Bytes) + pt.Equal(index%2 == 0, entry.Bool) + pt.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + pt.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + pt.True(entry.Int >= 3) + pt.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + pt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + pt.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + pt.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + pt.Fail("Should not have returned any records") + }}, + ) +} + +func (pt *postgresTest) TestQuery_ValueExpressions() { + type wrappedEntry struct { + entry + BoolValue bool `db:"bool_value"` + } + expectedDate, err := time.Parse(time.RFC3339Nano, "2015-02-22T19:19:55.000000000-00:00") + pt.NoError(err) + ds := pt.db.From("entry").Select(dbv2.Star(), dbv2.V(true).As("bool_value")).Where(dbv2.Ex{"int": 1}) + var we wrappedEntry + found, err := ds.ScanStruct(&we) + pt.NoError(err) + pt.True(found) + pt.Equal(1, we.Int) + pt.Equal(0.100000, we.Float) + pt.Equal("0.100000", we.String) + pt.Equal(expectedDate.Unix(), we.Time.Unix()) + pt.Equal(false, we.Bool) + pt.Equal([]byte("0.100000"), we.Bytes) + pt.True(we.BoolValue) +} + +func (pt *postgresTest) TestCount() { + ds := pt.db.From("entry") + count, err := ds.Count() + pt.NoError(err) + pt.Equal(int64(10), count) + count, err = ds.Where(dbv2.C("int").Gt(4)).Count() + pt.NoError(err) + pt.Equal(int64(5), count) + count, err = ds.Where(dbv2.C("int").Gte(4)).Count() + pt.NoError(err) + pt.Equal(int64(6), count) + count, err = ds.Where(dbv2.C("string").Like("0.1%")).Count() + pt.NoError(err) + pt.Equal(int64(1), count) + count, err = ds.Where(dbv2.C("string").IsNull()).Count() + pt.NoError(err) + pt.Equal(int64(0), count) +} + +func (pt *postgresTest) TestInsert() { + ds := pt.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Executor().Exec() + pt.NoError(err) + + var insertedEntry entry + found, err := ds.Where(dbv2.C("int").Eq(10)).ScanStruct(&insertedEntry) + pt.NoError(err) + pt.True(found) + pt.True(insertedEntry.ID > 0) + + entries := []entry{ + {Int: 11, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")}, + {Int: 12, Float: 1.200000, String: "1.200000", Time: now, Bool: true, Bytes: []byte("1.200000")}, + {Int: 13, Float: 1.300000, String: "1.300000", Time: now, Bool: false, Bytes: []byte("1.300000")}, + {Int: 14, Float: 1.400000, String: "1.400000", Time: now, Bool: true, Bytes: []byte("1.400000")}, + } + _, err = ds.Insert().Rows(entries).Executor().Exec() + pt.NoError(err) + + var newEntries []entry + + pt.NoError(ds.Where(dbv2.C("int").In([]uint32{11, 12, 13, 14})).ScanStructs(&newEntries)) + pt.Len(newEntries, 4) + for i, e := range newEntries { + pt.Equal(entries[i].Int, e.Int) + pt.Equal(entries[i].Float, e.Float) + pt.Equal(entries[i].String, e.String) + pt.Equal(entries[i].Time.Unix(), e.Time.Unix()) + pt.Equal(entries[i].Bool, e.Bool) + pt.Equal(entries[i].Bytes, e.Bytes) + } + + _, err = ds.Insert().Rows( + entry{Int: 15, Float: 1.500000, String: "1.500000", Time: now, Bool: false, Bytes: []byte("1.500000")}, + entry{Int: 16, Float: 1.600000, String: "1.600000", Time: now, Bool: true, Bytes: []byte("1.600000")}, + entry{Int: 17, Float: 1.700000, String: "1.700000", Time: now, Bool: false, Bytes: []byte("1.700000")}, + entry{Int: 18, Float: 1.800000, String: "1.800000", Time: now, Bool: true, Bytes: []byte("1.800000")}, + ).Executor().Exec() + pt.NoError(err) + + newEntries = newEntries[0:0] + pt.NoError(ds.Where(dbv2.C("int").In([]uint32{15, 16, 17, 18})).ScanStructs(&newEntries)) + pt.Len(newEntries, 4) +} + +func (pt *postgresTest) TestInsertReturning() { + ds := pt.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + found, err := ds.Insert().Rows(e).Returning(dbv2.Star()).Executor().ScanStruct(&e) + pt.NoError(err) + pt.True(found) + pt.True(e.ID > 0) + + var ids []uint32 + pt.NoError(ds.Insert().Rows([]entry{ + {Int: 11, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")}, + {Int: 12, Float: 1.200000, String: "1.200000", Time: now, Bool: true, Bytes: []byte("1.200000")}, + {Int: 13, Float: 1.300000, String: "1.300000", Time: now, Bool: false, Bytes: []byte("1.300000")}, + {Int: 14, Float: 1.400000, String: "1.400000", Time: now, Bool: true, Bytes: []byte("1.400000")}, + }).Returning("id").Executor().ScanVals(&ids)) + pt.Len(ids, 4) + for _, id := range ids { + pt.True(id > 0) + } + + var ints []int64 + pt.NoError(ds.Insert().Rows( + entry{Int: 15, Float: 1.500000, String: "1.500000", Time: now, Bool: false, Bytes: []byte("1.500000")}, + entry{Int: 16, Float: 1.600000, String: "1.600000", Time: now, Bool: true, Bytes: []byte("1.600000")}, + entry{Int: 17, Float: 1.700000, String: "1.700000", Time: now, Bool: false, Bytes: []byte("1.700000")}, + entry{Int: 18, Float: 1.800000, String: "1.800000", Time: now, Bool: true, Bytes: []byte("1.800000")}, + ).Returning("int").Executor().ScanVals(&ints)) + pt.True(found) + pt.Equal(ints, []int64{15, 16, 17, 18}) +} + +func (pt *postgresTest) TestUpdate() { + ds := pt.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + pt.NoError(err) + pt.True(found) + e.Int = 11 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Update().Set(e).Executor().Exec() + pt.NoError(err) + + count, err := ds.Where(dbv2.C("int").Eq(11)).Count() + pt.NoError(err) + pt.Equal(int64(1), count) + + var id uint32 + found, err = ds.Where(dbv2.C("int").Eq(11)). + Update(). + Set(dbv2.Record{"int": 9}). + Returning("id").Executor().ScanVal(&id) + pt.NoError(err) + pt.True(found) + pt.Equal(id, e.ID) +} + +func (pt *postgresTest) TestUpdateSQL_multipleTables() { + ds := pt.db.Update("test") + updateSQL, _, err := ds. + Set(dbv2.Record{"foo": "bar"}). + From("test_2"). + Where(dbv2.I("test.id").Eq(dbv2.I("test_2.test_id"))). + ToSQL() + pt.NoError(err) + pt.Equal(`UPDATE "test" SET "foo"='bar' FROM "test_2" WHERE ("test"."id" = "test_2"."test_id")`, updateSQL) +} + +func (pt *postgresTest) TestDelete() { + ds := pt.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + pt.NoError(err) + pt.True(found) + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Executor().Exec() + pt.NoError(err) + + count, err := ds.Count() + pt.NoError(err) + pt.Equal(int64(9), count) + + var id uint32 + found, err = ds.Where(dbv2.C("id").Eq(e.ID)).ScanVal(&id) + pt.NoError(err) + pt.False(found) + + e = entry{} + found, err = ds.Where(dbv2.C("int").Eq(8)).Select("id").ScanStruct(&e) + pt.NoError(err) + pt.True(found) + pt.NotEqual(e.ID, int64(0)) + + id = 0 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Returning("id").Executor().ScanVal(&id) + pt.NoError(err) + pt.Equal(id, e.ID) +} + +func (pt *postgresTest) TestInsert_OnConflict() { + ds := pt.db.From("entry") + now := time.Now() + + // DO NOTHING insert + e := entry{Int: 10, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")} + _, err := ds.Insert().Rows(e).OnConflict(dbv2.DoNothing()).Executor().Exec() + pt.NoError(err) + + // DO NOTHING duplicate + e = entry{Int: 10, Float: 2.100000, String: "2.100000", Time: now.Add(time.Hour * 100), Bool: false, Bytes: []byte("2.100000")} + _, err = ds.Insert().Rows(e).OnConflict(dbv2.DoNothing()).Executor().Exec() + pt.NoError(err) + + // DO NOTHING update + var entryActual entry + e2 := entry{Int: 0, String: "2.000000"} + _, err = ds.Insert(). + Rows(e2). + OnConflict(dbv2.DoUpdate("int", dbv2.Record{"string": "upsert"})). + Executor().Exec() + pt.NoError(err) + _, err = ds.Where(dbv2.C("int").Eq(0)).ScanStruct(&entryActual) + pt.NoError(err) + pt.Equal("upsert", entryActual.String) + + // DO NOTHING update where + entries := []entry{ + {Int: 1, Float: 6.100000, String: "6.100000", Time: now, Bytes: []byte("6.100000")}, + {Int: 2, Float: 7.200000, String: "7.200000", Time: now, Bytes: []byte("7.200000")}, + } + _, err = ds.Insert(). + Rows(entries). + OnConflict(dbv2.DoUpdate("int", dbv2.Record{"string": "upsert"}).Where(dbv2.I("excluded.int").Eq(2))). + Executor(). + Exec() + pt.NoError(err) + + var entry8, entry9 entry + _, err = ds.Where(dbv2.Ex{"int": 1}).ScanStruct(&entry8) + pt.NoError(err) + pt.Equal("0.100000", entry8.String) + + _, err = ds.Where(dbv2.Ex{"int": 2}).ScanStruct(&entry9) + pt.NoError(err) + pt.Equal("upsert", entry9.String) +} + +func (pt *postgresTest) TestWindowFunction() { + ds := pt.db.From("entry"). + Select("int", dbv2.ROW_NUMBER().OverName(dbv2.I("w")).As("id")). + Window(dbv2.W("w").OrderBy(dbv2.I("int").Desc())) + + var entries []entry + pt.NoError(ds.ScanStructs(&entries)) + + pt.Equal([]entry{ + {Int: 9, ID: 1}, + {Int: 8, ID: 2}, + {Int: 7, ID: 3}, + {Int: 6, ID: 4}, + {Int: 5, ID: 5}, + {Int: 4, ID: 6}, + {Int: 3, ID: 7}, + {Int: 2, ID: 8}, + {Int: 1, ID: 9}, + {Int: 0, ID: 10}, + }, entries) +} + +func (pt *postgresTest) TestOrderByFunction() { + ds := pt.db.From("entry"). + Select(dbv2.ROW_NUMBER().Over(dbv2.W()).As("id")).Window().Order(dbv2.ROW_NUMBER().Over(dbv2.W()).Desc()) + + var entries []entry + pt.NoError(ds.ScanStructs(&entries)) + + pt.Equal([]entry{ + {ID: 10}, + {ID: 9}, + {ID: 8}, + {ID: 7}, + {ID: 6}, + {ID: 5}, + {ID: 4}, + {ID: 3}, + {ID: 2}, + {ID: 1}, + }, entries) +} + +func TestPostgresSuite(t *testing.T) { + suite.Run(t, new(postgresTest)) +} diff --git a/dialect/sqlite3/sqlite3.go b/dialect/sqlite3/sqlite3.go new file mode 100644 index 0000000..e2f7e0b --- /dev/null +++ b/dialect/sqlite3/sqlite3.go @@ -0,0 +1,76 @@ +package sqlite3 + +import ( + "time" + + "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" +) + +func DialectOptions() *db.SQLDialectOptions { + opts := db.DefaultDialectOptions() + + opts.SupportsReturn = true + opts.SupportsOrderByOnUpdate = true + opts.SupportsLimitOnUpdate = true + opts.SupportsOrderByOnDelete = true + opts.SupportsLimitOnDelete = true + opts.SupportsConflictUpdateWhere = false + opts.SupportsInsertIgnoreSyntax = true + opts.SupportsConflictTarget = true + opts.SupportsMultipleUpdateTables = false + opts.WrapCompoundsInParens = false + opts.SupportsDistinctOn = false + opts.SupportsWindowFunction = false + opts.SupportsLateral = false + + opts.PlaceHolderFragment = []byte("?") + opts.IncludePlaceholderNum = false + opts.QuoteRune = '`' + opts.DefaultValuesFragment = []byte("") + opts.True = []byte("1") + opts.False = []byte("0") + opts.TimeFormat = time.RFC3339Nano + opts.BooleanOperatorLookup = map[exp.BooleanOperation][]byte{ + exp.EqOp: []byte("="), + exp.NeqOp: []byte("!="), + exp.GtOp: []byte(">"), + exp.GteOp: []byte(">="), + exp.LtOp: []byte("<"), + exp.LteOp: []byte("<="), + exp.InOp: []byte("IN"), + exp.NotInOp: []byte("NOT IN"), + exp.IsOp: []byte("IS"), + exp.IsNotOp: []byte("IS NOT"), + exp.LikeOp: []byte("LIKE"), + exp.NotLikeOp: []byte("NOT LIKE"), + exp.ILikeOp: []byte("LIKE"), + exp.NotILikeOp: []byte("NOT LIKE"), + exp.RegexpLikeOp: []byte("REGEXP"), + exp.RegexpNotLikeOp: []byte("NOT REGEXP"), + exp.RegexpILikeOp: []byte("REGEXP"), + exp.RegexpNotILikeOp: []byte("NOT REGEXP"), + } + opts.UseLiteralIsBools = false + opts.BitwiseOperatorLookup = map[exp.BitwiseOperation][]byte{ + exp.BitwiseOrOp: []byte("|"), + exp.BitwiseAndOp: []byte("&"), + exp.BitwiseLeftShiftOp: []byte("<<"), + exp.BitwiseRightShiftOp: []byte(">>"), + } + opts.EscapedRunes = map[rune][]byte{ + '\'': []byte("''"), + } + opts.InsertIgnoreClause = []byte("INSERT OR IGNORE INTO ") + opts.ConflictFragment = []byte(" ON CONFLICT ") + opts.ConflictDoUpdateFragment = []byte(" DO UPDATE SET ") + opts.ConflictDoNothingFragment = []byte(" DO NOTHING ") + opts.ForUpdateFragment = []byte("") + opts.OfFragment = []byte("") + opts.NowaitFragment = []byte("") + return opts +} + +func init() { + db.RegisterDialect("sqlite3", DialectOptions()) +} diff --git a/dialect/sqlite3/sqlite3_dialect_test.go b/dialect/sqlite3/sqlite3_dialect_test.go new file mode 100644 index 0000000..bfe6495 --- /dev/null +++ b/dialect/sqlite3/sqlite3_dialect_test.go @@ -0,0 +1,158 @@ +package sqlite3_test + +import ( + "regexp" + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type ( + sqlite3DialectSuite struct { + suite.Suite + } + sqlTestCase struct { + ds exp.SQLExpression + sql string + err string + isPrepared bool + args []interface{} + } +) + +func (sds *sqlite3DialectSuite) GetDs(table string) *dbv2.SelectDataset { + return dbv2.Dialect("sqlite3").From(table) +} + +func (sds *sqlite3DialectSuite) assertSQL(cases ...sqlTestCase) { + for i, c := range cases { + actualSQL, actualArgs, err := c.ds.ToSQL() + if c.err == "" { + sds.NoError(err, "test case %d failed", i) + } else { + sds.EqualError(err, c.err, "test case %d failed", i) + } + sds.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + sds.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + sds.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (sds *sqlite3DialectSuite) TestIdentifiers() { + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Select( + "a", + dbv2.I("a.b.c"), + dbv2.I("c.d"), + dbv2.C("test").As("test"), + ), sql: "SELECT `a`, `a`.`b`.`c`, `c`.`d`, `test` AS `test` FROM `test`"}, + ) +} + +func (sds *sqlite3DialectSuite) TestUpdateSQL_multipleTables() { + ds := sds.GetDs("test").Update() + sds.assertSQL( + sqlTestCase{ + ds: ds. + Set(dbv2.Record{"foo": "bar"}). + From("test_2"). + Where(dbv2.I("test.id").Eq(dbv2.I("test_2.test_id"))), + err: "db: sqlite3 dialect does not support multiple tables in UPDATE", + }, + ) +} + +func (sds *sqlite3DialectSuite) TestCompoundExpressions() { + ds1 := sds.GetDs("test").Select("a") + ds2 := sds.GetDs("test2").Select("b") + sds.assertSQL( + sqlTestCase{ds: ds1.Union(ds2), sql: "SELECT `a` FROM `test` UNION SELECT `b` FROM `test2`"}, + sqlTestCase{ds: ds1.UnionAll(ds2), sql: "SELECT `a` FROM `test` UNION ALL SELECT `b` FROM `test2`"}, + sqlTestCase{ds: ds1.Intersect(ds2), sql: "SELECT `a` FROM `test` INTERSECT SELECT `b` FROM `test2`"}, + ) +} + +func (sds *sqlite3DialectSuite) TestLiteralString() { + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test'test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test''test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(`test"test`)), sql: "SELECT * FROM `test` WHERE (`a` = 'test\"test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(`test\test`)), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test\ntest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\ntest')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test\rtest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\rtest')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test\x00test")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\x00test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq("test\x1atest")), sql: "SELECT * FROM `test` WHERE (`a` = 'test\x1atest')"}, + ) +} + +func (sds *sqlite3DialectSuite) TestLiteralBytes() { + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test'test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test''test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte(`test"test`))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\"test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte(`test\test`))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\\test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test\ntest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\ntest')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test\rtest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\rtest')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test\x00test"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\x00test')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq([]byte("test\x1atest"))), sql: "SELECT * FROM `test` WHERE (`a` = 'test\x1atest')"}, + ) +} + +func (sds *sqlite3DialectSuite) TestBooleanOperations() { + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(true)), sql: "SELECT * FROM `test` WHERE (`a` IS 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(false)), sql: "SELECT * FROM `test` WHERE (`a` IS 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Is(true)), sql: "SELECT * FROM `test` WHERE (`a` IS 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Is(false)), sql: "SELECT * FROM `test` WHERE (`a` IS 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsTrue()), sql: "SELECT * FROM `test` WHERE (`a` IS 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsFalse()), sql: "SELECT * FROM `test` WHERE (`a` IS 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Neq(true)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Neq(false)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsNot(true)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsNot(false)), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsNotTrue()), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").IsNotFalse()), sql: "SELECT * FROM `test` WHERE (`a` IS NOT 0)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Like("a%")), sql: "SELECT * FROM `test` WHERE (`a` LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").NotLike("a%")), sql: "SELECT * FROM `test` WHERE (`a` NOT LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").ILike("a%")), sql: "SELECT * FROM `test` WHERE (`a` LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").NotILike("a%")), sql: "SELECT * FROM `test` WHERE (`a` NOT LIKE 'a%')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Like(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` REGEXP '[ab]')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").NotLike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` NOT REGEXP '[ab]')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").ILike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` REGEXP '[ab]')"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").NotILike(regexp.MustCompile("[ab]"))), sql: "SELECT * FROM `test` WHERE (`a` NOT REGEXP '[ab]')"}, + ) +} + +func (sds *sqlite3DialectSuite) TestBitwiseOperations() { + col := dbv2.C("a") + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(col.BitwiseInversion()), err: "dbv2: bitwise operator 'Inversion' not supported"}, + sqlTestCase{ds: ds.Where(col.BitwiseAnd(1)), sql: "SELECT * FROM `test` WHERE (`a` & 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseOr(1)), sql: "SELECT * FROM `test` WHERE (`a` | 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseXor(1)), err: "dbv2: bitwise operator 'XOR' not supported"}, + sqlTestCase{ds: ds.Where(col.BitwiseLeftShift(1)), sql: "SELECT * FROM `test` WHERE (`a` << 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseRightShift(1)), sql: "SELECT * FROM `test` WHERE (`a` >> 1)"}, + ) +} + +func (sds *sqlite3DialectSuite) TestForUpdate() { + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(1)).ForUpdate(dbv2.Wait), sql: "SELECT * FROM `test` WHERE (`a` = 1)"}, + sqlTestCase{ds: ds.Where(dbv2.C("a").Eq(1)).ForUpdate(dbv2.NoWait), sql: "SELECT * FROM `test` WHERE (`a` = 1)"}, + ) +} + +func TestDatasetAdapterSuite(t *testing.T) { + suite.Run(t, new(sqlite3DialectSuite)) +} diff --git a/dialect/sqlite3/sqlite3_test.go b/dialect/sqlite3/sqlite3_test.go new file mode 100644 index 0000000..3aa207d --- /dev/null +++ b/dialect/sqlite3/sqlite3_test.go @@ -0,0 +1,469 @@ +package sqlite3_test + +import ( + "database/sql" + "fmt" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/dialect/mysql" + "git.fsdpf.net/go/db/v2/dialect/sqlite3" + + "github.com/stretchr/testify/suite" +) + +const ( + dropTable = "DROP TABLE IF EXISTS `entry`;" + createTable = "CREATE TABLE `entry` (" + + "`id` INTEGER PRIMARY KEY," + + "`int` INT NOT NULL ," + + "`float` FLOAT NOT NULL ," + + "`string` VARCHAR(255) NOT NULL ," + + "`time` DATETIME NOT NULL ," + + "`bool` TINYINT NOT NULL ," + + "`bytes` BLOB NOT NULL" + + ");" + insertDefaultRecords = "INSERT INTO `entry` (`int`, `float`, `string`, `time`, `bool`, `bytes`) VALUES" + + "(0, 0.000000, '0.000000', '2015-02-22T18:19:55.000000000-00:00', 1, '0.000000')," + + "(1, 0.100000, '0.100000', '2015-02-22T19:19:55.000000000-00:00', 0, '0.100000')," + + "(2, 0.200000, '0.200000', '2015-02-22T20:19:55.000000000-00:00', 1, '0.200000')," + + "(3, 0.300000, '0.300000', '2015-02-22T21:19:55.000000000-00:00', 0, '0.300000')," + + "(4, 0.400000, '0.400000', '2015-02-22T22:19:55.000000000-00:00', 1, '0.400000')," + + "(5, 0.500000, '0.500000', '2015-02-22T23:19:55.000000000-00:00', 0, '0.500000')," + + "(6, 0.600000, '0.600000', '2015-02-23T00:19:55.000000000-00:00', 1, '0.600000')," + + "(7, 0.700000, '0.700000', '2015-02-23T01:19:55.000000000-00:00', 0, '0.700000')," + + "(8, 0.800000, '0.800000', '2015-02-23T02:19:55.000000000-00:00', 1, '0.800000')," + + "(9, 0.900000, '0.900000', '2015-02-23T03:19:55.000000000-00:00', 0, '0.900000');" +) + +var dbURI = ":memory:" + +type ( + sqlite3Suite struct { + suite.Suite + db *dbv2.Database + } + entry struct { + ID uint32 `db:"id" ff:"skipinsert,skipupdate"` + Int int `db:"int"` + Float float64 `db:"float"` + String string `db:"string"` + Time time.Time `db:"time"` + Bool bool `db:"bool"` + Bytes []byte `db:"bytes"` + } + entryTestCase struct { + ds *dbv2.SelectDataset + len int + check func(entry entry, index int) + err string + } +) + +func (st *sqlite3Suite) SetupSuite() { + db, err := sql.Open("sqlite3", dbURI) + if err != nil { + panic(err.Error()) + } + st.db = dbv2.New("sqlite3", db) +} + +func (st *sqlite3Suite) assertSQL(cases ...sqlTestCase) { + for i, c := range cases { + actualSQL, actualArgs, err := c.ds.ToSQL() + if c.err == "" { + st.NoError(err, "test case %d failed", i) + } else { + st.EqualError(err, c.err, "test case %d failed", i) + } + st.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + st.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + st.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (st *sqlite3Suite) assertEntries(cases ...entryTestCase) { + for i, c := range cases { + var entries []entry + err := c.ds.ScanStructs(&entries) + if c.err == "" { + st.NoError(err, "test case %d failed", i) + } else { + st.EqualError(err, c.err, "test case %d failed", i) + } + st.Len(entries, c.len) + for index, entry := range entries { + c.check(entry, index) + } + } +} + +func (st *sqlite3Suite) SetupTest() { + if _, err := st.db.Exec(dropTable); err != nil { + panic(err) + } + if _, err := st.db.Exec(createTable); err != nil { + panic(err) + } + if _, err := st.db.Exec(insertDefaultRecords); err != nil { + panic(err) + } +} + +func (st *sqlite3Suite) TestSelectSQL() { + ds := st.db.From("entry") + st.assertSQL( + sqlTestCase{ds: ds.Select("id", "float", "string", "time", "bool"), sql: "SELECT `id`, `float`, `string`, `time`, `bool` FROM `entry`"}, + sqlTestCase{ds: ds.Where(dbv2.C("int").Eq(10)), sql: "SELECT * FROM `entry` WHERE (`int` = 10)"}, + sqlTestCase{ + ds: ds.Prepared(true).Where(dbv2.L("? = ?", dbv2.C("int"), 10)), + sql: "SELECT * FROM `entry` WHERE `int` = ?", + args: []interface{}{int64(10)}, + }, + ) +} + +func (st *sqlite3Suite) TestCompoundQueries() { + ds1 := st.db.From("entry").Select("int").Where(dbv2.C("int").Gt(0)) + ds2 := st.db.From("entry").Select("int").Where(dbv2.C("int").Gt(5)) + + var ids []int64 + err := ds1.Union(ds2).ScanVals(&ids) + st.NoError(err) + st.Equal([]int64{1, 2, 3, 4, 5, 6, 7, 8, 9}, ids) + + ids = ids[0:0] + err = ds1.UnionAll(ds2).ScanVals(&ids) + st.NoError(err) + st.Equal([]int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 7, 8, 9}, ids) + + ids = ids[0:0] + err = ds1.Intersect(ds2).ScanVals(&ids) + st.NoError(err) + st.Equal([]int64{6, 7, 8, 9}, ids) +} + +func (st *sqlite3Suite) TestQuery() { + ds := st.db.From("entry") + floatVal := float64(0) + baseDate, err := time.Parse(sqlite3.DialectOptions().TimeFormat, "2015-02-22T18:19:55.000000000-00:00") + st.NoError(err) + st.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + st.Equal(uint32(index+1), entry.ID) + st.Equal(index, entry.Int) + st.Equal(f, fmt.Sprintf("%f", entry.Float)) + st.Equal(f, entry.String) + st.Equal([]byte(f), entry.Bytes) + st.Equal(index%2 == 0, entry.Bool) + st.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + st.True(entry.Int >= 3) + st.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + st.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + st.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + st.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + st.Fail("Should not have returned any records") + }}, + ) +} + +func (st *sqlite3Suite) TestQuery_Prepared() { + ds := st.db.From("entry").Prepared(true) + floatVal := float64(0) + baseDate, err := time.Parse(sqlite3.DialectOptions().TimeFormat, "2015-02-22T18:19:55.000000000-00:00") + st.NoError(err) + st.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + st.Equal(uint32(index+1), entry.ID) + st.Equal(index, entry.Int) + st.Equal(f, fmt.Sprintf("%f", entry.Float)) + st.Equal(f, entry.String) + st.Equal([]byte(f), entry.Bytes) + st.Equal(index%2 == 0, entry.Bool) + st.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Bool) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + st.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + st.True(entry.Int >= 3) + st.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + st.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + st.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + st.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + st.Fail("Should not have returned any records") + }}, + ) +} + +func (st *sqlite3Suite) TestQuery_ValueExpressions() { + type wrappedEntry struct { + entry + BoolValue bool `db:"bool_value"` + } + expectedDate, err := time.Parse("2006-01-02T15:04:05.000000000-00:00", "2015-02-22T19:19:55.000000000-00:00") + st.NoError(err) + ds := st.db.From("entry").Select(dbv2.Star(), dbv2.V(true).As("bool_value")).Where(dbv2.Ex{"int": 1}) + var we wrappedEntry + found, err := ds.ScanStruct(&we) + st.NoError(err) + st.True(found) + st.Equal(we, wrappedEntry{ + entry{2, 1, 0.100000, "0.100000", expectedDate, false, []byte("0.100000")}, + true, + }) +} + +func (st *sqlite3Suite) TestCount() { + ds := st.db.From("entry") + count, err := ds.Count() + st.NoError(err) + st.Equal(int64(10), count) + count, err = ds.Where(dbv2.C("int").Gt(4)).Count() + st.NoError(err) + st.Equal(int64(5), count) + count, err = ds.Where(dbv2.C("int").Gte(4)).Count() + st.NoError(err) + st.Equal(int64(6), count) + count, err = ds.Where(dbv2.C("string").Like("0.1%")).Count() + st.NoError(err) + st.Equal(int64(1), count) + count, err = ds.Where(dbv2.C("string").IsNull()).Count() + st.NoError(err) + st.Equal(int64(0), count) +} + +func (st *sqlite3Suite) TestInsert() { + ds := st.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Executor().Exec() + st.NoError(err) + + var insertedEntry entry + found, err := ds.Where(dbv2.C("int").Eq(10)).ScanStruct(&insertedEntry) + st.NoError(err) + st.True(found) + st.True(insertedEntry.ID > 0) + + entries := []entry{ + {Int: 11, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")}, + {Int: 12, Float: 1.200000, String: "1.200000", Time: now, Bool: true, Bytes: []byte("1.200000")}, + {Int: 13, Float: 1.300000, String: "1.300000", Time: now, Bool: false, Bytes: []byte("1.300000")}, + {Int: 14, Float: 1.400000, String: "1.400000", Time: now, Bool: true, Bytes: []byte("1.400000")}, + {Int: 14, Float: 1.400000, String: `abc'd"e"f\\gh\n\ri\x00`, Time: now, Bool: true, Bytes: []byte("1.400000")}, + } + _, err = ds.Insert().Rows(entries).Executor().Exec() + st.NoError(err) + + var newEntries []entry + st.NoError(ds.Where(dbv2.C("int").In([]uint32{11, 12, 13, 14})).ScanStructs(&newEntries)) + for i, e := range newEntries { + st.Equal(entries[i].Int, e.Int) + st.Equal(entries[i].Float, e.Float) + st.Equal(entries[i].String, e.String) + st.Equal(entries[i].Time.UTC().Format(mysql.DialectOptions().TimeFormat), e.Time.Format(mysql.DialectOptions().TimeFormat)) + st.Equal(entries[i].Bool, e.Bool) + st.Equal(entries[i].Bytes, e.Bytes) + } + + _, err = ds.Insert().Rows( + entry{Int: 15, Float: 1.500000, String: "1.500000", Time: now, Bool: false, Bytes: []byte("1.500000")}, + entry{Int: 16, Float: 1.600000, String: "1.600000", Time: now, Bool: true, Bytes: []byte("1.600000")}, + entry{Int: 17, Float: 1.700000, String: "1.700000", Time: now, Bool: false, Bytes: []byte("1.700000")}, + entry{Int: 18, Float: 1.800000, String: "1.800000", Time: now, Bool: true, Bytes: []byte("1.800000")}, + entry{Int: 18, Float: 1.800000, String: `abc'd"e"f\\gh\n\ri\x00`, Time: now, Bool: true, Bytes: []byte("1.800000")}, + ).Executor().Exec() + st.NoError(err) + + newEntries = newEntries[0:0] + st.NoError(ds.Where(dbv2.C("int").In([]uint32{15, 16, 17, 18})).ScanStructs(&newEntries)) + st.Len(newEntries, 5) +} + +func (st *sqlite3Suite) TestInsert_returning() { + ds := st.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Returning(dbv2.Star()).Executor().ScanStruct(&e) + st.Error(err) +} + +func (st *sqlite3Suite) TestUpdate() { + ds := st.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + st.NoError(err) + st.True(found) + e.Int = 11 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Update().Set(e).Executor().Exec() + st.NoError(err) + + count, err := ds.Where(dbv2.C("int").Eq(11)).Count() + st.NoError(err) + st.Equal(int64(1), count) +} + +func (st *sqlite3Suite) TestUpdateReturning() { + ds := st.db.From("entry") + var id uint32 + _, err := ds. + Where(dbv2.C("id").Eq(1)). + Update(). + Set(dbv2.Record{"int": 11}). + Returning("id"). + Executor().ScanVal(&id) + st.NoError(err) + st.GreaterOrEqual(id, uint32(0)) +} + +func (st *sqlite3Suite) TestDelete() { + ds := st.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + st.NoError(err) + st.True(found) + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Executor().Exec() + st.NoError(err) + + count, err := ds.Count() + st.NoError(err) + st.Equal(int64(9), count) + + var id uint32 + found, err = ds.Where(dbv2.C("id").Eq(e.ID)).ScanVal(&id) + st.NoError(err) + st.False(found) + + e = entry{} + found, err = ds.Where(dbv2.C("int").Eq(8)).Select("id").ScanStruct(&e) + st.NoError(err) + st.True(found) + st.NotEqual(int64(0), e.ID) + + id = 0 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Returning("id").Executor().ScanVal(&id) + st.NoError(err) + st.GreaterOrEqual(id, uint32(0)) +} + +func (st *sqlite3Suite) TestInsert_OnConflict() { + ds := st.db.From("entry") + now := time.Now() + + // insert new record with ID = 11 + e := entry{Int: 11, Float: 1.100000, String: "1.100000", Time: now, Bool: false, Bytes: []byte("1.100000")} + _, err := ds.Insert().Rows(e).OnConflict(dbv2.DoNothing()).Executor().Exec() + st.NoError(err) + + var entryActual entry + _, err = ds.Where(dbv2.C("id").Eq(11)).ScanStruct(&entryActual) + st.NoError(err) + st.Equal("1.100000", entryActual.String) + + // duplicate with ON CONFLICT DO NOTHING should not be actually inserted + _, err = ds.Insert().Rows( + dbv2.Record{ + "id": 11, + "int": 99999999, + "float": "0.99999999", + "string": "99999999", + "time": now, + "bool": true, + "bytes": []byte("0.99999999"), + }, + ).OnConflict(dbv2.DoNothing()).Executor().Exec() + st.NoError(err) + + _, err = ds.Where(dbv2.C("id").Eq(11)).ScanStruct(&entryActual) + st.NoError(err) + st.Equal("1.100000", entryActual.String) + + // UPSERT record with ID primary key value conflict + _, err = ds.Insert().Rows( + dbv2.Record{ + "id": 11, + "int": 11, + "float": "1.100000", + "string": "1.100000", + "time": now, + "bool": true, + "bytes": []byte("1.100000"), + }, + ).OnConflict(dbv2.DoUpdate("id", dbv2.Record{"string": "upsert"})).Executor().Exec() + st.NoError(err) + + _, err = ds.Where(dbv2.C("id").Eq(11)).ScanStruct(&entryActual) + st.NoError(err) + st.Equal("upsert", entryActual.String) + + // UPDATE ... ON CONFLICT (...) WHERE ... SET ... should result in error for now + entries := []entry{ + {Int: 8, Float: 6.100000, String: "6.100000", Time: now, Bytes: []byte("6.100000")}, + {Int: 9, Float: 7.200000, String: "7.200000", Time: now, Bytes: []byte("7.200000")}, + } + _, err = ds.Insert(). + Rows(entries). + OnConflict(dbv2.DoUpdate("id", dbv2.Record{"string": "upsert"}).Where(dbv2.C("id").Eq(9))). + Executor().Exec() + st.EqualError(err, "db: dialect does not support upsert with where clause [dialect=sqlite3]") +} + +func TestSqlite3Suite(t *testing.T) { + suite.Run(t, new(sqlite3Suite)) +} diff --git a/dialect/sqlserver/sqlserver.go b/dialect/sqlserver/sqlserver.go new file mode 100644 index 0000000..9e28702 --- /dev/null +++ b/dialect/sqlserver/sqlserver.go @@ -0,0 +1,99 @@ +package sqlserver + +import ( + "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/sqlgen" +) + +func DialectOptions() *db.SQLDialectOptions { + opts := db.DefaultDialectOptions() + + opts.BooleanDataTypeSupported = false + opts.UseLiteralIsBools = false + + opts.SupportsReturn = false + opts.SupportsOrderByOnUpdate = false + opts.SupportsLimitOnUpdate = false + opts.SupportsLimitOnDelete = false + opts.SupportsOrderByOnDelete = true + opts.SupportsConflictUpdateWhere = false + opts.SupportsInsertIgnoreSyntax = false + opts.SupportsConflictTarget = false + opts.SupportsWithCTE = false + opts.SupportsWithCTERecursive = false + opts.SupportsDistinctOn = false + opts.SupportsWindowFunction = false + opts.SurroundLimitWithParentheses = true + + opts.PlaceHolderFragment = []byte("@p") + opts.LimitFragment = []byte(" TOP ") + opts.IncludePlaceholderNum = true + opts.DefaultValuesFragment = []byte("") + opts.True = []byte("1") + opts.False = []byte("0") + opts.TimeFormat = "2006-01-02 15:04:05" + opts.BooleanOperatorLookup = map[exp.BooleanOperation][]byte{ + exp.EqOp: []byte("="), + exp.NeqOp: []byte("!="), + exp.GtOp: []byte(">"), + exp.GteOp: []byte(">="), + exp.LtOp: []byte("<"), + exp.LteOp: []byte("<="), + exp.InOp: []byte("IN"), + exp.NotInOp: []byte("NOT IN"), + exp.IsOp: []byte("IS"), + exp.IsNotOp: []byte("IS NOT"), + exp.LikeOp: []byte("LIKE"), + exp.NotLikeOp: []byte("NOT LIKE"), + exp.ILikeOp: []byte("LIKE"), + exp.NotILikeOp: []byte("NOT LIKE"), + exp.RegexpLikeOp: []byte("REGEXP BINARY"), + exp.RegexpNotLikeOp: []byte("NOT REGEXP BINARY"), + exp.RegexpILikeOp: []byte("REGEXP"), + exp.RegexpNotILikeOp: []byte("NOT REGEXP"), + } + opts.BitwiseOperatorLookup = map[exp.BitwiseOperation][]byte{ + exp.BitwiseInversionOp: []byte("~"), + exp.BitwiseOrOp: []byte("|"), + exp.BitwiseAndOp: []byte("&"), + exp.BitwiseXorOp: []byte("^"), + } + + opts.FetchFragment = []byte(" FETCH FIRST ") + + opts.SelectSQLOrder = []sqlgen.SQLFragmentType{ + sqlgen.CommonTableSQLFragment, + sqlgen.SelectWithLimitSQLFragment, + sqlgen.FromSQLFragment, + sqlgen.JoinSQLFragment, + sqlgen.WhereSQLFragment, + sqlgen.GroupBySQLFragment, + sqlgen.HavingSQLFragment, + sqlgen.WindowSQLFragment, + sqlgen.CompoundsSQLFragment, + sqlgen.OrderWithOffsetFetchSQLFragment, + sqlgen.ForSQLFragment, + } + + opts.EscapedRunes = map[rune][]byte{ + '\'': []byte("\\'"), + '"': []byte("\\\""), + '\\': []byte("\\\\"), + '\n': []byte("\\n"), + '\r': []byte("\\r"), + 0: []byte("\\x00"), + 0x1a: []byte("\\x1a"), + } + + opts.OfFragment = []byte("") + opts.ConflictFragment = []byte("") + opts.ConflictDoUpdateFragment = []byte("") + opts.ConflictDoNothingFragment = []byte("") + + return opts +} + +func init() { + db.RegisterDialect("sqlserver", DialectOptions()) +} diff --git a/dialect/sqlserver/sqlserver_dialect_test.go b/dialect/sqlserver/sqlserver_dialect_test.go new file mode 100644 index 0000000..da0cd58 --- /dev/null +++ b/dialect/sqlserver/sqlserver_dialect_test.go @@ -0,0 +1,60 @@ +package sqlserver_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type ( + sqlserverDialectSuite struct { + suite.Suite + } + sqlTestCase struct { + ds exp.SQLExpression + sql string + err string + isPrepared bool + args []interface{} + } +) + +func (sds *sqlserverDialectSuite) GetDs(table string) *dbv2.SelectDataset { + return dbv2.Dialect("sqlserver").From(table) +} + +func (sds *sqlserverDialectSuite) assertSQL(cases ...sqlTestCase) { + for i, c := range cases { + actualSQL, actualArgs, err := c.ds.ToSQL() + if c.err == "" { + sds.NoError(err, "test case %d failed", i) + } else { + sds.EqualError(err, c.err, "test case %d failed", i) + } + sds.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + sds.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + sds.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (sds *sqlserverDialectSuite) TestBitwiseOperations() { + col := dbv2.C("a") + ds := sds.GetDs("test") + sds.assertSQL( + sqlTestCase{ds: ds.Where(col.BitwiseInversion()), sql: "SELECT * FROM \"test\" WHERE (~ \"a\")"}, + sqlTestCase{ds: ds.Where(col.BitwiseAnd(1)), sql: "SELECT * FROM \"test\" WHERE (\"a\" & 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseOr(1)), sql: "SELECT * FROM \"test\" WHERE (\"a\" | 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseXor(1)), sql: "SELECT * FROM \"test\" WHERE (\"a\" ^ 1)"}, + sqlTestCase{ds: ds.Where(col.BitwiseLeftShift(1)), err: "db: bitwise operator 'Left Shift' not supported"}, + sqlTestCase{ds: ds.Where(col.BitwiseRightShift(1)), err: "db: bitwise operator 'Right Shift' not supported"}, + ) +} + +func TestDatasetAdapterSuite(t *testing.T) { + suite.Run(t, new(sqlserverDialectSuite)) +} diff --git a/dialect/sqlserver/sqlserver_test.go b/dialect/sqlserver/sqlserver_test.go new file mode 100644 index 0000000..f934cb9 --- /dev/null +++ b/dialect/sqlserver/sqlserver_test.go @@ -0,0 +1,485 @@ +package sqlserver_test + +import ( + "database/sql" + "fmt" + "os" + "testing" + "time" + + "git.fsdpf.net/go/db/v2/dialect/mysql" + + dbv2 "git.fsdpf.net/go/db/v2" + _ "git.fsdpf.net/go/db/v2/dialect/sqlserver" + _ "github.com/denisenkom/go-mssqldb" + "github.com/stretchr/testify/suite" +) + +const ( + dropTable = "DROP TABLE IF EXISTS \"entry\";" + createTable = "CREATE TABLE \"entry\" (" + + "\"id\" INT NOT NULL IDENTITY(1,1)," + + "\"int\" INT NOT NULL UNIQUE," + + "\"float\" FLOAT NOT NULL ," + + "\"string\" VARCHAR(255) NOT NULL ," + + "\"time\" DATETIME NOT NULL ," + + "\"bool\" BIT NOT NULL ," + + "\"bytes\" VARBINARY(100) NOT NULL ," + + "PRIMARY KEY (\"id\") );" + insertDefaultRecords = "INSERT INTO [entry] ([int], [float], [string], [time], [bool], [bytes]) VALUES" + + "(0, 0.000000, '0.000000', '2015-02-22 18:19:55', 1, CONVERT(BINARY(8), '0.000000'))," + + "(1, 0.100000, '0.100000', '2015-02-22 19:19:55', 0, CONVERT(BINARY(8), '0.100000'))," + + "(2, 0.200000, '0.200000', '2015-02-22 20:19:55', 1, CONVERT(BINARY(8), '0.200000'))," + + "(3, 0.300000, '0.300000', '2015-02-22 21:19:55', 0, CONVERT(BINARY(8), '0.300000'))," + + "(4, 0.400000, '0.400000', '2015-02-22 22:19:55', 1, CONVERT(BINARY(8), '0.400000'))," + + "(5, 0.500000, '0.500000', '2015-02-22 23:19:55', 0, CONVERT(BINARY(8), '0.500000'))," + + "(6, 0.600000, '0.600000', '2015-02-23 00:19:55', 1, CONVERT(BINARY(8), '0.600000'))," + + "(7, 0.700000, '0.700000', '2015-02-23 01:19:55', 0, CONVERT(BINARY(8), '0.700000'))," + + "(8, 0.800000, '0.800000', '2015-02-23 02:19:55', 1, CONVERT(BINARY(8), '0.800000'))," + + "(9, 0.900000, '0.900000', '2015-02-23 03:19:55', 0, CONVERT(BINARY(8), '0.900000'));" +) + +const defaultDBURI = "sqlserver://sa:qwe123QWE@127.0.0.1:1433?database=master&connection+timeout=30" + +type ( + sqlserverTest struct { + suite.Suite + db *dbv2.Database + } + entry struct { + ID uint32 `db:"id" ff:"skipinsert,skipupdate"` + Int int `db:"int"` + Float float64 `db:"float"` + String string `db:"string"` + Time time.Time `db:"time"` + Bool bool `db:"bool"` + Bytes []byte `db:"bytes"` + } + entryTestCase struct { + ds *dbv2.SelectDataset + len int + check func(entry entry, index int) + err string + } +) + +func (sst *sqlserverTest) assertEntries(cases ...entryTestCase) { + for i, c := range cases { + var entries []entry + err := c.ds.ScanStructs(&entries) + if c.err == "" { + sst.NoError(err, "test case %d failed", i) + } else { + sst.EqualError(err, c.err, "test case %d failed", i) + } + sst.Len(entries, c.len) + for index, entry := range entries { + c.check(entry, index) + } + } +} + +func (sst *sqlserverTest) SetupSuite() { + dbURI := os.Getenv("SQLSERVER_URI") + if dbURI == "" { + dbURI = defaultDBURI + } + db, err := sql.Open("sqlserver", dbURI) + if err != nil { + panic(err.Error()) + } + sst.db = dbv2.New("sqlserver", db) +} + +func (sst *sqlserverTest) SetupTest() { + if _, err := sst.db.Exec(dropTable); err != nil { + panic(err) + } + if _, err := sst.db.Exec(createTable); err != nil { + panic(err) + } + if _, err := sst.db.Exec(insertDefaultRecords); err != nil { + panic(err) + } +} + +func (sst *sqlserverTest) TestToSQL() { + ds := sst.db.From("entry") + s, _, err := ds.Select("id", "float", "string", "time", "bool").ToSQL() + sst.NoError(err) + sst.Equal("SELECT \"id\", \"float\", \"string\", \"time\", \"bool\" FROM \"entry\"", s) + + s, _, err = ds.Where(dbv2.C("int").Eq(10)).ToSQL() + sst.NoError(err) + sst.Equal("SELECT * FROM \"entry\" WHERE (\"int\" = 10)", s) + + s, args, err := ds.Prepared(true).Where(dbv2.L("? = ?", dbv2.C("int"), 10)).ToSQL() + sst.NoError(err) + sst.Equal([]interface{}{int64(10)}, args) + sst.Equal("SELECT * FROM \"entry\" WHERE \"int\" = @p1", s) +} + +func (sst *sqlserverTest) TestQuery() { + ds := sst.db.From("entry") + floatVal := float64(0) + baseDate, err := time.Parse( + "2006-01-02 15:04:05", + "2015-02-22 18:19:55", + ) + sst.NoError(err) + sst.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + sst.Equal(uint32(index+1), entry.ID) + sst.Equal(index, entry.Int) + sst.Equal(f, fmt.Sprintf("%f", entry.Float)) + sst.Equal(f, entry.String) + sst.Equal([]byte(f), entry.Bytes) + sst.Equal(index%2 == 0, entry.Bool) + sst.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ + ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), + err: "dbv2: boolean data type is not supported by dialect \"sqlserver\"", + }, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + sst.True(entry.Int >= 3) + sst.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + sst.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + sst.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + sst.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + sst.Fail("Should not have returned any records") + }}, + ) +} + +func (sst *sqlserverTest) TestQuery_Prepared() { + ds := sst.db.From("entry").Prepared(true) + floatVal := float64(0) + baseDate, err := time.Parse( + "2006-01-02 15:04:05", + "2015-02-22 18:19:55", + ) + sst.NoError(err) + sst.assertEntries( + entryTestCase{ds: ds.Order(dbv2.C("id").Asc()), len: 10, check: func(entry entry, index int) { + f := fmt.Sprintf("%f", floatVal) + sst.Equal(uint32(index+1), entry.ID) + sst.Equal(index, entry.Int) + sst.Equal(f, fmt.Sprintf("%f", entry.Float)) + sst.Equal(f, entry.String) + sst.Equal([]byte(f), entry.Bytes) + sst.Equal(index%2 == 0, entry.Bool) + sst.Equal(baseDate.Add(time.Duration(index)*time.Hour).Unix(), entry.Time.Unix()) + floatVal += float64(0.1) + }}, + entryTestCase{ + ds: ds.Where(dbv2.C("bool").IsTrue()).Order(dbv2.C("id").Asc()), + err: "dbv2: boolean data type is not supported by dialect \"sqlserver\"", + }, + entryTestCase{ds: ds.Where(dbv2.C("int").Gt(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int > 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Gte(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int >= 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lt(5)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int < 5) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Lte(4)).Order(dbv2.C("id").Asc()), len: 5, check: func(entry entry, _ int) { + sst.True(entry.Int <= 4) + }}, + entryTestCase{ds: ds.Where(dbv2.C("int").Between(dbv2.Range(3, 6))).Order(dbv2.C("id").Asc()), len: 4, check: func(entry entry, _ int) { + sst.True(entry.Int >= 3) + sst.True(entry.Int <= 6) + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Eq("0.100000")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + sst.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").Like("0.1%")).Order(dbv2.C("id").Asc()), len: 1, check: func(entry entry, _ int) { + sst.Equal(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").NotLike("0.1%")).Order(dbv2.C("id").Asc()), len: 9, check: func(entry entry, _ int) { + sst.NotEqual(entry.String, "0.100000") + }}, + entryTestCase{ds: ds.Where(dbv2.C("string").IsNull()).Order(dbv2.C("id").Asc()), len: 0, check: func(entry entry, _ int) { + sst.Fail("Should not have returned any records") + }}, + ) +} + +func (sst *sqlserverTest) TestQuery_ValueExpressions() { + type wrappedEntry struct { + entry + BoolValue bool `db:"bool_value"` + } + expectedDate, err := time.Parse("2006-01-02 15:04:05", "2015-02-22 19:19:55") + sst.NoError(err) + ds := sst.db.From("entry").Select(dbv2.Star(), dbv2.V(true).As("bool_value")).Where(dbv2.Ex{"int": 1}) + var we wrappedEntry + found, err := ds.ScanStruct(&we) + sst.NoError(err) + sst.True(found) + sst.Equal(wrappedEntry{ + entry{2, 1, 0.100000, "0.100000", expectedDate, false, []byte("0.100000")}, + true, + }, we) +} + +func (sst *sqlserverTest) TestCount() { + ds := sst.db.From("entry") + count, err := ds.Count() + sst.NoError(err) + sst.Equal(int64(10), count) + count, err = ds.Where(dbv2.C("int").Gt(4)).Count() + sst.NoError(err) + sst.Equal(int64(5), count) + count, err = ds.Where(dbv2.C("int").Gte(4)).Count() + sst.NoError(err) + sst.Equal(int64(6), count) + count, err = ds.Where(dbv2.C("string").Like("0.1%")).Count() + sst.NoError(err) + sst.Equal(int64(1), count) + count, err = ds.Where(dbv2.C("string").IsNull()).Count() + sst.NoError(err) + sst.Equal(int64(0), count) +} + +func (sst *sqlserverTest) TestLimitOffset() { + ds := sst.db.From("entry").Where(dbv2.C("id").Gte(1)).Limit(1) + var e entry + found, err := ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(1), e.ID) + + ds = sst.db.From("entry").Where(dbv2.C("id").Gte(1)).Order(dbv2.C("id").Desc()).Limit(1) + found, err = ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(10), e.ID) + + ds = sst.db.From("entry").Where(dbv2.C("id").Gte(1)).Order(dbv2.C("id").Asc()).Offset(1).Limit(1) + found, err = ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(2), e.ID) +} + +func (sst *sqlserverTest) TestLimitOffsetParameterized() { + ds := sst.db.From("entry").Prepared(true).Where(dbv2.C("id").Gte(1)).Limit(1) + var e entry + found, err := ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(1), e.ID) + + ds = sst.db.From("entry").Prepared(true).Where(dbv2.C("id").Gte(1)).Order(dbv2.C("id").Desc()).Limit(1) + found, err = ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(10), e.ID) + + ds = sst.db.From("entry").Prepared(true).Where(dbv2.C("id").Gte(1)).Order(dbv2.C("id").Asc()).Offset(1).Limit(1) + found, err = ds.ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.Equal(uint32(2), e.ID) +} + +func (sst *sqlserverTest) TestInsert() { + ds := sst.db.From("entry") + now := time.Now() + _, err := ds.Insert().Rows(dbv2.Record{ + "Int": 10, + "Float": 1.00000, + "String": "1.000000", + "Time": now, + "Bool": true, + "Bytes": dbv2.Cast(dbv2.V([]byte("1.000000")), "BINARY(8)"), + }).Executor().Exec() + sst.NoError(err) + + var insertedEntry entry + found, err := ds.Where(dbv2.C("int").Eq(10)).ScanStruct(&insertedEntry) + sst.NoError(err) + sst.True(found) + sst.True(insertedEntry.ID > 0) + + entries := []dbv2.Record{ + { + "Int": 11, "Float": 1.100000, "String": "1.100000", "Time": now, + "Bool": false, "Bytes": dbv2.Cast(dbv2.V([]byte("1.100000")), "BINARY(8)"), + }, + { + "Int": 12, "Float": 1.200000, "String": "1.200000", "Time": now, + "Bool": true, "Bytes": dbv2.Cast(dbv2.V([]byte("1.200000")), "BINARY(8)"), + }, + { + "Int": 13, "Float": 1.300000, "String": "1.300000", "Time": now, + "Bool": false, "Bytes": dbv2.Cast(dbv2.V([]byte("1.300000")), "BINARY(8)"), + }, + { + "Int": 14, "Float": 1.400000, "String": "1.400000", "Time": now, + "Bool": true, "Bytes": dbv2.Cast(dbv2.V([]byte("1.400000")), "BINARY(8)"), + }, + } + _, err = ds.Insert().Rows(entries).Executor().Exec() + sst.NoError(err) + + var newEntries []entry + sst.NoError(ds.Where(dbv2.C("int").In([]uint32{11, 12, 13, 14})).ScanStructs(&newEntries)) + sst.Len(newEntries, 4) + for i, e := range newEntries { + sst.Equal(entries[i]["Int"], e.Int) + sst.Equal(entries[i]["Float"], e.Float) + sst.Equal(entries[i]["String"], e.String) + sst.Equal( + entries[i]["Time"].(time.Time).UTC().Format(mysql.DialectOptions().TimeFormat), + e.Time.Format(mysql.DialectOptions().TimeFormat), + ) + sst.Equal(entries[i]["Bool"], e.Bool) + sst.Equal([]byte(entries[i]["String"].(string)), e.Bytes) + } + + _, err = ds.Insert().Rows( + dbv2.Record{ + "Int": 15, "Float": 1.500000, "String": "1.500000", "Time": now, + "Bool": false, "Bytes": dbv2.Cast(dbv2.V([]byte("1.500000")), "BINARY(8)"), + }, + dbv2.Record{ + "Int": 16, "Float": 1.600000, "String": "1.600000", "Time": now, + "Bool": true, "Bytes": dbv2.Cast(dbv2.V([]byte("1.600000")), "BINARY(8)"), + }, + dbv2.Record{ + "Int": 17, "Float": 1.700000, "String": "1.700000", "Time": now, + "Bool": false, "Bytes": dbv2.Cast(dbv2.V([]byte("1.700000")), "BINARY(8)"), + }, + dbv2.Record{ + "Int": 18, "Float": 1.800000, "String": "1.800000", "Time": now, + "Bool": true, "Bytes": dbv2.Cast(dbv2.V([]byte("1.800000")), "BINARY(8)"), + }, + ).Executor().Exec() + sst.NoError(err) + + newEntries = newEntries[0:0] + sst.NoError(ds.Where(dbv2.C("int").In([]uint32{15, 16, 17, 18})).ScanStructs(&newEntries)) + sst.Len(newEntries, 4) +} + +func (sst *sqlserverTest) TestInsertReturningProducesError() { + ds := sst.db.From("entry") + now := time.Now() + e := entry{Int: 10, Float: 1.000000, String: "1.000000", Time: now, Bool: true, Bytes: []byte("1.000000")} + _, err := ds.Insert().Rows(e).Returning(dbv2.Star()).Executor().ScanStruct(&e) + sst.Error(err) +} + +func (sst *sqlserverTest) TestUpdate() { + ds := sst.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + sst.NoError(err) + sst.True(found) + e.Int = 11 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Update().Set(dbv2.Record{"Int": e.Int}).Executor().Exec() + sst.NoError(err) + + count, err := ds.Where(dbv2.C("int").Eq(11)).Count() + sst.NoError(err) + sst.Equal(int64(1), count) +} + +func (sst *sqlserverTest) TestUpdateReturning() { + ds := sst.db.From("entry") + var id uint32 + _, err := ds.Where(dbv2.C("int").Eq(11)). + Update(). + Set(dbv2.Record{"int": 9}). + Returning("id"). + Executor().ScanVal(&id) + sst.Error(err) + sst.EqualError(err, "dbv2: dialect does not support RETURNING clause [dialect=sqlserver]") +} + +func (sst *sqlserverTest) TestDelete() { + ds := sst.db.From("entry") + var e entry + found, err := ds.Where(dbv2.C("int").Eq(9)).Select("id").ScanStruct(&e) + sst.NoError(err) + sst.True(found) + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Executor().Exec() + sst.NoError(err) + + count, err := ds.Count() + sst.NoError(err) + sst.Equal(int64(9), count) + + var id uint32 + found, err = ds.Where(dbv2.C("id").Eq(e.ID)).ScanVal(&id) + sst.NoError(err) + sst.False(found) + + e = entry{} + found, err = ds.Where(dbv2.C("int").Eq(8)).Select("id").ScanStruct(&e) + sst.NoError(err) + sst.True(found) + sst.NotEqual(0, e.ID) + + id = 0 + _, err = ds.Where(dbv2.C("id").Eq(e.ID)).Delete().Returning("id").Executor().ScanVal(&id) + sst.EqualError(err, "db: dialect does not support RETURNING clause [dialect=sqlserver]") +} + +func (sst *sqlserverTest) TestInsertIgnoreNotSupported() { + ds := sst.db.From("entry") + now := time.Now() + + // insert one + entries := []dbv2.Record{ + { + "Int": 8, "Float": 6.100000, "String": "6.100000", "Bool": false, "Time": now, + "Bytes": dbv2.Cast(dbv2.V([]byte("6.100000")), "BINARY(8)"), + }, + { + "Int": 9, "Float": 7.200000, "String": "7.200000", "Bool": false, "Time": now, + "Bytes": dbv2.Cast(dbv2.V([]byte("7.200000")), "BINARY(8)"), + }, + { + "Int": 10, "Float": 7.200000, "String": "7.200000", "Bool": false, "Time": now, + "Bytes": dbv2.Cast(dbv2.V([]byte("7.200000")), "BINARY(8)"), + }, + } + _, err := ds.Insert().Rows(entries).OnConflict(dbv2.DoNothing()).Executor().Exec() + sst.Error(err) + sst.Contains(err.Error(), "Cannot insert duplicate key in object 'dbo.entry'. The duplicate key value is (8)") + + count, err := ds.Count() + sst.NoError(err) + sst.Equal(count, int64(10)) +} + +func TestSqlServerSuite(t *testing.T) { + suite.Run(t, new(sqlserverTest)) +} diff --git a/engine/engine.go b/engine/engine.go new file mode 100644 index 0000000..a4e4d8a --- /dev/null +++ b/engine/engine.go @@ -0,0 +1,89 @@ +package engine + +import ( + "database/sql" + "fmt" + + "github.com/lib/pq" + + _ "github.com/denisenkom/go-mssqldb" + _ "github.com/go-sql-driver/mysql" + _ "github.com/mattn/go-sqlite3" + + _ "git.fsdpf.net/go/db/v2/dialect/mysql" + _ "git.fsdpf.net/go/db/v2/dialect/postgres" + _ "git.fsdpf.net/go/db/v2/dialect/sqlite3" + _ "git.fsdpf.net/go/db/v2/dialect/sqlserver" + + "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/internal/errors" +) + +type Engine struct { + configs map[string]DBConfig + dbs map[string]*sql.DB +} + +var _engine *Engine + +func init() { + _engine = &Engine{ + configs: make(map[string]DBConfig), + dbs: make(map[string]*sql.DB), + } +} + +func (e Engine) Connection(name string) *db.Database { + cfg, ok := e.configs[name] + + if !ok { + panic(errors.New(fmt.Sprintf("Database connection %s not configured.", name))) + } + + _db, ok := e.dbs[name] + + if !ok { + _db = e.MakeConnection(cfg) + e.dbs[name] = _db + } + + return db.New(cfg.Driver, _db) +} + +func (e Engine) MakeConnection(cfg DBConfig) *sql.DB { + dsn := cfg.ToDSN() + + switch cfg.Driver { + case "mysql": + case "sqlite3": + case "sqlserver": + case "postgres": + if url, err := pq.ParseURL(dsn); err == nil { + dsn = url + } else { + panic(err) + } + default: + panic(fmt.Sprintf("Unsupported driver: %s", cfg.Driver)) + } + + db, err := sql.Open(cfg.Driver, dsn) + + if err != nil { + panic(err) + } + + if err := db.Ping(); err != nil { + panic(err) + } + + return db +} + +func Open(cfgs map[string]DBConfig) *Engine { + for n, cfg := range cfgs { + _engine.configs[n] = cfg + } + + return _engine +} diff --git a/engine/engine_config.go b/engine/engine_config.go new file mode 100644 index 0000000..827ec9d --- /dev/null +++ b/engine/engine_config.go @@ -0,0 +1,399 @@ +package engine + +import ( + "fmt" + "net/url" + "time" +) + +// DBConfig 数据库配置结构体 +type DBConfig struct { + Driver string + Host string + Port string + Database string + Username string + Password string + Charset string + Prefix string + ConnMaxLifetime time.Duration + ConnMaxIdleTime time.Duration + MaxIdleConns int + MaxOpenConns int + ParseTime bool + EnableLog bool + ReadHosts []string + WriteHosts []string + + MySQL struct { + Dsn string + Collation string + UnixSocket string + MultiStatements bool + } + + PostgreSQL struct { + Sslmode string + TLS string + SearchPath string + ConnectTimeout int + ApplicationName string + } + + SQLite struct { + File string + Journal string + Locking string + Mode string + Synchronous int + Cache string + BusyTimeout int + } + + SQLServer struct { + Instance string + Encrypt string + TrustServerCert bool + AppName string + FailoverPartner string + PacketSize int + WorkstationID string + ConnectionTimeout int + KeepAlive int + Dsn string + } +} + +// Option 配置选项类型 +type Option func(*DBConfig) + +// ToDSN 生成对应驱动的 DSN +func (c *DBConfig) ToDSN() string { + switch c.Driver { + case "mysql": + return c.toMySQLDSN() + case "pgsql": + return c.toPostgreSQLDSN() + case "sqlite3": + return c.toSQLiteDSN() + case "sqlserver": + return c.toSQLServerDSN() + default: + panic(fmt.Sprintf("Unsupported driver for DSN generation: %s", c.Driver)) + } +} + +// toMySQLDSN 生成 MySQL DSN +func (c *DBConfig) toMySQLDSN() string { + if c.MySQL.Dsn != "" { + return c.MySQL.Dsn + } + var dsn string + if c.Username != "" { + if c.Password != "" { + dsn = fmt.Sprintf("%s:%s@", c.Username, c.Password) + } else { + dsn = fmt.Sprintf("%s@", c.Username) + } + } + if c.MySQL.UnixSocket != "" { + dsn += fmt.Sprintf("unix(%s)", c.MySQL.UnixSocket) + } else { + host := c.Host + if host == "" && len(c.WriteHosts) > 0 { + host = c.WriteHosts[0] + } + if host == "" && len(c.ReadHosts) > 0 { + host = c.ReadHosts[0] + } + if c.Port == "" { + c.Port = "3306" + } + dsn += fmt.Sprintf("tcp(%s:%s)", host, c.Port) + } + dsn += fmt.Sprintf("/%s", c.Database) + params := url.Values{} + if c.Charset != "" { + params.Add("charset", c.Charset) + } + if c.ParseTime { + params.Add("parseTime", "true") + } + if c.MySQL.Collation != "" { + params.Add("collation", c.MySQL.Collation) + } + if c.MySQL.MultiStatements { + params.Add("multiStatements", "true") + } + if len(params) > 0 { + dsn += "?" + params.Encode() + } + return dsn +} + +// toPostgreSQLDSN 生成 PostgreSQL DSN +func (c *DBConfig) toPostgreSQLDSN() string { + params := url.Values{} + if c.Host != "" { + params.Add("host", c.Host) + } else if len(c.WriteHosts) > 0 { + params.Add("host", c.WriteHosts[0]) + } else if len(c.ReadHosts) > 0 { + params.Add("host", c.ReadHosts[0]) + } + if c.Port == "" { + c.Port = "5432" // 默认 PostgreSQL 端口 + } + params.Add("port", c.Port) + params.Add("dbname", c.Database) + params.Add("user", c.Username) + if c.Password != "" { + params.Add("password", c.Password) + } + if c.PostgreSQL.Sslmode != "" { + params.Add("sslmode", c.PostgreSQL.Sslmode) + } + if c.PostgreSQL.ConnectTimeout > 0 { + params.Add("connect_timeout", fmt.Sprintf("%d", c.PostgreSQL.ConnectTimeout)) + } + if c.PostgreSQL.ApplicationName != "" { + params.Add("application_name", c.PostgreSQL.ApplicationName) + } + return "postgres://" + c.Username + ":" + url.QueryEscape(c.Password) + "@" + params.Get("host") + ":" + c.Port + "/" + c.Database + "?" + params.Encode() +} + +// toSQLiteDSN 生成 SQLite DSN +func (c *DBConfig) toSQLiteDSN() string { + if c.SQLite.File == "" { + return "" + } + dsn := c.SQLite.File + params := url.Values{} + if c.SQLite.Journal != "" { + params.Add("_journal", c.SQLite.Journal) + } + if c.SQLite.Locking != "" { + params.Add("_locking", c.SQLite.Locking) + } + if c.SQLite.Mode != "" { + params.Add("_mode", c.SQLite.Mode) + } + if c.SQLite.Synchronous > 0 { + params.Add("_synchronous", fmt.Sprintf("%d", c.SQLite.Synchronous)) + } + if c.SQLite.Cache != "" { + params.Add("_cache", c.SQLite.Cache) + } + if c.SQLite.BusyTimeout > 0 { + params.Add("_busy_timeout", fmt.Sprintf("%d", c.SQLite.BusyTimeout)) + } + if len(params) > 0 { + dsn += "?" + params.Encode() + } + return dsn +} + +// toSQLServerDSN 生成 SQL Server DSN +func (c *DBConfig) toSQLServerDSN() string { + if c.SQLServer.Dsn != "" { + return c.SQLServer.Dsn + } + host := c.Host + if host == "" && len(c.WriteHosts) > 0 { + host = c.WriteHosts[0] + } + if host == "" && len(c.ReadHosts) > 0 { + host = c.ReadHosts[0] + } + if c.Port == "" { + c.Port = "1433" // 默认 SQL Server 端口 + } + if c.SQLServer.Instance != "" { + host += "\\" + c.SQLServer.Instance + } + params := url.Values{} + params.Add("server", host) + params.Add("database", c.Database) + if c.SQLServer.Encrypt != "" { + params.Add("encrypt", c.SQLServer.Encrypt) + } + if c.SQLServer.TrustServerCert { + params.Add("TrustServerCertificate", "true") + } + if c.SQLServer.AppName != "" { + params.Add("app name", c.SQLServer.AppName) + } + if c.SQLServer.ConnectionTimeout > 0 { + params.Add("connection timeout", fmt.Sprintf("%d", c.SQLServer.ConnectionTimeout)) + } + return "sqlserver://" + c.Username + ":" + url.QueryEscape(c.Password) + "@" + host + ":" + c.Port + "?" + params.Encode() +} + +// NewDBConfig 创建新的 DBConfig 实例,检查关键参数 +func NewDBConfig(driver string, options ...Option) DBConfig { + if driver == "" { + panic("Driver is required and cannot be empty") + } + + // 支持的驱动类型 + validDrivers := map[string]bool{ + "mysql": true, + "pgsql": true, + "sqlite3": true, + "sqlserver": true, + } + if !validDrivers[driver] { + panic(fmt.Sprintf("Unsupported driver: %s", driver)) + } + + // 初始化配置 + config := &DBConfig{ + Driver: driver, + Charset: "utf8mb4", + ConnMaxLifetime: 2 * time.Hour, + ConnMaxIdleTime: 30 * time.Minute, + MaxIdleConns: 10, + MaxOpenConns: 100, + } + + // 应用所有选项 + for _, opt := range options { + opt(config) + } + + // 检查关键参数 + switch config.Driver { + case "mysql", "pgsql", "sqlserver": + if config.Host == "" && len(config.ReadHosts) == 0 && len(config.WriteHosts) == 0 { + panic(fmt.Sprintf("Host, ReadHosts, or WriteHosts is required for %s driver", config.Driver)) + } + if config.Database == "" { + panic(fmt.Sprintf("Database is required for %s driver", config.Driver)) + } + if config.Username == "" { + panic(fmt.Sprintf("Username is required for %s driver", config.Driver)) + } + if config.Password == "" { + panic(fmt.Sprintf("Password is required for %s driver", config.Driver)) + } + case "sqlite3": + if config.SQLite.File == "" { + panic("File is required for sqlite3 driver") + } + } + + return *config +} + +// 通用的 WithOption 配置函数 +func WithHost(host string) Option { + return func(c *DBConfig) { + c.Host = host + } +} + +func WithPort(port string) Option { + return func(c *DBConfig) { + c.Port = port + } +} + +func WithDatabase(database string) Option { + return func(c *DBConfig) { + c.Database = database + } +} + +func WithUsername(username string) Option { + return func(c *DBConfig) { + c.Username = username + } +} + +func WithPassword(password string) Option { + return func(c *DBConfig) { + c.Password = password + } +} + +func WithParseTime(parseTime bool) Option { + return func(c *DBConfig) { + c.ParseTime = parseTime + } +} + +func WithCharset(charset string) Option { + return func(c *DBConfig) { + c.Charset = charset + } +} + +func WithReadHosts(hosts []string) Option { + return func(c *DBConfig) { + c.ReadHosts = hosts + } +} + +func WithWriteHosts(hosts []string) Option { + return func(c *DBConfig) { + c.WriteHosts = hosts + } +} + +// MySQL 专用选项 +func WithMySQLCollation(collation string) Option { + return func(c *DBConfig) { + if c.Driver != "mysql" { + panic("WithMySQLCollation is only valid for mysql driver") + } + c.MySQL.Collation = collation + } +} + +func WithMySQLUnixSocket(socket string) Option { + return func(c *DBConfig) { + if c.Driver != "mysql" { + panic("WithMySQLUnixSocket is only valid for mysql driver") + } + c.MySQL.UnixSocket = socket + } +} + +// PostgreSQL 专用选项 +func WithPgSslmode(sslmode string) Option { + return func(c *DBConfig) { + if c.Driver != "pgsql" { + panic("WithPgSslmode is only valid for pgsql driver") + } + c.PostgreSQL.Sslmode = sslmode + } +} + +// SQLite 专用选项 +func WithSQLiteFile(file string) Option { + return func(c *DBConfig) { + if c.Driver != "sqlite3" { + panic("WithSQLiteFile is only valid for sqlite3 driver") + } + c.SQLite.File = file + } +} + +func WithSQLiteJournal(journal string) Option { + return func(c *DBConfig) { + if c.Driver != "sqlite3" { + panic("WithSQLiteJournal is only valid for sqlite3 driver") + } + c.SQLite.Journal = journal + } +} + +// SQL Server 专用选项 +func WithSQLServerInstance(instance string) Option { + return func(c *DBConfig) { + if c.Driver != "sqlserver" { + panic("WithSQLServerInstance is only valid for sqlserver driver") + } + c.SQLServer.Instance = instance + } +} diff --git a/engine/engine_config_test.go b/engine/engine_config_test.go new file mode 100644 index 0000000..388c297 --- /dev/null +++ b/engine/engine_config_test.go @@ -0,0 +1,383 @@ +package engine_test + +import ( + "testing" + "time" + + "git.fsdpf.net/go/db/v2/engine" +) + +// TestMySQLConfig 测试 MySQL 驱动的配置 +func TestMySQLConfig(t *testing.T) { + tests := []struct { + name string + options []engine.Option + wantErr bool + errMsg string + }{ + { + name: "Valid Config", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithUsername("root"), + engine.WithPassword("password"), + engine.WithMySQLCollation("utf8mb4_unicode_ci"), + }, + wantErr: false, + }, + { + name: "Missing Host", + options: []engine.Option{ + engine.WithDatabase("testdb"), + engine.WithUsername("root"), + engine.WithPassword("password"), + }, + wantErr: true, + errMsg: "Host, ReadHosts, or WriteHosts is required for mysql driver", + }, + { + name: "Missing Database", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithUsername("root"), + engine.WithPassword("password"), + }, + wantErr: true, + errMsg: "Database is required for mysql driver", + }, + { + name: "engine.engine.With SQL Server engine.Option", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithUsername("root"), + engine.WithPassword("password"), + engine.WithSQLServerInstance("SQLEXPRESS"), + }, + wantErr: true, + errMsg: "engine.WithSQLServerInstance is only valid for sqlserver driver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + config := engine.NewDBConfig("mysql", tt.options...) + if !tt.wantErr && (config.Driver != "mysql" || config.MySQL.Collation != "utf8mb4_unicode_ci") { + t.Errorf("MySQL config not set correctly: %+v", config) + } + }) + } +} + +// TestPostgreSQLConfig 测试 PostgreSQL 驱动的配置 +func TestPostgreSQLConfig(t *testing.T) { + tests := []struct { + name string + options []engine.Option + wantErr bool + errMsg string + }{ + { + name: "Valid Config", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithUsername("postgres"), + engine.WithPassword("password"), + engine.WithPgSslmode("disable"), + }, + wantErr: false, + }, + { + name: "Missing Username", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithPassword("password"), + }, + wantErr: true, + errMsg: "Username is required for pgsql driver", + }, + { + name: "engine.engine.With MySQL engine.Option", + options: []engine.Option{ + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithUsername("postgres"), + engine.WithPassword("password"), + engine.WithMySQLCollation("utf8mb4_unicode_ci"), + }, + wantErr: true, + errMsg: "engine.WithMySQLCollation is only valid for mysql driver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + config := engine.NewDBConfig("pgsql", tt.options...) + if !tt.wantErr && (config.Driver != "pgsql" || config.PostgreSQL.Sslmode != "disable") { + t.Errorf("PostgreSQL config not set correctly: %+v", config) + } + }) + } +} + +// TestSQLiteConfig 测试 SQLite 驱动的配置 +func TestSQLiteConfig(t *testing.T) { + tests := []struct { + name string + options []engine.Option + wantErr bool + errMsg string + }{ + { + name: "Valid Config", + options: []engine.Option{ + engine.WithSQLiteFile("test.db"), + engine.WithSQLiteJournal("WAL"), + }, + wantErr: false, + }, + { + name: "Missing File", + options: []engine.Option{}, + wantErr: true, + errMsg: "File is required for sqlite3 driver", + }, + { + name: "engine.engine.With PostgreSQL engine.Option", + options: []engine.Option{ + engine.WithSQLiteFile("test.db"), + engine.WithPgSslmode("disable"), + }, + wantErr: true, + errMsg: "engine.WithPgSslmode is only valid for pgsql driver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + config := engine.NewDBConfig("sqlite3", tt.options...) + if !tt.wantErr && (config.Driver != "sqlite3" || config.SQLite.File != "test.db") { + t.Errorf("SQLite config not set correctly: %+v", config) + } + }) + } +} + +// TestSQLServerConfig 测试 SQL Server 驱动的配置 +func TestSQLServerConfig(t *testing.T) { + tests := []struct { + name string + options []engine.Option + wantErr bool + errMsg string + }{ + { + name: "Valid Config", + options: []engine.Option{ + engine.WithHost("sqlserver.example.com"), + engine.WithDatabase("master"), + engine.WithUsername("sa"), + engine.WithPassword("password"), + engine.WithSQLServerInstance("SQLEXPRESS"), + }, + wantErr: false, + }, + { + name: "Missing Password", + options: []engine.Option{ + engine.WithHost("sqlserver.example.com"), + engine.WithDatabase("master"), + engine.WithUsername("sa"), + }, + wantErr: true, + errMsg: "Password is required for sqlserver driver", + }, + { + name: "engine.engine.With SQLite engine.Option", + options: []engine.Option{ + engine.WithHost("sqlserver.example.com"), + engine.WithDatabase("master"), + engine.WithUsername("sa"), + engine.WithPassword("password"), + engine.WithSQLiteFile("test.db"), + }, + wantErr: true, + errMsg: "engine.WithSQLiteFile is only valid for sqlite3 driver", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + config := engine.NewDBConfig("sqlserver", tt.options...) + if !tt.wantErr && (config.Driver != "sqlserver" || config.SQLServer.Instance != "SQLEXPRESS") { + t.Errorf("SQL Server config not set correctly: %+v", config) + } + }) + } +} + +// TestInvalidDriver 测试不支持的驱动 +func TestInvalidDriver(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if r != "Unsupported driver: oracle" { + t.Errorf("engine.NewDBConfig() panic = %v, want 'Unsupported driver: oracle'", r) + } + } else { + t.Errorf("engine.NewDBConfig() did not panic for invalid driver") + } + }() + + engine.NewDBConfig("oracle") +} + +// TestDefaultValues 测试默认值 +func TestDefaultValues(t *testing.T) { + config := engine.NewDBConfig("mysql", + engine.WithHost("localhost"), + engine.WithDatabase("testdb"), + engine.WithUsername("root"), + engine.WithPassword("password"), + ) + + if config.Charset != "utf8mb4" { + t.Errorf("Default Charset = %v, want utf8mb4", config.Charset) + } + if config.ConnMaxLifetime != 2*time.Hour { + t.Errorf("Default ConnMaxLifetime = %v, want 2h", config.ConnMaxLifetime) + } + if config.MaxOpenConns != 100 { + t.Errorf("Default MaxOpenConns = %v, want 100", config.MaxOpenConns) + } +} + +func TestToDSN(t *testing.T) { + tests := []struct { + name string + config engine.DBConfig + wantDSN string + wantErr bool + errMsg string + }{ + { + name: "MySQL DSN", + config: engine.NewDBConfig("mysql", + engine.WithHost("localhost"), + engine.WithPort("3306"), + engine.WithDatabase("testdb"), + engine.WithUsername("root"), + engine.WithPassword("password"), + engine.WithMySQLCollation("utf8mb4_unicode_ci"), + ), + wantDSN: "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&collation=utf8mb4_unicode_ci", + wantErr: false, + }, + { + name: "PostgreSQL DSN", + config: engine.NewDBConfig("pgsql", + engine.WithHost("localhost"), + engine.WithPort("5432"), + engine.WithDatabase("testdb"), + engine.WithUsername("postgres"), + engine.WithPassword("password"), + engine.WithPgSslmode("disable"), + ), + wantDSN: "postgres://postgres:password@localhost:5432/testdb?host=localhost&port=5432&dbname=testdb&user=postgres&password=password&sslmode=disable", + wantErr: false, + }, + { + name: "SQLite DSN", + config: engine.NewDBConfig("sqlite3", + engine.WithSQLiteFile("test.db"), + engine.WithSQLiteJournal("WAL"), + ), + wantDSN: "test.db?_journal=WAL", + wantErr: false, + }, + { + name: "SQL Server DSN", + config: engine.NewDBConfig("sqlserver", + engine.WithHost("sqlserver.example.com"), + engine.WithPort("1433"), + engine.WithDatabase("master"), + engine.WithUsername("sa"), + engine.WithPassword("password"), + engine.WithSQLServerInstance("SQLEXPRESS"), + ), + wantDSN: "sqlserver://sa:password@sqlserver.example.com\\SQLEXPRESS:1433?server=sqlserver.example.com\\SQLEXPRESS&database=master", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("ToDSN() panicked unexpectedly: %v", r) + } else if tt.errMsg != "" && r != tt.errMsg { + t.Errorf("ToDSN() panic = %v, want %v", r, tt.errMsg) + } + } else if tt.wantErr { + t.Errorf("ToDSN() did not panic, expected panic with: %v", tt.errMsg) + } + }() + + dsn := tt.config.ToDSN() + if !tt.wantErr && dsn != tt.wantDSN { + t.Errorf("ToDSN() = %v, want %v", dsn, tt.wantDSN) + } + }) + } +} diff --git a/exec/query_executor.go b/exec/query_executor.go new file mode 100644 index 0000000..e83885b --- /dev/null +++ b/exec/query_executor.go @@ -0,0 +1,253 @@ +package exec + +import ( + "context" + gsql "database/sql" + "reflect" + + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/util" +) + +type ( + QueryExecutor struct { + de DbExecutor + err error + query string + args []interface{} + } +) + +var ( + errUnsupportedScanStructType = errors.New("type must be a pointer to a struct when scanning into a struct") + errUnsupportedScanStructsType = errors.New("type must be a pointer to a slice when scanning into structs") + errUnsupportedScanValsType = errors.New("type must be a pointer to a slice when scanning into vals") + errScanValPointer = errors.New("type must be a pointer when scanning into val") + errScanValNonSlice = errors.New("type cannot be a pointer to a slice when scanning into val") +) + +func newQueryExecutor(de DbExecutor, err error, query string, args ...interface{}) QueryExecutor { + return QueryExecutor{de: de, err: err, query: query, args: args} +} + +func (q QueryExecutor) ToSQL() (sql string, args []interface{}, err error) { + return q.query, q.args, q.err +} + +func (q QueryExecutor) Exec() (gsql.Result, error) { + return q.ExecContext(context.Background()) +} + +func (q QueryExecutor) ExecContext(ctx context.Context) (gsql.Result, error) { + if q.err != nil { + return nil, q.err + } + return q.de.ExecContext(ctx, q.query, q.args...) +} + +func (q QueryExecutor) Query() (*gsql.Rows, error) { + return q.QueryContext(context.Background()) +} + +func (q QueryExecutor) QueryContext(ctx context.Context) (*gsql.Rows, error) { + if q.err != nil { + return nil, q.err + } + return q.de.QueryContext(ctx, q.query, q.args...) +} + +// This will execute the SQL and append results to the slice +// +// var myStructs []MyStruct +// if err := db.From("test").ScanStructs(&myStructs); err != nil{ +// panic(err.Error() +// } +// //use your structs +// +// i: A pointer to a slice of structs. +func (q QueryExecutor) ScanStructs(i interface{}) error { + return q.ScanStructsContext(context.Background(), i) +} + +// This will execute the SQL and append results to the slice +// +// var myStructs []MyStruct +// if err := db.From("test").ScanStructsContext(ctx, &myStructs); err != nil{ +// panic(err.Error() +// } +// //use your structs +// +// i: A pointer to a slice of structs. +func (q QueryExecutor) ScanStructsContext(ctx context.Context, i interface{}) error { + scanner, err := q.ScannerContext(ctx) + if err != nil { + return err + } + defer func() { _ = scanner.Close() }() + return scanner.ScanStructs(i) +} + +// This will execute the SQL and fill out the struct with the fields returned. +// This method returns a boolean value that is false if no record was found +// +// var myStruct MyStruct +// found, err := db.From("test").Limit(1).ScanStruct(&myStruct) +// if err != nil{ +// panic(err.Error() +// } +// if !found{ +// fmt.Println("NOT FOUND") +// } +// +// i: A pointer to a struct +func (q QueryExecutor) ScanStruct(i interface{}) (bool, error) { + return q.ScanStructContext(context.Background(), i) +} + +// This will execute the SQL and fill out the struct with the fields returned. +// This method returns a boolean value that is false if no record was found +// +// var myStruct MyStruct +// found, err := db.From("test").Limit(1).ScanStructContext(ctx, &myStruct) +// if err != nil{ +// panic(err.Error() +// } +// if !found{ +// fmt.Println("NOT FOUND") +// } +// +// i: A pointer to a struct +func (q QueryExecutor) ScanStructContext(ctx context.Context, i interface{}) (bool, error) { + val := reflect.ValueOf(i) + if !util.IsPointer(val.Kind()) { + return false, errUnsupportedScanStructType + } + val = reflect.Indirect(val) + if !util.IsStruct(val.Kind()) { + return false, errUnsupportedScanStructType + } + + scanner, err := q.ScannerContext(ctx) + if err != nil { + return false, err + } + + defer func() { _ = scanner.Close() }() + + if scanner.Next() { + err = scanner.ScanStruct(i) + if err != nil { + return false, err + } + + return true, scanner.Err() + } + + return false, scanner.Err() +} + +// This will execute the SQL and append results to the slice. +// +// var ids []uint32 +// if err := db.From("test").Select("id").ScanVals(&ids); err != nil{ +// panic(err.Error() +// } +// +// i: Takes a pointer to a slice of primitive values. +func (q QueryExecutor) ScanVals(i interface{}) error { + return q.ScanValsContext(context.Background(), i) +} + +// This will execute the SQL and append results to the slice. +// +// var ids []uint32 +// if err := db.From("test").Select("id").ScanValsContext(ctx, &ids); err != nil{ +// panic(err.Error() +// } +// +// i: Takes a pointer to a slice of primitive values. +func (q QueryExecutor) ScanValsContext(ctx context.Context, i interface{}) error { + scanner, err := q.ScannerContext(ctx) + if err != nil { + return err + } + defer func() { _ = scanner.Close() }() + return scanner.ScanVals(i) +} + +// This will execute the SQL and set the value of the primitive. This method will return false if no record is found. +// +// var id uint32 +// found, err := db.From("test").Select("id").Limit(1).ScanVal(&id) +// if err != nil{ +// panic(err.Error() +// } +// if !found{ +// fmt.Println("NOT FOUND") +// } +// +// i: Takes a pointer to a primitive value. +func (q QueryExecutor) ScanVal(i interface{}) (bool, error) { + return q.ScanValContext(context.Background(), i) +} + +// This will execute the SQL and set the value of the primitive. This method will return false if no record is found. +// +// var id uint32 +// found, err := db.From("test").Select("id").Limit(1).ScanValContext(ctx, &id) +// if err != nil{ +// panic(err.Error() +// } +// if !found{ +// fmt.Println("NOT FOUND") +// } +// +// i: Takes a pointer to a primitive value. +func (q QueryExecutor) ScanValContext(ctx context.Context, i interface{}) (bool, error) { + val := reflect.ValueOf(i) + if !util.IsPointer(val.Kind()) { + return false, errScanValPointer + } + val = reflect.Indirect(val) + if util.IsSlice(val.Kind()) { + switch i.(type) { + case *gsql.RawBytes: // do nothing + case *[]byte: // do nothing + case gsql.Scanner: // do nothing + default: + return false, errScanValNonSlice + } + } + + scanner, err := q.ScannerContext(ctx) + if err != nil { + return false, err + } + + defer func() { _ = scanner.Close() }() + + if scanner.Next() { + err = scanner.ScanVal(i) + if err != nil { + return false, err + } + + return true, scanner.Err() + } + + return false, scanner.Err() +} + +// Scanner will return a Scanner that can be used for manually scanning rows. +func (q QueryExecutor) Scanner() (Scanner, error) { + return q.ScannerContext(context.Background()) +} + +// ScannerContext will return a Scanner that can be used for manually scanning rows. +func (q QueryExecutor) ScannerContext(ctx context.Context) (Scanner, error) { + rows, err := q.QueryContext(ctx) + if err != nil { + return nil, err + } + return NewScanner(rows), nil +} diff --git a/exec/query_executor_internal_test.go b/exec/query_executor_internal_test.go new file mode 100644 index 0000000..7b84bbe --- /dev/null +++ b/exec/query_executor_internal_test.go @@ -0,0 +1,1247 @@ +package exec + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" +) + +var ( + testAddr1 = "111 Test Addr" + testAddr2 = "211 Test Addr" + testName1 = "Test1" + testName2 = "Test2" + testPhone1 = "111-111-1111" + testPhone2 = "222-222-2222" + testAge1 int64 = 10 + testAge2 int64 = 20 + testByteSliceContent = "byte slice result" + otherAddr1 = "111 Test Addr Other" + otherAddr2 = "211 Test Addr Other" + otherName1 = "Test1 Other" + otherName2 = "Test2 Other" +) + +type queryExecutorSuite struct { + suite.Suite +} + +func (qes *queryExecutorSuite) TestWithError() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + ctx := context.Background() + db, _, err := sqlmock.New() + qes.NoError(err) + expectedErr := fmt.Errorf("crud exec error") + e := newQueryExecutor(db, expectedErr, `SELECT * FROM "items"`) + var items []StructWithTags + qes.EqualError(e.ScanStructs(&items), expectedErr.Error()) + qes.EqualError(e.ScanStructsContext(ctx, &items), expectedErr.Error()) + found, err := e.ScanStruct(&StructWithTags{}) + qes.EqualError(err, expectedErr.Error()) + qes.False(found) + found, err = e.ScanStructContext(ctx, &StructWithTags{}) + qes.EqualError(err, expectedErr.Error()) + qes.False(found) + var vals []string + qes.EqualError(e.ScanVals(&vals), expectedErr.Error()) + qes.EqualError(e.ScanValsContext(ctx, &vals), expectedErr.Error()) + var val string + found, err = e.ScanVal(&val) + qes.EqualError(err, expectedErr.Error()) + qes.False(found) + found, err = e.ScanValContext(ctx, &val) + qes.EqualError(err, expectedErr.Error()) + qes.False(found) +} + +func (qes *queryExecutorSuite) TestToSQL() { + db, _, err := sqlmock.New() + qes.NoError(err) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + query, args, err := e.ToSQL() + qes.NoError(err) + qes.Equal(`SELECT * FROM "items"`, query) + qes.Empty(args) +} + +func (qes *queryExecutorSuite) TestScanStructs_withTaggedFields() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithTags + qes.NoError(e.ScanStructs(&items)) + qes.Equal([]StructWithTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructs_withUntaggedFields() { + type StructWithNoTags struct { + Address string + Name string + } + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2)) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithNoTags + qes.NoError(e.ScanStructs(&items)) + qes.Equal([]StructWithNoTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructs_withPointerFields() { + type StructWithPointerFields struct { + Str *string + Time *time.Time + Bool *bool + Int *int64 + Float *float64 + } + db, mock, err := sqlmock.New() + qes.NoError(err) + now := time.Now() + str1, str2 := "str1", "str2" + t := true + var i1, i2 int64 = 1, 2 + f1, f2 := 1.1, 2.1 + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"str", "time", "bool", "int", "float"}). + AddRow(str1, now, true, i1, f1). + AddRow(str2, now, true, i2, f2). + AddRow(nil, nil, nil, nil, nil), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithPointerFields + qes.NoError(e.ScanStructs(&items)) + qes.Equal([]StructWithPointerFields{ + {Str: &str1, Time: &now, Bool: &t, Int: &i1, Float: &f1}, + {Str: &str2, Time: &now, Bool: &t, Int: &i2, Float: &f2}, + {}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructs_withPrivateFields() { + type StructWithPrivateTags struct { + private string //nolint:structcheck,unused // need for test + Address string `db:"address"` + Name string `db:"name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithPrivateTags + qes.NoError(e.ScanStructs(&items)) + qes.Equal([]StructWithPrivateTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructs_pointers() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []*StructWithTags + qes.NoError(e.ScanStructs(&items)) + qes.Equal([]*StructWithTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructs_withIgnoredEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredStruct struct { + StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + AddRow(testPhone1, testAge1).AddRow(testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]ComposedIgnoredStruct{ + {StructWithTags: StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_withEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]ComposedStruct{ + {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]*ComposedStruct{ + {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStructDuplicateFields() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedStructWithDuplicateFields struct { + StructWithTags + Address string `db:"other_address"` + Name string `db:"other_name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "other_address", "other_name"}). + AddRow(testAddr1, testName1, otherAddr1, otherName1). + AddRow(testAddr2, testName2, otherAddr2, otherName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedStructWithDuplicateFields + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]*ComposedStructWithDuplicateFields{ + { + StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, + Address: otherAddr1, + Name: otherName1, + }, + { + StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, + Address: otherAddr2, + Name: otherName2, + }, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedPointerDuplicateFields() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithWithPointerWithDuplicateFields struct { + *StructWithTags + Address string `db:"other_address"` + Name string `db:"other_name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "other_address", "other_name"}). + AddRow(testAddr1, testName1, otherAddr1, otherName1). + AddRow(testAddr2, testName2, otherAddr2, otherName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedWithWithPointerWithDuplicateFields + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]*ComposedWithWithPointerWithDuplicateFields{ + { + StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, + Address: otherAddr1, + Name: otherName1, + }, + { + StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, + Address: otherAddr2, + Name: otherName2, + }, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_withIgnoredEmbeddedPointerStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredPointerStruct struct { + *StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + AddRow(testPhone1, testAge1). + AddRow(testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredPointerStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]ComposedIgnoredPointerStruct{ + {PhoneNumber: testPhone1, Age: testAge1}, + {PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_withEmbeddedStructPointer() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedWithPointerStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStructPointer() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedWithPointerStruct + qes.NoError(e.ScanStructs(&composed)) + qes.Equal([]*ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructs_badValue() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + tests := []struct { + name string + items interface{} + }{ + { + name: "non-pointer items", + items: []StructWithTags{}, + }, + { + name: "non-slice items", + items: &StructWithTags{}, + }, + } + for i := range tests { + test := tests[i] + qes.Run(test.name, func() { + db, mock, err := sqlmock.New() + qes.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1).AddRow(testAddr2, testName2), + ) + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + qes.Equal(errUnsupportedScanStructsType, e.ScanStructs(test.items)) + }) + } +} + +func (qes *queryExecutorSuite) TestScanStructs_queryError() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WillReturnError(fmt.Errorf("queryExecutor error")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithTags + qes.EqualError(e.ScanStructs(&items), "queryExecutor error") +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withTaggedFields() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithTags + qes.NoError(e.ScanStructsContext(ctx, &items)) + qes.Equal([]StructWithTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withUntaggedFields() { + type StructWithNoTags struct { + Address string + Name string + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithNoTags + qes.NoError(e.ScanStructsContext(ctx, &items)) + qes.Equal([]StructWithNoTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withPointerFields() { + type StructWithPointerFields struct { + Address *string + Name *string + } + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithPointerFields + qes.NoError(e.ScanStructsContext(ctx, &items)) + qes.Equal([]StructWithPointerFields{ + {Address: &testAddr1, Name: &testName1}, + {Address: &testAddr2, Name: &testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_pointers() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []*StructWithTags + qes.NoError(e.ScanStructsContext(ctx, &items)) + qes.Equal([]*StructWithTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedStruct + qes.NoError(e.ScanStructsContext(ctx, &composed)) + qes.Equal([]ComposedStruct{ + {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withIgnoredEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredStruct struct { + StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + AddRow(testPhone1, testAge1). + AddRow(testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredStruct + qes.NoError(e.ScanStructsContext(ctx, &composed)) + qes.Equal([]ComposedIgnoredStruct{ + {StructWithTags: StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_pointersWithEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedStruct + qes.NoError(e.ScanStructsContext(ctx, &composed)) + qes.Equal([]*ComposedStruct{ + {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_withEmbeddedStructPointer() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedWithPointerStruct + qes.NoError(e.ScanStructsContext(ctx, &composed)) + qes.Equal([]ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_pointersWithEmbeddedStructPointer() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1). + AddRow(testAddr2, testName2, testPhone2, testAge2), + ) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []*ComposedWithPointerStruct + qes.NoError(e.ScanStructsContext(ctx, &composed)) + qes.Equal([]*ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, + {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, + }, composed) +} + +func (qes *queryExecutorSuite) TestScanStructsContext_badValue() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + tests := []struct { + name string + items interface{} + }{ + { + name: "non-pointer items", + items: []StructWithTags{}, + }, + { + name: "non-slice items", + items: &StructWithTags{}, + }, + } + for i := range tests { + test := tests[i] + qes.Run(test.name, func() { + db, mock, err := sqlmock.New() + qes.NoError(err) + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1).AddRow(testAddr2, testName2), + ) + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + qes.Equal(errUnsupportedScanStructsType, e.ScanStructsContext(context.Background(), test.items)) + }) + } +} + +func (qes *queryExecutorSuite) TestScanStructsContext_queryError() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WillReturnError(fmt.Errorf("queryExecutor error")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithTags + qes.EqualError(e.ScanStructsContext(ctx, &items), "queryExecutor error") +} + +func (qes *queryExecutorSuite) TestScanStruct() { + type StructWithNoTags struct { + Address string + Name string + } + + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WillReturnError(fmt.Errorf("queryExecutor error")) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(nil, nil), + ) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"})) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1), + ) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1), + ) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). + AddRow(testAddr1, testName1, testPhone1, testAge1), + ) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).AddRow(testAddr1, testName1)) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var slicePtr []StructWithTags + var item StructWithTags + found, err := e.ScanStruct(item) + qes.Equal(errUnsupportedScanStructType, err) + qes.False(found) + found, err = e.ScanStruct(&slicePtr) + qes.Equal(errUnsupportedScanStructType, err) + qes.False(found) + found, err = e.ScanStruct(&item) + qes.EqualError(err, "queryExecutor error") + qes.False(found) + + found, err = e.ScanStruct(&item) + qes.Error(err) + qes.False(found) + + found, err = e.ScanStruct(&item) + qes.NoError(err) + qes.False(found) + + found, err = e.ScanStruct(&item) + qes.NoError(err) + qes.True(found) + qes.Equal(StructWithTags{ + Address: testAddr1, + Name: testName1, + }, item) + + var composed ComposedStruct + found, err = e.ScanStruct(&composed) + qes.NoError(err) + qes.True(found) + qes.Equal(ComposedStruct{ + StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, + PhoneNumber: testPhone1, + Age: testAge1, + }, composed) + + var embeddedPtr ComposedWithPointerStruct + found, err = e.ScanStruct(&embeddedPtr) + qes.NoError(err) + qes.True(found) + qes.Equal(ComposedWithPointerStruct{ + StructWithTags: &StructWithTags{ + Address: testAddr1, + Name: testName1, + }, + PhoneNumber: testPhone1, + Age: testAge1, + }, embeddedPtr) + + var noTag StructWithNoTags + found, err = e.ScanStruct(&noTag) + qes.NoError(err) + qes.True(found) + qes.Equal(StructWithNoTags{ + Address: testAddr1, + Name: testName1, + }, noTag) +} + +func (qes *queryExecutorSuite) TestScanStruct_taggedStructs() { + type StructWithNoTags struct { + Address string + Name string + } + + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + type StructWithTaggedStructs struct { + NoTags StructWithNoTags `db:"notags"` + Tags StructWithTags `db:"tags"` + Composed ComposedStruct `db:"composedstruct"` + ComposedPointer ComposedWithPointerStruct `db:"composedptrstruct"` + } + + db, mock, err := sqlmock.New() + qes.NoError(err) + + cols := []string{ + "notags.address", "notags.name", + "tags.address", "tags.name", + "composedstruct.address", "composedstruct.name", "composedstruct.phone_number", "composedstruct.age", + "composedptrstruct.address", "composedptrstruct.name", "composedptrstruct.phone_number", "composedptrstruct.age", + } + + q := `SELECT` + strings.Join(cols, ", ") + ` FROM "items"` + + mock.ExpectQuery(q). + WithArgs(). + WillReturnRows(sqlmock.NewRows(cols).AddRow( + testAddr1, testName1, + testAddr2, testName2, + testAddr1, testName1, testPhone1, testAge1, + testAddr2, testName2, testPhone2, testAge2, + )) + + e := newQueryExecutor(db, nil, q) + + var item StructWithTaggedStructs + found, err := e.ScanStruct(&item) + qes.NoError(err) + qes.True(found) + qes.Equal(StructWithTaggedStructs{ + NoTags: StructWithNoTags{Address: testAddr1, Name: testName1}, + Tags: StructWithTags{Address: testAddr2, Name: testName2}, + Composed: ComposedStruct{ + StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, + PhoneNumber: testPhone1, + Age: testAge1, + }, + ComposedPointer: ComposedWithPointerStruct{ + StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, + PhoneNumber: testPhone2, + Age: testAge2, + }, + }, item) +} + +func (qes *queryExecutorSuite) TestScanVals() { + db, mock, err := sqlmock.New() + qes.NoError(err) + + var id1, id2 int64 = 1, 2 + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WillReturnError(fmt.Errorf("queryExecutor error")) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).RowError(0, fmt.Errorf("row error"))) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow("a")) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow(id2)) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow(id2)) + + e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) + + var ids []int64 + qes.EqualError(e.ScanVals(&ids), "queryExecutor error") + qes.EqualError(e.ScanVals(&ids), "row error") + qes.Error(e.ScanVals(&ids)) + + ids = ids[0:0] + qes.NoError(e.ScanVals(&ids)) + qes.Equal(ids, []int64{id1, id2}) + + var pointers []*int64 + qes.NoError(e.ScanVals(&pointers)) + qes.Len(pointers, 2) + qes.Equal(&id1, pointers[0]) + qes.Equal(&id2, pointers[1]) +} + +func (qes *queryExecutorSuite) TestScanValsError() { + var id int64 + + tests := []struct { + name string + items interface{} + }{ + { + name: "non-pointer items", + items: []int64{}, + }, + { + name: "non-slice items", + items: &id, + }, + } + for i := range tests { + test := tests[i] + qes.Run(test.name, func() { + db, mock, err := sqlmock.New() + qes.NoError(err) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1).AddRow(2)) + + e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) + qes.Equal(errUnsupportedScanValsType, e.ScanVals(test.items)) + }) + } +} + +func (qes *queryExecutorSuite) TestScanVal() { + db, mock, err := sqlmock.New() + qes.NoError(err) + + id1 := int64(1) + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WillReturnError(fmt.Errorf("queryExecutor error")) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).RowError(0, fmt.Errorf("row error")).AddRow(id1)) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("c")) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1)) + + e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) + + var id int64 + var ids []int64 + found, err := e.ScanVal(id) + qes.Equal(errScanValPointer, err) + qes.False(found) + found, err = e.ScanVal(&ids) + qes.Equal(errScanValNonSlice, err) + qes.False(found) + found, err = e.ScanVal(&id) + qes.EqualError(err, "queryExecutor error") + qes.False(found) + + found, err = e.ScanVal(&id) + qes.EqualError(err, "row error") + qes.False(found) + + found, err = e.ScanVal(&id) + qes.Error(err) + qes.False(found) + + var ptrID *int64 + found, err = e.ScanVal(&ptrID) + qes.NoError(err) + qes.True(found) + qes.Equal(&id1, ptrID) +} + +func (qes *queryExecutorSuite) TestScanVal_withByteSlice() { + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(testByteSliceContent)) + + e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) + + var bytes []byte + found, err := e.ScanVal(bytes) + qes.Equal(errScanValPointer, err) + qes.False(found) + + found, err = e.ScanVal(&bytes) + qes.NoError(err) + qes.True(found) + qes.Equal([]byte(testByteSliceContent), bytes) +} + +func (qes *queryExecutorSuite) TestScanVal_withRawBytes() { + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(testByteSliceContent)) + + e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) + + var bytes sql.RawBytes + found, err := e.ScanVal(bytes) + qes.Equal(errScanValPointer, err) + qes.False(found) + + found, err = e.ScanVal(&bytes) + qes.NoError(err) + qes.True(found) + qes.Equal(sql.RawBytes(testByteSliceContent), bytes) +} + +type JSONBoolArray []bool + +func (b *JSONBoolArray) Scan(src interface{}) error { + return json.Unmarshal(src.([]byte), b) +} + +func (qes *queryExecutorSuite) TestScanVal_withValuerSlice() { + db, mock, err := sqlmock.New() + qes.NoError(err) + + mock.ExpectQuery(`SELECT "bools" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"bools"}).FromCSVString(`"[true, false, true]"`)) + + e := newQueryExecutor(db, nil, `SELECT "bools" FROM "items"`) + + var bools JSONBoolArray + found, err := e.ScanVal(bools) + qes.Equal(errScanValPointer, err) + qes.False(found) + + found, err = e.ScanVal(&bools) + qes.NoError(err) + qes.True(found) + qes.Equal(JSONBoolArray{true, false, true}, bools) +} + +func TestQueryExecutorSuite(t *testing.T) { + suite.Run(t, new(queryExecutorSuite)) +} diff --git a/exec/query_factory.go b/exec/query_factory.go new file mode 100644 index 0000000..d900872 --- /dev/null +++ b/exec/query_factory.go @@ -0,0 +1,36 @@ +package exec + +import ( + "context" + "database/sql" + + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + //nolint:stylecheck // keep name for backwards compatibility + DbExecutor interface { + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + } + QueryFactory interface { + FromSQL(sql string, args ...interface{}) QueryExecutor + FromSQLBuilder(b sb.SQLBuilder) QueryExecutor + } + querySupport struct { + de DbExecutor + } +) + +func NewQueryFactory(de DbExecutor) QueryFactory { + return &querySupport{de} +} + +func (qs *querySupport) FromSQL(query string, args ...interface{}) QueryExecutor { + return newQueryExecutor(qs.de, nil, query, args...) +} + +func (qs *querySupport) FromSQLBuilder(b sb.SQLBuilder) QueryExecutor { + query, args, err := b.ToSQL() + return newQueryExecutor(qs.de, err, query, args...) +} diff --git a/exec/scanner.go b/exec/scanner.go new file mode 100644 index 0000000..d70fbb3 --- /dev/null +++ b/exec/scanner.go @@ -0,0 +1,168 @@ +package exec + +import ( + "database/sql" + "reflect" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/util" +) + +type ( + // Scanner knows how to scan sql.Rows into structs. + Scanner interface { + Next() bool + ScanStruct(i interface{}) error + ScanStructs(i interface{}) error + ScanVal(i interface{}) error + ScanVals(i interface{}) error + Close() error + Err() error + } + + scanner struct { + rows *sql.Rows + columnMap util.ColumnMap + columns []string + } +) + +func unableToFindFieldError(col string) error { + return errors.New(`unable to find corresponding field to column "%s" returned by query`, col) +} + +// NewScanner returns a scanner that can be used for scanning rows into structs. +func NewScanner(rows *sql.Rows) Scanner { + return &scanner{rows: rows} +} + +// Next prepares the next row for Scanning. See sql.Rows#Next for more +// information. +func (s *scanner) Next() bool { + return s.rows.Next() +} + +// Err returns the error, if any that was encountered during iteration. See +// sql.Rows#Err for more information. +func (s *scanner) Err() error { + return s.rows.Err() +} + +// ScanStruct will scan the current row into i. +func (s *scanner) ScanStruct(i interface{}) error { + // Setup columnMap and columns, but only once. + if s.columnMap == nil || s.columns == nil { + cm, err := util.GetColumnMap(i) + if err != nil { + return err + } + + cols, err := s.rows.Columns() + if err != nil { + return err + } + + s.columnMap = cm + s.columns = cols + } + + scans := make([]interface{}, 0, len(s.columns)) + for _, col := range s.columns { + data, ok := s.columnMap[col] + switch { + case !ok: + return unableToFindFieldError(col) + default: + scans = append(scans, reflect.New(data.GoType).Interface()) + } + } + + if err := s.rows.Scan(scans...); err != nil { + return err + } + + record := exp.Record{} + for index, col := range s.columns { + record[col] = scans[index] + } + + util.AssignStructVals(i, record, s.columnMap) + + return s.Err() +} + +// ScanStructs scans results in slice of structs +func (s *scanner) ScanStructs(i interface{}) error { + val, err := checkScanStructsTarget(i) + if err != nil { + return err + } + return s.scanIntoSlice(val, func(i interface{}) error { + return s.ScanStruct(i) + }) +} + +// ScanVal will scan the current row and column into i. +func (s *scanner) ScanVal(i interface{}) error { + if err := s.rows.Scan(i); err != nil { + return err + } + + return s.Err() +} + +// ScanStructs scans results in slice of values +func (s *scanner) ScanVals(i interface{}) error { + val, err := checkScanValsTarget(i) + if err != nil { + return err + } + return s.scanIntoSlice(val, func(i interface{}) error { + return s.ScanVal(i) + }) +} + +// Close closes the Rows, preventing further enumeration. See sql.Rows#Close +// for more info. +func (s *scanner) Close() error { + return s.rows.Close() +} + +func (s *scanner) scanIntoSlice(val reflect.Value, it func(i interface{}) error) error { + elemType := util.GetSliceElementType(val) + + for s.Next() { + row := reflect.New(elemType) + if rowErr := it(row.Interface()); rowErr != nil { + return rowErr + } + util.AppendSliceElement(val, row) + } + + return s.Err() +} + +func checkScanStructsTarget(i interface{}) (reflect.Value, error) { + val := reflect.ValueOf(i) + if !util.IsPointer(val.Kind()) { + return val, errUnsupportedScanStructsType + } + val = reflect.Indirect(val) + if !util.IsSlice(val.Kind()) { + return val, errUnsupportedScanStructsType + } + return val, nil +} + +func checkScanValsTarget(i interface{}) (reflect.Value, error) { + val := reflect.ValueOf(i) + if !util.IsPointer(val.Kind()) { + return val, errUnsupportedScanValsType + } + val = reflect.Indirect(val) + if !util.IsSlice(val.Kind()) { + return val, errUnsupportedScanValsType + } + return val, nil +} diff --git a/exec/scanner_internal_test.go b/exec/scanner_internal_test.go new file mode 100644 index 0000000..0a73b1f --- /dev/null +++ b/exec/scanner_internal_test.go @@ -0,0 +1,69 @@ +package exec + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" +) + +type scannerSuite struct { + suite.Suite +} + +func TestScanner(t *testing.T) { + suite.Run(t, &scannerSuite{}) +} + +func (s *scannerSuite) TestScanStructs() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + db, mock, err := sqlmock.New() + s.Require().NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + AddRow(testAddr1, testName1). + AddRow(testAddr2, testName2), + ) + rows, err := db.Query(`SELECT * FROM "items"`) + s.Require().NoError(err) + + sc := NewScanner(rows) + + result := make([]StructWithTags, 0) + err = sc.ScanStructs(result) + s.Require().EqualError(err, errUnsupportedScanStructsType.Error()) + + err = sc.ScanStructs(&result) + s.Require().NoError(err) + s.Require().ElementsMatch( + []StructWithTags{{Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}}, + result, + ) +} + +func (s *scannerSuite) TestScanVals() { + db, mock, err := sqlmock.New() + s.Require().NoError(err) + + mock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1).AddRow(2)) + + rows, err := db.Query(`SELECT "id" FROM "items"`) + s.Require().NoError(err) + + sc := NewScanner(rows) + + result := make([]int, 0) + err = sc.ScanVals(result) + s.Require().EqualError(err, errUnsupportedScanValsType.Error()) + + err = sc.ScanVals(&result) + s.Require().NoError(err) + s.Require().ElementsMatch([]int{1, 2}, result) +} diff --git a/exp/alias.go b/exp/alias.go new file mode 100644 index 0000000..202f77f --- /dev/null +++ b/exp/alias.go @@ -0,0 +1,60 @@ +package exp + +import "fmt" + +type ( + aliasExpression struct { + aliased Expression + alias IdentifierExpression + } +) + +// Creates a new AliasedExpression for the Expression and alias +func NewAliasExpression(exp Expression, alias interface{}) AliasedExpression { + switch v := alias.(type) { + case string: + return aliasExpression{aliased: exp, alias: ParseIdentifier(v)} + case IdentifierExpression: + return aliasExpression{aliased: exp, alias: v} + default: + panic(fmt.Sprintf("Cannot create alias from %+v", v)) + } +} + +func (ae aliasExpression) Clone() Expression { + return NewAliasExpression(ae.aliased, ae.alias.Clone()) +} + +func (ae aliasExpression) Expression() Expression { + return ae +} + +func (ae aliasExpression) Aliased() Expression { + return ae.aliased +} + +func (ae aliasExpression) GetAs() IdentifierExpression { + return ae.alias +} + +// Returns a new IdentifierExpression with the specified schema +func (ae aliasExpression) Schema(schema string) IdentifierExpression { + return ae.alias.Schema(schema) +} + +// Returns a new IdentifierExpression with the specified table +func (ae aliasExpression) Table(table string) IdentifierExpression { + return ae.alias.Table(table) +} + +// Returns a new IdentifierExpression with the specified column +func (ae aliasExpression) Col(col interface{}) IdentifierExpression { + return ae.alias.Col(col) +} + +// Returns a new IdentifierExpression with the column set to * +// +// I("my_table").As("t").All() //"t".* +func (ae aliasExpression) All() IdentifierExpression { + return ae.alias.All() +} diff --git a/exp/alias_test.go b/exp/alias_test.go new file mode 100644 index 0000000..571ed47 --- /dev/null +++ b/exp/alias_test.go @@ -0,0 +1,69 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type aliasExpressionSuite struct { + suite.Suite +} + +func TestAliasExpressionSuite(t *testing.T) { + suite.Run(t, &aliasExpressionSuite{}) +} + +func (aes *aliasExpressionSuite) TestClone() { + ae := exp.NewAliasExpression(exp.NewIdentifierExpression("", "", "col"), "c") + aes.Equal(ae, ae.Clone()) +} + +func (aes *aliasExpressionSuite) TestExpression() { + ae := exp.NewAliasExpression(exp.NewIdentifierExpression("", "", "col"), "c") + aes.Equal(ae, ae.Expression()) +} + +func (aes *aliasExpressionSuite) TestAliased() { + ident := exp.NewIdentifierExpression("", "", "col") + ae := exp.NewAliasExpression(ident, "c") + aes.Equal(ident, ae.Aliased()) +} + +func (aes *aliasExpressionSuite) TestGetAs() { + ae := exp.NewAliasExpression(exp.NewIdentifierExpression("", "", "col"), "c") + aes.Equal(exp.NewIdentifierExpression("", "", "c"), ae.GetAs()) +} + +func (aes *aliasExpressionSuite) TestSchema() { + si := exp.NewAliasExpression( + exp.NewIdentifierExpression("", "t", nil), + exp.NewIdentifierExpression("", "t", nil), + ).Schema("s") + aes.Equal(exp.NewIdentifierExpression("s", "t", nil), si) +} + +func (aes *aliasExpressionSuite) TestTable() { + si := exp.NewAliasExpression( + exp.NewIdentifierExpression("schema", "", nil), + exp.NewIdentifierExpression("s", "", nil), + ).Table("t") + aes.Equal(exp.NewIdentifierExpression("s", "t", nil), si) +} + +func (aes *aliasExpressionSuite) TestCol() { + si := exp.NewAliasExpression( + exp.NewIdentifierExpression("", "table", nil), + exp.NewIdentifierExpression("", "t", nil), + ).Col("c") + aes.Equal(exp.NewIdentifierExpression("", "t", "c"), si) +} + +func (aes *aliasExpressionSuite) TestAll() { + si := exp.NewAliasExpression( + exp.NewIdentifierExpression("", "table", nil), + exp.NewIdentifierExpression("", "t", nil), + ).All() + aes.Equal(exp.NewIdentifierExpression("", "t", exp.Star()), si) +} diff --git a/exp/bitwise.go b/exp/bitwise.go new file mode 100644 index 0000000..eede5a2 --- /dev/null +++ b/exp/bitwise.go @@ -0,0 +1,89 @@ +package exp + +type bitwise struct { + lhs Expression + rhs interface{} + op BitwiseOperation +} + +func NewBitwiseExpression(op BitwiseOperation, lhs Expression, rhs interface{}) BitwiseExpression { + return bitwise{op: op, lhs: lhs, rhs: rhs} +} + +func (b bitwise) Clone() Expression { + return NewBitwiseExpression(b.op, b.lhs.Clone(), b.rhs) +} + +func (b bitwise) RHS() interface{} { + return b.rhs +} + +func (b bitwise) LHS() Expression { + return b.lhs +} + +func (b bitwise) Op() BitwiseOperation { + return b.op +} + +func (b bitwise) Expression() Expression { return b } +func (b bitwise) As(val interface{}) AliasedExpression { return NewAliasExpression(b, val) } +func (b bitwise) Eq(val interface{}) BooleanExpression { return eq(b, val) } +func (b bitwise) Neq(val interface{}) BooleanExpression { return neq(b, val) } +func (b bitwise) Gt(val interface{}) BooleanExpression { return gt(b, val) } +func (b bitwise) Gte(val interface{}) BooleanExpression { return gte(b, val) } +func (b bitwise) Lt(val interface{}) BooleanExpression { return lt(b, val) } +func (b bitwise) Lte(val interface{}) BooleanExpression { return lte(b, val) } +func (b bitwise) Asc() OrderedExpression { return asc(b) } +func (b bitwise) Desc() OrderedExpression { return desc(b) } +func (b bitwise) Like(i interface{}) BooleanExpression { return like(b, i) } +func (b bitwise) NotLike(i interface{}) BooleanExpression { return notLike(b, i) } +func (b bitwise) ILike(i interface{}) BooleanExpression { return iLike(b, i) } +func (b bitwise) NotILike(i interface{}) BooleanExpression { return notILike(b, i) } +func (b bitwise) RegexpLike(val interface{}) BooleanExpression { return regexpLike(b, val) } +func (b bitwise) RegexpNotLike(val interface{}) BooleanExpression { return regexpNotLike(b, val) } +func (b bitwise) RegexpILike(val interface{}) BooleanExpression { return regexpILike(b, val) } +func (b bitwise) RegexpNotILike(val interface{}) BooleanExpression { return regexpNotILike(b, val) } +func (b bitwise) In(i ...interface{}) BooleanExpression { return in(b, i...) } +func (b bitwise) NotIn(i ...interface{}) BooleanExpression { return notIn(b, i...) } +func (b bitwise) Is(i interface{}) BooleanExpression { return is(b, i) } +func (b bitwise) IsNot(i interface{}) BooleanExpression { return isNot(b, i) } +func (b bitwise) IsNull() BooleanExpression { return is(b, nil) } +func (b bitwise) IsNotNull() BooleanExpression { return isNot(b, nil) } +func (b bitwise) IsTrue() BooleanExpression { return is(b, true) } +func (b bitwise) IsNotTrue() BooleanExpression { return isNot(b, true) } +func (b bitwise) IsFalse() BooleanExpression { return is(b, false) } +func (b bitwise) IsNotFalse() BooleanExpression { return isNot(b, false) } +func (b bitwise) Distinct() SQLFunctionExpression { return NewSQLFunctionExpression("DISTINCT", b) } +func (b bitwise) Between(val RangeVal) RangeExpression { return between(b, val) } +func (b bitwise) NotBetween(val RangeVal) RangeExpression { return notBetween(b, val) } + +// used internally to create a Bitwise Inversion BitwiseExpression +func bitwiseInversion(rhs Expression) BitwiseExpression { + return NewBitwiseExpression(BitwiseInversionOp, nil, rhs) +} + +// used internally to create a Bitwise OR BitwiseExpression +func bitwiseOr(lhs Expression, rhs interface{}) BitwiseExpression { + return NewBitwiseExpression(BitwiseOrOp, lhs, rhs) +} + +// used internally to create a Bitwise AND BitwiseExpression +func bitwiseAnd(lhs Expression, rhs interface{}) BitwiseExpression { + return NewBitwiseExpression(BitwiseAndOp, lhs, rhs) +} + +// used internally to create a Bitwise XOR BitwiseExpression +func bitwiseXor(lhs Expression, rhs interface{}) BitwiseExpression { + return NewBitwiseExpression(BitwiseXorOp, lhs, rhs) +} + +// used internally to create a Bitwise LEFT SHIFT BitwiseExpression +func bitwiseLeftShift(lhs Expression, rhs interface{}) BitwiseExpression { + return NewBitwiseExpression(BitwiseLeftShiftOp, lhs, rhs) +} + +// used internally to create a Bitwise RIGHT SHIFT BitwiseExpression +func bitwiseRightShift(lhs Expression, rhs interface{}) BitwiseExpression { + return NewBitwiseExpression(BitwiseRightShiftOp, lhs, rhs) +} diff --git a/exp/bitwise_test.go b/exp/bitwise_test.go new file mode 100644 index 0000000..b24347c --- /dev/null +++ b/exp/bitwise_test.go @@ -0,0 +1,84 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type bitwiseExpressionSuite struct { + suite.Suite +} + +func TestBitwiseExpressionSuite(t *testing.T) { + suite.Run(t, &bitwiseExpressionSuite{}) +} + +func (bes *bitwiseExpressionSuite) TestClone() { + be := exp.NewBitwiseExpression(exp.BitwiseAndOp, exp.NewIdentifierExpression("", "", "col"), 1) + bes.Equal(be, be.Clone()) +} + +func (bes *bitwiseExpressionSuite) TestExpression() { + be := exp.NewBitwiseExpression(exp.BitwiseAndOp, exp.NewIdentifierExpression("", "", "col"), 1) + bes.Equal(be, be.Expression()) +} + +func (bes *bitwiseExpressionSuite) TestAs() { + be := exp.NewBitwiseExpression(exp.BitwiseInversionOp, exp.NewIdentifierExpression("", "", "col"), 1) + bes.Equal(exp.NewAliasExpression(be, "a"), be.As("a")) +} + +func (bes *bitwiseExpressionSuite) TestAsc() { + be := exp.NewBitwiseExpression(exp.BitwiseAndOp, exp.NewIdentifierExpression("", "", "col"), 1) + bes.Equal(exp.NewOrderedExpression(be, exp.AscDir, exp.NoNullsSortType), be.Asc()) +} + +func (bes *bitwiseExpressionSuite) TestDesc() { + be := exp.NewBitwiseExpression(exp.BitwiseOrOp, exp.NewIdentifierExpression("", "", "col"), 1) + bes.Equal(exp.NewOrderedExpression(be, exp.DescSortDir, exp.NoNullsSortType), be.Desc()) +} + +func (bes *bitwiseExpressionSuite) TestAllOthers() { + be := exp.NewBitwiseExpression(exp.BitwiseRightShiftOp, exp.NewIdentifierExpression("", "", "col"), 1) + rv := exp.NewRangeVal(1, 2) + pattern := "bitwiseExp like%" + inVals := []interface{}{1, 2} + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: be.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, be, 1)}, + {Ex: be.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, be, 1)}, + {Ex: be.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, be, 1)}, + {Ex: be.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, be, 1)}, + {Ex: be.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, be, 1)}, + {Ex: be.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, be, 1)}, + {Ex: be.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, be, rv)}, + {Ex: be.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, be, rv)}, + {Ex: be.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, be, pattern)}, + {Ex: be.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, be, pattern)}, + {Ex: be.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, be, pattern)}, + {Ex: be.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, be, pattern)}, + {Ex: be.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, be, pattern)}, + {Ex: be.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, be, pattern)}, + {Ex: be.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, be, pattern)}, + {Ex: be.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, be, pattern)}, + {Ex: be.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, be, inVals)}, + {Ex: be.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, be, inVals)}, + {Ex: be.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, be, true)}, + {Ex: be.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, be, true)}, + {Ex: be.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, be, nil)}, + {Ex: be.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, be, nil)}, + {Ex: be.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, be, true)}, + {Ex: be.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, be, true)}, + {Ex: be.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, be, false)}, + {Ex: be.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, be, false)}, + {Ex: be.Distinct(), Expected: exp.NewSQLFunctionExpression("DISTINCT", be)}, + } + + for _, tc := range testCases { + bes.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/bool.go b/exp/bool.go new file mode 100644 index 0000000..a38f356 --- /dev/null +++ b/exp/bool.go @@ -0,0 +1,185 @@ +package exp + +import ( + "reflect" + "regexp" +) + +type boolean struct { + lhs Expression + rhs interface{} + op BooleanOperation +} + +func NewBooleanExpression(op BooleanOperation, lhs Expression, rhs interface{}) BooleanExpression { + return boolean{op: op, lhs: lhs, rhs: rhs} +} + +func (b boolean) Clone() Expression { + return NewBooleanExpression(b.op, b.lhs.Clone(), b.rhs) +} + +func (b boolean) Expression() Expression { + return b +} + +func (b boolean) RHS() interface{} { + return b.rhs +} + +func (b boolean) LHS() Expression { + return b.lhs +} + +func (b boolean) Op() BooleanOperation { + return b.op +} + +func (b boolean) As(val interface{}) AliasedExpression { + return NewAliasExpression(b, val) +} + +// used internally to create an equality BooleanExpression +func eq(lhs Expression, rhs interface{}) BooleanExpression { + return checkBoolExpType(EqOp, lhs, rhs, false) +} + +// used internally to create an in-equality BooleanExpression +func neq(lhs Expression, rhs interface{}) BooleanExpression { + return checkBoolExpType(EqOp, lhs, rhs, true) +} + +// used internally to create an gt comparison BooleanExpression +func gt(lhs Expression, rhs interface{}) BooleanExpression { + return NewBooleanExpression(GtOp, lhs, rhs) +} + +// used internally to create an gte comparison BooleanExpression +func gte(lhs Expression, rhs interface{}) BooleanExpression { + return NewBooleanExpression(GteOp, lhs, rhs) +} + +// used internally to create an lt comparison BooleanExpression +func lt(lhs Expression, rhs interface{}) BooleanExpression { + return NewBooleanExpression(LtOp, lhs, rhs) +} + +// used internally to create an lte comparison BooleanExpression +func lte(lhs Expression, rhs interface{}) BooleanExpression { + return NewBooleanExpression(LteOp, lhs, rhs) +} + +// used internally to create an IN BooleanExpression +func in(lhs Expression, vals ...interface{}) BooleanExpression { + if len(vals) == 1 && reflect.Indirect(reflect.ValueOf(vals[0])).Kind() == reflect.Slice { + return NewBooleanExpression(InOp, lhs, vals[0]) + } + return NewBooleanExpression(InOp, lhs, vals) +} + +// used internally to create a NOT IN BooleanExpression +func notIn(lhs Expression, vals ...interface{}) BooleanExpression { + if len(vals) == 1 && reflect.Indirect(reflect.ValueOf(vals[0])).Kind() == reflect.Slice { + return NewBooleanExpression(NotInOp, lhs, vals[0]) + } + return NewBooleanExpression(NotInOp, lhs, vals) +} + +// used internally to create an IS BooleanExpression +func is(lhs Expression, val interface{}) BooleanExpression { + return checkBoolExpType(IsOp, lhs, val, false) +} + +// used internally to create an IS NOT BooleanExpression +func isNot(lhs Expression, val interface{}) BooleanExpression { + return checkBoolExpType(IsOp, lhs, val, true) +} + +// used internally to create a LIKE BooleanExpression +func like(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(LikeOp, lhs, val, false) +} + +// used internally to create an ILIKE BooleanExpression +func iLike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(ILikeOp, lhs, val, false) +} + +// used internally to create a NOT LIKE BooleanExpression +func notLike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(LikeOp, lhs, val, true) +} + +// used internally to create a NOT ILIKE BooleanExpression +func notILike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(ILikeOp, lhs, val, true) +} + +// used internally to create a LIKE BooleanExpression +func regexpLike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(RegexpLikeOp, lhs, val, false) +} + +// used internally to create an ILIKE BooleanExpression +func regexpILike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(RegexpILikeOp, lhs, val, false) +} + +// used internally to create a NOT LIKE BooleanExpression +func regexpNotLike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(RegexpLikeOp, lhs, val, true) +} + +// used internally to create a NOT ILIKE BooleanExpression +func regexpNotILike(lhs Expression, val interface{}) BooleanExpression { + return checkLikeExp(RegexpILikeOp, lhs, val, true) +} + +// checks an like rhs to create the proper like expression for strings or regexps +func checkLikeExp(op BooleanOperation, lhs Expression, val interface{}, invert bool) BooleanExpression { + rhs := val + + if t, ok := val.(*regexp.Regexp); ok { + if op == LikeOp { + op = RegexpLikeOp + } else if op == ILikeOp { + op = RegexpILikeOp + } + rhs = t.String() + } + if invert { + op = operatorInversions[op] + } + return NewBooleanExpression(op, lhs, rhs) +} + +// checks a boolean operation normalizing the operation based on the RHS (e.g. "a" = true vs "a" IS TRUE +func checkBoolExpType(op BooleanOperation, lhs Expression, rhs interface{}, invert bool) BooleanExpression { + if rhs == nil { + op = IsOp + } else { + switch reflect.Indirect(reflect.ValueOf(rhs)).Kind() { + case reflect.Bool: + op = IsOp + case reflect.Slice: + // if its a slice of bytes dont treat as an IN + if _, ok := rhs.([]byte); !ok { + op = InOp + } + case reflect.Struct: + switch rhs.(type) { + case SQLExpression: + op = InOp + case AppendableExpression: + op = InOp + case *regexp.Regexp: + return checkLikeExp(LikeOp, lhs, rhs, invert) + } + default: + } + } + if invert { + op = operatorInversions[op] + } + return NewBooleanExpression(op, lhs, rhs) +} diff --git a/exp/case.go b/exp/case.go new file mode 100644 index 0000000..d20800a --- /dev/null +++ b/exp/case.go @@ -0,0 +1,78 @@ +package exp + +type ( + caseElse struct { + result interface{} + } + caseWhen struct { + caseElse + condition interface{} + } + caseExpression struct { + value interface{} + whens []CaseWhen + elseCondition CaseElse + } +) + +func NewCaseElse(result interface{}) CaseElse { + return caseElse{result: result} +} + +func (ce caseElse) Result() interface{} { + return ce.result +} + +func NewCaseWhen(condition, result interface{}) CaseWhen { + return caseWhen{caseElse: caseElse{result: result}, condition: condition} +} + +func (cw caseWhen) Condition() interface{} { + return cw.condition +} + +func NewCaseExpression() CaseExpression { + return caseExpression{value: nil, whens: []CaseWhen{}, elseCondition: nil} +} + +func (c caseExpression) Expression() Expression { + return c +} + +func (c caseExpression) Clone() Expression { + return caseExpression{value: c.value, whens: c.whens, elseCondition: c.elseCondition} +} + +func (c caseExpression) As(alias interface{}) AliasedExpression { + return NewAliasExpression(c, alias) +} + +func (c caseExpression) GetValue() interface{} { + return c.value +} + +func (c caseExpression) GetWhens() []CaseWhen { + return c.whens +} + +func (c caseExpression) GetElse() CaseElse { + return c.elseCondition +} + +func (c caseExpression) Value(value interface{}) CaseExpression { + c.value = value + return c +} + +func (c caseExpression) When(condition, result interface{}) CaseExpression { + c.whens = append(c.whens, NewCaseWhen(condition, result)) + return c +} + +func (c caseExpression) Else(result interface{}) CaseExpression { + c.elseCondition = NewCaseElse(result) + return c +} + +func (c caseExpression) Asc() OrderedExpression { return asc(c) } +func (c caseExpression) Desc() OrderedExpression { return desc(c) } diff --git a/exp/case_test.go b/exp/case_test.go new file mode 100644 index 0000000..0cb23b2 --- /dev/null +++ b/exp/case_test.go @@ -0,0 +1,99 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type caseExpressionSuite struct { + suite.Suite +} + +func TestCaseExpressionSuite(t *testing.T) { + suite.Run(t, &caseExpressionSuite{}) +} + +func (ces *caseExpressionSuite) TestClone() { + ce := exp.NewCaseExpression() + ces.Equal(ce, ce.Clone()) +} + +func (ces *caseExpressionSuite) TestExpression() { + ce := exp.NewCaseExpression() + ces.Equal(ce, ce.Expression()) +} + +func (ces *caseExpressionSuite) TestAs() { + ce := exp.NewCaseExpression() + ces.Equal(exp.NewAliasExpression(ce, "a"), ce.As("a")) +} + +func (ces *caseExpressionSuite) TestValue() { + ce := exp.NewCaseExpression() + ces.Nil(ce.GetValue()) + + ce = exp.NewCaseExpression().Value(exp.NewIdentifierExpression("", "", "a")) + ces.Equal(exp.NewIdentifierExpression("", "", "a"), ce.GetValue()) +} + +func (ces *caseExpressionSuite) TestWhen() { + condition1 := exp.NewIdentifierExpression("", "", "a").Eq(10) + condition2 := exp.NewIdentifierExpression("", "", "b").Eq(20) + ce := exp.NewCaseExpression() + ces.Equal([]exp.CaseWhen{ + exp.NewCaseWhen(condition1, "a"), + exp.NewCaseWhen(condition2, "b"), + }, ce.When(condition1, "a").When(condition2, "b").GetWhens()) + + ces.Empty(ce.GetWhens()) +} + +func (ces *caseExpressionSuite) TestElse() { + ce := exp.NewCaseExpression() + ces.Equal(exp.NewCaseElse("a"), ce.Else("a").GetElse()) + + ces.Nil(ce.GetElse()) +} + +func (ces *caseExpressionSuite) TestAsc() { + ce := exp.NewCaseExpression() + ces.Equal(exp.NewOrderedExpression(ce, exp.AscDir, exp.NoNullsSortType), ce.Asc()) +} + +func (ces *caseExpressionSuite) TestDesc() { + ce := exp.NewCaseExpression() + ces.Equal(exp.NewOrderedExpression(ce, exp.DescSortDir, exp.NoNullsSortType), ce.Desc()) +} + +type caseWhenSuite struct { + suite.Suite +} + +func TestCaseWhenSuite(t *testing.T) { + suite.Run(t, &caseWhenSuite{}) +} + +func (cws *caseWhenSuite) TestCondition() { + ce := exp.NewCaseWhen(true, false) + cws.Equal(true, ce.Condition()) +} + +func (cws *caseWhenSuite) TestResult() { + ce := exp.NewCaseWhen(true, false) + cws.Equal(false, ce.Result()) +} + +type caseElseSuite struct { + suite.Suite +} + +func TestCaseElseSuite(t *testing.T) { + suite.Run(t, &caseElseSuite{}) +} + +func (ces *caseElseSuite) TestResult() { + ce := exp.NewCaseElse(false) + ces.Equal(false, ce.Result()) +} diff --git a/exp/cast.go b/exp/cast.go new file mode 100644 index 0000000..f1045d9 --- /dev/null +++ b/exp/cast.go @@ -0,0 +1,57 @@ +package exp + +type cast struct { + casted Expression + t LiteralExpression +} + +// Creates a new Casted expression +// +// Cast(I("a"), "NUMERIC") -> CAST("a" AS NUMERIC) +func NewCastExpression(e Expression, t string) CastExpression { + return cast{casted: e, t: NewLiteralExpression(t)} +} + +func (c cast) Casted() Expression { + return c.casted +} + +func (c cast) Type() LiteralExpression { + return c.t +} + +func (c cast) Clone() Expression { + return cast{casted: c.casted.Clone(), t: c.t} +} + +func (c cast) Expression() Expression { return c } +func (c cast) As(val interface{}) AliasedExpression { return NewAliasExpression(c, val) } +func (c cast) Eq(val interface{}) BooleanExpression { return eq(c, val) } +func (c cast) Neq(val interface{}) BooleanExpression { return neq(c, val) } +func (c cast) Gt(val interface{}) BooleanExpression { return gt(c, val) } +func (c cast) Gte(val interface{}) BooleanExpression { return gte(c, val) } +func (c cast) Lt(val interface{}) BooleanExpression { return lt(c, val) } +func (c cast) Lte(val interface{}) BooleanExpression { return lte(c, val) } +func (c cast) Asc() OrderedExpression { return asc(c) } +func (c cast) Desc() OrderedExpression { return desc(c) } +func (c cast) Like(i interface{}) BooleanExpression { return like(c, i) } +func (c cast) NotLike(i interface{}) BooleanExpression { return notLike(c, i) } +func (c cast) ILike(i interface{}) BooleanExpression { return iLike(c, i) } +func (c cast) NotILike(i interface{}) BooleanExpression { return notILike(c, i) } +func (c cast) RegexpLike(val interface{}) BooleanExpression { return regexpLike(c, val) } +func (c cast) RegexpNotLike(val interface{}) BooleanExpression { return regexpNotLike(c, val) } +func (c cast) RegexpILike(val interface{}) BooleanExpression { return regexpILike(c, val) } +func (c cast) RegexpNotILike(val interface{}) BooleanExpression { return regexpNotILike(c, val) } +func (c cast) In(i ...interface{}) BooleanExpression { return in(c, i...) } +func (c cast) NotIn(i ...interface{}) BooleanExpression { return notIn(c, i...) } +func (c cast) Is(i interface{}) BooleanExpression { return is(c, i) } +func (c cast) IsNot(i interface{}) BooleanExpression { return isNot(c, i) } +func (c cast) IsNull() BooleanExpression { return is(c, nil) } +func (c cast) IsNotNull() BooleanExpression { return isNot(c, nil) } +func (c cast) IsTrue() BooleanExpression { return is(c, true) } +func (c cast) IsNotTrue() BooleanExpression { return isNot(c, true) } +func (c cast) IsFalse() BooleanExpression { return is(c, false) } +func (c cast) IsNotFalse() BooleanExpression { return isNot(c, false) } +func (c cast) Distinct() SQLFunctionExpression { return NewSQLFunctionExpression("DISTINCT", c) } +func (c cast) Between(val RangeVal) RangeExpression { return between(c, val) } +func (c cast) NotBetween(val RangeVal) RangeExpression { return notBetween(c, val) } diff --git a/exp/cast_test.go b/exp/cast_test.go new file mode 100644 index 0000000..212c51d --- /dev/null +++ b/exp/cast_test.go @@ -0,0 +1,81 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type castExpressionSuite struct { + suite.Suite + ce exp.CastExpression +} + +func TestCastExpressionSuite(t *testing.T) { + suite.Run(t, &castExpressionSuite{ + ce: exp.NewCastExpression(exp.NewIdentifierExpression("", "", "a"), "TEXT"), + }) +} + +func (ces *castExpressionSuite) TestClone() { + ces.Equal(ces.ce, ces.ce.Clone()) +} + +func (ces *castExpressionSuite) TestExpression() { + ces.Equal(ces.ce, ces.ce.Expression()) +} + +func (ces *castExpressionSuite) TestCasted() { + ces.Equal(exp.NewIdentifierExpression("", "", "a"), ces.ce.Casted()) +} + +func (ces *castExpressionSuite) TestType() { + ces.Equal(exp.NewLiteralExpression("TEXT"), ces.ce.Type()) +} + +func (ces *castExpressionSuite) TestAllOthers() { + ce := ces.ce + rv := exp.NewRangeVal(1, 2) + pattern := "cast like%" + inVals := []interface{}{1, 2} + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: ce.As("a"), Expected: exp.NewAliasExpression(ce, "a")}, + {Ex: ce.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, ce, 1)}, + {Ex: ce.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, ce, 1)}, + {Ex: ce.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, ce, 1)}, + {Ex: ce.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, ce, 1)}, + {Ex: ce.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, ce, 1)}, + {Ex: ce.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, ce, 1)}, + {Ex: ce.Asc(), Expected: exp.NewOrderedExpression(ce, exp.AscDir, exp.NoNullsSortType)}, + {Ex: ce.Desc(), Expected: exp.NewOrderedExpression(ce, exp.DescSortDir, exp.NoNullsSortType)}, + {Ex: ce.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, ce, rv)}, + {Ex: ce.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, ce, rv)}, + {Ex: ce.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, ce, pattern)}, + {Ex: ce.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, ce, pattern)}, + {Ex: ce.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, ce, pattern)}, + {Ex: ce.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, ce, pattern)}, + {Ex: ce.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, ce, pattern)}, + {Ex: ce.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, ce, pattern)}, + {Ex: ce.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, ce, pattern)}, + {Ex: ce.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, ce, pattern)}, + {Ex: ce.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, ce, inVals)}, + {Ex: ce.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, ce, inVals)}, + {Ex: ce.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, ce, true)}, + {Ex: ce.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, ce, true)}, + {Ex: ce.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, ce, nil)}, + {Ex: ce.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ce, nil)}, + {Ex: ce.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, ce, true)}, + {Ex: ce.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ce, true)}, + {Ex: ce.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, ce, false)}, + {Ex: ce.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ce, false)}, + {Ex: ce.Distinct(), Expected: exp.NewSQLFunctionExpression("DISTINCT", ce)}, + } + + for _, tc := range testCases { + ces.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/col.go b/exp/col.go new file mode 100644 index 0000000..4a88315 --- /dev/null +++ b/exp/col.go @@ -0,0 +1,85 @@ +package exp + +import ( + "fmt" + "reflect" + + "git.fsdpf.net/go/db/v2/internal/util" +) + +type columnList struct { + columns []Expression +} + +func NewColumnListExpression(vals ...interface{}) ColumnListExpression { + cols := []Expression{} + for _, val := range vals { + switch t := val.(type) { + case nil: // do nothing + case string: + cols = append(cols, ParseIdentifier(t)) + case ColumnListExpression: + cols = append(cols, t.Columns()...) + case Expression: + cols = append(cols, t) + default: + _, valKind := util.GetTypeInfo(val, reflect.Indirect(reflect.ValueOf(val))) + + if valKind == reflect.Struct { + cm, err := util.GetColumnMap(val) + if err != nil { + panic(err.Error()) + } + structCols := cm.Cols() + for _, col := range structCols { + i := ParseIdentifier(col) + var sc Expression = i + if i.IsQualified() { + sc = i.As(NewIdentifierExpression("", "", col)) + } + cols = append(cols, sc) + } + } else { + panic(fmt.Sprintf("Cannot create expression from %+v", val)) + } + } + } + return columnList{columns: cols} +} + +func NewOrderedColumnList(vals ...OrderedExpression) ColumnListExpression { + exps := make([]interface{}, 0, len(vals)) + for _, col := range vals { + exps = append(exps, col.Expression()) + } + return NewColumnListExpression(exps...) +} + +func (cl columnList) Clone() Expression { + newExps := make([]Expression, 0, len(cl.columns)) + for _, exp := range cl.columns { + newExps = append(newExps, exp.Clone()) + } + return columnList{columns: newExps} +} + +func (cl columnList) Expression() Expression { + return cl +} + +func (cl columnList) IsEmpty() bool { + return len(cl.columns) == 0 +} + +func (cl columnList) Columns() []Expression { + return cl.columns +} + +func (cl columnList) Append(cols ...Expression) ColumnListExpression { + ret := columnList{} + exps := ret.columns + exps = append(exps, cl.columns...) + exps = append(exps, cols...) + ret.columns = exps + return ret +} diff --git a/exp/compound.go b/exp/compound.go new file mode 100644 index 0000000..d631991 --- /dev/null +++ b/exp/compound.go @@ -0,0 +1,19 @@ +package exp + +type compound struct { + t CompoundType + rhs AppendableExpression +} + +func NewCompoundExpression(ct CompoundType, rhs AppendableExpression) CompoundExpression { + return compound{t: ct, rhs: rhs} +} + +func (c compound) Expression() Expression { return c } + +func (c compound) Clone() Expression { + return compound{t: c.t, rhs: c.rhs.Clone().(AppendableExpression)} +} + +func (c compound) Type() CompoundType { return c.t } +func (c compound) RHS() AppendableExpression { return c.rhs } diff --git a/exp/conflict.go b/exp/conflict.go new file mode 100644 index 0000000..99f2de0 --- /dev/null +++ b/exp/conflict.go @@ -0,0 +1,89 @@ +package exp + +type ( + doNothingConflict struct{} + // ConflictUpdate is the struct that represents the UPDATE fragment of an + // INSERT ... ON CONFLICT/ON DUPLICATE KEY DO UPDATE statement + conflictUpdate struct { + target string + update interface{} + whereClause ExpressionList + } +) + +// Creates a conflict struct to be passed to InsertConflict to ignore constraint errors +// +// InsertConflict(DoNothing(),...) -> INSERT INTO ... ON CONFLICT DO NOTHING +func NewDoNothingConflictExpression() ConflictExpression { + return &doNothingConflict{} +} + +func (c doNothingConflict) Expression() Expression { + return c +} + +func (c doNothingConflict) Clone() Expression { + return c +} + +func (c doNothingConflict) Action() ConflictAction { + return DoNothingConflictAction +} + +// Creates a ConflictUpdate struct to be passed to InsertConflict +// Represents a ON CONFLICT DO UPDATE portion of an INSERT statement (ON DUPLICATE KEY UPDATE for mysql) +// +// InsertConflict(DoUpdate("target_column", update),...) -> +// INSERT INTO ... ON CONFLICT DO UPDATE SET a=b +// InsertConflict(DoUpdate("target_column", update).Where(Ex{"a": 1},...) -> +// INSERT INTO ... ON CONFLICT DO UPDATE SET a=b WHERE a=1 +func NewDoUpdateConflictExpression(target string, update interface{}) ConflictUpdateExpression { + return &conflictUpdate{target: target, update: update} +} + +func (c conflictUpdate) Expression() Expression { + return c +} + +func (c conflictUpdate) Clone() Expression { + return &conflictUpdate{ + target: c.target, + update: c.update, + whereClause: c.whereClause.Clone().(ExpressionList), + } +} + +func (c conflictUpdate) Action() ConflictAction { + return DoUpdateConflictAction +} + +// Returns the target conflict column. Only necessary for Postgres. +// Will return an error for mysql/sqlite. Will also return an error if missing from a postgres ConflictUpdate. +func (c conflictUpdate) TargetColumn() string { + return c.target +} + +// Returns the Updates which represent the ON CONFLICT DO UPDATE portion of an insert statement. If nil, +// there are no updates. +func (c conflictUpdate) Update() interface{} { + return c.update +} + +// Append to the existing Where clause for an ON CONFLICT DO UPDATE ... WHERE ... +// +// InsertConflict(DoNothing(),...) -> INSERT INTO ... ON CONFLICT DO NOTHING +func (c *conflictUpdate) Where(expressions ...Expression) ConflictUpdateExpression { + if c.whereClause == nil { + c.whereClause = NewExpressionList(AndType, expressions...) + } else { + c.whereClause = c.whereClause.Append(expressions...) + } + return c +} + +// Append to the existing Where clause for an ON CONFLICT DO UPDATE ... WHERE ... +// +// InsertConflict(DoNothing(),...) -> INSERT INTO ... ON CONFLICT DO NOTHING +func (c *conflictUpdate) WhereClause() ExpressionList { + return c.whereClause +} diff --git a/exp/cte.go b/exp/cte.go new file mode 100644 index 0000000..4282d0b --- /dev/null +++ b/exp/cte.go @@ -0,0 +1,23 @@ +package exp + +type commonExpr struct { + recursive bool + name LiteralExpression + subQuery Expression +} + +// Creates a new WITH common table expression for a SQLExpression, typically Datasets'. This function is used +// internally by Dataset when a CTE is added to another Dataset +func NewCommonTableExpression(recursive bool, name string, subQuery Expression) CommonTableExpression { + return commonExpr{recursive: recursive, name: NewLiteralExpression(name), subQuery: subQuery} +} + +func (ce commonExpr) Expression() Expression { return ce } + +func (ce commonExpr) Clone() Expression { + return commonExpr{recursive: ce.recursive, name: ce.name, subQuery: ce.subQuery.Clone().(SQLExpression)} +} + +func (ce commonExpr) IsRecursive() bool { return ce.recursive } +func (ce commonExpr) Name() LiteralExpression { return ce.name } +func (ce commonExpr) SubQuery() Expression { return ce.subQuery } diff --git a/exp/delete_clauses.go b/exp/delete_clauses.go new file mode 100644 index 0000000..25af851 --- /dev/null +++ b/exp/delete_clauses.go @@ -0,0 +1,177 @@ +package exp + +type ( + DeleteClauses interface { + HasFrom() bool + clone() *deleteClauses + + CommonTables() []CommonTableExpression + CommonTablesAppend(cte CommonTableExpression) DeleteClauses + + From() IdentifierExpression + SetFrom(table IdentifierExpression) DeleteClauses + + Where() ExpressionList + ClearWhere() DeleteClauses + WhereAppend(expressions ...Expression) DeleteClauses + + Order() ColumnListExpression + HasOrder() bool + ClearOrder() DeleteClauses + SetOrder(oes ...OrderedExpression) DeleteClauses + OrderAppend(...OrderedExpression) DeleteClauses + OrderPrepend(...OrderedExpression) DeleteClauses + + Limit() interface{} + HasLimit() bool + ClearLimit() DeleteClauses + SetLimit(limit interface{}) DeleteClauses + + Returning() ColumnListExpression + HasReturning() bool + SetReturning(cl ColumnListExpression) DeleteClauses + } + deleteClauses struct { + commonTables []CommonTableExpression + from IdentifierExpression + where ExpressionList + order ColumnListExpression + limit interface{} + returning ColumnListExpression + } +) + +func NewDeleteClauses() DeleteClauses { + return &deleteClauses{} +} + +func (dc *deleteClauses) HasFrom() bool { + return dc.from != nil +} + +func (dc *deleteClauses) clone() *deleteClauses { + return &deleteClauses{ + commonTables: dc.commonTables, + from: dc.from, + + where: dc.where, + order: dc.order, + limit: dc.limit, + returning: dc.returning, + } +} + +func (dc *deleteClauses) CommonTables() []CommonTableExpression { + return dc.commonTables +} + +func (dc *deleteClauses) CommonTablesAppend(cte CommonTableExpression) DeleteClauses { + ret := dc.clone() + ret.commonTables = append(ret.commonTables, cte) + return ret +} + +func (dc *deleteClauses) From() IdentifierExpression { + return dc.from +} + +func (dc *deleteClauses) SetFrom(table IdentifierExpression) DeleteClauses { + ret := dc.clone() + ret.from = table + return ret +} + +func (dc *deleteClauses) Where() ExpressionList { + return dc.where +} + +func (dc *deleteClauses) ClearWhere() DeleteClauses { + ret := dc.clone() + ret.where = nil + return ret +} + +func (dc *deleteClauses) WhereAppend(expressions ...Expression) DeleteClauses { + if len(expressions) == 0 { + return dc + } + ret := dc.clone() + if ret.where == nil { + ret.where = NewExpressionList(AndType, expressions...) + } else { + ret.where = ret.where.Append(expressions...) + } + return ret +} + +func (dc *deleteClauses) Order() ColumnListExpression { + return dc.order +} + +func (dc *deleteClauses) HasOrder() bool { + return dc.order != nil +} + +func (dc *deleteClauses) ClearOrder() DeleteClauses { + ret := dc.clone() + ret.order = nil + return ret +} + +func (dc *deleteClauses) SetOrder(oes ...OrderedExpression) DeleteClauses { + ret := dc.clone() + ret.order = NewOrderedColumnList(oes...) + return ret +} + +func (dc *deleteClauses) OrderAppend(oes ...OrderedExpression) DeleteClauses { + if dc.order == nil { + return dc.SetOrder(oes...) + } + ret := dc.clone() + ret.order = ret.order.Append(NewOrderedColumnList(oes...).Columns()...) + return ret +} + +func (dc *deleteClauses) OrderPrepend(oes ...OrderedExpression) DeleteClauses { + if dc.order == nil { + return dc.SetOrder(oes...) + } + ret := dc.clone() + ret.order = NewOrderedColumnList(oes...).Append(ret.order.Columns()...) + return ret +} + +func (dc *deleteClauses) Limit() interface{} { + return dc.limit +} + +func (dc *deleteClauses) HasLimit() bool { + return dc.limit != nil +} + +func (dc *deleteClauses) ClearLimit() DeleteClauses { + ret := dc.clone() + ret.limit = nil + return ret +} + +func (dc *deleteClauses) SetLimit(limit interface{}) DeleteClauses { + ret := dc.clone() + ret.limit = limit + return ret +} + +func (dc *deleteClauses) Returning() ColumnListExpression { + return dc.returning +} + +func (dc *deleteClauses) HasReturning() bool { + return dc.returning != nil && !dc.returning.IsEmpty() +} + +func (dc *deleteClauses) SetReturning(cl ColumnListExpression) DeleteClauses { + ret := dc.clone() + ret.returning = cl + return ret +} diff --git a/exp/delete_clauses_test.go b/exp/delete_clauses_test.go new file mode 100644 index 0000000..0a04f6f --- /dev/null +++ b/exp/delete_clauses_test.go @@ -0,0 +1,256 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type deleteClausesSuite struct { + suite.Suite +} + +func TestDeleteClausesSuite(t *testing.T) { + suite.Run(t, new(deleteClausesSuite)) +} + +func (dcs *deleteClausesSuite) TestHasFrom() { + c := exp.NewDeleteClauses() + c2 := c.SetFrom(exp.NewIdentifierExpression("", "test", "")) + + dcs.False(c.HasFrom()) + + dcs.True(c2.HasFrom()) +} + +func (dcs *deleteClausesSuite) TestFrom() { + c := exp.NewDeleteClauses() + ti := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetFrom(ti) + + dcs.Nil(c.From()) + + dcs.Equal(ti, c2.From()) +} + +func (dcs *deleteClausesSuite) TestSetFrom() { + c := exp.NewDeleteClauses() + ti := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetFrom(ti) + + dcs.Nil(c.From()) + + dcs.Equal(ti, c2.From()) +} + +func (dcs *deleteClausesSuite) TestWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewDeleteClauses() + c2 := c.WhereAppend(w) + + dcs.Nil(c.Where()) + + dcs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) +} + +func (dcs *deleteClausesSuite) TestClearWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewDeleteClauses().WhereAppend(w) + c2 := c.ClearWhere() + + dcs.Equal(exp.NewExpressionList(exp.AndType, w), c.Where()) + + dcs.Nil(c2.Where()) +} + +func (dcs *deleteClausesSuite) TestWhereAppend() { + w := exp.Ex{"a": 1} + w2 := exp.Ex{"b": 2} + + c := exp.NewDeleteClauses() + c2 := c.WhereAppend(w) + + c3 := c.WhereAppend(w).WhereAppend(w2) + + c4 := c.WhereAppend(w, w2) + + dcs.Nil(c.Where()) + + dcs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) + dcs.Equal(exp.NewExpressionList(exp.AndType, w).Append(w2), c3.Where()) + dcs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Where()) +} + +func (dcs *deleteClausesSuite) TestOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewDeleteClauses() + c2 := c.SetOrder(oe) + + dcs.Nil(c.Order()) + + dcs.Equal(exp.NewColumnListExpression(oe), c2.Order()) +} + +func (dcs *deleteClausesSuite) TestHasOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewDeleteClauses() + c2 := c.SetOrder(oe) + + dcs.False(c.HasOrder()) + + dcs.True(c2.HasOrder()) +} + +func (dcs *deleteClausesSuite) TestClearOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewDeleteClauses().SetOrder(oe) + c2 := c.ClearOrder() + + dcs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + dcs.Nil(c2.Order()) +} + +func (dcs *deleteClausesSuite) TestSetOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewDeleteClauses().SetOrder(oe) + c2 := c.SetOrder(oe2) + + dcs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + dcs.Equal(exp.NewColumnListExpression(oe2), c2.Order()) +} + +func (dcs *deleteClausesSuite) TestOrderAppend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewDeleteClauses().SetOrder(oe) + c2 := c.OrderAppend(oe2) + + dcs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + dcs.Equal(exp.NewColumnListExpression(oe, oe2), c2.Order()) +} + +func (dcs *deleteClausesSuite) TestOrderPrepend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewDeleteClauses().SetOrder(oe) + c2 := c.OrderPrepend(oe2) + + dcs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + dcs.Equal(exp.NewColumnListExpression(oe2, oe), c2.Order()) +} + +func (dcs *deleteClausesSuite) TestLimit() { + l := 1 + + c := exp.NewDeleteClauses() + c2 := c.SetLimit(l) + + dcs.Nil(c.Limit()) + + dcs.Equal(l, c2.Limit()) +} + +func (dcs *deleteClausesSuite) TestHasLimit() { + l := 1 + + c := exp.NewDeleteClauses() + c2 := c.SetLimit(l) + + dcs.False(c.HasLimit()) + + dcs.True(c2.HasLimit()) +} + +func (dcs *deleteClausesSuite) TestCLearLimit() { + l := 1 + + c := exp.NewDeleteClauses().SetLimit(l) + c2 := c.ClearLimit() + + dcs.True(c.HasLimit()) + + dcs.False(c2.HasLimit()) +} + +func (dcs *deleteClausesSuite) TestSetLimit() { + l := 1 + l2 := 2 + + c := exp.NewDeleteClauses().SetLimit(l) + c2 := c.SetLimit(2) + + dcs.Equal(l, c.Limit()) + + dcs.Equal(l2, c2.Limit()) +} + +func (dcs *deleteClausesSuite) TestCommonTables() { + cte := exp.NewCommonTableExpression(true, "test", newTestAppendableExpression(`SELECT * FROM "foo"`, []interface{}{})) + + c := exp.NewDeleteClauses() + c2 := c.CommonTablesAppend(cte) + + dcs.Nil(c.CommonTables()) + + dcs.Equal([]exp.CommonTableExpression{cte}, c2.CommonTables()) +} + +func (dcs *deleteClausesSuite) TestAddCommonTablesAppend() { + cte := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte")) + cte2 := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte2")) + + c := exp.NewDeleteClauses().CommonTablesAppend(cte) + c2 := c.CommonTablesAppend(cte2) + + dcs.Equal([]exp.CommonTableExpression{cte}, c.CommonTables()) + + dcs.Equal([]exp.CommonTableExpression{cte, cte2}, c2.CommonTables()) +} + +func (dcs *deleteClausesSuite) TestReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewDeleteClauses() + c2 := c.SetReturning(cl) + + dcs.Nil(c.Returning()) + + dcs.Equal(cl, c2.Returning()) +} + +func (dcs *deleteClausesSuite) TestHasReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewDeleteClauses() + c2 := c.SetReturning(cl) + + dcs.False(c.HasReturning()) + + dcs.True(c2.HasReturning()) +} + +func (dcs *deleteClausesSuite) TestSetReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + cl2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col2")) + + c := exp.NewDeleteClauses().SetReturning(cl) + c2 := c.SetReturning(cl2) + + dcs.Equal(cl, c.Returning()) + + dcs.Equal(cl2, c2.Returning()) +} diff --git a/exp/exp.go b/exp/exp.go new file mode 100644 index 0000000..83b533c --- /dev/null +++ b/exp/exp.go @@ -0,0 +1,734 @@ +package exp + +import ( + "fmt" + + "git.fsdpf.net/go/db/v2/internal/sb" +) + +// Behaviors +type ( + + // Interface that an expression should implement if it can be aliased. + Aliaseable interface { + // Returns an AliasedExpression + // I("col").As("other_col") //"col" AS "other_col" + // I("col").As(I("other_col")) //"col" AS "other_col" + As(interface{}) AliasedExpression + } + + // Interface that an expression should implement if it can be casted to another SQL type . + Castable interface { + // Casts an expression to the specified type + // I("a").Cast("numeric")//CAST("a" AS numeric) + Cast(val string) CastExpression + } + + Inable interface { + // Creates a Boolean expression for IN clauses + // I("col").In([]string{"a", "b", "c"}) //("col" IN ('a', 'b', 'c')) + In(...interface{}) BooleanExpression + // Creates a Boolean expression for NOT IN clauses + // I("col").NotIn([]string{"a", "b", "c"}) //("col" NOT IN ('a', 'b', 'c')) + NotIn(...interface{}) BooleanExpression + } + + Isable interface { + // Creates an Boolean expression IS clauses + // ds.Where(I("a").Is(nil)) //("a" IS NULL) + // ds.Where(I("a").Is(true)) //("a" IS TRUE) + // ds.Where(I("a").Is(false)) //("a" IS FALSE) + Is(interface{}) BooleanExpression + // Creates an Boolean expression IS NOT clauses + // ds.Where(I("a").IsNot(nil)) //("a" IS NOT NULL) + // ds.Where(I("a").IsNot(true)) //("a" IS NOT TRUE) + // ds.Where(I("a").IsNot(false)) //("a" IS NOT FALSE) + IsNot(interface{}) BooleanExpression + // Shortcut for Is(nil) + IsNull() BooleanExpression + // Shortcut for IsNot(nil) + IsNotNull() BooleanExpression + // Shortcut for Is(true) + IsTrue() BooleanExpression + // Shortcut for IsNot(true) + IsNotTrue() BooleanExpression + // Shortcut for Is(false) + IsFalse() BooleanExpression + // Shortcut for IsNot(false) + IsNotFalse() BooleanExpression + } + + Likeable interface { + // Creates an Boolean expression for LIKE clauses + // ds.Where(I("a").Like("a%")) //("a" LIKE 'a%') + Like(interface{}) BooleanExpression + // Creates an Boolean expression for NOT LIKE clauses + // ds.Where(I("a").NotLike("a%")) //("a" NOT LIKE 'a%') + NotLike(interface{}) BooleanExpression + // Creates an Boolean expression for case insensitive LIKE clauses + // ds.Where(I("a").ILike("a%")) //("a" ILIKE 'a%') + ILike(interface{}) BooleanExpression + // Creates an Boolean expression for case insensitive NOT LIKE clauses + // ds.Where(I("a").NotILike("a%")) //("a" NOT ILIKE 'a%') + NotILike(interface{}) BooleanExpression + + // Creates an Boolean expression for REGEXP LIKE clauses + // ds.Where(I("a").RegexpLike("a%")) //("a" ~ 'a%') + RegexpLike(interface{}) BooleanExpression + // Creates an Boolean expression for REGEXP NOT LIKE clauses + // ds.Where(I("a").RegexpNotLike("a%")) //("a" !~ 'a%') + RegexpNotLike(interface{}) BooleanExpression + // Creates an Boolean expression for case insensitive REGEXP ILIKE clauses + // ds.Where(I("a").RegexpILike("a%")) //("a" ~* 'a%') + RegexpILike(interface{}) BooleanExpression + // Creates an Boolean expression for case insensitive REGEXP NOT ILIKE clauses + // ds.Where(I("a").RegexpNotILike("a%")) //("a" !~* 'a%') + RegexpNotILike(interface{}) BooleanExpression + } + + // Interface that an expression should implement if it can be compared with other values. + Comparable interface { + // Creates a Boolean expression comparing equality + // I("col").Eq(1) //("col" = 1) + Eq(interface{}) BooleanExpression + // Creates a Boolean expression comparing in-equality + // I("col").Neq(1) //("col" != 1) + Neq(interface{}) BooleanExpression + // Creates a Boolean expression for greater than comparisons + // I("col").Gt(1) //("col" > 1) + Gt(interface{}) BooleanExpression + // Creates a Boolean expression for greater than or equal to than comparisons + // I("col").Gte(1) //("col" >= 1) + Gte(interface{}) BooleanExpression + // Creates a Boolean expression for less than comparisons + // I("col").Lt(1) //("col" < 1) + Lt(interface{}) BooleanExpression + // Creates a Boolean expression for less than or equal to comparisons + // I("col").Lte(1) //("col" <= 1) + Lte(interface{}) BooleanExpression + } + + // Interface that an expression should implement if it can be used in a DISTINCT epxression. + Distinctable interface { + // Creates a DISTINCT clause + // I("a").Distinct() //DISTINCT("a") + Distinct() SQLFunctionExpression + } + + // Interface that an expression should implement if it can be ORDERED. + Orderable interface { + // Creates an Ordered Expression for sql ASC order + // ds.Order(I("a").Asc()) //ORDER BY "a" ASC + Asc() OrderedExpression + // Creates an Ordered Expression for sql DESC order + // ds.Order(I("a").Desc()) //ORDER BY "a" DESC + Desc() OrderedExpression + } + + Rangeable interface { + // Creates a Range expression for between comparisons + // I("col").Between(RangeVal{Start:1, End:10}) //("col" BETWEEN 1 AND 10) + Between(RangeVal) RangeExpression + // Creates a Range expression for between comparisons + // I("col").NotBetween(RangeVal{Start:1, End:10}) //("col" NOT BETWEEN 1 AND 10) + NotBetween(RangeVal) RangeExpression + } + + Updateable interface { + // Used internally by update sql + Set(interface{}) UpdateExpression + } + + Bitwiseable interface { + // Creates a Bit Operation Expresion for sql ~ + // I("col").BitiInversion() // (~ "col") + BitwiseInversion() BitwiseExpression + // Creates a Bit Operation Expresion for sql | + // I("col").BitOr(1) // ("col" | 1) + BitwiseOr(interface{}) BitwiseExpression + // Creates a Bit Operation Expresion for sql & + // I("col").BitAnd(1) // ("col" & 1) + BitwiseAnd(interface{}) BitwiseExpression + // Creates a Bit Operation Expresion for sql ^ + // I("col").BitXor(1) // ("col" ^ 1) + BitwiseXor(interface{}) BitwiseExpression + // Creates a Bit Operation Expresion for sql << + // I("col").BitLeftShift(1) // ("col" << 1) + BitwiseLeftShift(interface{}) BitwiseExpression + // Creates a Bit Operation Expresion for sql >> + // I("col").BitRighttShift(1) // ("col" >> 1) + BitwiseRightShift(interface{}) BitwiseExpression + } +) + +type ( + Vals []interface{} + // Parent of all expression types + Expression interface { + Clone() Expression + Expression() Expression + } + // An Expression that generates its own sql (e.g Dataset) + SQLExpression interface { + Expression + ToSQL() (string, []interface{}, error) + IsPrepared() bool + } + + AppendableExpression interface { + Expression + AppendSQL(b sb.SQLBuilder) + // Returns the alias value as an identiier expression + GetAs() IdentifierExpression + + // Returns true if this expression returns columns. + // Used to determine if a Select, Update, Insert, or Delete query returns columns + ReturnsColumns() bool + } + // Expression for Aliased expressions + // I("a").As("b") -> "a" AS "b" + // SUM("a").As(I("a_sum")) -> SUM("a") AS "a_sum" + AliasedExpression interface { + Expression + // Returns the Epxression being aliased + Aliased() Expression + // Returns the alias value as an identiier expression + GetAs() IdentifierExpression + + // Returns a new IdentifierExpression with the specified schema + Schema(string) IdentifierExpression + // Returns a new IdentifierExpression with the specified table + Table(string) IdentifierExpression + // Returns a new IdentifierExpression with the specified column + Col(interface{}) IdentifierExpression + // Returns a new IdentifierExpression with the column set to * + // I("my_table").All() //"my_table".* + All() IdentifierExpression + } + + BooleanOperation int + BooleanExpression interface { + Expression + Aliaseable + // Returns the operator for the expression + Op() BooleanOperation + // The left hand side of the expression (e.g. I("a") + LHS() Expression + // The right hand side of the expression could be a primitive value, dataset, or expression + RHS() interface{} + } + + BitwiseOperation int + BitwiseExpression interface { + Expression + Aliaseable + Comparable + Isable + Inable + Likeable + Rangeable + Orderable + Distinctable + // Returns the operator for the expression + Op() BitwiseOperation + // The left hand side of the expression (e.g. I("a") + LHS() Expression + // The right hand side of the expression could be a primitive value, dataset, or expression + RHS() interface{} + } + + // An Expression that represents another Expression casted to a SQL type + CastExpression interface { + Expression + Aliaseable + Comparable + Inable + Isable + Likeable + Orderable + Distinctable + Rangeable + // The exression being casted + Casted() Expression + // The the SQL type to cast the expression to + Type() LiteralExpression + } + // A list of columns. Typically used internally by Select, Order, From + ColumnListExpression interface { + Expression + // Returns the list of columns + Columns() []Expression + // Returns true if the column list is empty + IsEmpty() bool + // Returns a new ColumnListExpression with the columns appended. + Append(...Expression) ColumnListExpression + } + CompoundType int + CompoundExpression interface { + Expression + Type() CompoundType + RHS() AppendableExpression + } + // An Expression that the ON CONFLICT/ON DUPLICATE KEY portion of an INSERT statement + ConflictAction int + ConflictExpression interface { + Expression + Action() ConflictAction + } + ConflictUpdateExpression interface { + ConflictExpression + TargetColumn() string + Where(expressions ...Expression) ConflictUpdateExpression + WhereClause() ExpressionList + Update() interface{} + } + CommonTableExpression interface { + Expression + IsRecursive() bool + // Returns the alias name for the extracted expression + Name() LiteralExpression + // Returns the Expression being extracted + SubQuery() Expression + } + ExpressionListType int + // A list of expressions that should be joined together + // And(I("a").Eq(10), I("b").Eq(11)) //(("a" = 10) AND ("b" = 11)) + // Or(I("a").Eq(10), I("b").Eq(11)) //(("a" = 10) OR ("b" = 11)) + ExpressionList interface { + Expression + // Returns type (e.g. OR, AND) + Type() ExpressionListType + // Slice of expressions that should be joined together + Expressions() []Expression + // Returns a new expression list with the given expressions appended to the current Expressions list + Append(...Expression) ExpressionList + + IsEmpty() bool + } + // An Identifier that can contain schema, table and column identifiers + IdentifierExpression interface { + Expression + Aliaseable + Comparable + Inable + Isable + Likeable + Rangeable + Orderable + Updateable + Distinctable + Castable + Bitwiseable + // returns true if this identifier has more more than on part (Schema, Table or Col) + // "schema" -> true //cant qualify anymore + // "schema.table" -> true + // "table" -> false + // "schema"."table"."col" -> true + // "table"."col" -> true + // "col" -> false + IsQualified() bool + // Returns a new IdentifierExpression with the specified schema + Schema(string) IdentifierExpression + // Returns the current schema + GetSchema() string + // Returns a new IdentifierExpression with the specified table + Table(string) IdentifierExpression + // Returns the current table + GetTable() string + // Returns a new IdentifierExpression with the specified column + Col(interface{}) IdentifierExpression + // Returns the current column + GetCol() interface{} + // Returns a new IdentifierExpression with the column set to * + // I("my_table").All() //"my_table".* + All() IdentifierExpression + + // Returns true if schema table and identifier are all zero values. + IsEmpty() bool + } + InsertExpression interface { + Expression + IsEmpty() bool + IsInsertFrom() bool + From() AppendableExpression + Cols() ColumnListExpression + SetCols(cols ColumnListExpression) InsertExpression + Vals() [][]interface{} + SetVals([][]interface{}) InsertExpression + } + + JoinType int + JoinExpression interface { + Expression + JoinType() JoinType + IsConditioned() bool + Table() Expression + } + // Parent type for join expressions + ConditionedJoinExpression interface { + JoinExpression + Condition() JoinCondition + IsConditionEmpty() bool + } + LateralExpression interface { + Expression + Aliaseable + Table() AppendableExpression + } + + // Expression for representing "literal" sql. + // L("col = 1") -> col = 1) + // L("? = ?", I("col"), 1) -> "col" = 1 + LiteralExpression interface { + Expression + Aliaseable + Comparable + Isable + Inable + Likeable + Rangeable + Orderable + Bitwiseable + // Returns the literal sql + Literal() string + // Arguments to be replaced within the sql + Args() []interface{} + } + + NullSortType int + SortDirection int + // An expression for specifying sort order and options + OrderedExpression interface { + Expression + // The expression being sorted + SortExpression() Expression + // Sort direction (e.g. ASC, DESC) + IsAsc() bool + // If the adapter supports it null sort type (e.g. NULLS FIRST, NULLS LAST) + NullSortType() NullSortType + // Returns a new OrderedExpression with NullSortType set to NULLS_FIRST + NullsFirst() OrderedExpression + // Returns a new OrderedExpression with NullSortType set to NULLS_LAST + NullsLast() OrderedExpression + } + + RangeOperation int + RangeExpression interface { + Expression + // Returns the operator for the expression + Op() RangeOperation + // The left hand side of the expression (e.g. I("a") + LHS() Expression + // The right hand side of the expression could be a primitive value, dataset, or expression + RHS() RangeVal + } + RangeVal interface { + Start() interface{} + End() interface{} + } + + Windowable interface { + Over(WindowExpression) SQLWindowFunctionExpression + OverName(IdentifierExpression) SQLWindowFunctionExpression + } + + // Expression for representing a SQLFunction(e.g. COUNT, SUM, MIN, MAX...) + SQLFunctionExpression interface { + Expression + Aliaseable + Rangeable + Comparable + Orderable + Isable + Inable + Likeable + Windowable + // The function name + Name() string + // Arguments to be passed to the function + Args() []interface{} + } + + UpdateExpression interface { + Col() IdentifierExpression + Val() interface{} + } + + SQLWindowFunctionExpression interface { + Expression + Aliaseable + Rangeable + Comparable + Orderable + Isable + Inable + Likeable + Func() SQLFunctionExpression + + Window() WindowExpression + WindowName() IdentifierExpression + + HasWindow() bool + HasWindowName() bool + } + + WindowExpression interface { + Expression + + Name() IdentifierExpression + HasName() bool + + Parent() IdentifierExpression + HasParent() bool + PartitionCols() ColumnListExpression + HasPartitionBy() bool + OrderCols() ColumnListExpression + HasOrder() bool + + Inherit(parent string) WindowExpression + PartitionBy(cols ...interface{}) WindowExpression + OrderBy(cols ...interface{}) WindowExpression + } + CaseElse interface { + Result() interface{} + } + CaseWhen interface { + Condition() interface{} + Result() interface{} + } + CaseExpression interface { + Expression + Aliaseable + Orderable + GetValue() interface{} + GetWhens() []CaseWhen + GetElse() CaseElse + Value(val interface{}) CaseExpression + When(condition, result interface{}) CaseExpression + Else(result interface{}) CaseExpression + } +) + +const ( + UnionCompoundType CompoundType = iota + UnionAllCompoundType + IntersectCompoundType + IntersectAllCompoundType + + DoNothingConflictAction ConflictAction = iota + DoUpdateConflictAction + + AndType ExpressionListType = iota + OrType + + InnerJoinType JoinType = iota + FullOuterJoinType + RightOuterJoinType + LeftOuterJoinType + FullJoinType + RightJoinType + LeftJoinType + NaturalJoinType + NaturalLeftJoinType + NaturalRightJoinType + NaturalFullJoinType + CrossJoinType + + UsingJoinCondType JoinConditionType = iota + OnJoinCondType + + // Default null sort type with no null sort order + NoNullsSortType NullSortType = iota + // NULLS FIRST + NullsFirstSortType + // NULLS LAST + NullsLastSortType + // ASC + AscDir SortDirection = iota + // DESC + DescSortDir + + // BETWEEN + BetweenOp RangeOperation = iota + // NOT BETWEEN + NotBetweenOp + + // = + EqOp BooleanOperation = iota + // != or <> + NeqOp + // IS + IsOp + // IS NOT + IsNotOp + // > + GtOp + // >= + GteOp + // < + LtOp + // <= + LteOp + // IN + InOp + // NOT IN + NotInOp + // LIKE, LIKE BINARY... + LikeOp + // NOT LIKE, NOT LIKE BINARY... + NotLikeOp + // ILIKE, LIKE + ILikeOp + // NOT ILIKE, NOT LIKE + NotILikeOp + // ~, REGEXP BINARY + RegexpLikeOp + // !~, NOT REGEXP BINARY + RegexpNotLikeOp + // ~*, REGEXP + RegexpILikeOp + // !~*, NOT REGEXP + RegexpNotILikeOp + + betweenStr = "between" + + BitwiseInversionOp BitwiseOperation = iota + BitwiseOrOp + BitwiseAndOp + BitwiseXorOp + BitwiseLeftShiftOp + BitwiseRightShiftOp +) + +var ( + ConditionedJoinTypes = map[JoinType]bool{ + InnerJoinType: true, + FullOuterJoinType: true, + RightOuterJoinType: true, + LeftOuterJoinType: true, + FullJoinType: true, + RightJoinType: true, + LeftJoinType: true, + } + // used internally for inverting operators + operatorInversions = map[BooleanOperation]BooleanOperation{ + IsOp: IsNotOp, + EqOp: NeqOp, + GtOp: LteOp, + GteOp: LtOp, + LtOp: GteOp, + LteOp: GtOp, + InOp: NotInOp, + LikeOp: NotLikeOp, + ILikeOp: NotILikeOp, + RegexpLikeOp: RegexpNotLikeOp, + RegexpILikeOp: RegexpNotILikeOp, + IsNotOp: IsOp, + NeqOp: EqOp, + NotInOp: InOp, + NotLikeOp: LikeOp, + NotILikeOp: ILikeOp, + RegexpNotLikeOp: RegexpLikeOp, + RegexpNotILikeOp: RegexpILikeOp, + } +) + +func (bo BooleanOperation) String() string { + switch bo { + case EqOp: + return "eq" + case NeqOp: + return "neq" + case IsOp: + return "is" + case IsNotOp: + return "isnot" + case GtOp: + return "gt" + case GteOp: + return "gte" + case LtOp: + return "lt" + case LteOp: + return "lte" + case InOp: + return "in" + case NotInOp: + return "notin" + case LikeOp: + return "like" + case NotLikeOp: + return "notlike" + case ILikeOp: + return "ilike" + case NotILikeOp: + return "notilike" + case RegexpLikeOp: + return "regexplike" + case RegexpNotLikeOp: + return "regexpnotlike" + case RegexpILikeOp: + return "regexpilike" + case RegexpNotILikeOp: + return "regexpnotilike" + } + return fmt.Sprintf("%d", bo) +} + +func (bi BitwiseOperation) String() string { + switch bi { + case BitwiseInversionOp: + return "Inversion" + case BitwiseOrOp: + return "OR" + case BitwiseAndOp: + return "AND" + case BitwiseXorOp: + return "XOR" + case BitwiseLeftShiftOp: + return "Left Shift" + case BitwiseRightShiftOp: + return "Right Shift" + } + return fmt.Sprintf("%d", bi) +} + +func (ro RangeOperation) String() string { + switch ro { + case BetweenOp: + return betweenStr + case NotBetweenOp: + return "not between" + } + return fmt.Sprintf("%d", ro) +} + +func (jt JoinType) String() string { + switch jt { + case InnerJoinType: + return "InnerJoinType" + case FullOuterJoinType: + return "FullOuterJoinType" + case RightOuterJoinType: + return "RightOuterJoinType" + case LeftOuterJoinType: + return "LeftOuterJoinType" + case FullJoinType: + return "FullJoinType" + case RightJoinType: + return "RightJoinType" + case LeftJoinType: + return "LeftJoinType" + case NaturalJoinType: + return "NaturalJoinType" + case NaturalLeftJoinType: + return "NaturalLeftJoinType" + case NaturalRightJoinType: + return "NaturalRightJoinType" + case NaturalFullJoinType: + return "NaturalFullJoinType" + case CrossJoinType: + return "CrossJoinType" + } + return fmt.Sprintf("%d", jt) +} diff --git a/exp/exp_list.go b/exp/exp_list.go new file mode 100644 index 0000000..c11bee0 --- /dev/null +++ b/exp/exp_list.go @@ -0,0 +1,67 @@ +package exp + +type ( + expressionList struct { + operator ExpressionListType + expressions []Expression + } +) + +// A list of expressions that should be ORed together +// +// Or(I("a").Eq(10), I("b").Eq(11)) //(("a" = 10) OR ("b" = 11)) +func NewExpressionList(operator ExpressionListType, expressions ...Expression) ExpressionList { + el := expressionList{operator: operator} + exps := make([]Expression, 0, len(el.expressions)) + for _, e := range expressions { + switch t := e.(type) { + case ExpressionList: + if !t.IsEmpty() { + exps = append(exps, e) + } + case Ex: + if len(t) > 0 { + exps = append(exps, e) + } + case ExOr: + if len(t) > 0 { + exps = append(exps, e) + } + default: + exps = append(exps, e) + } + } + el.expressions = exps + return el +} + +func (el expressionList) Clone() Expression { + newExps := make([]Expression, 0, len(el.expressions)) + for _, exp := range el.expressions { + newExps = append(newExps, exp.Clone()) + } + return expressionList{operator: el.operator, expressions: newExps} +} + +func (el expressionList) Expression() Expression { + return el +} + +func (el expressionList) IsEmpty() bool { + return len(el.expressions) == 0 +} + +func (el expressionList) Type() ExpressionListType { + return el.operator +} + +func (el expressionList) Expressions() []Expression { + return el.expressions +} + +func (el expressionList) Append(expressions ...Expression) ExpressionList { + exps := make([]Expression, 0, len(el.expressions)+len(expressions)) + exps = append(exps, el.expressions...) + exps = append(exps, expressions...) + return NewExpressionList(el.operator, exps...) +} diff --git a/exp/exp_map.go b/exp/exp_map.go new file mode 100644 index 0000000..38fc8e3 --- /dev/null +++ b/exp/exp_map.go @@ -0,0 +1,164 @@ +package exp + +import ( + "sort" + "strings" + + "git.fsdpf.net/go/db/v2/internal/errors" +) + +type ( + // A map of expressions to be ANDed together where the keys are string that will be used as Identifiers and values + // will be used in a boolean operation. + // The Ex map can be used in tandem with Op map to create more complex expression such as LIKE, GT, LT... + // See examples. + Ex map[string]interface{} + // A map of expressions to be ORed together where the keys are string that will be used as Identifiers and values + // will be used in a boolean operation. + // The Ex map can be used in tandem with Op map to create more complex expression such as LIKE, GT, LT... + // See examples. + ExOr map[string]interface{} + // Used in tandem with the Ex map to create complex comparisons such as LIKE, GT, LT... See examples + Op map[string]interface{} +) + +func (e Ex) Expression() Expression { + return e +} + +func (e Ex) Clone() Expression { + ret := Ex{} + for key, val := range e { + ret[key] = val + } + return ret +} + +func (e Ex) IsEmpty() bool { + return len(e) == 0 +} + +func (e Ex) ToExpressions() (ExpressionList, error) { + return mapToExpressionList(e, AndType) +} + +func (eo ExOr) Expression() Expression { + return eo +} + +func (eo ExOr) Clone() Expression { + ret := ExOr{} + for key, val := range eo { + ret[key] = val + } + return ret +} + +func (eo ExOr) IsEmpty() bool { + return len(eo) == 0 +} + +func (eo ExOr) ToExpressions() (ExpressionList, error) { + return mapToExpressionList(eo, OrType) +} + +func getExMapKeys(ex map[string]interface{}) []string { + keys := make([]string, 0, len(ex)) + for key := range ex { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func mapToExpressionList(ex map[string]interface{}, eType ExpressionListType) (ExpressionList, error) { + keys := getExMapKeys(ex) + ret := make([]Expression, 0, len(keys)) + for _, key := range keys { + lhs := ParseIdentifier(key) + rhs := ex[key] + var exp Expression + if op, ok := rhs.(Op); ok { + ors, err := createOredExpressionFromMap(lhs, op) + if err != nil { + return nil, err + } + exp = NewExpressionList(OrType, ors...) + } else { + exp = lhs.Eq(rhs) + } + ret = append(ret, exp) + } + if eType == OrType { + return NewExpressionList(OrType, ret...), nil + } + return NewExpressionList(AndType, ret...), nil +} + +func createOredExpressionFromMap(lhs IdentifierExpression, op Op) ([]Expression, error) { + opKeys := getExMapKeys(op) + ors := make([]Expression, 0, len(opKeys)) + for _, opKey := range opKeys { + if exp, err := createExpressionFromOp(lhs, opKey, op); err != nil { + return nil, err + } else if exp != nil { + ors = append(ors, exp) + } + } + return ors, nil +} + +//nolint:gocyclo // not complex just long +func createExpressionFromOp(lhs IdentifierExpression, opKey string, op Op) (exp Expression, err error) { + switch strings.ToLower(opKey) { + case EqOp.String(): + exp = lhs.Eq(op[opKey]) + case NeqOp.String(): + exp = lhs.Neq(op[opKey]) + case IsOp.String(): + exp = lhs.Is(op[opKey]) + case IsNotOp.String(): + exp = lhs.IsNot(op[opKey]) + case GtOp.String(): + exp = lhs.Gt(op[opKey]) + case GteOp.String(): + exp = lhs.Gte(op[opKey]) + case LtOp.String(): + exp = lhs.Lt(op[opKey]) + case LteOp.String(): + exp = lhs.Lte(op[opKey]) + case InOp.String(): + exp = lhs.In(op[opKey]) + case NotInOp.String(): + exp = lhs.NotIn(op[opKey]) + case LikeOp.String(): + exp = lhs.Like(op[opKey]) + case NotLikeOp.String(): + exp = lhs.NotLike(op[opKey]) + case ILikeOp.String(): + exp = lhs.ILike(op[opKey]) + case NotILikeOp.String(): + exp = lhs.NotILike(op[opKey]) + case RegexpLikeOp.String(): + exp = lhs.RegexpLike(op[opKey]) + case RegexpNotLikeOp.String(): + exp = lhs.RegexpNotLike(op[opKey]) + case RegexpILikeOp.String(): + exp = lhs.RegexpILike(op[opKey]) + case RegexpNotILikeOp.String(): + exp = lhs.RegexpNotILike(op[opKey]) + case betweenStr: + rangeVal, ok := op[opKey].(RangeVal) + if ok { + exp = lhs.Between(rangeVal) + } + case "notbetween": + rangeVal, ok := op[opKey].(RangeVal) + if ok { + exp = lhs.NotBetween(rangeVal) + } + default: + err = errors.New("unsupported expression type %s", opKey) + } + return exp, err +} diff --git a/exp/exp_map_test.go b/exp/exp_map_test.go new file mode 100644 index 0000000..938a950 --- /dev/null +++ b/exp/exp_map_test.go @@ -0,0 +1,320 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type exTestSuite struct { + suite.Suite +} + +func TestExSuite(t *testing.T) { + suite.Run(t, new(exTestSuite)) +} + +func (ets *exTestSuite) TestExpression() { + ex := exp.Ex{"a": "b"} + ets.Equal(ex, ex.Expression()) +} + +func (ets *exTestSuite) TestClone() { + ex := exp.Ex{"a": "b"} + ets.Equal(ex, ex.Clone()) +} + +func (ets *exTestSuite) TestIsEmpty() { + ets.False(exp.Ex{"a": "b"}.IsEmpty()) + ets.True(exp.Ex{}.IsEmpty()) +} + +func (ets *exTestSuite) TestToExpression() { + ident := exp.NewIdentifierExpression("", "", "a") + testCases := []struct { + ExMap exp.Ex + El exp.ExpressionList + Err string + }{ + { + ExMap: exp.Ex{"a": "b"}, + El: exp.NewExpressionList(exp.AndType, ident.Eq("b")), + }, + { + ExMap: exp.Ex{"a": "b", "b": "c"}, + El: exp.NewExpressionList( + exp.AndType, + ident.Eq("b"), + exp.NewIdentifierExpression("", "", "b").Eq("c"), + ), + }, + { + ExMap: exp.Ex{"a": exp.Op{"eq": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Eq("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"neq": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Neq("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"is": nil}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Is(nil))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"isNot": nil}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.IsNot(nil))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"gt": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Gt("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"gte": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Gte("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"lt": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Lt("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"lte": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Lte("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"in": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.In("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"notIn": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.NotIn("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"like": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Like("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"notLike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.NotLike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"iLike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.ILike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"notILike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.NotILike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"regexpLike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.RegexpLike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"regexpNotLike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.RegexpNotLike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"regexpILike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.RegexpILike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"regexpNotILike": "b"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.RegexpNotILike("b"))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"between": exp.NewRangeVal("a", "z")}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Between(exp.NewRangeVal("a", "z")))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"notBetween": exp.NewRangeVal("a", "z")}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.NotBetween(exp.NewRangeVal("a", "z")))), + }, + { + ExMap: exp.Ex{"a": exp.Op{"foo": "z"}}, + Err: "db: unsupported expression type foo", + }, + { + ExMap: exp.Ex{"a": exp.Op{"eq": "b", "neq": "c", "gt": "m"}}, + El: exp.NewExpressionList(exp.AndType, exp.NewExpressionList(exp.OrType, ident.Eq("b"), ident.Gt("m"), ident.Neq("c"))), + }, + + { + ExMap: exp.Ex{ + "a": "b", + "c": "d", + }, + El: exp.NewExpressionList( + exp.AndType, + ident.Eq("b"), + exp.NewIdentifierExpression("", "", "c").Eq("d"), + ), + }, + } + + for _, tc := range testCases { + el, err := tc.ExMap.ToExpressions() + + if tc.Err == "" { + ets.NoError(err) + ets.Equal(tc.El, el, "For Ex %v", tc.ExMap) + } else { + ets.EqualError(err, tc.Err) + } + } +} + +type exOrTestSuite struct { + suite.Suite +} + +func TestExOrSuite(t *testing.T) { + suite.Run(t, new(exOrTestSuite)) +} + +func (ets *exOrTestSuite) TestExpression() { + ex := exp.ExOr{"a": "b"} + ets.Equal(ex, ex.Expression()) +} + +func (ets *exOrTestSuite) TestClone() { + ex := exp.ExOr{"a": "b"} + ets.Equal(ex, ex.Clone()) +} + +func (ets *exOrTestSuite) TestIsEmpty() { + ets.False(exp.ExOr{"a": "b"}.IsEmpty()) + ets.True(exp.ExOr{}.IsEmpty()) +} + +func (ets *exOrTestSuite) TestToExpression() { + ident := exp.NewIdentifierExpression("", "", "a") + testCases := []struct { + ExMap exp.ExOr + El exp.ExpressionList + Err string + }{ + { + ExMap: exp.ExOr{"a": "b"}, + El: exp.NewExpressionList(exp.OrType, ident.Eq("b")), + }, + { + ExMap: exp.ExOr{"a": "b", "b": "c"}, + El: exp.NewExpressionList( + exp.OrType, + ident.Eq("b"), + exp.NewIdentifierExpression("", "", "b").Eq("c"), + ), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"eq": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Eq("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"neq": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Neq("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"is": nil}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Is(nil))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"isNot": nil}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.IsNot(nil))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"gt": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Gt("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"gte": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Gte("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"lt": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Lt("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"lte": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Lte("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"in": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.In("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"notIn": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.NotIn("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"like": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Like("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"notLike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.NotLike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"iLike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.ILike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"notILike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.NotILike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"regexpLike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.RegexpLike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"regexpNotLike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.RegexpNotLike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"regexpILike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.RegexpILike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"regexpNotILike": "b"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.RegexpNotILike("b"))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"between": exp.NewRangeVal("a", "z")}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Between(exp.NewRangeVal("a", "z")))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"notBetween": exp.NewRangeVal("a", "z")}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.NotBetween(exp.NewRangeVal("a", "z")))), + }, + { + ExMap: exp.ExOr{"a": exp.Op{"foo": "z"}}, + Err: "db: unsupported expression type foo", + }, + { + ExMap: exp.ExOr{"a": exp.Op{"eq": "b", "neq": "c", "gt": "m"}}, + El: exp.NewExpressionList(exp.OrType, exp.NewExpressionList(exp.OrType, ident.Eq("b"), ident.Gt("m"), ident.Neq("c"))), + }, + + { + ExMap: exp.ExOr{ + "a": "b", + "c": "d", + }, + El: exp.NewExpressionList( + exp.OrType, + ident.Eq("b"), + exp.NewIdentifierExpression("", "", "c").Eq("d"), + ), + }, + } + + for _, tc := range testCases { + el, err := tc.ExMap.ToExpressions() + + if tc.Err == "" { + ets.NoError(err) + ets.Equal(tc.El, el, "For Ex %v", tc.ExMap) + } else { + ets.EqualError(err, tc.Err) + } + } +} diff --git a/exp/func.go b/exp/func.go new file mode 100644 index 0000000..0ab8cfd --- /dev/null +++ b/exp/func.go @@ -0,0 +1,89 @@ +package exp + +type ( + sqlFunctionExpression struct { + name string + args []interface{} + } +) + +// Creates a new SQLFunctionExpression with the given name and arguments +func NewSQLFunctionExpression(name string, args ...interface{}) SQLFunctionExpression { + return sqlFunctionExpression{name: name, args: args} +} + +func (sfe sqlFunctionExpression) Clone() Expression { + return sqlFunctionExpression{name: sfe.name, args: sfe.args} +} + +func (sfe sqlFunctionExpression) Expression() Expression { return sfe } + +func (sfe sqlFunctionExpression) Args() []interface{} { return sfe.args } + +func (sfe sqlFunctionExpression) Name() string { return sfe.name } + +func (sfe sqlFunctionExpression) As(val interface{}) AliasedExpression { + return NewAliasExpression(sfe, val) +} + +func (sfe sqlFunctionExpression) Eq(val interface{}) BooleanExpression { return eq(sfe, val) } +func (sfe sqlFunctionExpression) Neq(val interface{}) BooleanExpression { return neq(sfe, val) } + +func (sfe sqlFunctionExpression) Gt(val interface{}) BooleanExpression { return gt(sfe, val) } +func (sfe sqlFunctionExpression) Gte(val interface{}) BooleanExpression { return gte(sfe, val) } +func (sfe sqlFunctionExpression) Lt(val interface{}) BooleanExpression { return lt(sfe, val) } +func (sfe sqlFunctionExpression) Lte(val interface{}) BooleanExpression { return lte(sfe, val) } + +func (sfe sqlFunctionExpression) Between(val RangeVal) RangeExpression { return between(sfe, val) } + +func (sfe sqlFunctionExpression) NotBetween(val RangeVal) RangeExpression { + return notBetween(sfe, val) +} + +func (sfe sqlFunctionExpression) Like(val interface{}) BooleanExpression { return like(sfe, val) } +func (sfe sqlFunctionExpression) NotLike(val interface{}) BooleanExpression { return notLike(sfe, val) } +func (sfe sqlFunctionExpression) ILike(val interface{}) BooleanExpression { return iLike(sfe, val) } + +func (sfe sqlFunctionExpression) NotILike(val interface{}) BooleanExpression { + return notILike(sfe, val) +} + +func (sfe sqlFunctionExpression) RegexpLike(val interface{}) BooleanExpression { + return regexpLike(sfe, val) +} + +func (sfe sqlFunctionExpression) RegexpNotLike(val interface{}) BooleanExpression { + return regexpNotLike(sfe, val) +} + +func (sfe sqlFunctionExpression) RegexpILike(val interface{}) BooleanExpression { + return regexpILike(sfe, val) +} + +func (sfe sqlFunctionExpression) RegexpNotILike(val interface{}) BooleanExpression { + return regexpNotILike(sfe, val) +} + +func (sfe sqlFunctionExpression) In(vals ...interface{}) BooleanExpression { return in(sfe, vals...) } +func (sfe sqlFunctionExpression) NotIn(vals ...interface{}) BooleanExpression { + return notIn(sfe, vals...) +} +func (sfe sqlFunctionExpression) Is(val interface{}) BooleanExpression { return is(sfe, val) } +func (sfe sqlFunctionExpression) IsNot(val interface{}) BooleanExpression { return isNot(sfe, val) } +func (sfe sqlFunctionExpression) IsNull() BooleanExpression { return is(sfe, nil) } +func (sfe sqlFunctionExpression) IsNotNull() BooleanExpression { return isNot(sfe, nil) } +func (sfe sqlFunctionExpression) IsTrue() BooleanExpression { return is(sfe, true) } +func (sfe sqlFunctionExpression) IsNotTrue() BooleanExpression { return isNot(sfe, true) } +func (sfe sqlFunctionExpression) IsFalse() BooleanExpression { return is(sfe, false) } +func (sfe sqlFunctionExpression) IsNotFalse() BooleanExpression { return isNot(sfe, false) } + +func (sfe sqlFunctionExpression) Over(we WindowExpression) SQLWindowFunctionExpression { + return NewSQLWindowFunctionExpression(sfe, nil, we) +} + +func (sfe sqlFunctionExpression) OverName(windowName IdentifierExpression) SQLWindowFunctionExpression { + return NewSQLWindowFunctionExpression(sfe, windowName, nil) +} + +func (sfe sqlFunctionExpression) Asc() OrderedExpression { return asc(sfe) } +func (sfe sqlFunctionExpression) Desc() OrderedExpression { return desc(sfe) } diff --git a/exp/func_test.go b/exp/func_test.go new file mode 100644 index 0000000..c95ec75 --- /dev/null +++ b/exp/func_test.go @@ -0,0 +1,81 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type sqlFunctionExpressionSuite struct { + suite.Suite + fn exp.SQLFunctionExpression +} + +func TestSQLFunctionExpressionSuite(t *testing.T) { + suite.Run(t, &sqlFunctionExpressionSuite{ + fn: exp.NewSQLFunctionExpression("COUNT", exp.Star()), + }) +} + +func (sfes *sqlFunctionExpressionSuite) TestClone() { + sfes.Equal(sfes.fn, sfes.fn.Clone()) +} + +func (sfes *sqlFunctionExpressionSuite) TestExpression() { + sfes.Equal(sfes.fn, sfes.fn.Expression()) +} + +func (sfes *sqlFunctionExpressionSuite) TestArgs() { + sfes.Equal([]interface{}{exp.Star()}, sfes.fn.Args()) +} + +func (sfes *sqlFunctionExpressionSuite) TestName() { + sfes.Equal("COUNT", sfes.fn.Name()) +} + +func (sfes *sqlFunctionExpressionSuite) TestAllOthers() { + fn := sfes.fn + + rv := exp.NewRangeVal(1, 2) + pattern := "func like%" + inVals := []interface{}{1, 2} + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: fn.As("a"), Expected: exp.NewAliasExpression(fn, "a")}, + {Ex: fn.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, fn, 1)}, + {Ex: fn.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, fn, 1)}, + {Ex: fn.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, fn, 1)}, + {Ex: fn.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, fn, 1)}, + {Ex: fn.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, fn, 1)}, + {Ex: fn.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, fn, 1)}, + {Ex: fn.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, fn, rv)}, + {Ex: fn.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, fn, rv)}, + {Ex: fn.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, fn, pattern)}, + {Ex: fn.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, fn, pattern)}, + {Ex: fn.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, fn, pattern)}, + {Ex: fn.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, fn, pattern)}, + {Ex: fn.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, fn, pattern)}, + {Ex: fn.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, fn, pattern)}, + {Ex: fn.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, fn, pattern)}, + {Ex: fn.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, fn, pattern)}, + {Ex: fn.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, fn, inVals)}, + {Ex: fn.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, fn, inVals)}, + {Ex: fn.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, fn, true)}, + {Ex: fn.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, fn, true)}, + {Ex: fn.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, fn, nil)}, + {Ex: fn.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, fn, nil)}, + {Ex: fn.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, fn, true)}, + {Ex: fn.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, fn, true)}, + {Ex: fn.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, fn, false)}, + {Ex: fn.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, fn, false)}, + {Ex: fn.Desc(), Expected: exp.NewOrderedExpression(fn, exp.DescSortDir, exp.NoNullsSortType)}, + {Ex: fn.Asc(), Expected: exp.NewOrderedExpression(fn, exp.AscDir, exp.NoNullsSortType)}, + } + + for _, tc := range testCases { + sfes.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/ident.go b/exp/ident.go new file mode 100644 index 0000000..cbfda7e --- /dev/null +++ b/exp/ident.go @@ -0,0 +1,216 @@ +package exp + +import ( + "strings" +) + +type ( + identifier struct { + schema string + table string + col interface{} + } +) + +var ( + tableAndColumnParts = 2 + schemaTableAndColumnIdentifierParts = 3 +) + +func ParseIdentifier(ident string) IdentifierExpression { + parts := strings.Split(ident, ".") + switch len(parts) { + case tableAndColumnParts: + return NewIdentifierExpression("", parts[0], parts[1]) + case schemaTableAndColumnIdentifierParts: + return NewIdentifierExpression(parts[0], parts[1], parts[2]) + } + return NewIdentifierExpression("", "", ident) +} + +func NewIdentifierExpression(schema, table string, col interface{}) IdentifierExpression { + return identifier{}.Schema(schema).Table(table).Col(col) +} + +func (i identifier) clone() identifier { + return identifier{schema: i.schema, table: i.table, col: i.col} +} + +func (i identifier) Clone() Expression { + return i.clone() +} + +func (i identifier) IsQualified() bool { + schema, table, col := i.schema, i.table, i.col + switch c := col.(type) { + case string: + if c != "" { + return len(table) > 0 || len(schema) > 0 + } + default: + if c != nil { + return len(table) > 0 || len(schema) > 0 + } + } + if len(table) > 0 { + return len(schema) > 0 + } + return false +} + +// Sets the table on the current identifier +// +// I("col").Table("table") -> "table"."col" //postgres +// I("col").Table("table") -> `table`.`col` //mysql +// I("col").Table("table") -> `table`.`col` //sqlite3 +func (i identifier) Table(table string) IdentifierExpression { + i.table = table + return i +} + +func (i identifier) GetTable() string { + return i.table +} + +// Sets the table on the current identifier +// +// I("table").Schema("schema") -> "schema"."table" //postgres +// I("col").Schema("table") -> `schema`.`table` //mysql +// I("col").Schema("table") -> `schema`.`table` //sqlite3 +func (i identifier) Schema(schema string) IdentifierExpression { + i.schema = schema + return i +} + +func (i identifier) GetSchema() string { + return i.schema +} + +// Sets the table on the current identifier +// +// I("table").Col("col") -> "table"."col" //postgres +// I("table").Schema("col") -> `table`.`col` //mysql +// I("table").Schema("col") -> `table`.`col` //sqlite3 +func (i identifier) Col(col interface{}) IdentifierExpression { + if col == "*" { + i.col = Star() + } else { + i.col = col + } + return i +} + +func (i identifier) Expression() Expression { return i } + +// Qualifies the epression with a * literal (e.g. "table".*) +func (i identifier) All() IdentifierExpression { return i.Col("*") } + +func (i identifier) IsEmpty() bool { + isEmpty := i.schema == "" && i.table == "" + if isEmpty { + switch t := i.col.(type) { + case nil: + return true + case string: + return t == "" + default: + return false + } + } + return isEmpty +} + +// Gets the column identifier +func (i identifier) GetCol() interface{} { return i.col } + +// Used within updates to set a column value +func (i identifier) Set(val interface{}) UpdateExpression { return set(i, val) } + +// Alias an identifier (e.g "my_col" AS "other_col") +func (i identifier) As(val interface{}) AliasedExpression { + if v, ok := val.(string); ok { + ident := ParseIdentifier(v) + if i.col != nil && i.col != "" { + return NewAliasExpression(i, ident) + } + aliasCol := ident.GetCol() + if i.table != "" { + return NewAliasExpression(i, NewIdentifierExpression("", aliasCol.(string), nil)) + } else if i.schema != "" { + return NewAliasExpression(i, NewIdentifierExpression(aliasCol.(string), "", nil)) + } + } + return NewAliasExpression(i, val) +} + +// Returns a BooleanExpression for equality (e.g "my_col" = 1) +func (i identifier) Eq(val interface{}) BooleanExpression { return eq(i, val) } + +// Returns a BooleanExpression for in equality (e.g "my_col" != 1) +func (i identifier) Neq(val interface{}) BooleanExpression { return neq(i, val) } + +// Returns a BooleanExpression for checking that a identifier is greater than another value (e.g "my_col" > 1) +func (i identifier) Gt(val interface{}) BooleanExpression { return gt(i, val) } + +// Returns a BooleanExpression for checking that a identifier is greater than or equal to another value +// (e.g "my_col" >= 1) +func (i identifier) Gte(val interface{}) BooleanExpression { return gte(i, val) } + +// Returns a BooleanExpression for checking that a identifier is less than another value (e.g "my_col" < 1) +func (i identifier) Lt(val interface{}) BooleanExpression { return lt(i, val) } + +// Returns a BooleanExpression for checking that a identifier is less than or equal to another value +// (e.g "my_col" <= 1) +func (i identifier) Lte(val interface{}) BooleanExpression { return lte(i, val) } + +// Returns a BooleanExpression for bit inversion (e.g ~ "my_col") +func (i identifier) BitwiseInversion() BitwiseExpression { return bitwiseInversion(i) } + +// Returns a BooleanExpression for bit OR (e.g "my_col" | 1) +func (i identifier) BitwiseOr(val interface{}) BitwiseExpression { return bitwiseOr(i, val) } + +// Returns a BooleanExpression for bit AND (e.g "my_col" & 1) +func (i identifier) BitwiseAnd(val interface{}) BitwiseExpression { return bitwiseAnd(i, val) } + +// Returns a BooleanExpression for bit XOR (e.g "my_col" ^ 1) +func (i identifier) BitwiseXor(val interface{}) BitwiseExpression { return bitwiseXor(i, val) } + +// Returns a BooleanExpression for bit LEFT shift (e.g "my_col" << 1) +func (i identifier) BitwiseLeftShift(val interface{}) BitwiseExpression { + return bitwiseLeftShift(i, val) +} + +// Returns a BooleanExpression for bit RIGHT shift (e.g "my_col" >> 1) +func (i identifier) BitwiseRightShift(val interface{}) BitwiseExpression { + return bitwiseRightShift(i, val) +} + +// Returns a BooleanExpression for checking that a identifier is in a list of values or (e.g "my_col" > 1) +func (i identifier) In(vals ...interface{}) BooleanExpression { return in(i, vals...) } +func (i identifier) NotIn(vals ...interface{}) BooleanExpression { return notIn(i, vals...) } +func (i identifier) Like(val interface{}) BooleanExpression { return like(i, val) } +func (i identifier) NotLike(val interface{}) BooleanExpression { return notLike(i, val) } +func (i identifier) ILike(val interface{}) BooleanExpression { return iLike(i, val) } +func (i identifier) NotILike(val interface{}) BooleanExpression { return notILike(i, val) } +func (i identifier) RegexpLike(val interface{}) BooleanExpression { return regexpLike(i, val) } +func (i identifier) RegexpNotLike(val interface{}) BooleanExpression { return regexpNotLike(i, val) } +func (i identifier) RegexpILike(val interface{}) BooleanExpression { return regexpILike(i, val) } +func (i identifier) RegexpNotILike(val interface{}) BooleanExpression { return regexpNotILike(i, val) } +func (i identifier) Is(val interface{}) BooleanExpression { return is(i, val) } +func (i identifier) IsNot(val interface{}) BooleanExpression { return isNot(i, val) } +func (i identifier) IsNull() BooleanExpression { return is(i, nil) } +func (i identifier) IsNotNull() BooleanExpression { return isNot(i, nil) } +func (i identifier) IsTrue() BooleanExpression { return is(i, true) } +func (i identifier) IsNotTrue() BooleanExpression { return isNot(i, true) } +func (i identifier) IsFalse() BooleanExpression { return is(i, false) } +func (i identifier) IsNotFalse() BooleanExpression { return isNot(i, false) } +func (i identifier) Asc() OrderedExpression { return asc(i) } +func (i identifier) Desc() OrderedExpression { return desc(i) } +func (i identifier) Distinct() SQLFunctionExpression { return NewSQLFunctionExpression("DISTINCT", i) } +func (i identifier) Cast(t string) CastExpression { return NewCastExpression(i, t) } + +// Returns a RangeExpression for checking that a identifier is between two values (e.g "my_col" BETWEEN 1 AND 10) +func (i identifier) Between(val RangeVal) RangeExpression { return between(i, val) } + +// Returns a RangeExpression for checking that a identifier is between two values (e.g "my_col" BETWEEN 1 AND 10) +func (i identifier) NotBetween(val RangeVal) RangeExpression { return notBetween(i, val) } diff --git a/exp/ident_test.go b/exp/ident_test.go new file mode 100644 index 0000000..0b64c9e --- /dev/null +++ b/exp/ident_test.go @@ -0,0 +1,247 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type identifierExpressionSuite struct { + suite.Suite +} + +func TestIdentifierExpressionSuite(t *testing.T) { + suite.Run(t, new(identifierExpressionSuite)) +} + +func (ies *identifierExpressionSuite) TestParseIdentifier() { + cases := []struct { + ToParse string + Expected exp.IdentifierExpression + }{ + {ToParse: "one", Expected: exp.NewIdentifierExpression("", "", "one")}, + {ToParse: "one.two", Expected: exp.NewIdentifierExpression("", "one", "two")}, + {ToParse: "one.two.three", Expected: exp.NewIdentifierExpression("one", "two", "three")}, + } + for _, tc := range cases { + ies.Equal(tc.Expected, exp.ParseIdentifier(tc.ToParse)) + } +} + +func (ies *identifierExpressionSuite) TestClone() { + cases := []struct { + Expected exp.IdentifierExpression + }{ + {Expected: exp.NewIdentifierExpression("", "", "one")}, + {Expected: exp.NewIdentifierExpression("", "two", "one")}, + {Expected: exp.NewIdentifierExpression("three", "two", "one")}, + } + for _, tc := range cases { + ies.Equal(tc.Expected, tc.Expected.Clone()) + } +} + +func (ies *identifierExpressionSuite) TestIsQualified() { + cases := []struct { + Ident exp.IdentifierExpression + IsQualified bool + }{ + {Ident: exp.NewIdentifierExpression("", "", "col"), IsQualified: false}, + {Ident: exp.NewIdentifierExpression("", "table", ""), IsQualified: false}, + {Ident: exp.NewIdentifierExpression("", "table", nil), IsQualified: false}, + {Ident: exp.NewIdentifierExpression("schema", "", ""), IsQualified: false}, + {Ident: exp.NewIdentifierExpression("schema", "", nil), IsQualified: false}, + {Ident: exp.NewIdentifierExpression("", "table", exp.NewLiteralExpression("*")), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("", "table", "col"), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "table", exp.NewLiteralExpression("*")), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "table", ""), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col"), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "", "col"), IsQualified: true}, + {Ident: exp.NewIdentifierExpression("schema", "", exp.NewLiteralExpression("*")), IsQualified: true}, + } + for _, tc := range cases { + ies.Equal(tc.IsQualified, tc.Ident.IsQualified(), "expected %s IsQualified to be %b", tc.Ident, tc.IsQualified) + } +} + +func (ies *identifierExpressionSuite) TestGetTable() { + cases := []struct { + Ident exp.IdentifierExpression + Table string + }{ + {Ident: exp.NewIdentifierExpression("", "", "col"), Table: ""}, + {Ident: exp.NewIdentifierExpression("", "table", "col"), Table: "table"}, + {Ident: exp.NewIdentifierExpression("schema", "", "col"), Table: ""}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil), Table: "table"}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col"), Table: "table"}, + } + for _, tc := range cases { + ies.Equal(tc.Table, tc.Ident.GetTable()) + } +} + +func (ies *identifierExpressionSuite) TestGetSchema() { + cases := []struct { + Ident exp.IdentifierExpression + Schema string + }{ + {Ident: exp.NewIdentifierExpression("", "", "col"), Schema: ""}, + {Ident: exp.NewIdentifierExpression("", "table", "col"), Schema: ""}, + {Ident: exp.NewIdentifierExpression("schema", "", "col"), Schema: "schema"}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil), Schema: "schema"}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col"), Schema: "schema"}, + } + for _, tc := range cases { + ies.Equal(tc.Schema, tc.Ident.GetSchema()) + } +} + +func (ies *identifierExpressionSuite) TestGetCol() { + cases := []struct { + Ident exp.IdentifierExpression + Col interface{} + }{ + {Ident: exp.NewIdentifierExpression("", "", "col"), Col: "col"}, + {Ident: exp.NewIdentifierExpression("", "", "*"), Col: exp.NewLiteralExpression("*")}, + {Ident: exp.NewIdentifierExpression("", "table", "col"), Col: "col"}, + {Ident: exp.NewIdentifierExpression("schema", "", "col"), Col: "col"}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil), Col: nil}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col"), Col: "col"}, + } + for _, tc := range cases { + ies.Equal(tc.Col, tc.Ident.GetCol()) + } +} + +func (ies *identifierExpressionSuite) TestExpression() { + i := exp.NewIdentifierExpression("", "", "col") + ies.Equal(i, i.Expression()) +} + +func (ies *identifierExpressionSuite) TestAll() { + cases := []struct { + Ident exp.IdentifierExpression + }{ + {Ident: exp.NewIdentifierExpression("", "", "col")}, + {Ident: exp.NewIdentifierExpression("", "table", "col")}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col")}, + {Ident: exp.NewIdentifierExpression("", "", nil)}, + {Ident: exp.NewIdentifierExpression("", "table", nil)}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil)}, + } + for _, tc := range cases { + ies.Equal( + exp.NewIdentifierExpression(tc.Ident.GetSchema(), tc.Ident.GetTable(), exp.NewLiteralExpression("*")), + tc.Ident.All(), + ) + } +} + +func (ies *identifierExpressionSuite) TestIsEmpty() { + cases := []struct { + Ident exp.IdentifierExpression + IsEmpty bool + }{ + {Ident: exp.NewIdentifierExpression("", "", ""), IsEmpty: true}, + {Ident: exp.NewIdentifierExpression("", "", nil), IsEmpty: true}, + {Ident: exp.NewIdentifierExpression("", "", "col"), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("", "", exp.NewLiteralExpression("*")), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("", "table", ""), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("", "table", nil), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "", ""), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "", nil), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("", "table", exp.NewLiteralExpression("*")), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("", "table", "col"), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "table", nil), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "table", exp.NewLiteralExpression("*")), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "table", ""), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "table", "col"), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "", "col"), IsEmpty: false}, + {Ident: exp.NewIdentifierExpression("schema", "", exp.NewLiteralExpression("*")), IsEmpty: false}, + } + for _, tc := range cases { + ies.Equal(tc.IsEmpty, tc.Ident.IsEmpty(), "expected %s IsEmpty to be %b", tc.Ident, tc.IsEmpty) + } +} + +func (ies *identifierExpressionSuite) TestAs() { + cases := []struct { + Alias exp.AliasedExpression + Expected exp.Expression + }{ + { + Alias: exp.NewIdentifierExpression("", "", "col").As("c"), + Expected: exp.NewAliasExpression(exp.NewIdentifierExpression("", "", "col"), exp.NewIdentifierExpression("", "", "c")), + }, + { + Alias: exp.NewIdentifierExpression("", "table", nil).As("t"), + Expected: exp.NewAliasExpression(exp.NewIdentifierExpression("", "table", nil), exp.NewIdentifierExpression("", "t", nil)), + }, + { + Alias: exp.NewIdentifierExpression("", "table", nil).As("s.t"), + Expected: exp.NewAliasExpression(exp.NewIdentifierExpression("", "table", nil), exp.NewIdentifierExpression("", "t", nil)), + }, + { + Alias: exp.NewIdentifierExpression("schema", "", nil).As("s"), + Expected: exp.NewAliasExpression(exp.NewIdentifierExpression("schema", "", nil), exp.NewIdentifierExpression("s", "", nil)), + }, + } + for _, tc := range cases { + ies.Equal(tc.Expected, tc.Alias) + } +} + +func (ies *identifierExpressionSuite) TestAllOthers() { + ident := exp.NewIdentifierExpression("", "", "a") + rv := exp.NewRangeVal(1, 2) + pattern := "ident like%" + inVals := []interface{}{1, 2} + bitwiseVals := 2 + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: ident.As("a"), Expected: exp.NewAliasExpression(ident, "a")}, + {Ex: ident.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, ident, 1)}, + {Ex: ident.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, ident, 1)}, + {Ex: ident.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, ident, 1)}, + {Ex: ident.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, ident, 1)}, + {Ex: ident.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, ident, 1)}, + {Ex: ident.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, ident, 1)}, + {Ex: ident.Asc(), Expected: exp.NewOrderedExpression(ident, exp.AscDir, exp.NoNullsSortType)}, + {Ex: ident.Desc(), Expected: exp.NewOrderedExpression(ident, exp.DescSortDir, exp.NoNullsSortType)}, + {Ex: ident.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, ident, rv)}, + {Ex: ident.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, ident, rv)}, + {Ex: ident.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, ident, pattern)}, + {Ex: ident.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, ident, pattern)}, + {Ex: ident.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, ident, pattern)}, + {Ex: ident.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, ident, pattern)}, + {Ex: ident.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, ident, pattern)}, + {Ex: ident.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, ident, pattern)}, + {Ex: ident.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, ident, pattern)}, + {Ex: ident.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, ident, pattern)}, + {Ex: ident.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, ident, inVals)}, + {Ex: ident.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, ident, inVals)}, + {Ex: ident.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, ident, true)}, + {Ex: ident.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, ident, true)}, + {Ex: ident.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, ident, nil)}, + {Ex: ident.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ident, nil)}, + {Ex: ident.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, ident, true)}, + {Ex: ident.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ident, true)}, + {Ex: ident.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, ident, false)}, + {Ex: ident.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, ident, false)}, + {Ex: ident.Distinct(), Expected: exp.NewSQLFunctionExpression("DISTINCT", ident)}, + {Ex: ident.BitwiseInversion(), Expected: exp.NewBitwiseExpression(exp.BitwiseInversionOp, nil, ident)}, + {Ex: ident.BitwiseOr(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseOrOp, ident, bitwiseVals)}, + {Ex: ident.BitwiseAnd(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseAndOp, ident, bitwiseVals)}, + {Ex: ident.BitwiseXor(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseXorOp, ident, bitwiseVals)}, + {Ex: ident.BitwiseLeftShift(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseLeftShiftOp, ident, bitwiseVals)}, + {Ex: ident.BitwiseRightShift(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseRightShiftOp, ident, bitwiseVals)}, + } + + for _, tc := range testCases { + ies.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/insert.go b/exp/insert.go new file mode 100644 index 0000000..8a03663 --- /dev/null +++ b/exp/insert.go @@ -0,0 +1,163 @@ +package exp + +import ( + "reflect" + "sort" + + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/util" +) + +type ( + insert struct { + from AppendableExpression + cols ColumnListExpression + vals [][]interface{} + } +) + +func NewInsertExpression(rows ...interface{}) (insertExpression InsertExpression, err error) { + switch len(rows) { + case 0: + return new(insert), nil + case 1: + val := reflect.ValueOf(rows[0]) + if val.Kind() == reflect.Slice { + vals := make([]interface{}, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + vals = append(vals, val.Index(i).Interface()) + } + return NewInsertExpression(vals...) + } + if ae, ok := rows[0].(AppendableExpression); ok { + return &insert{from: ae}, nil + } + } + return newInsert(rows...) +} + +func (i *insert) Expression() Expression { + return i +} + +func (i *insert) Clone() Expression { + return i.clone() +} + +func (i *insert) clone() *insert { + return &insert{from: i.from, cols: i.cols, vals: i.vals} +} + +func (i *insert) IsEmpty() bool { + return i.from == nil && (i.cols == nil || i.cols.IsEmpty()) +} + +func (i *insert) IsInsertFrom() bool { + return i.from != nil +} + +func (i *insert) From() AppendableExpression { + return i.from +} + +func (i *insert) Cols() ColumnListExpression { + return i.cols +} + +func (i *insert) SetCols(cols ColumnListExpression) InsertExpression { + ci := i.clone() + ci.cols = cols + return ci +} + +func (i *insert) Vals() [][]interface{} { + return i.vals +} + +func (i *insert) SetVals(vals [][]interface{}) InsertExpression { + ci := i.clone() + ci.vals = vals + return ci +} + +// parses the rows gathering and sorting unique columns and values for each record +func newInsert(rows ...interface{}) (insertExp InsertExpression, err error) { + var mapKeys util.ValueSlice + rowValue := reflect.Indirect(reflect.ValueOf(rows[0])) + rowType := rowValue.Type() + rowKind := rowValue.Kind() + if rowKind == reflect.Struct { + return createStructSliceInsert(rows...) + } + vals := make([][]interface{}, 0, len(rows)) + var columns ColumnListExpression + for _, row := range rows { + if rowType != reflect.Indirect(reflect.ValueOf(row)).Type() { + return nil, errors.New( + "rows must be all the same type expected %+v got %+v", + rowType, + reflect.Indirect(reflect.ValueOf(row)).Type(), + ) + } + newRowValue := reflect.Indirect(reflect.ValueOf(row)) + switch rowKind { + case reflect.Map: + if columns == nil { + mapKeys = util.ValueSlice(newRowValue.MapKeys()) + sort.Sort(mapKeys) + colKeys := make([]interface{}, 0, len(mapKeys)) + for _, key := range mapKeys { + colKeys = append(colKeys, key.Interface()) + } + columns = NewColumnListExpression(colKeys...) + } + newMapKeys := util.ValueSlice(newRowValue.MapKeys()) + if len(newMapKeys) != len(mapKeys) { + return nil, errors.New("rows with different value length expected %d got %d", len(mapKeys), len(newMapKeys)) + } + if !mapKeys.Equal(newMapKeys) { + return nil, errors.New("rows with different keys expected %s got %s", mapKeys.String(), newMapKeys.String()) + } + rowVals := make([]interface{}, 0, len(mapKeys)) + for _, key := range mapKeys { + rowVals = append(rowVals, newRowValue.MapIndex(key).Interface()) + } + vals = append(vals, rowVals) + default: + return nil, errors.New( + "unsupported insert must be map, db.Record, or struct type got: %T", + row, + ) + } + } + return &insert{cols: columns, vals: vals}, nil +} + +func createStructSliceInsert(rows ...interface{}) (insertExp InsertExpression, err error) { + rowValue := reflect.Indirect(reflect.ValueOf(rows[0])) + rowType := rowValue.Type() + recordRows := make([]interface{}, 0, len(rows)) + for _, row := range rows { + if rowType != reflect.Indirect(reflect.ValueOf(row)).Type() { + return nil, errors.New( + "rows must be all the same type expected %+v got %+v", + rowType, + reflect.Indirect(reflect.ValueOf(row)).Type(), + ) + } + newRowValue := reflect.Indirect(reflect.ValueOf(row)) + record, err := getFieldsValuesFromStruct(newRowValue) + if err != nil { + return nil, err + } + recordRows = append(recordRows, record) + } + return newInsert(recordRows...) +} + +func getFieldsValuesFromStruct(value reflect.Value) (row Record, err error) { + if value.IsValid() { + return NewRecordFromStruct(value.Interface(), true, false) + } + return +} diff --git a/exp/insert_clauses.go b/exp/insert_clauses.go new file mode 100644 index 0000000..ac7a330 --- /dev/null +++ b/exp/insert_clauses.go @@ -0,0 +1,205 @@ +package exp + +type ( + InsertClauses interface { + CommonTables() []CommonTableExpression + CommonTablesAppend(cte CommonTableExpression) InsertClauses + + HasInto() bool + clone() *insertClauses + + Cols() ColumnListExpression + HasCols() bool + ColsAppend(cols ColumnListExpression) InsertClauses + SetCols(cols ColumnListExpression) InsertClauses + + Into() Expression + SetInto(cl Expression) InsertClauses + + Returning() ColumnListExpression + HasReturning() bool + SetReturning(cl ColumnListExpression) InsertClauses + + From() AppendableExpression + HasFrom() bool + SetFrom(ae AppendableExpression) InsertClauses + + Rows() []interface{} + HasRows() bool + SetRows(rows []interface{}) InsertClauses + + HasAlias() bool + Alias() IdentifierExpression + SetAlias(ie IdentifierExpression) InsertClauses + + Vals() [][]interface{} + HasVals() bool + SetVals(vals [][]interface{}) InsertClauses + ValsAppend(vals [][]interface{}) InsertClauses + + OnConflict() ConflictExpression + SetOnConflict(expression ConflictExpression) InsertClauses + } + insertClauses struct { + commonTables []CommonTableExpression + cols ColumnListExpression + into Expression + returning ColumnListExpression + alias IdentifierExpression + rows []interface{} + values [][]interface{} + from AppendableExpression + conflict ConflictExpression + } +) + +func NewInsertClauses() InsertClauses { + return &insertClauses{} +} + +func (ic *insertClauses) HasInto() bool { + return ic.into != nil +} + +func (ic *insertClauses) clone() *insertClauses { + return &insertClauses{ + commonTables: ic.commonTables, + cols: ic.cols, + into: ic.into, + returning: ic.returning, + alias: ic.alias, + rows: ic.rows, + values: ic.values, + from: ic.from, + conflict: ic.conflict, + } +} + +func (ic *insertClauses) CommonTables() []CommonTableExpression { + return ic.commonTables +} + +func (ic *insertClauses) CommonTablesAppend(cte CommonTableExpression) InsertClauses { + ret := ic.clone() + ret.commonTables = append(ret.commonTables, cte) + return ret +} + +func (ic *insertClauses) Cols() ColumnListExpression { + return ic.cols +} + +func (ic *insertClauses) HasCols() bool { + return ic.cols != nil && !ic.cols.IsEmpty() +} + +func (ic *insertClauses) ColsAppend(cl ColumnListExpression) InsertClauses { + ret := ic.clone() + ret.cols = ret.cols.Append(cl.Columns()...) + return ret +} + +func (ic *insertClauses) SetCols(cl ColumnListExpression) InsertClauses { + ret := ic.clone() + ret.cols = cl + return ret +} + +func (ic *insertClauses) Into() Expression { + return ic.into +} + +func (ic *insertClauses) SetInto(into Expression) InsertClauses { + ret := ic.clone() + ret.into = into + return ret +} + +func (ic *insertClauses) Returning() ColumnListExpression { + return ic.returning +} + +func (ic *insertClauses) HasReturning() bool { + return ic.returning != nil && !ic.returning.IsEmpty() +} + +func (ic *insertClauses) HasAlias() bool { + return ic.alias != nil +} + +func (ic *insertClauses) Alias() IdentifierExpression { + return ic.alias +} + +func (ic *insertClauses) SetAlias(ie IdentifierExpression) InsertClauses { + ret := ic.clone() + ret.alias = ie + return ret +} + +func (ic *insertClauses) SetReturning(cl ColumnListExpression) InsertClauses { + ret := ic.clone() + ret.returning = cl + return ret +} + +func (ic *insertClauses) From() AppendableExpression { + return ic.from +} + +func (ic *insertClauses) HasFrom() bool { + return ic.from != nil +} + +func (ic *insertClauses) SetFrom(ae AppendableExpression) InsertClauses { + ret := ic.clone() + ret.from = ae + return ret +} + +func (ic *insertClauses) Rows() []interface{} { + return ic.rows +} + +func (ic *insertClauses) HasRows() bool { + return ic.rows != nil && len(ic.rows) > 0 +} + +func (ic *insertClauses) SetRows(rows []interface{}) InsertClauses { + ret := ic.clone() + ret.rows = rows + return ret +} + +func (ic *insertClauses) Vals() [][]interface{} { + return ic.values +} + +func (ic *insertClauses) HasVals() bool { + return ic.values != nil && len(ic.values) > 0 +} + +func (ic *insertClauses) SetVals(vals [][]interface{}) InsertClauses { + ret := ic.clone() + ret.values = vals + return ret +} + +func (ic *insertClauses) ValsAppend(vals [][]interface{}) InsertClauses { + ret := ic.clone() + newVals := make([][]interface{}, 0, len(ic.values)+len(vals)) + newVals = append(newVals, ic.values...) + newVals = append(newVals, vals...) + ret.values = newVals + return ret +} + +func (ic *insertClauses) OnConflict() ConflictExpression { + return ic.conflict +} + +func (ic *insertClauses) SetOnConflict(expression ConflictExpression) InsertClauses { + ret := ic.clone() + ret.conflict = expression + return ret +} diff --git a/exp/insert_clauses_test.go b/exp/insert_clauses_test.go new file mode 100644 index 0000000..77a673b --- /dev/null +++ b/exp/insert_clauses_test.go @@ -0,0 +1,242 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type insertClausesSuite struct { + suite.Suite +} + +func TestInsertClausesSuite(t *testing.T) { + suite.Run(t, new(insertClausesSuite)) +} + +func (ics *insertClausesSuite) TestInto() { + c := exp.NewInsertClauses() + ti := exp.NewIdentifierExpression("", "test", "") + c2 := c.SetInto(ti) + + ics.Nil(c.Into()) + + ics.Equal(ti, c2.Into()) +} + +func (ics *insertClausesSuite) TestHasInto() { + c := exp.NewInsertClauses() + ti := exp.NewIdentifierExpression("", "test", "") + c2 := c.SetInto(ti) + + ics.False(c.HasInto()) + + ics.True(c2.HasInto()) +} + +func (ics *insertClausesSuite) TestFrom() { + c := exp.NewInsertClauses() + ae := newTestAppendableExpression("select * from test", nil) + c2 := c.SetFrom(ae) + + ics.Nil(c.From()) + + ics.Equal(ae, c2.From()) +} + +func (ics *insertClausesSuite) TestHasFrom() { + c := exp.NewInsertClauses() + ae := newTestAppendableExpression("select * from test", nil) + c2 := c.SetFrom(ae) + + ics.False(c.HasFrom()) + + ics.True(c2.HasFrom()) +} + +func (ics *insertClausesSuite) TestSetFrom() { + c := exp.NewInsertClauses() + ae := newTestAppendableExpression("select * from test", nil) + c2 := c.SetFrom(ae) + + ics.Nil(c.From()) + + ics.Equal(ae, c2.From()) +} + +func (ics *insertClausesSuite) TestCols() { + c := exp.NewInsertClauses() + cle := exp.NewColumnListExpression("a", "b") + c2 := c.SetCols(cle) + + ics.Nil(c.Cols()) + + ics.Equal(cle, c2.Cols()) +} + +func (ics *insertClausesSuite) TestHasCols() { + c := exp.NewInsertClauses() + cle := exp.NewColumnListExpression("a", "b") + c2 := c.SetCols(cle) + + ics.False(c.HasCols()) + + ics.True(c2.HasCols()) +} + +func (ics *insertClausesSuite) TestColsAppend() { + cle := exp.NewColumnListExpression("a") + cle2 := exp.NewColumnListExpression("b") + c := exp.NewInsertClauses().SetCols(cle) + c2 := c.ColsAppend(cle2) + + ics.Equal(cle, c.Cols()) + + ics.Equal(exp.NewColumnListExpression("a", "b"), c2.Cols()) +} + +func (ics *insertClausesSuite) TestVals() { + c := exp.NewInsertClauses() + vals := [][]interface{}{{"a", "b"}} + c2 := c.SetVals(vals) + + ics.Nil(c.Vals()) + + ics.Equal(vals, c2.Vals()) +} + +func (ics *insertClausesSuite) TestHasVals() { + c := exp.NewInsertClauses() + vals := [][]interface{}{{"a", "b"}} + c2 := c.SetVals(vals) + + ics.False(c.HasVals()) + + ics.True(c2.HasVals()) +} + +func (ics *insertClausesSuite) TestValsAppend() { + vals := [][]interface{}{{"a", "b"}} + vals2 := [][]interface{}{{"c", "d"}} + c := exp.NewInsertClauses().SetVals(vals) + c2 := c.ValsAppend(vals2) + + ics.Equal(vals, c.Vals()) + + ics.Equal([][]interface{}{ + {"a", "b"}, + {"c", "d"}, + }, c2.Vals()) +} + +func (ics *insertClausesSuite) TestRows() { + c := exp.NewInsertClauses() + rs := []interface{}{exp.Record{"a": "a1", "b": "b1"}} + c2 := c.SetRows(rs) + + ics.Nil(c.Rows()) + + ics.Equal(rs, c2.Rows()) +} + +func (ics *insertClausesSuite) TestHasRows() { + c := exp.NewInsertClauses() + rs := []interface{}{exp.Record{"a": "a1", "b": "b1"}} + c2 := c.SetRows(rs) + + ics.False(c.HasRows()) + + ics.True(c2.HasRows()) +} + +func (ics *insertClausesSuite) TestSetRows() { + rs := []interface{}{exp.Record{"a": "a1", "b": "b1"}} + c := exp.NewInsertClauses().SetRows(rs) + rs2 := []interface{}{exp.Record{"a": "a2", "b": "b2"}} + c2 := c.SetRows(rs2) + + ics.Equal(rs, c.Rows()) + + ics.Equal(rs2, c2.Rows()) +} + +func (ics *insertClausesSuite) TestCommonTables() { + cte := exp.NewCommonTableExpression(true, "test", newTestAppendableExpression(`SELECT * FROM "foo"`, []interface{}{})) + + c := exp.NewInsertClauses() + c2 := c.CommonTablesAppend(cte) + + ics.Nil(c.CommonTables()) + + ics.Equal([]exp.CommonTableExpression{cte}, c2.CommonTables()) +} + +func (ics *insertClausesSuite) TestAddCommonTablesAppend() { + cte := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte")) + cte2 := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte2")) + + c := exp.NewInsertClauses().CommonTablesAppend(cte) + c2 := c.CommonTablesAppend(cte2) + + ics.Equal([]exp.CommonTableExpression{cte}, c.CommonTables()) + + ics.Equal([]exp.CommonTableExpression{cte, cte2}, c2.CommonTables()) +} + +func (ics *insertClausesSuite) TestOnConflict() { + ce := exp.NewDoNothingConflictExpression() + + c := exp.NewInsertClauses() + c2 := c.SetOnConflict(ce) + + ics.Nil(c.OnConflict()) + + ics.Equal(ce, c2.OnConflict()) +} + +func (ics *insertClausesSuite) TestSetOnConflict() { + ce := exp.NewDoNothingConflictExpression() + + c := exp.NewInsertClauses().SetOnConflict(ce) + ce2 := exp.NewDoUpdateConflictExpression("test", exp.Record{"a": "a1"}) + c2 := c.SetOnConflict(ce2) + + ics.Equal(ce, c.OnConflict()) + + ics.Equal(ce2, c2.OnConflict()) +} + +func (ics *insertClausesSuite) TestReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewInsertClauses() + c2 := c.SetReturning(cl) + + ics.Nil(c.Returning()) + + ics.Equal(cl, c2.Returning()) +} + +func (ics *insertClausesSuite) TestHasReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewInsertClauses() + c2 := c.SetReturning(cl) + + ics.False(c.HasReturning()) + + ics.True(c2.HasReturning()) +} + +func (ics *insertClausesSuite) TestSetReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + cl2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col2")) + + c := exp.NewInsertClauses().SetReturning(cl) + c2 := c.SetReturning(cl2) + + ics.Equal(cl, c.Returning()) + + ics.Equal(cl2, c2.Returning()) +} diff --git a/exp/insert_test.go b/exp/insert_test.go new file mode 100644 index 0000000..a073e72 --- /dev/null +++ b/exp/insert_test.go @@ -0,0 +1,383 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type testAppendableExpression struct { + exp.AppendableExpression + sql string + args []interface{} + clauses exp.SelectClauses +} + +func newTestAppendableExpression(sql string, args []interface{}) exp.AppendableExpression { + return &testAppendableExpression{sql: sql, args: args} +} + +func (tae *testAppendableExpression) Expression() exp.Expression { + return tae +} + +func (tae *testAppendableExpression) GetClauses() exp.SelectClauses { + return tae.clauses +} + +func (tae *testAppendableExpression) Clone() exp.Expression { + return tae +} + +type insertExpressionTestSuite struct { + suite.Suite +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentRecordTypes() { + type testRecord struct { + C string `db:"c"` + } + type testRecord2 struct { + C string `db:"c"` + } + _, err := exp.NewInsertExpression( + testRecord{C: "v1"}, + exp.Record{"c": "v2"}, + ) + iets.EqualError(err, "db: rows must be all the same type expected exp_test.testRecord got exp.Record") + _, err = exp.NewInsertExpression( + testRecord{C: "v1"}, + testRecord2{C: "v2"}, + ) + iets.EqualError(err, "db: rows must be all the same type expected exp_test.testRecord got exp_test.testRecord2") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withInvalidValue() { + _, err := exp.NewInsertExpression(true) + iets.EqualError(err, "db: unsupported insert must be map, dbv2.Record, or struct type got: bool") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentTypes() { + _, err := exp.NewInsertExpression(exp.Record{"a": "a1"}, true) + iets.EqualError(err, "db: rows must be all the same type expected exp.Record got bool") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withNoValues() { + ie, err := exp.NewInsertExpression() + iets.NoError(err) + iets.Nil(ie.Cols()) + iets.Nil(ie.Vals()) + iets.True(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_Vals() { + ie, err := exp.NewInsertExpression() + iets.NoError(err) + vals := [][]interface{}{ + {"a", "b"}, + } + ie = ie.SetCols(exp.NewColumnListExpression("a", "b")).SetVals(vals) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) + iets.Equal(vals, ie.Vals()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_Cols() { + ie, err := exp.NewInsertExpression() + iets.NoError(err) + vals := [][]interface{}{ + {"a", "b"}, + } + ce := exp.NewColumnListExpression("a", "b") + ie = ie.SetCols(ce).SetVals(vals) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) + iets.Equal(vals, ie.Vals()) + iets.Equal(ce, ie.Cols()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_From() { + ae := newTestAppendableExpression("select * from test", []interface{}{}) + ie, err := exp.NewInsertExpression(ae) + iets.NoError(err) + iets.False(ie.IsEmpty()) + iets.True(ie.IsInsertFrom()) + iets.Equal(ae, ie.From()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_appendableExpression() { + ae := newTestAppendableExpression("test ae", nil) + + ie, err := exp.NewInsertExpression(ae) + iets.NoError(err) + iets.False(ie.IsEmpty()) + iets.True(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecords() { + ie, err := exp.NewInsertExpression(exp.Record{"c": "a"}, exp.Record{"c": "b"}) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordsSlice() { + ie, err := exp.NewInsertExpression([]exp.Record{{"c": "a"}, {"c": "b"}}) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordOfDifferentLength() { + _, err := exp.NewInsertExpression(exp.Record{"c": "a"}, exp.Record{"c": "b", "c2": "d"}) + iets.EqualError(err, "db: rows with different value length expected 1 got 2") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordWithDifferentkeys() { + _, err := exp.NewInsertExpression(exp.Record{"c1": "a"}, exp.Record{"c2": "b"}) + iets.EqualError(err, `db: rows with different keys expected ["c1"] got ["c2"]`) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withMap() { + ie, err := exp.NewInsertExpression( + map[string]interface{}{"c": "a"}, + map[string]interface{}{"c": "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructs() { + type testRecord struct { + C string `db:"c"` + } + ie, err := exp.NewInsertExpression( + testRecord{C: "a"}, + testRecord{C: "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructSlice() { + type testRecord struct { + C string `db:"c"` + } + ie, err := exp.NewInsertExpression([]testRecord{ + {C: "a"}, + {C: "b"}, + }) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithoutTags() { + type testRecord struct { + FieldA int64 + FieldB bool + FieldC string + } + ie, err := exp.NewInsertExpression( + testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, + testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("fielda", "fieldb", "fieldc"), ie.Cols()) + iets.Equal([][]interface{}{{int64(1), true, "a"}, {int64(2), false, "b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsIgnoredDbTag() { + type testRecord struct { + FieldA int64 `db:"-"` + FieldB bool + FieldC string + } + ie, err := exp.NewInsertExpression( + testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, + testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("fieldb", "fieldc"), ie.Cols()) + iets.Equal([][]interface{}{{true, "a"}, {false, "b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithDbv2SkipInsert() { + type testRecord struct { + FieldA int64 + FieldB bool `ff:"skipupdate"` + FieldC string `ff:"skipinsert"` + } + ie, err := exp.NewInsertExpression( + testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, + testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("fielda", "fieldb"), ie.Cols()) + iets.Equal([][]interface{}{{int64(1), true}, {int64(2), false}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructPointers() { + type testRecord struct { + C string `db:"c"` + } + ie, err := exp.NewInsertExpression( + &testRecord{C: "a"}, + &testRecord{C: "b"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("c"), ie.Cols()) + iets.Equal([][]interface{}{{"a"}, {"b"}}, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEmbeddedStructs() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1", Phone: Phone{Home: "123123", Primary: "456456"}}, + item{Address: "211 Test Addr", Name: "Test2", Phone: Phone{Home: "123123", Primary: "456456"}}, + item{Address: "311 Test Addr", Name: "Test3", Phone: Phone{Home: "123123", Primary: "456456"}}, + item{Address: "411 Test Addr", Name: "Test4", Phone: Phone{Home: "123123", Primary: "456456"}}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("address", "home_phone", "name", "primary_phone"), ie.Cols()) + iets.Equal([][]interface{}{ + {"111 Test Addr", "123123", "Test1", "456456"}, + {"211 Test Addr", "123123", "Test2", "456456"}, + {"311 Test Addr", "123123", "Test3", "456456"}, + {"411 Test Addr", "123123", "Test4", "456456"}, + }, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1", Phone: &Phone{Home: "123123", Primary: "456456"}}, + item{Address: "211 Test Addr", Name: "Test2", Phone: &Phone{Home: "123123", Primary: "456456"}}, + item{Address: "311 Test Addr", Name: "Test3", Phone: &Phone{Home: "123123", Primary: "456456"}}, + item{Address: "411 Test Addr", Name: "Test4", Phone: &Phone{Home: "123123", Primary: "456456"}}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("address", "home_phone", "name", "primary_phone"), ie.Cols()) + iets.Equal([][]interface{}{ + {"111 Test Addr", "123123", "Test1", "456456"}, + {"211 Test Addr", "123123", "Test2", "456456"}, + {"311 Test Addr", "123123", "Test3", "456456"}, + {"411 Test Addr", "123123", "Test4", "456456"}, + }, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withNilEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1"}, + item{Address: "211 Test Addr", Name: "Test2"}, + item{Address: "311 Test Addr", Name: "Test3"}, + item{Address: "411 Test Addr", Name: "Test4"}, + ) + iets.NoError(err) + iets.Equal(exp.NewColumnListExpression("address", "name"), ie.Cols()) + iets.Equal([][]interface{}{ + {"111 Test Addr", "Test1"}, + {"211 Test Addr", "Test2"}, + {"311 Test Addr", "Test3"}, + {"411 Test Addr", "Test4"}, + }, ie.Vals()) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentStructTypes() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + _, err := exp.NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1"}, + Phone{Home: "123123", Primary: "456456"}, + item{Address: "311 Test Addr", Name: "Test3"}, + Phone{Home: "123123", Primary: "456456"}, + ) + iets.EqualError(err, "db: rows must be all the same type expected exp_test.item got exp_test.Phone") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentColumnLengths() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type Phone2 struct { + Primary string `db:"primary_phone2"` + Home string `db:"home_phone2"` + } + type item struct { + *Phone + *Phone2 + Address string `db:"address"` + Name string `db:"name"` + } + _, err := exp.NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1", Phone2: &Phone2{Home: "123123", Primary: "456456"}}, + item{Address: "311 Test Addr", Name: "Test3", Phone: &Phone{Home: "123123", Primary: "456456"}}, + ) + iets.EqualError(err, `db: rows with different keys expected `+ + `["address","home_phone2","name","primary_phone2"] got ["address","home_phone","name","primary_phone"]`) +} + +func TestInsertExpressionSuite(t *testing.T) { + suite.Run(t, new(insertExpressionTestSuite)) +} diff --git a/exp/join.go b/exp/join.go new file mode 100644 index 0000000..e321331 --- /dev/null +++ b/exp/join.go @@ -0,0 +1,140 @@ +package exp + +type ( + joinExpression struct { + isConditioned bool + // The JoinType + joinType JoinType + // The table expressions (e.g. LEFT JOIN "my_table", ON (....)) + table Expression + } + // Container for all joins within a dataset + conditionedJoin struct { + joinExpression + // The condition to join (e.g. USING("a", "b"), ON("my_table"."fkey" = "other_table"."id") + condition JoinCondition + } + JoinExpressions []JoinExpression +) + +func NewUnConditionedJoinExpression(joinType JoinType, table Expression) JoinExpression { + return joinExpression{ + joinType: joinType, + table: table, + isConditioned: false, + } +} + +func (je joinExpression) Clone() Expression { + return je +} + +func (je joinExpression) Expression() Expression { + return je +} + +func (je joinExpression) IsConditioned() bool { + return je.isConditioned +} + +func (je joinExpression) JoinType() JoinType { + return je.joinType +} + +func (je joinExpression) Table() Expression { + return je.table +} + +func NewConditionedJoinExpression(joinType JoinType, table Expression, condition JoinCondition) ConditionedJoinExpression { + return conditionedJoin{ + joinExpression: joinExpression{ + joinType: joinType, + table: table, + isConditioned: true, + }, + condition: condition, + } +} + +func (je conditionedJoin) Clone() Expression { + return je +} + +func (je conditionedJoin) Expression() Expression { + return je +} + +func (je conditionedJoin) Condition() JoinCondition { + return je.condition +} + +func (je conditionedJoin) IsConditionEmpty() bool { + return je.condition == nil || je.condition.IsEmpty() +} + +func (jes JoinExpressions) Clone() JoinExpressions { + ret := make(JoinExpressions, 0, len(jes)) + for _, jc := range jes { + ret = append(ret, jc.Clone().(JoinExpression)) + } + return ret +} + +type ( + JoinConditionType int + JoinCondition interface { + Type() JoinConditionType + IsEmpty() bool + } + JoinOnCondition interface { + JoinCondition + On() ExpressionList + } + JoinUsingCondition interface { + JoinCondition + Using() ColumnListExpression + } + joinOnCondition struct { + on ExpressionList + } + + joinUsingCondition struct { + using ColumnListExpression + } +) + +// Creates a new ON clause to be used within a join +// +// ds.Join(I("my_table"), On(I("my_table.fkey").Eq(I("other_table.id"))) +func NewJoinOnCondition(expressions ...Expression) JoinCondition { + return joinOnCondition{on: NewExpressionList(AndType, expressions...)} +} + +func (joc joinOnCondition) Type() JoinConditionType { + return OnJoinCondType +} + +func (joc joinOnCondition) On() ExpressionList { + return joc.on +} + +func (joc joinOnCondition) IsEmpty() bool { + return len(joc.on.Expressions()) == 0 +} + +// Creates a new USING clause to be used within a join +func NewJoinUsingCondition(expressions ...interface{}) JoinCondition { + return joinUsingCondition{using: NewColumnListExpression(expressions...)} +} + +func (juc joinUsingCondition) Type() JoinConditionType { + return UsingJoinCondType +} + +func (juc joinUsingCondition) Using() ColumnListExpression { + return juc.using +} + +func (juc joinUsingCondition) IsEmpty() bool { + return len(juc.using.Columns()) == 0 +} diff --git a/exp/lateral.go b/exp/lateral.go new file mode 100644 index 0000000..eeecd1e --- /dev/null +++ b/exp/lateral.go @@ -0,0 +1,25 @@ +package exp + +type ( + lateral struct { + table AppendableExpression + } +) + +// Creates a new SQL lateral expression +// +// L(From("test")) -> LATERAL (SELECT * FROM "tests") +func NewLateralExpression(table AppendableExpression) LateralExpression { + return lateral{table: table} +} + +func (l lateral) Clone() Expression { + return NewLateralExpression(l.table) +} + +func (l lateral) Table() AppendableExpression { + return l.table +} + +func (l lateral) Expression() Expression { return l } +func (l lateral) As(val interface{}) AliasedExpression { return NewAliasExpression(l, val) } diff --git a/exp/lateral_test.go b/exp/lateral_test.go new file mode 100644 index 0000000..ffec863 --- /dev/null +++ b/exp/lateral_test.go @@ -0,0 +1,36 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type lateralExpressionSuite struct { + suite.Suite +} + +func TestLateralExpressionSuite(t *testing.T) { + suite.Run(t, &lateralExpressionSuite{}) +} + +func (les *lateralExpressionSuite) TestClone() { + le := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})), le.Clone()) +} + +func (les *lateralExpressionSuite) TestExpression() { + le := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(le, le.Expression()) +} + +func (les *lateralExpressionSuite) TestLateral() { + le := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{}), le.Table()) +} + +func (les *lateralExpressionSuite) TestAs() { + le := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(exp.NewAliasExpression(le, "foo"), le.As("foo")) +} diff --git a/exp/literal.go b/exp/literal.go new file mode 100644 index 0000000..cbc043c --- /dev/null +++ b/exp/literal.go @@ -0,0 +1,85 @@ +package exp + +type ( + literal struct { + literal string + args []interface{} + } +) + +// Creates a new SQL literal with the provided arguments. +// +// L("a = 1") -> a = 1 +// +// You can also you placeholders. All placeholders within a Literal are represented by '?' +// +// L("a = ?", "b") -> a = 'b' +// +// Literals can also contain placeholders for other expressions +// +// L("(? AND ?) OR (?)", I("a").Eq(1), I("b").Eq("b"), I("c").In([]string{"a", "b", "c"})) +func NewLiteralExpression(sql string, args ...interface{}) LiteralExpression { + return literal{literal: sql, args: args} +} + +// Returns a literal for the '*' operator +func Star() LiteralExpression { + return NewLiteralExpression("*") +} + +// Returns a literal for the 'DEFAULT' +func Default() LiteralExpression { + return NewLiteralExpression("DEFAULT") +} + +func (l literal) Clone() Expression { + return NewLiteralExpression(l.literal, l.args...) +} + +func (l literal) Literal() string { + return l.literal +} + +func (l literal) Args() []interface{} { + return l.args +} + +func (l literal) Expression() Expression { return l } +func (l literal) As(val interface{}) AliasedExpression { return NewAliasExpression(l, val) } +func (l literal) Eq(val interface{}) BooleanExpression { return eq(l, val) } +func (l literal) Neq(val interface{}) BooleanExpression { return neq(l, val) } +func (l literal) Gt(val interface{}) BooleanExpression { return gt(l, val) } +func (l literal) Gte(val interface{}) BooleanExpression { return gte(l, val) } +func (l literal) Lt(val interface{}) BooleanExpression { return lt(l, val) } +func (l literal) Lte(val interface{}) BooleanExpression { return lte(l, val) } +func (l literal) Asc() OrderedExpression { return asc(l) } +func (l literal) Desc() OrderedExpression { return desc(l) } +func (l literal) Between(val RangeVal) RangeExpression { return between(l, val) } +func (l literal) NotBetween(val RangeVal) RangeExpression { return notBetween(l, val) } +func (l literal) Like(val interface{}) BooleanExpression { return like(l, val) } +func (l literal) NotLike(val interface{}) BooleanExpression { return notLike(l, val) } +func (l literal) ILike(val interface{}) BooleanExpression { return iLike(l, val) } +func (l literal) NotILike(val interface{}) BooleanExpression { return notILike(l, val) } +func (l literal) RegexpLike(val interface{}) BooleanExpression { return regexpLike(l, val) } +func (l literal) RegexpNotLike(val interface{}) BooleanExpression { return regexpNotLike(l, val) } +func (l literal) RegexpILike(val interface{}) BooleanExpression { return regexpILike(l, val) } +func (l literal) RegexpNotILike(val interface{}) BooleanExpression { return regexpNotILike(l, val) } +func (l literal) In(vals ...interface{}) BooleanExpression { return in(l, vals...) } +func (l literal) NotIn(vals ...interface{}) BooleanExpression { return notIn(l, vals...) } +func (l literal) Is(val interface{}) BooleanExpression { return is(l, val) } +func (l literal) IsNot(val interface{}) BooleanExpression { return isNot(l, val) } +func (l literal) IsNull() BooleanExpression { return is(l, nil) } +func (l literal) IsNotNull() BooleanExpression { return isNot(l, nil) } +func (l literal) IsTrue() BooleanExpression { return is(l, true) } +func (l literal) IsNotTrue() BooleanExpression { return isNot(l, true) } +func (l literal) IsFalse() BooleanExpression { return is(l, false) } +func (l literal) IsNotFalse() BooleanExpression { return isNot(l, false) } + +func (l literal) BitwiseInversion() BitwiseExpression { return bitwiseInversion(l) } +func (l literal) BitwiseOr(val interface{}) BitwiseExpression { return bitwiseOr(l, val) } +func (l literal) BitwiseAnd(val interface{}) BitwiseExpression { return bitwiseAnd(l, val) } +func (l literal) BitwiseXor(val interface{}) BitwiseExpression { return bitwiseXor(l, val) } +func (l literal) BitwiseLeftShift(val interface{}) BitwiseExpression { return bitwiseLeftShift(l, val) } +func (l literal) BitwiseRightShift(val interface{}) BitwiseExpression { + return bitwiseRightShift(l, val) +} diff --git a/exp/literal_test.go b/exp/literal_test.go new file mode 100644 index 0000000..29db7f5 --- /dev/null +++ b/exp/literal_test.go @@ -0,0 +1,87 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type literalExpressionSuite struct { + suite.Suite + le exp.LiteralExpression +} + +func TestLiteralExpressionSuite(t *testing.T) { + suite.Run(t, &literalExpressionSuite{ + le: exp.NewLiteralExpression("? + ?", 1, 2), + }) +} + +func (les *literalExpressionSuite) TestClone() { + les.Equal(les.le, les.le.Clone()) +} + +func (les *literalExpressionSuite) TestExpression() { + les.Equal(les.le, les.le.Expression()) +} + +func (les *literalExpressionSuite) TestLiteral() { + les.Equal("? + ?", les.le.Literal()) +} + +func (les *literalExpressionSuite) TestArgs() { + les.Equal([]interface{}{1, 2}, les.le.Args()) +} + +func (les *literalExpressionSuite) TestAllOthers() { + le := les.le + rv := exp.NewRangeVal(1, 2) + pattern := "literal like%" + inVals := []interface{}{1, 2} + bitwiseVals := 2 + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: le.As("a"), Expected: exp.NewAliasExpression(le, "a")}, + {Ex: le.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, le, 1)}, + {Ex: le.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, le, 1)}, + {Ex: le.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, le, 1)}, + {Ex: le.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, le, 1)}, + {Ex: le.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, le, 1)}, + {Ex: le.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, le, 1)}, + {Ex: le.Asc(), Expected: exp.NewOrderedExpression(le, exp.AscDir, exp.NoNullsSortType)}, + {Ex: le.Desc(), Expected: exp.NewOrderedExpression(le, exp.DescSortDir, exp.NoNullsSortType)}, + {Ex: le.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, le, rv)}, + {Ex: le.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, le, rv)}, + {Ex: le.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, le, pattern)}, + {Ex: le.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, le, pattern)}, + {Ex: le.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, le, pattern)}, + {Ex: le.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, le, pattern)}, + {Ex: le.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, le, pattern)}, + {Ex: le.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, le, pattern)}, + {Ex: le.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, le, pattern)}, + {Ex: le.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, le, pattern)}, + {Ex: le.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, le, inVals)}, + {Ex: le.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, le, inVals)}, + {Ex: le.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, le, true)}, + {Ex: le.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, le, true)}, + {Ex: le.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, le, nil)}, + {Ex: le.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, le, nil)}, + {Ex: le.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, le, true)}, + {Ex: le.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, le, true)}, + {Ex: le.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, le, false)}, + {Ex: le.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, le, false)}, + {Ex: le.BitwiseInversion(), Expected: exp.NewBitwiseExpression(exp.BitwiseInversionOp, nil, le)}, + {Ex: le.BitwiseOr(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseOrOp, le, bitwiseVals)}, + {Ex: le.BitwiseAnd(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseAndOp, le, bitwiseVals)}, + {Ex: le.BitwiseXor(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseXorOp, le, bitwiseVals)}, + {Ex: le.BitwiseLeftShift(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseLeftShiftOp, le, bitwiseVals)}, + {Ex: le.BitwiseRightShift(bitwiseVals), Expected: exp.NewBitwiseExpression(exp.BitwiseRightShiftOp, le, bitwiseVals)}, + } + + for _, tc := range testCases { + les.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/lock.go b/exp/lock.go new file mode 100644 index 0000000..9b8bf72 --- /dev/null +++ b/exp/lock.go @@ -0,0 +1,48 @@ +package exp + +type ( + LockStrength int + WaitOption int + Lock interface { + Strength() LockStrength + WaitOption() WaitOption + Of() []IdentifierExpression + } + lock struct { + strength LockStrength + waitOption WaitOption + of []IdentifierExpression + } +) + +const ( + ForNolock LockStrength = iota + ForUpdate + ForNoKeyUpdate + ForShare + ForKeyShare + + Wait WaitOption = iota + NoWait + SkipLocked +) + +func NewLock(strength LockStrength, option WaitOption, of ...IdentifierExpression) Lock { + return lock{ + strength: strength, + waitOption: option, + of: of, + } +} + +func (l lock) Strength() LockStrength { + return l.strength +} + +func (l lock) WaitOption() WaitOption { + return l.waitOption +} + +func (l lock) Of() []IdentifierExpression { + return l.of +} diff --git a/exp/order.go b/exp/order.go new file mode 100644 index 0000000..640f1f6 --- /dev/null +++ b/exp/order.go @@ -0,0 +1,52 @@ +package exp + +type ( + orderedExpression struct { + sortExpression Expression + direction SortDirection + nullSortType NullSortType + } +) + +// used internally to create a new SORT_ASC OrderedExpression +func asc(exp Expression) OrderedExpression { + return NewOrderedExpression(exp, AscDir, NoNullsSortType) +} + +// used internally to create a new SORT_DESC OrderedExpression +func desc(exp Expression) OrderedExpression { + return NewOrderedExpression(exp, DescSortDir, NoNullsSortType) +} + +// used internally to create a new SORT_ASC OrderedExpression +func NewOrderedExpression(exp Expression, direction SortDirection, sortType NullSortType) OrderedExpression { + return orderedExpression{sortExpression: exp, direction: direction, nullSortType: sortType} +} + +func (oe orderedExpression) Clone() Expression { + return NewOrderedExpression(oe.sortExpression, oe.direction, oe.nullSortType) +} + +func (oe orderedExpression) Expression() Expression { + return oe +} + +func (oe orderedExpression) SortExpression() Expression { + return oe.sortExpression +} + +func (oe orderedExpression) IsAsc() bool { + return oe.direction == AscDir +} + +func (oe orderedExpression) NullSortType() NullSortType { + return oe.nullSortType +} + +func (oe orderedExpression) NullsFirst() OrderedExpression { + return NewOrderedExpression(oe.sortExpression, oe.direction, NullsFirstSortType) +} + +func (oe orderedExpression) NullsLast() OrderedExpression { + return NewOrderedExpression(oe.sortExpression, oe.direction, NullsLastSortType) +} diff --git a/exp/range.go b/exp/range.go new file mode 100644 index 0000000..63c1dff --- /dev/null +++ b/exp/range.go @@ -0,0 +1,62 @@ +package exp + +type ( + ranged struct { + lhs Expression + rhs RangeVal + op RangeOperation + } + rangeVal struct { + start interface{} + end interface{} + } +) + +// used internally to create an BETWEEN comparison RangeExpression +func between(lhs Expression, rhs RangeVal) RangeExpression { + return NewRangeExpression(BetweenOp, lhs, rhs) +} + +// used internally to create an NOT BETWEEN comparison RangeExpression +func notBetween(lhs Expression, rhs RangeVal) RangeExpression { + return NewRangeExpression(NotBetweenOp, lhs, rhs) +} + +func NewRangeExpression(op RangeOperation, lhs Expression, rhs RangeVal) RangeExpression { + return ranged{op: op, lhs: lhs, rhs: rhs} +} + +func (r ranged) Clone() Expression { + return NewRangeExpression(r.op, r.lhs.Clone(), r.rhs) +} + +func (r ranged) Expression() Expression { + return r +} + +func (r ranged) RHS() RangeVal { + return r.rhs +} + +func (r ranged) LHS() Expression { + return r.lhs +} + +func (r ranged) Op() RangeOperation { + return r.op +} + +// Creates a new Range to be used with a Between expression +// +// exp.C("col").Between(exp.Range(1, 10)) +func NewRangeVal(start, end interface{}) RangeVal { + return rangeVal{start: start, end: end} +} + +func (rv rangeVal) Start() interface{} { + return rv.start +} + +func (rv rangeVal) End() interface{} { + return rv.end +} diff --git a/exp/record.go b/exp/record.go new file mode 100644 index 0000000..6a2d55f --- /dev/null +++ b/exp/record.go @@ -0,0 +1,68 @@ +package exp + +import ( + "reflect" + "sort" + + "git.fsdpf.net/go/db/v2/internal/util" +) + +// Alternative to writing map[string]interface{}. Can be used for Inserts, Updates or Deletes +type Record map[string]interface{} + +func (r Record) Cols() []string { + cols := make([]string, 0, len(r)) + for col := range r { + cols = append(cols, col) + } + sort.Strings(cols) + return cols +} + +func NewRecordFromStruct(i interface{}, forInsert, forUpdate bool) (r Record, err error) { + value := reflect.ValueOf(i) + if value.IsValid() { + cm, err := util.GetColumnMap(value.Interface()) + if err != nil { + return nil, err + } + cols := cm.Cols() + r = make(map[string]interface{}, len(cols)) + for _, col := range cols { + f := cm[col] + if !shouldSkipField(f, forInsert, forUpdate) { + if fieldValue, isAvailable := util.SafeGetFieldByIndex(value, f.FieldIndex); isAvailable { + if !shouldOmitField(fieldValue, f) { + r[f.ColumnName] = getRecordValue(fieldValue, f) + } + } + } + } + } + return +} + +func shouldSkipField(f util.ColumnData, forInsert, forUpdate bool) bool { + shouldSkipInsert := forInsert && !f.ShouldInsert + shouldSkipUpdate := forUpdate && !f.ShouldUpdate + return shouldSkipInsert || shouldSkipUpdate +} + +func shouldOmitField(val reflect.Value, f util.ColumnData) bool { + if f.OmitNil && util.IsNil(val) { + return true + } else if f.OmitEmpty && util.IsEmptyValue(val) { + return true + } + return false +} + +func getRecordValue(val reflect.Value, f util.ColumnData) interface{} { + if f.DefaultIfEmpty && util.IsEmptyValue(val) { + return Default() + } else if val.IsValid() { + return val.Interface() + } else { + return reflect.Zero(f.GoType).Interface() + } +} diff --git a/exp/select_clauses.go b/exp/select_clauses.go new file mode 100644 index 0000000..f802a88 --- /dev/null +++ b/exp/select_clauses.go @@ -0,0 +1,379 @@ +package exp + +type ( + SelectClauses interface { + HasSources() bool + IsDefaultSelect() bool + clone() *selectClauses + + Select() ColumnListExpression + SelectAppend(cl ColumnListExpression) SelectClauses + SetSelect(cl ColumnListExpression) SelectClauses + + Distinct() ColumnListExpression + SetDistinct(cle ColumnListExpression) SelectClauses + + From() ColumnListExpression + SetFrom(cl ColumnListExpression) SelectClauses + + HasAlias() bool + Alias() IdentifierExpression + SetAlias(ie IdentifierExpression) SelectClauses + + Joins() JoinExpressions + JoinsAppend(jc JoinExpression) SelectClauses + + Where() ExpressionList + ClearWhere() SelectClauses + WhereAppend(expressions ...Expression) SelectClauses + + Having() ExpressionList + ClearHaving() SelectClauses + HavingAppend(expressions ...Expression) SelectClauses + + Order() ColumnListExpression + HasOrder() bool + ClearOrder() SelectClauses + SetOrder(oes ...OrderedExpression) SelectClauses + OrderAppend(...OrderedExpression) SelectClauses + OrderPrepend(...OrderedExpression) SelectClauses + + GroupBy() ColumnListExpression + SetGroupBy(cl ColumnListExpression) SelectClauses + GroupByAppend(cl ColumnListExpression) SelectClauses + + Limit() interface{} + HasLimit() bool + ClearLimit() SelectClauses + SetLimit(limit interface{}) SelectClauses + + Offset() uint + ClearOffset() SelectClauses + SetOffset(offset uint) SelectClauses + + Compounds() []CompoundExpression + CompoundsAppend(ce CompoundExpression) SelectClauses + + Lock() Lock + SetLock(l Lock) SelectClauses + + CommonTables() []CommonTableExpression + CommonTablesAppend(cte CommonTableExpression) SelectClauses + + Windows() []WindowExpression + SetWindows(ws []WindowExpression) SelectClauses + WindowsAppend(ws ...WindowExpression) SelectClauses + ClearWindows() SelectClauses + } + selectClauses struct { + commonTables []CommonTableExpression + selectColumns ColumnListExpression + distinct ColumnListExpression + from ColumnListExpression + joins JoinExpressions + where ExpressionList + alias IdentifierExpression + groupBy ColumnListExpression + having ExpressionList + order ColumnListExpression + limit interface{} + offset uint + compounds []CompoundExpression + lock Lock + windows []WindowExpression + } +) + +func NewSelectClauses() SelectClauses { + return &selectClauses{ + selectColumns: NewColumnListExpression(Star()), + } +} + +func (c *selectClauses) HasSources() bool { + return c.from != nil && len(c.from.Columns()) > 0 +} + +func (c *selectClauses) IsDefaultSelect() bool { + ret := false + if c.selectColumns != nil { + selects := c.selectColumns.Columns() + if len(selects) == 1 { + if l, ok := selects[0].(LiteralExpression); ok && l.Literal() == "*" { + ret = true + } + } + } + return ret +} + +func (c *selectClauses) clone() *selectClauses { + return &selectClauses{ + commonTables: c.commonTables, + selectColumns: c.selectColumns, + distinct: c.distinct, + from: c.from, + joins: c.joins[0:len(c.joins):len(c.joins)], + where: c.where, + alias: c.alias, + groupBy: c.groupBy, + having: c.having, + order: c.order, + limit: c.limit, + offset: c.offset, + compounds: c.compounds, + lock: c.lock, + windows: c.windows, + } +} + +func (c *selectClauses) CommonTables() []CommonTableExpression { + return c.commonTables +} + +func (c *selectClauses) CommonTablesAppend(cte CommonTableExpression) SelectClauses { + ret := c.clone() + ret.commonTables = append(ret.commonTables, cte) + return ret +} + +func (c *selectClauses) Select() ColumnListExpression { + return c.selectColumns +} + +func (c *selectClauses) SelectAppend(cl ColumnListExpression) SelectClauses { + ret := c.clone() + ret.selectColumns = ret.selectColumns.Append(cl.Columns()...) + return ret +} + +func (c *selectClauses) SetSelect(cl ColumnListExpression) SelectClauses { + ret := c.clone() + ret.selectColumns = cl + return ret +} + +func (c *selectClauses) Distinct() ColumnListExpression { + return c.distinct +} + +func (c *selectClauses) SetDistinct(cle ColumnListExpression) SelectClauses { + ret := c.clone() + ret.distinct = cle + return ret +} + +func (c *selectClauses) From() ColumnListExpression { + return c.from +} + +func (c *selectClauses) SetFrom(cl ColumnListExpression) SelectClauses { + ret := c.clone() + ret.from = cl + return ret +} + +func (c *selectClauses) HasAlias() bool { + return c.alias != nil +} + +func (c *selectClauses) Alias() IdentifierExpression { + return c.alias +} + +func (c *selectClauses) SetAlias(ie IdentifierExpression) SelectClauses { + ret := c.clone() + ret.alias = ie + return ret +} + +func (c *selectClauses) Joins() JoinExpressions { + return c.joins +} + +func (c *selectClauses) JoinsAppend(jc JoinExpression) SelectClauses { + ret := c.clone() + ret.joins = append(ret.joins, jc) + return ret +} + +func (c *selectClauses) Where() ExpressionList { + return c.where +} + +func (c *selectClauses) ClearWhere() SelectClauses { + ret := c.clone() + ret.where = nil + return ret +} + +func (c *selectClauses) WhereAppend(expressions ...Expression) SelectClauses { + if len(expressions) == 0 { + return c + } + ret := c.clone() + if ret.where == nil { + ret.where = NewExpressionList(AndType, expressions...) + } else { + ret.where = ret.where.Append(expressions...) + } + return ret +} + +func (c *selectClauses) Having() ExpressionList { + return c.having +} + +func (c *selectClauses) ClearHaving() SelectClauses { + ret := c.clone() + ret.having = nil + return ret +} + +func (c *selectClauses) HavingAppend(expressions ...Expression) SelectClauses { + if len(expressions) == 0 { + return c + } + ret := c.clone() + if ret.having == nil { + ret.having = NewExpressionList(AndType, expressions...) + } else { + ret.having = ret.having.Append(expressions...) + } + return ret +} + +func (c *selectClauses) Lock() Lock { + return c.lock +} + +func (c *selectClauses) SetLock(l Lock) SelectClauses { + ret := c.clone() + ret.lock = l + return ret +} + +func (c *selectClauses) Order() ColumnListExpression { + return c.order +} + +func (c *selectClauses) HasOrder() bool { + return c.order != nil +} + +func (c *selectClauses) ClearOrder() SelectClauses { + ret := c.clone() + ret.order = nil + return ret +} + +func (c *selectClauses) SetOrder(oes ...OrderedExpression) SelectClauses { + ret := c.clone() + ret.order = NewOrderedColumnList(oes...) + return ret +} + +func (c *selectClauses) OrderAppend(oes ...OrderedExpression) SelectClauses { + if c.order == nil { + return c.SetOrder(oes...) + } + ret := c.clone() + ret.order = ret.order.Append(NewOrderedColumnList(oes...).Columns()...) + return ret +} + +func (c *selectClauses) OrderPrepend(oes ...OrderedExpression) SelectClauses { + if c.order == nil { + return c.SetOrder(oes...) + } + ret := c.clone() + ret.order = NewOrderedColumnList(oes...).Append(ret.order.Columns()...) + return ret +} + +func (c *selectClauses) GroupBy() ColumnListExpression { + return c.groupBy +} + +func (c *selectClauses) GroupByAppend(cl ColumnListExpression) SelectClauses { + if c.groupBy == nil { + return c.SetGroupBy(cl) + } + ret := c.clone() + ret.groupBy = ret.groupBy.Append(cl.Columns()...) + return ret +} + +func (c *selectClauses) SetGroupBy(cl ColumnListExpression) SelectClauses { + ret := c.clone() + ret.groupBy = cl + return ret +} + +func (c *selectClauses) Limit() interface{} { + return c.limit +} + +func (c *selectClauses) HasLimit() bool { + return c.limit != nil +} + +func (c *selectClauses) ClearLimit() SelectClauses { + ret := c.clone() + ret.limit = nil + return ret +} + +func (c *selectClauses) SetLimit(limit interface{}) SelectClauses { + ret := c.clone() + ret.limit = limit + return ret +} + +func (c *selectClauses) Offset() uint { + return c.offset +} + +func (c *selectClauses) ClearOffset() SelectClauses { + ret := c.clone() + ret.offset = 0 + return ret +} + +func (c *selectClauses) SetOffset(offset uint) SelectClauses { + ret := c.clone() + ret.offset = offset + return ret +} + +func (c *selectClauses) Compounds() []CompoundExpression { + return c.compounds +} + +func (c *selectClauses) CompoundsAppend(ce CompoundExpression) SelectClauses { + ret := c.clone() + ret.compounds = append(ret.compounds, ce) + return ret +} + +func (c *selectClauses) Windows() []WindowExpression { + return c.windows +} + +func (c *selectClauses) SetWindows(ws []WindowExpression) SelectClauses { + ret := c.clone() + ret.windows = ws + return ret +} + +func (c *selectClauses) WindowsAppend(ws ...WindowExpression) SelectClauses { + ret := c.clone() + ret.windows = append(ret.windows, ws...) + return ret +} + +func (c *selectClauses) ClearWindows() SelectClauses { + ret := c.clone() + ret.windows = nil + return ret +} diff --git a/exp/select_clauses_test.go b/exp/select_clauses_test.go new file mode 100644 index 0000000..b3a5490 --- /dev/null +++ b/exp/select_clauses_test.go @@ -0,0 +1,578 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type testSQLExpression string + +func (tse testSQLExpression) Expression() exp.Expression { + return tse +} + +func (tse testSQLExpression) Clone() exp.Expression { + return tse +} + +func (tse testSQLExpression) ToSQL() (sql string, args []interface{}, err error) { + return "", nil, nil +} + +func (tse testSQLExpression) IsPrepared() bool { + return false +} + +type selectClausesSuite struct { + suite.Suite +} + +func TestSelectClausesSuite(t *testing.T) { + suite.Run(t, new(selectClausesSuite)) +} + +func (scs *selectClausesSuite) TestHasSources() { + c := exp.NewSelectClauses() + c2 := c.SetFrom(exp.NewColumnListExpression("test")) + + scs.False(c.HasSources()) + + scs.True(c2.HasSources()) +} + +func (scs *selectClausesSuite) TestIsDefaultSelect() { + c := exp.NewSelectClauses() + c2 := c.SelectAppend(exp.NewColumnListExpression("a")) + + scs.True(c.IsDefaultSelect()) + + scs.False(c2.IsDefaultSelect()) +} + +func (scs *selectClausesSuite) TestSelect() { + c := exp.NewSelectClauses() + c2 := c.SetSelect(exp.NewColumnListExpression("a")) + + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) + + scs.Equal(exp.NewColumnListExpression("a"), c2.Select()) +} + +func (scs *selectClausesSuite) TestSelectAppend() { + c := exp.NewSelectClauses() + c2 := c.SelectAppend(exp.NewColumnListExpression("a")) + + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) + scs.Equal(exp.NewColumnListExpression(exp.Star(), "a"), c2.Select()) +} + +func (scs *selectClausesSuite) TestSetSelect() { + c := exp.NewSelectClauses() + c2 := c.SetSelect(exp.NewColumnListExpression("a")) + + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) + scs.Equal(exp.NewColumnListExpression("a"), c2.Select()) +} + +func (scs *selectClausesSuite) TestDistinct() { + c := exp.NewSelectClauses() + c2 := c.SetDistinct(exp.NewColumnListExpression("a")) + + scs.Nil(c.Distinct()) + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) + + scs.Equal(exp.NewColumnListExpression("a"), c2.Distinct()) + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) +} + +func (scs *selectClausesSuite) TestSetSelectDistinct() { + c := exp.NewSelectClauses() + c2 := c.SetDistinct(exp.NewColumnListExpression("a")) + + scs.Nil(c.Distinct()) + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) + + scs.Equal(exp.NewColumnListExpression("a"), c2.Distinct()) + scs.Equal(exp.NewColumnListExpression(exp.Star()), c.Select()) +} + +func (scs *selectClausesSuite) TestFrom() { + c := exp.NewSelectClauses() + c2 := c.SetFrom(exp.NewColumnListExpression("a")) + + scs.Nil(c.From()) + + scs.Equal(exp.NewColumnListExpression("a"), c2.From()) +} + +func (scs *selectClausesSuite) TestSetFrom() { + c := exp.NewSelectClauses() + c2 := c.SetFrom(exp.NewColumnListExpression("a")) + + scs.Nil(c.From()) + + scs.Equal(exp.NewColumnListExpression("a"), c2.From()) +} + +func (scs *selectClausesSuite) TestHasAlias() { + c := exp.NewSelectClauses() + c2 := c.SetAlias(exp.NewIdentifierExpression("", "", "a")) + + scs.False(c.HasAlias()) + + scs.True(c2.HasAlias()) +} + +func (scs *selectClausesSuite) TestAlias() { + c := exp.NewSelectClauses() + a := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetAlias(a) + + scs.Nil(c.Alias()) + + scs.Equal(a, c2.Alias()) +} + +func (scs *selectClausesSuite) TestSetAlias() { + c := exp.NewSelectClauses() + a := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetAlias(a) + + scs.Nil(c.Alias()) + + scs.Equal(a, c2.Alias()) +} + +func (scs *selectClausesSuite) TestJoins() { + jc := exp.NewConditionedJoinExpression( + exp.LeftJoinType, + exp.NewIdentifierExpression("", "test", ""), + nil, + ) + c := exp.NewSelectClauses() + c2 := c.JoinsAppend(jc) + + scs.Nil(c.Joins()) + + scs.Equal(exp.JoinExpressions{jc}, c2.Joins()) +} + +func (scs *selectClausesSuite) TestJoinsAppend() { + jc := exp.NewConditionedJoinExpression( + exp.LeftJoinType, + exp.NewIdentifierExpression("", "test1", ""), + nil, + ) + jc2 := exp.NewUnConditionedJoinExpression( + exp.LeftJoinType, + exp.NewIdentifierExpression("", "test2", ""), + ) + jc3 := exp.NewUnConditionedJoinExpression( + exp.InnerJoinType, + exp.NewIdentifierExpression("", "test3", ""), + ) + c := exp.NewSelectClauses() + c2 := c.JoinsAppend(jc) + c3 := c2.JoinsAppend(jc2) + + c4 := c3.JoinsAppend(jc2) // len(c4.joins) == 3, cap(c4.joins) == 4 + // next two appends shouldn't affect one another + c5 := c4.JoinsAppend(jc2) + c6 := c4.JoinsAppend(jc3) + + scs.Nil(c.Joins()) + + scs.Equal(exp.JoinExpressions{jc}, c2.Joins()) + scs.Equal(exp.JoinExpressions{jc, jc2}, c3.Joins()) + scs.Equal(exp.JoinExpressions{jc, jc2, jc2}, c4.Joins()) + scs.Equal(exp.JoinExpressions{jc, jc2, jc2, jc2}, c5.Joins()) + scs.Equal(exp.JoinExpressions{jc, jc2, jc2, jc3}, c6.Joins()) +} + +func (scs *selectClausesSuite) TestWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses() + c2 := c.WhereAppend(w) + + scs.Nil(c.Where()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) +} + +func (scs *selectClausesSuite) TestClearWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses().WhereAppend(w) + c2 := c.ClearWhere() + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c.Where()) + + scs.Nil(c2.Where()) +} + +func (scs *selectClausesSuite) TestWhereAppend() { + w := exp.Ex{"a": 1} + w2 := exp.Ex{"b": 2} + + c := exp.NewSelectClauses() + c2 := c.WhereAppend(w) + + c3 := c.WhereAppend(w).WhereAppend(w2) + + c4 := c.WhereAppend(w, w2) + + scs.Nil(c.Where()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) + scs.Equal(exp.NewExpressionList(exp.AndType, w).Append(w2), c3.Where()) + scs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Where()) +} + +func (scs *selectClausesSuite) TestHaving() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses() + c2 := c.HavingAppend(w) + + scs.Nil(c.Having()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Having()) +} + +func (scs *selectClausesSuite) TestClearHaving() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses().HavingAppend(w) + c2 := c.ClearHaving() + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c.Having()) + + scs.Nil(c2.Having()) +} + +func (scs *selectClausesSuite) TestHavingAppend() { + w := exp.Ex{"a": 1} + w2 := exp.Ex{"b": 2} + + c := exp.NewSelectClauses() + c2 := c.HavingAppend(w) + + c3 := c.HavingAppend(w).HavingAppend(w2) + + c4 := c.HavingAppend(w, w2) + + scs.Nil(c.Having()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Having()) + scs.Equal(exp.NewExpressionList(exp.AndType, w).Append(w2), c3.Having()) + scs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Having()) +} + +func (scs *selectClausesSuite) TestWindows() { + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + + c := exp.NewSelectClauses() + c2 := c.WindowsAppend(w) + + scs.Nil(c.Windows()) + + scs.Equal([]exp.WindowExpression{w}, c2.Windows()) +} + +func (scs *selectClausesSuite) TestSetWindows() { + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + + c := exp.NewSelectClauses() + c2 := c.SetWindows([]exp.WindowExpression{w}) + + scs.Nil(c.Windows()) + + scs.Equal([]exp.WindowExpression{w}, c2.Windows()) +} + +func (scs *selectClausesSuite) TestWindowsAppend() { + w1 := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w1"), nil, nil, nil) + w2 := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w2"), nil, nil, nil) + + c := exp.NewSelectClauses() + c2 := c.WindowsAppend(w1).WindowsAppend(w2) + + scs.Nil(c.Windows()) + + scs.Equal([]exp.WindowExpression{w1, w2}, c2.Windows()) +} + +func (scs *selectClausesSuite) TestClearWindows() { + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + + c := exp.NewSelectClauses().SetWindows([]exp.WindowExpression{w}) + scs.Nil(c.ClearWindows().Windows()) + scs.Equal([]exp.WindowExpression{w}, c.Windows()) +} + +func (scs *selectClausesSuite) TestOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewSelectClauses() + c2 := c.SetOrder(oe) + + scs.Nil(c.Order()) + + scs.Equal(exp.NewColumnListExpression(oe), c2.Order()) +} + +func (scs *selectClausesSuite) TestHasOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewSelectClauses() + c2 := c.SetOrder(oe) + + scs.False(c.HasOrder()) + + scs.True(c2.HasOrder()) +} + +func (scs *selectClausesSuite) TestClearOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewSelectClauses().SetOrder(oe) + c2 := c.ClearOrder() + + scs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + scs.Nil(c2.Order()) +} + +func (scs *selectClausesSuite) TestSetOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewSelectClauses().SetOrder(oe) + c2 := c.SetOrder(oe2) + + scs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + scs.Equal(exp.NewColumnListExpression(oe2), c2.Order()) +} + +func (scs *selectClausesSuite) TestOrderAppend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewSelectClauses().SetOrder(oe) + c2 := c.OrderAppend(oe2) + + scs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + scs.Equal(exp.NewColumnListExpression(oe, oe2), c2.Order()) +} + +func (scs *selectClausesSuite) TestOrderPrepend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewSelectClauses().SetOrder(oe) + c2 := c.OrderPrepend(oe2) + + scs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + scs.Equal(exp.NewColumnListExpression(oe2, oe), c2.Order()) +} + +func (scs *selectClausesSuite) TestGroupBy() { + g := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "a")) + + c := exp.NewSelectClauses() + c2 := c.SetGroupBy(g) + + scs.Nil(c.GroupBy()) + + scs.Equal(g, c2.GroupBy()) +} + +func (scs *selectClausesSuite) TestGroupByAppend() { + g := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "a")) + g2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "b")) + + c := exp.NewSelectClauses().SetGroupBy(g) + c2 := c.GroupByAppend(g2) + + scs.Equal(g, c.GroupBy()) + + scs.Equal(exp.NewColumnListExpression(g, g2), c2.GroupBy()) +} + +func (scs *selectClausesSuite) TestGroupByAppend_NoPreviousGroupBy() { + g := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "a")) + g2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "b")) + + c := exp.NewSelectClauses().GroupByAppend(g) + c2 := c.GroupByAppend(g2) + + scs.Equal(g, c.GroupBy()) + + scs.Equal(exp.NewColumnListExpression(g, g2), c2.GroupBy()) +} + +func (scs *selectClausesSuite) TestSetGroupBy() { + g := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "a")) + g2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "b")) + + c := exp.NewSelectClauses().SetGroupBy(g) + c2 := c.SetGroupBy(g2) + + scs.Equal(g, c.GroupBy()) + + scs.Equal(g2, c2.GroupBy()) +} + +func (scs *selectClausesSuite) TestLimit() { + l := 1 + + c := exp.NewSelectClauses() + c2 := c.SetLimit(l) + + scs.Nil(c.Limit()) + + scs.Equal(l, c2.Limit()) +} + +func (scs *selectClausesSuite) TestHasLimit() { + l := 1 + + c := exp.NewSelectClauses() + c2 := c.SetLimit(l) + + scs.False(c.HasLimit()) + + scs.True(c2.HasLimit()) +} + +func (scs *selectClausesSuite) TestCLearLimit() { + l := 1 + + c := exp.NewSelectClauses().SetLimit(l) + c2 := c.ClearLimit() + + scs.True(c.HasLimit()) + + scs.False(c2.HasLimit()) +} + +func (scs *selectClausesSuite) TestSetLimit() { + l := 1 + l2 := 2 + + c := exp.NewSelectClauses().SetLimit(l) + c2 := c.SetLimit(2) + + scs.Equal(l, c.Limit()) + + scs.Equal(l2, c2.Limit()) +} + +func (scs *selectClausesSuite) TestOffset() { + o := uint(1) + + c := exp.NewSelectClauses() + c2 := c.SetOffset(o) + + scs.Equal(uint(0), c.Offset()) + + scs.Equal(o, c2.Offset()) +} + +func (scs *selectClausesSuite) TestClearOffset() { + o := uint(1) + + c := exp.NewSelectClauses().SetOffset(o) + c2 := c.ClearOffset() + + scs.Equal(o, c.Offset()) + + scs.Equal(uint(0), c2.Offset()) +} + +func (scs *selectClausesSuite) TestSetOffset() { + o := uint(1) + o2 := uint(2) + + c := exp.NewSelectClauses().SetOffset(o) + c2 := c.SetOffset(2) + + scs.Equal(o, c.Offset()) + + scs.Equal(o2, c2.Offset()) +} + +func (scs *selectClausesSuite) TestCompounds() { + ce := exp.NewCompoundExpression(exp.UnionCompoundType, newTestAppendableExpression("SELECT * FROM foo", []interface{}{})) + + c := exp.NewSelectClauses() + c2 := c.CompoundsAppend(ce) + + scs.Nil(c.Compounds()) + + scs.Equal([]exp.CompoundExpression{ce}, c2.Compounds()) +} + +func (scs *selectClausesSuite) TestCompoundsAppend() { + ce := exp.NewCompoundExpression(exp.UnionCompoundType, newTestAppendableExpression("SELECT * FROM foo1", []interface{}{})) + ce2 := exp.NewCompoundExpression(exp.UnionCompoundType, newTestAppendableExpression("SELECT * FROM foo2", []interface{}{})) + + c := exp.NewSelectClauses().CompoundsAppend(ce) + c2 := c.CompoundsAppend(ce2) + + scs.Equal([]exp.CompoundExpression{ce}, c.Compounds()) + + scs.Equal([]exp.CompoundExpression{ce, ce2}, c2.Compounds()) +} + +func (scs *selectClausesSuite) TestLock() { + l := exp.NewLock(exp.ForUpdate, exp.Wait) + + c := exp.NewSelectClauses() + c2 := c.SetLock(l) + + scs.Nil(c.Lock()) + + scs.Equal(l, c2.Lock()) +} + +func (scs *selectClausesSuite) TestSetLock() { + l := exp.NewLock(exp.ForUpdate, exp.Wait) + l2 := exp.NewLock(exp.ForUpdate, exp.NoWait) + + c := exp.NewSelectClauses().SetLock(l) + c2 := c.SetLock(l2) + + scs.Equal(l, c.Lock()) + + scs.Equal(l2, c2.Lock()) +} + +func (scs *selectClausesSuite) TestCommonTables() { + cte := exp.NewCommonTableExpression(true, "test", newTestAppendableExpression(`SELECT * FROM "foo"`, []interface{}{})) + + c := exp.NewSelectClauses() + c2 := c.CommonTablesAppend(cte) + + scs.Nil(c.CommonTables()) + + scs.Equal([]exp.CommonTableExpression{cte}, c2.CommonTables()) +} + +func (scs *selectClausesSuite) TestAddCommonTablesAppend() { + cte := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte")) + cte2 := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte2")) + + c := exp.NewSelectClauses().CommonTablesAppend(cte) + c2 := c.CommonTablesAppend(cte2) + + scs.Equal([]exp.CommonTableExpression{cte}, c.CommonTables()) + + scs.Equal([]exp.CommonTableExpression{cte, cte2}, c2.CommonTables()) +} diff --git a/exp/truncate.go b/exp/truncate.go new file mode 100644 index 0000000..1486a00 --- /dev/null +++ b/exp/truncate.go @@ -0,0 +1,11 @@ +package exp + +// Options to use when generating a TRUNCATE statement +type TruncateOptions struct { + // Set to true to add CASCADE to the TRUNCATE statement + Cascade bool + // Set to true to add RESTRICT to the TRUNCATE statement + Restrict bool + // Set to true to specify IDENTITY options, (e.g. RESTART, CONTINUE) to the TRUNCATE statement + Identity string +} diff --git a/exp/truncate_clauses.go b/exp/truncate_clauses.go new file mode 100644 index 0000000..2b3bd8b --- /dev/null +++ b/exp/truncate_clauses.go @@ -0,0 +1,50 @@ +package exp + +type ( + TruncateClauses interface { + HasTable() bool + clone() *truncateClauses + + Table() ColumnListExpression + SetTable(tables ColumnListExpression) TruncateClauses + + Options() TruncateOptions + SetOptions(opts TruncateOptions) TruncateClauses + } + truncateClauses struct { + tables ColumnListExpression + options TruncateOptions + } +) + +func NewTruncateClauses() TruncateClauses { + return &truncateClauses{} +} + +func (tc *truncateClauses) HasTable() bool { + return tc.tables != nil +} + +func (tc *truncateClauses) clone() *truncateClauses { + return &truncateClauses{ + tables: tc.tables, + } +} + +func (tc *truncateClauses) Table() ColumnListExpression { + return tc.tables +} +func (tc *truncateClauses) SetTable(tables ColumnListExpression) TruncateClauses { + ret := tc.clone() + ret.tables = tables + return ret +} + +func (tc *truncateClauses) Options() TruncateOptions { + return tc.options +} +func (tc *truncateClauses) SetOptions(opts TruncateOptions) TruncateClauses { + ret := tc.clone() + ret.options = opts + return ret +} diff --git a/exp/truncate_clauses_test.go b/exp/truncate_clauses_test.go new file mode 100644 index 0000000..13bdc4e --- /dev/null +++ b/exp/truncate_clauses_test.go @@ -0,0 +1,68 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type truncateClausesSuite struct { + suite.Suite +} + +func TestTruncateClausesSuite(t *testing.T) { + suite.Run(t, new(truncateClausesSuite)) +} + +func (tcs *truncateClausesSuite) TestHasTable() { + c := exp.NewTruncateClauses() + cle := exp.NewColumnListExpression("test1", "test2") + c2 := c.SetTable(cle) + + tcs.False(c.HasTable()) + + tcs.True(c2.HasTable()) +} + +func (tcs *truncateClausesSuite) TestTable() { + c := exp.NewTruncateClauses() + cle := exp.NewColumnListExpression("test1", "test2") + c2 := c.SetTable(cle) + + tcs.Nil(c.Table()) + + tcs.Equal(cle, c2.Table()) +} + +func (tcs *truncateClausesSuite) TestSetTable() { + cle := exp.NewColumnListExpression("test1", "test2") + c := exp.NewTruncateClauses().SetTable(cle) + cle2 := exp.NewColumnListExpression("test3", "test4") + c2 := c.SetTable(cle2) + + tcs.Equal(cle, c.Table()) + + tcs.Equal(cle2, c2.Table()) +} + +func (tcs *truncateClausesSuite) TestOptions() { + c := exp.NewTruncateClauses() + opts := exp.TruncateOptions{Restrict: true, Identity: "RESTART", Cascade: true} + c2 := c.SetOptions(opts) + + tcs.Equal(exp.TruncateOptions{}, c.Options()) + + tcs.Equal(opts, c2.Options()) +} + +func (tcs *truncateClausesSuite) TestSetOptions() { + opts := exp.TruncateOptions{Restrict: true, Identity: "RESTART", Cascade: true} + c := exp.NewTruncateClauses().SetOptions(opts) + opts2 := exp.TruncateOptions{Restrict: false, Identity: "RESTART", Cascade: false} + c2 := c.SetOptions(opts2) + + tcs.Equal(opts, c.Options()) + + tcs.Equal(opts2, c2.Options()) +} diff --git a/exp/update.go b/exp/update.go new file mode 100644 index 0000000..11b7ee8 --- /dev/null +++ b/exp/update.go @@ -0,0 +1,73 @@ +package exp + +import ( + "reflect" + "sort" + + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/util" +) + +type ( + update struct { + col IdentifierExpression + val interface{} + } +) + +func set(col IdentifierExpression, val interface{}) UpdateExpression { + return update{col: col, val: val} +} + +func NewUpdateExpressions(update interface{}) (updates []UpdateExpression, err error) { + if us, ok := update.([]UpdateExpression); ok { + updates = append(updates, us...) + return updates, nil + } + if u, ok := update.(UpdateExpression); ok { + updates = append(updates, u) + return updates, nil + } + updateValue := reflect.Indirect(reflect.ValueOf(update)) + switch updateValue.Kind() { + case reflect.Map: + keys := util.ValueSlice(updateValue.MapKeys()) + sort.Sort(keys) + for _, key := range keys { + updates = append(updates, ParseIdentifier(key.String()).Set(updateValue.MapIndex(key).Interface())) + } + case reflect.Struct: + return getUpdateExpressionsStruct(updateValue) + default: + return nil, errors.New("unsupported update interface type %+v", updateValue.Type()) + } + return updates, nil +} + +func getUpdateExpressionsStruct(value reflect.Value) (updates []UpdateExpression, err error) { + r, err := NewRecordFromStruct(value.Interface(), false, true) + if err != nil { + return updates, err + } + cols := r.Cols() + for _, col := range cols { + updates = append(updates, ParseIdentifier(col).Set(r[col])) + } + return updates, nil +} + +func (u update) Expression() Expression { + return u +} + +func (u update) Clone() Expression { + return update{col: u.col.Clone().(IdentifierExpression), val: u.val} +} + +func (u update) Col() IdentifierExpression { + return u.col +} + +func (u update) Val() interface{} { + return u.val +} diff --git a/exp/update_clauses.go b/exp/update_clauses.go new file mode 100644 index 0000000..562ca8d --- /dev/null +++ b/exp/update_clauses.go @@ -0,0 +1,216 @@ +package exp + +type ( + UpdateClauses interface { + HasTable() bool + clone() *updateClauses + + CommonTables() []CommonTableExpression + CommonTablesAppend(cte CommonTableExpression) UpdateClauses + + Table() Expression + SetTable(table Expression) UpdateClauses + + SetValues() interface{} + HasSetValues() bool + SetSetValues(values interface{}) UpdateClauses + + From() ColumnListExpression + HasFrom() bool + SetFrom(tables ColumnListExpression) UpdateClauses + + Where() ExpressionList + ClearWhere() UpdateClauses + WhereAppend(expressions ...Expression) UpdateClauses + + Order() ColumnListExpression + HasOrder() bool + ClearOrder() UpdateClauses + SetOrder(oes ...OrderedExpression) UpdateClauses + OrderAppend(...OrderedExpression) UpdateClauses + OrderPrepend(...OrderedExpression) UpdateClauses + + Limit() interface{} + HasLimit() bool + ClearLimit() UpdateClauses + SetLimit(limit interface{}) UpdateClauses + + Returning() ColumnListExpression + HasReturning() bool + SetReturning(cl ColumnListExpression) UpdateClauses + } + updateClauses struct { + commonTables []CommonTableExpression + table Expression + setValues interface{} + from ColumnListExpression + where ExpressionList + order ColumnListExpression + limit interface{} + returning ColumnListExpression + } +) + +func NewUpdateClauses() UpdateClauses { + return &updateClauses{} +} + +func (uc *updateClauses) HasTable() bool { + return uc.table != nil +} + +func (uc *updateClauses) clone() *updateClauses { + return &updateClauses{ + commonTables: uc.commonTables, + table: uc.table, + setValues: uc.setValues, + from: uc.from, + where: uc.where, + order: uc.order, + limit: uc.limit, + returning: uc.returning, + } +} + +func (uc *updateClauses) CommonTables() []CommonTableExpression { + return uc.commonTables +} + +func (uc *updateClauses) CommonTablesAppend(cte CommonTableExpression) UpdateClauses { + ret := uc.clone() + ret.commonTables = append(ret.commonTables, cte) + return ret +} + +func (uc *updateClauses) Table() Expression { + return uc.table +} + +func (uc *updateClauses) SetTable(table Expression) UpdateClauses { + ret := uc.clone() + ret.table = table + return ret +} + +func (uc *updateClauses) SetValues() interface{} { + return uc.setValues +} + +func (uc *updateClauses) HasSetValues() bool { + return uc.setValues != nil +} + +func (uc *updateClauses) SetSetValues(values interface{}) UpdateClauses { + ret := uc.clone() + ret.setValues = values + return ret +} + +func (uc *updateClauses) From() ColumnListExpression { + return uc.from +} + +func (uc *updateClauses) HasFrom() bool { + return uc.from != nil && !uc.from.IsEmpty() +} + +func (uc *updateClauses) SetFrom(from ColumnListExpression) UpdateClauses { + ret := uc.clone() + ret.from = from + return ret +} + +func (uc *updateClauses) Where() ExpressionList { + return uc.where +} + +func (uc *updateClauses) ClearWhere() UpdateClauses { + ret := uc.clone() + ret.where = nil + return ret +} + +func (uc *updateClauses) WhereAppend(expressions ...Expression) UpdateClauses { + if len(expressions) == 0 { + return uc + } + ret := uc.clone() + if ret.where == nil { + ret.where = NewExpressionList(AndType, expressions...) + } else { + ret.where = ret.where.Append(expressions...) + } + return ret +} + +func (uc *updateClauses) Order() ColumnListExpression { + return uc.order +} + +func (uc *updateClauses) HasOrder() bool { + return uc.order != nil +} + +func (uc *updateClauses) ClearOrder() UpdateClauses { + ret := uc.clone() + ret.order = nil + return ret +} + +func (uc *updateClauses) SetOrder(oes ...OrderedExpression) UpdateClauses { + ret := uc.clone() + ret.order = NewOrderedColumnList(oes...) + return ret +} + +func (uc *updateClauses) OrderAppend(oes ...OrderedExpression) UpdateClauses { + if uc.order == nil { + return uc.SetOrder(oes...) + } + ret := uc.clone() + ret.order = ret.order.Append(NewOrderedColumnList(oes...).Columns()...) + return ret +} + +func (uc *updateClauses) OrderPrepend(oes ...OrderedExpression) UpdateClauses { + if uc.order == nil { + return uc.SetOrder(oes...) + } + ret := uc.clone() + ret.order = NewOrderedColumnList(oes...).Append(ret.order.Columns()...) + return ret +} + +func (uc *updateClauses) Limit() interface{} { + return uc.limit +} + +func (uc *updateClauses) HasLimit() bool { + return uc.limit != nil +} + +func (uc *updateClauses) ClearLimit() UpdateClauses { + ret := uc.clone() + ret.limit = nil + return ret +} + +func (uc *updateClauses) SetLimit(limit interface{}) UpdateClauses { + ret := uc.clone() + ret.limit = limit + return ret +} + +func (uc *updateClauses) Returning() ColumnListExpression { + return uc.returning +} + +func (uc *updateClauses) HasReturning() bool { + return uc.returning != nil && !uc.returning.IsEmpty() +} + +func (uc *updateClauses) SetReturning(cl ColumnListExpression) UpdateClauses { + ret := uc.clone() + ret.returning = cl + return ret +} diff --git a/exp/update_clauses_test.go b/exp/update_clauses_test.go new file mode 100644 index 0000000..c032d96 --- /dev/null +++ b/exp/update_clauses_test.go @@ -0,0 +1,298 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type updateClausesSuite struct { + suite.Suite +} + +func TestUpdateClausesSuite(t *testing.T) { + suite.Run(t, new(updateClausesSuite)) +} + +func (ucs *updateClausesSuite) TestHasTable() { + c := exp.NewUpdateClauses() + c2 := c.SetTable(exp.NewIdentifierExpression("", "test", "")) + + ucs.False(c.HasTable()) + + ucs.True(c2.HasTable()) +} + +func (ucs *updateClausesSuite) TestTable() { + c := exp.NewUpdateClauses() + ti := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetTable(ti) + + ucs.Nil(c.Table()) + + ucs.Equal(ti, c2.Table()) +} + +func (ucs *updateClausesSuite) TestSetTable() { + c := exp.NewUpdateClauses() + ti := exp.NewIdentifierExpression("", "a", "") + c2 := c.SetTable(ti) + + ucs.Nil(c.Table()) + + ucs.Equal(ti, c2.Table()) +} + +func (ucs *updateClausesSuite) TestSetValues() { + c := exp.NewUpdateClauses() + r := exp.Record{"a": "a1", "b": "b1"} + c2 := c.SetSetValues(r) + + ucs.Nil(c.SetValues()) + + ucs.Equal(r, c2.SetValues()) +} + +func (ucs *updateClausesSuite) TestSetSetValues() { + r := exp.Record{"a": "a1", "b": "b1"} + c := exp.NewUpdateClauses().SetSetValues(r) + r2 := exp.Record{"a": "a2", "b": "b2"} + c2 := c.SetSetValues(r2) + + ucs.Equal(r, c.SetValues()) + + ucs.Equal(r2, c2.SetValues()) +} + +func (ucs *updateClausesSuite) TestFrom() { + c := exp.NewUpdateClauses() + ce := exp.NewColumnListExpression("a", "b") + c2 := c.SetFrom(ce) + + ucs.Nil(c.From()) + + ucs.Equal(ce, c2.From()) +} + +func (ucs *updateClausesSuite) TestSetFrom() { + ce1 := exp.NewColumnListExpression("a", "b") + c := exp.NewUpdateClauses().SetFrom(ce1) + ce2 := exp.NewColumnListExpression("a", "b") + c2 := c.SetFrom(ce2) + + ucs.Equal(ce1, c.From()) + + ucs.Equal(ce2, c2.From()) +} + +func (ucs *updateClausesSuite) TestWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewUpdateClauses() + c2 := c.WhereAppend(w) + + ucs.Nil(c.Where()) + + ucs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) +} + +func (ucs *updateClausesSuite) TestClearWhere() { + w := exp.Ex{"a": 1} + + c := exp.NewUpdateClauses().WhereAppend(w) + c2 := c.ClearWhere() + + ucs.Equal(exp.NewExpressionList(exp.AndType, w), c.Where()) + + ucs.Nil(c2.Where()) +} + +func (ucs *updateClausesSuite) TestWhereAppend() { + w := exp.Ex{"a": 1} + w2 := exp.Ex{"b": 2} + + c := exp.NewUpdateClauses() + c2 := c.WhereAppend(w) + + c3 := c.WhereAppend(w).WhereAppend(w2) + + c4 := c.WhereAppend(w, w2) + + ucs.Nil(c.Where()) + + ucs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Where()) + ucs.Equal(exp.NewExpressionList(exp.AndType, w).Append(w2), c3.Where()) + ucs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Where()) +} + +func (ucs *updateClausesSuite) TestOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewUpdateClauses() + c2 := c.SetOrder(oe) + + ucs.Nil(c.Order()) + + ucs.Equal(exp.NewColumnListExpression(oe), c2.Order()) +} + +func (ucs *updateClausesSuite) TestHasOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewUpdateClauses() + c2 := c.SetOrder(oe) + + ucs.False(c.HasOrder()) + + ucs.True(c2.HasOrder()) +} + +func (ucs *updateClausesSuite) TestClearOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + + c := exp.NewUpdateClauses().SetOrder(oe) + c2 := c.ClearOrder() + + ucs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + ucs.Nil(c2.Order()) +} + +func (ucs *updateClausesSuite) TestSetOrder() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewUpdateClauses().SetOrder(oe) + c2 := c.SetOrder(oe2) + + ucs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + ucs.Equal(exp.NewColumnListExpression(oe2), c2.Order()) +} + +func (ucs *updateClausesSuite) TestOrderAppend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewUpdateClauses().SetOrder(oe) + c2 := c.OrderAppend(oe2) + + ucs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + ucs.Equal(exp.NewColumnListExpression(oe, oe2), c2.Order()) +} + +func (ucs *updateClausesSuite) TestOrderPrepend() { + oe := exp.NewIdentifierExpression("", "", "a").Desc() + oe2 := exp.NewIdentifierExpression("", "", "b").Desc() + + c := exp.NewUpdateClauses().SetOrder(oe) + c2 := c.OrderPrepend(oe2) + + ucs.Equal(exp.NewColumnListExpression(oe), c.Order()) + + ucs.Equal(exp.NewColumnListExpression(oe2, oe), c2.Order()) +} + +func (ucs *updateClausesSuite) TestLimit() { + l := 1 + + c := exp.NewUpdateClauses() + c2 := c.SetLimit(l) + + ucs.Nil(c.Limit()) + + ucs.Equal(l, c2.Limit()) +} + +func (ucs *updateClausesSuite) TestHasLimit() { + l := 1 + + c := exp.NewUpdateClauses() + c2 := c.SetLimit(l) + + ucs.False(c.HasLimit()) + + ucs.True(c2.HasLimit()) +} + +func (ucs *updateClausesSuite) TestCLearLimit() { + l := 1 + + c := exp.NewUpdateClauses().SetLimit(l) + c2 := c.ClearLimit() + + ucs.True(c.HasLimit()) + + ucs.False(c2.HasLimit()) +} + +func (ucs *updateClausesSuite) TestSetLimit() { + l := 1 + l2 := 2 + + c := exp.NewUpdateClauses().SetLimit(l) + c2 := c.SetLimit(2) + + ucs.Equal(l, c.Limit()) + + ucs.Equal(l2, c2.Limit()) +} + +func (ucs *updateClausesSuite) TestCommonTables() { + cte := exp.NewCommonTableExpression(true, "test", newTestAppendableExpression(`SELECT * FROM "foo"`, []interface{}{})) + + c := exp.NewUpdateClauses() + c2 := c.CommonTablesAppend(cte) + + ucs.Nil(c.CommonTables()) + + ucs.Equal([]exp.CommonTableExpression{cte}, c2.CommonTables()) +} + +func (ucs *updateClausesSuite) TestAddCommonTablesAppend() { + cte := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte")) + cte2 := exp.NewCommonTableExpression(true, "test", testSQLExpression("test_cte2")) + + c := exp.NewUpdateClauses().CommonTablesAppend(cte) + c2 := c.CommonTablesAppend(cte2) + + ucs.Equal([]exp.CommonTableExpression{cte}, c.CommonTables()) + + ucs.Equal([]exp.CommonTableExpression{cte, cte2}, c2.CommonTables()) +} + +func (ucs *updateClausesSuite) TestReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewUpdateClauses() + c2 := c.SetReturning(cl) + + ucs.Nil(c.Returning()) + + ucs.Equal(cl, c2.Returning()) +} + +func (ucs *updateClausesSuite) TestHasReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + + c := exp.NewUpdateClauses() + c2 := c.SetReturning(cl) + + ucs.False(c.HasReturning()) + + ucs.True(c2.HasReturning()) +} + +func (ucs *updateClausesSuite) TestSetReturning() { + cl := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col")) + cl2 := exp.NewColumnListExpression(exp.NewIdentifierExpression("", "", "col2")) + + c := exp.NewUpdateClauses().SetReturning(cl) + c2 := c.SetReturning(cl2) + + ucs.Equal(cl, c.Returning()) + + ucs.Equal(cl2, c2.Returning()) +} diff --git a/exp/update_test.go b/exp/update_test.go new file mode 100644 index 0000000..8aa9eb9 --- /dev/null +++ b/exp/update_test.go @@ -0,0 +1,182 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type updateExpressionTestSuite struct { + suite.Suite +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withInvalidValue() { + _, err := exp.NewUpdateExpressions(true) + uets.EqualError(err, "db: unsupported update interface type bool") +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withRecords() { + ie, err := exp.NewUpdateExpressions(exp.Record{"c": "a", "b": "d"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "b").Set("d"), + exp.NewIdentifierExpression("", "", "c").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withMap() { + ie, err := exp.NewUpdateExpressions(map[string]interface{}{"c": "a", "b": "d"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "b").Set("d"), + exp.NewIdentifierExpression("", "", "c").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructs() { + type testRecord struct { + C string `db:"c"` + B string `db:"b"` + } + ie, err := exp.NewUpdateExpressions(testRecord{C: "a", B: "d"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "b").Set("d"), + exp.NewIdentifierExpression("", "", "c").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithoutTags() { + type testRecord struct { + FieldA int64 + FieldB bool + FieldC string + } + ie, err := exp.NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "fielda").Set(int64(1)), + exp.NewIdentifierExpression("", "", "fieldb").Set(true), + exp.NewIdentifierExpression("", "", "fieldc").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsIgnoredDbTag() { + type testRecord struct { + FieldA int64 `db:"-"` + FieldB bool + FieldC string + } + ie, err := exp.NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "fieldb").Set(true), + exp.NewIdentifierExpression("", "", "fieldc").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithDbv2SkipUpdate() { + type testRecord struct { + FieldA int64 + FieldB bool `ff:"skipupdate"` + FieldC string `ff:"skipinsert"` + } + ie, err := exp.NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "fielda").Set(int64(1)), + exp.NewIdentifierExpression("", "", "fieldc").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructPointers() { + type testRecord struct { + C string `db:"c"` + B string `db:"b"` + } + ie, err := exp.NewUpdateExpressions(&testRecord{C: "a", B: "d"}) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "b").Set("d"), + exp.NewIdentifierExpression("", "", "c").Set("a"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithEmbeddedStructs() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1", Phone: Phone{Home: "123123", Primary: "456456"}}, + ) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "address").Set("111 Test Addr"), + exp.NewIdentifierExpression("", "", "home_phone").Set("123123"), + exp.NewIdentifierExpression("", "", "name").Set("Test1"), + exp.NewIdentifierExpression("", "", "primary_phone").Set("456456"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1", Phone: &Phone{Home: "123123", Primary: "456456"}}, + ) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "address").Set("111 Test Addr"), + exp.NewIdentifierExpression("", "", "home_phone").Set("123123"), + exp.NewIdentifierExpression("", "", "name").Set("Test1"), + exp.NewIdentifierExpression("", "", "primary_phone").Set("456456"), + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withNilEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := exp.NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1"}, + ) + uets.NoError(err) + eie := []exp.UpdateExpression{ + exp.NewIdentifierExpression("", "", "address").Set("111 Test Addr"), + exp.NewIdentifierExpression("", "", "name").Set("Test1"), + } + uets.Equal(eie, ie) +} + +func TestUpdateExpressionSuite(t *testing.T) { + suite.Run(t, new(updateExpressionTestSuite)) +} diff --git a/exp/window.go b/exp/window.go new file mode 100644 index 0000000..a33178a --- /dev/null +++ b/exp/window.go @@ -0,0 +1,90 @@ +package exp + +type sqlWindowExpression struct { + name IdentifierExpression + parent IdentifierExpression + partitionCols ColumnListExpression + orderCols ColumnListExpression +} + +func NewWindowExpression(window, parent IdentifierExpression, partitionCols, orderCols ColumnListExpression) WindowExpression { + if partitionCols == nil { + partitionCols = NewColumnListExpression() + } + if orderCols == nil { + orderCols = NewColumnListExpression() + } + return sqlWindowExpression{ + name: window, + parent: parent, + partitionCols: partitionCols, + orderCols: orderCols, + } +} + +func (we sqlWindowExpression) clone() sqlWindowExpression { + return sqlWindowExpression{ + name: we.name, + parent: we.parent, + partitionCols: we.partitionCols.Clone().(ColumnListExpression), + orderCols: we.orderCols.Clone().(ColumnListExpression), + } +} + +func (we sqlWindowExpression) Clone() Expression { + return we.clone() +} + +func (we sqlWindowExpression) Expression() Expression { + return we +} + +func (we sqlWindowExpression) Name() IdentifierExpression { + return we.name +} + +func (we sqlWindowExpression) HasName() bool { + return we.name != nil +} + +func (we sqlWindowExpression) Parent() IdentifierExpression { + return we.parent +} + +func (we sqlWindowExpression) HasParent() bool { + return we.parent != nil +} + +func (we sqlWindowExpression) PartitionCols() ColumnListExpression { + return we.partitionCols +} + +func (we sqlWindowExpression) HasPartitionBy() bool { + return we.partitionCols != nil && !we.partitionCols.IsEmpty() +} + +func (we sqlWindowExpression) OrderCols() ColumnListExpression { + return we.orderCols +} + +func (we sqlWindowExpression) HasOrder() bool { + return we.orderCols != nil && !we.orderCols.IsEmpty() +} + +func (we sqlWindowExpression) PartitionBy(cols ...interface{}) WindowExpression { + ret := we.clone() + ret.partitionCols = NewColumnListExpression(cols...) + return ret +} + +func (we sqlWindowExpression) OrderBy(cols ...interface{}) WindowExpression { + ret := we.clone() + ret.orderCols = NewColumnListExpression(cols...) + return ret +} + +func (we sqlWindowExpression) Inherit(parent string) WindowExpression { + ret := we.clone() + ret.parent = ParseIdentifier(parent) + return ret +} diff --git a/exp/window_func.go b/exp/window_func.go new file mode 100644 index 0000000..d5ae553 --- /dev/null +++ b/exp/window_func.go @@ -0,0 +1,124 @@ +package exp + +type sqlWindowFunctionExpression struct { + fn SQLFunctionExpression + windowName IdentifierExpression + window WindowExpression +} + +func NewSQLWindowFunctionExpression( + fn SQLFunctionExpression, + windowName IdentifierExpression, + window WindowExpression) SQLWindowFunctionExpression { + return sqlWindowFunctionExpression{ + fn: fn, + windowName: windowName, + window: window, + } +} + +func (swfe sqlWindowFunctionExpression) clone() sqlWindowFunctionExpression { + return sqlWindowFunctionExpression{ + fn: swfe.fn.Clone().(SQLFunctionExpression), + windowName: swfe.windowName, + window: swfe.window, + } +} + +func (swfe sqlWindowFunctionExpression) Clone() Expression { + return swfe.clone() +} + +func (swfe sqlWindowFunctionExpression) Expression() Expression { + return swfe +} + +func (swfe sqlWindowFunctionExpression) As(val interface{}) AliasedExpression { + return NewAliasExpression(swfe, val) +} +func (swfe sqlWindowFunctionExpression) Eq(val interface{}) BooleanExpression { return eq(swfe, val) } +func (swfe sqlWindowFunctionExpression) Neq(val interface{}) BooleanExpression { return neq(swfe, val) } +func (swfe sqlWindowFunctionExpression) Gt(val interface{}) BooleanExpression { return gt(swfe, val) } +func (swfe sqlWindowFunctionExpression) Gte(val interface{}) BooleanExpression { return gte(swfe, val) } +func (swfe sqlWindowFunctionExpression) Lt(val interface{}) BooleanExpression { return lt(swfe, val) } +func (swfe sqlWindowFunctionExpression) Lte(val interface{}) BooleanExpression { return lte(swfe, val) } +func (swfe sqlWindowFunctionExpression) Between(val RangeVal) RangeExpression { + return between(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) NotBetween(val RangeVal) RangeExpression { + return notBetween(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) Like(val interface{}) BooleanExpression { + return like(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) NotLike(val interface{}) BooleanExpression { + return notLike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) ILike(val interface{}) BooleanExpression { + return iLike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) NotILike(val interface{}) BooleanExpression { + return notILike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) RegexpLike(val interface{}) BooleanExpression { + return regexpLike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) RegexpNotLike(val interface{}) BooleanExpression { + return regexpNotLike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) RegexpILike(val interface{}) BooleanExpression { + return regexpILike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) RegexpNotILike(val interface{}) BooleanExpression { + return regexpNotILike(swfe, val) +} + +func (swfe sqlWindowFunctionExpression) In(vals ...interface{}) BooleanExpression { + return in(swfe, vals...) +} + +func (swfe sqlWindowFunctionExpression) NotIn(vals ...interface{}) BooleanExpression { + return notIn(swfe, vals...) +} +func (swfe sqlWindowFunctionExpression) Is(val interface{}) BooleanExpression { return is(swfe, val) } +func (swfe sqlWindowFunctionExpression) IsNot(val interface{}) BooleanExpression { + return isNot(swfe, val) +} +func (swfe sqlWindowFunctionExpression) IsNull() BooleanExpression { return is(swfe, nil) } +func (swfe sqlWindowFunctionExpression) IsNotNull() BooleanExpression { return isNot(swfe, nil) } +func (swfe sqlWindowFunctionExpression) IsTrue() BooleanExpression { return is(swfe, true) } +func (swfe sqlWindowFunctionExpression) IsNotTrue() BooleanExpression { return isNot(swfe, true) } +func (swfe sqlWindowFunctionExpression) IsFalse() BooleanExpression { return is(swfe, false) } +func (swfe sqlWindowFunctionExpression) IsNotFalse() BooleanExpression { return isNot(swfe, false) } + +func (swfe sqlWindowFunctionExpression) Asc() OrderedExpression { return asc(swfe) } +func (swfe sqlWindowFunctionExpression) Desc() OrderedExpression { return desc(swfe) } + +func (swfe sqlWindowFunctionExpression) Func() SQLFunctionExpression { + return swfe.fn +} + +func (swfe sqlWindowFunctionExpression) Window() WindowExpression { + return swfe.window +} + +func (swfe sqlWindowFunctionExpression) WindowName() IdentifierExpression { + return swfe.windowName +} + +func (swfe sqlWindowFunctionExpression) HasWindow() bool { + return swfe.window != nil +} + +func (swfe sqlWindowFunctionExpression) HasWindowName() bool { + return swfe.windowName != nil +} diff --git a/exp/window_func_test.go b/exp/window_func_test.go new file mode 100644 index 0000000..914def2 --- /dev/null +++ b/exp/window_func_test.go @@ -0,0 +1,107 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type sqlWindowFunctionExpressionTest struct { + suite.Suite + fn exp.SQLFunctionExpression +} + +func TestSQLWindowFunctionExpressionSuite(t *testing.T) { + suite.Run(t, &sqlWindowFunctionExpressionTest{ + fn: exp.NewSQLFunctionExpression("COUNT", exp.Star()), + }) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestClone() { + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, exp.NewIdentifierExpression("", "", "a"), nil) + wf2 := wf.Clone() + swfet.Equal(wf, wf2) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestExpression() { + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, exp.NewIdentifierExpression("", "", "a"), nil) + wf2 := wf.Expression() + swfet.Equal(wf, wf2) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestFunc() { + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, exp.NewIdentifierExpression("", "", "a"), nil) + swfet.Equal(swfet.fn, wf.Func()) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestWindow() { + w := exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w"), + nil, + nil, + nil, + ) + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, exp.NewIdentifierExpression("", "", "a"), nil) + swfet.False(wf.HasWindow()) + + wf = swfet.fn.Over(w) + swfet.True(wf.HasWindow()) + swfet.Equal(wf.Window(), w) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestWindowName() { + windowName := exp.NewIdentifierExpression("", "", "a") + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, nil, nil) + swfet.False(wf.HasWindowName()) + + wf = swfet.fn.OverName(windowName) + swfet.True(wf.HasWindowName()) + swfet.Equal(wf.WindowName(), windowName) +} + +func (swfet *sqlWindowFunctionExpressionTest) TestAllOthers() { + wf := exp.NewSQLWindowFunctionExpression(swfet.fn, nil, nil) + + rv := exp.NewRangeVal(1, 2) + pattern := "a%" + inVals := []interface{}{1, 2} + testCases := []struct { + Ex exp.Expression + Expected exp.Expression + }{ + {Ex: wf.As("a"), Expected: exp.NewAliasExpression(wf, "a")}, + {Ex: wf.Eq(1), Expected: exp.NewBooleanExpression(exp.EqOp, wf, 1)}, + {Ex: wf.Neq(1), Expected: exp.NewBooleanExpression(exp.NeqOp, wf, 1)}, + {Ex: wf.Gt(1), Expected: exp.NewBooleanExpression(exp.GtOp, wf, 1)}, + {Ex: wf.Gte(1), Expected: exp.NewBooleanExpression(exp.GteOp, wf, 1)}, + {Ex: wf.Lt(1), Expected: exp.NewBooleanExpression(exp.LtOp, wf, 1)}, + {Ex: wf.Lte(1), Expected: exp.NewBooleanExpression(exp.LteOp, wf, 1)}, + {Ex: wf.Between(rv), Expected: exp.NewRangeExpression(exp.BetweenOp, wf, rv)}, + {Ex: wf.NotBetween(rv), Expected: exp.NewRangeExpression(exp.NotBetweenOp, wf, rv)}, + {Ex: wf.Like(pattern), Expected: exp.NewBooleanExpression(exp.LikeOp, wf, pattern)}, + {Ex: wf.NotLike(pattern), Expected: exp.NewBooleanExpression(exp.NotLikeOp, wf, pattern)}, + {Ex: wf.ILike(pattern), Expected: exp.NewBooleanExpression(exp.ILikeOp, wf, pattern)}, + {Ex: wf.NotILike(pattern), Expected: exp.NewBooleanExpression(exp.NotILikeOp, wf, pattern)}, + {Ex: wf.RegexpLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpLikeOp, wf, pattern)}, + {Ex: wf.RegexpNotLike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotLikeOp, wf, pattern)}, + {Ex: wf.RegexpILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpILikeOp, wf, pattern)}, + {Ex: wf.RegexpNotILike(pattern), Expected: exp.NewBooleanExpression(exp.RegexpNotILikeOp, wf, pattern)}, + {Ex: wf.In(inVals), Expected: exp.NewBooleanExpression(exp.InOp, wf, inVals)}, + {Ex: wf.NotIn(inVals), Expected: exp.NewBooleanExpression(exp.NotInOp, wf, inVals)}, + {Ex: wf.Is(true), Expected: exp.NewBooleanExpression(exp.IsOp, wf, true)}, + {Ex: wf.IsNot(true), Expected: exp.NewBooleanExpression(exp.IsNotOp, wf, true)}, + {Ex: wf.IsNull(), Expected: exp.NewBooleanExpression(exp.IsOp, wf, nil)}, + {Ex: wf.IsNotNull(), Expected: exp.NewBooleanExpression(exp.IsNotOp, wf, nil)}, + {Ex: wf.IsTrue(), Expected: exp.NewBooleanExpression(exp.IsOp, wf, true)}, + {Ex: wf.IsNotTrue(), Expected: exp.NewBooleanExpression(exp.IsNotOp, wf, true)}, + {Ex: wf.IsFalse(), Expected: exp.NewBooleanExpression(exp.IsOp, wf, false)}, + {Ex: wf.IsNotFalse(), Expected: exp.NewBooleanExpression(exp.IsNotOp, wf, false)}, + {Ex: wf.Desc(), Expected: exp.NewOrderedExpression(wf, exp.DescSortDir, exp.NoNullsSortType)}, + {Ex: wf.Asc(), Expected: exp.NewOrderedExpression(wf, exp.AscDir, exp.NoNullsSortType)}, + } + + for _, tc := range testCases { + swfet.Equal(tc.Expected, tc.Ex) + } +} diff --git a/exp/window_test.go b/exp/window_test.go new file mode 100644 index 0000000..ac5eb71 --- /dev/null +++ b/exp/window_test.go @@ -0,0 +1,84 @@ +package exp_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type windowExpressionTest struct { + suite.Suite +} + +func TestWindowExpressionSuite(t *testing.T) { + suite.Run(t, new(windowExpressionTest)) +} + +func (wet *windowExpressionTest) TestClone() { + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + w2 := w.Clone() + + wet.Equal(w, w2) +} + +func (wet *windowExpressionTest) TestExpression() { + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + w2 := w.Expression() + + wet.Equal(w, w2) +} + +func (wet *windowExpressionTest) TestName() { + name := exp.NewIdentifierExpression("", "", "w") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) + + wet.Equal(name, w.Name()) +} + +func (wet *windowExpressionTest) TestPartitionCols() { + cols := exp.NewColumnListExpression("a", "b") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, cols, nil) + + wet.Equal(cols, w.PartitionCols()) + wet.Equal(cols, w.Clone().(exp.WindowExpression).PartitionCols()) +} + +func (wet *windowExpressionTest) TestOrderCols() { + cols := exp.NewColumnListExpression("a", "b") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, cols) + + wet.Equal(cols, w.OrderCols()) + wet.Equal(cols, w.Clone().(exp.WindowExpression).OrderCols()) +} + +func (wet *windowExpressionTest) TestPartitionBy() { + cols := exp.NewColumnListExpression("a", "b") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil).PartitionBy("a", "b") + + wet.Equal(cols, w.PartitionCols()) +} + +func (wet *windowExpressionTest) TestOrderBy() { + cols := exp.NewColumnListExpression("a", "b") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil).OrderBy("a", "b") + + wet.Equal(cols, w.OrderCols()) +} + +func (wet *windowExpressionTest) TestParent() { + parent := exp.NewIdentifierExpression("", "", "w1") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), parent, nil, nil) + + wet.Equal(parent, w.Parent()) +} + +func (wet *windowExpressionTest) TestInherit() { + parent := exp.NewIdentifierExpression("", "", "w1") + w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), parent, nil, nil) + + wet.Equal(parent, w.Parent()) + + w = w.Inherit("w2") + wet.Equal(exp.NewIdentifierExpression("", "", "w2"), w.Parent()) +} diff --git a/expressions.go b/expressions.go new file mode 100644 index 0000000..9edfd7b --- /dev/null +++ b/expressions.go @@ -0,0 +1,333 @@ +package db + +import ( + "git.fsdpf.net/go/db/v2/exp" +) + +type ( + Expression = exp.Expression + Ex = exp.Ex + ExOr = exp.ExOr + Op = exp.Op + Record = exp.Record + Vals = exp.Vals + // Options to use when generating a TRUNCATE statement + TruncateOptions = exp.TruncateOptions +) + +// emptyWindow is an empty WINDOW clause without name +var emptyWindow = exp.NewWindowExpression(nil, nil, nil, nil) + +const ( + Wait = exp.Wait + NoWait = exp.NoWait + SkipLocked = exp.SkipLocked +) + +// Creates a new Casted expression +// +// Cast(I("a"), "NUMERIC") -> CAST("a" AS NUMERIC) +func Cast(e exp.Expression, t string) exp.CastExpression { + return exp.NewCastExpression(e, t) +} + +// Creates a conflict struct to be passed to InsertConflict to ignore constraint errors +// +// InsertConflict(DoNothing(),...) -> INSERT INTO ... ON CONFLICT DO NOTHING +func DoNothing() exp.ConflictExpression { + return exp.NewDoNothingConflictExpression() +} + +// Creates a ConflictUpdate struct to be passed to InsertConflict +// Represents a ON CONFLICT DO UPDATE portion of an INSERT statement (ON DUPLICATE KEY UPDATE for mysql) +// +// InsertConflict(DoUpdate("target_column", update),...) -> +// INSERT INTO ... ON CONFLICT DO UPDATE SET a=b +// InsertConflict(DoUpdate("target_column", update).Where(Ex{"a": 1},...) -> +// INSERT INTO ... ON CONFLICT DO UPDATE SET a=b WHERE a=1 +func DoUpdate(target string, update interface{}) exp.ConflictUpdateExpression { + return exp.NewDoUpdateConflictExpression(target, update) +} + +// A list of expressions that should be ORed together +// +// Or(I("a").Eq(10), I("b").Eq(11)) //(("a" = 10) OR ("b" = 11)) +func Or(expressions ...exp.Expression) exp.ExpressionList { + return exp.NewExpressionList(exp.OrType, expressions...) +} + +// A list of expressions that should be ANDed together +// +// And(I("a").Eq(10), I("b").Eq(11)) //(("a" = 10) AND ("b" = 11)) +func And(expressions ...exp.Expression) exp.ExpressionList { + return exp.NewExpressionList(exp.AndType, expressions...) +} + +// Creates a new SQLFunctionExpression with the given name and arguments +func Func(name string, args ...interface{}) exp.SQLFunctionExpression { + return exp.NewSQLFunctionExpression(name, args...) +} + +// used internally to normalize the column name if passed in as a string it should be turned into an identifier +func newIdentifierFunc(name string, col interface{}) exp.SQLFunctionExpression { + if s, ok := col.(string); ok { + col = I(s) + } + return Func(name, col) +} + +// Creates a new DISTINCT sql function +// +// DISTINCT("a") -> DISTINCT("a") +// DISTINCT(I("a")) -> DISTINCT("a") +func DISTINCT(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("DISTINCT", col) } + +// Creates a new COUNT sql function +// +// COUNT("a") -> COUNT("a") +// COUNT("*") -> COUNT("*") +// COUNT(I("a")) -> COUNT("a") +func COUNT(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("COUNT", col) } + +// Creates a new MIN sql function +// +// MIN("a") -> MIN("a") +// MIN(I("a")) -> MIN("a") +func MIN(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("MIN", col) } + +// Creates a new MAX sql function +// +// MAX("a") -> MAX("a") +// MAX(I("a")) -> MAX("a") +func MAX(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("MAX", col) } + +// Creates a new AVG sql function +// +// AVG("a") -> AVG("a") +// AVG(I("a")) -> AVG("a") +func AVG(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("AVG", col) } + +// Creates a new FIRST sql function +// +// FIRST("a") -> FIRST("a") +// FIRST(I("a")) -> FIRST("a") +func FIRST(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("FIRST", col) } + +// Creates a new LAST sql function +// +// LAST("a") -> LAST("a") +// LAST(I("a")) -> LAST("a") +func LAST(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("LAST", col) } + +// Creates a new SUM sql function +// +// SUM("a") -> SUM("a") +// SUM(I("a")) -> SUM("a") +func SUM(col interface{}) exp.SQLFunctionExpression { return newIdentifierFunc("SUM", col) } + +// Creates a new COALESCE sql function +// +// COALESCE(I("a"), "a") -> COALESCE("a", 'a') +// COALESCE(I("a"), I("b"), nil) -> COALESCE("a", "b", NULL) +func COALESCE(vals ...interface{}) exp.SQLFunctionExpression { + return Func("COALESCE", vals...) +} + +//nolint:stylecheck,revive // sql function name +func ROW_NUMBER() exp.SQLFunctionExpression { + return Func("ROW_NUMBER") +} + +func RANK() exp.SQLFunctionExpression { + return Func("RANK") +} + +//nolint:stylecheck,revive // sql function name +func DENSE_RANK() exp.SQLFunctionExpression { + return Func("DENSE_RANK") +} + +//nolint:stylecheck,revive // sql function name +func PERCENT_RANK() exp.SQLFunctionExpression { + return Func("PERCENT_RANK") +} + +//nolint:stylecheck,revive //sql function name +func CUME_DIST() exp.SQLFunctionExpression { + return Func("CUME_DIST") +} + +func NTILE(n int) exp.SQLFunctionExpression { + return Func("NTILE", n) +} + +//nolint:stylecheck,revive //sql function name +func FIRST_VALUE(val interface{}) exp.SQLFunctionExpression { + return newIdentifierFunc("FIRST_VALUE", val) +} + +//nolint:stylecheck,revive //sql function name +func LAST_VALUE(val interface{}) exp.SQLFunctionExpression { + return newIdentifierFunc("LAST_VALUE", val) +} + +//nolint:stylecheck,revive //sql function name +func NTH_VALUE(val interface{}, nth int) exp.SQLFunctionExpression { + if s, ok := val.(string); ok { + val = I(s) + } + return Func("NTH_VALUE", val, nth) +} + +// Creates a new Identifier, the generated sql will use adapter specific quoting or '"' by default, this ensures case +// sensitivity and in certain databases allows for special characters, (e.g. "curr-table", "my table"). +// +// The identifier will be split by '.' +// +// Table and Column example +// +// I("table.column") -> "table"."column" //A Column and table +// +// Schema table and column +// +// I("schema.table.column") -> "schema"."table"."column" +// +// Table with star +// +// I("table.*") -> "table".* +func I(ident string) exp.IdentifierExpression { + return exp.ParseIdentifier(ident) +} + +// Creates a new Column Identifier, the generated sql will use adapter specific quoting or '"' by default, this ensures case +// sensitivity and in certain databases allows for special characters, (e.g. "curr-table", "my table"). +// An Identifier can represent a one or a combination of schema, table, and/or column. +// +// C("column") -> "column" //A Column +// C("column").Table("table") -> "table"."column" //A Column and table +// C("column").Table("table").Schema("schema") //Schema table and column +// C("*") //Also handles the * operator +func C(col string) exp.IdentifierExpression { + return exp.NewIdentifierExpression("", "", col) +} + +// Creates a new Schema Identifier, the generated sql will use adapter specific quoting or '"' by default, this ensures case +// sensitivity and in certain databases allows for special characters, (e.g. "curr-schema", "my schema"). +// +// S("schema") -> "schema" //A Schema +// S("schema").Table("table") -> "schema"."table" //A Schema and table +// S("schema").Table("table").Col("col") //Schema table and column +// S("schema").Table("table").Col("*") //Schema table and all columns +func S(schema string) exp.IdentifierExpression { + return exp.NewIdentifierExpression(schema, "", "") +} + +// Creates a new Table Identifier, the generated sql will use adapter specific quoting or '"' by default, this ensures case +// sensitivity and in certain databases allows for special characters, (e.g. "curr-table", "my table"). +// +// T("table") -> "table" //A Column +// T("table").Col("col") -> "table"."column" //A Column and table +// T("table").Schema("schema").Col("col) -> "schema"."table"."column" //Schema table and column +// T("table").Schema("schema").Col("*") -> "schema"."table".* //Also handles the * operator +func T(table string) exp.IdentifierExpression { + return exp.NewIdentifierExpression("", table, "") +} + +// Create a new WINDOW clause +// +// W() -> () +// W().PartitionBy("a") -> (PARTITION BY "a") +// W().PartitionBy("a").OrderBy("b") -> (PARTITION BY "a" ORDER BY "b") +// W().PartitionBy("a").OrderBy("b").Inherit("w1") -> ("w1" PARTITION BY "a" ORDER BY "b") +// W().PartitionBy("a").OrderBy(I("b").Desc()).Inherit("w1") -> ("w1" PARTITION BY "a" ORDER BY "b" DESC) +// W("w") -> "w" AS () +// W("w", "w1") -> "w" AS ("w1") +// W("w").Inherit("w1") -> "w" AS ("w1") +// W("w").PartitionBy("a") -> "w" AS (PARTITION BY "a") +// W("w", "w1").PartitionBy("a") -> "w" AS ("w1" PARTITION BY "a") +// W("w", "w1").PartitionBy("a").OrderBy("b") -> "w" AS ("w1" PARTITION BY "a" ORDER BY "b") +func W(ws ...string) exp.WindowExpression { + switch len(ws) { + case 0: + return emptyWindow + case 1: + return exp.NewWindowExpression(I(ws[0]), nil, nil, nil) + default: + return exp.NewWindowExpression(I(ws[0]), I(ws[1]), nil, nil) + } +} + +// Creates a new ON clause to be used within a join +// +// ds.Join(db.T("my_table"), db.On( +// db.I("my_table.fkey").Eq(db.I("other_table.id")), +// )) +func On(expressions ...exp.Expression) exp.JoinCondition { + return exp.NewJoinOnCondition(expressions...) +} + +// Creates a new USING clause to be used within a join +// +// ds.Join(db.T("my_table"), db.Using("fkey")) +func Using(columns ...interface{}) exp.JoinCondition { + return exp.NewJoinUsingCondition(columns...) +} + +// Creates a new SQL literal with the provided arguments. +// +// L("a = 1") -> a = 1 +// +// You can also you placeholders. All placeholders within a Literal are represented by '?' +// +// L("a = ?", "b") -> a = 'b' +// +// Literals can also contain placeholders for other expressions +// +// L("(? AND ?) OR (?)", I("a").Eq(1), I("b").Eq("b"), I("c").In([]string{"a", "b", "c"})) +func L(sql string, args ...interface{}) exp.LiteralExpression { + return Literal(sql, args...) +} + +// Alias for db.L +func Literal(sql string, args ...interface{}) exp.LiteralExpression { + return exp.NewLiteralExpression(sql, args...) +} + +// Create a new SQL value ( alias for db.L("?", val) ). The prrimary use case for this would be in selects. +// See examples. +func V(val interface{}) exp.LiteralExpression { + return exp.NewLiteralExpression("?", val) +} + +// Creates a new Range to be used with a Between expression +// +// exp.C("col").Between(exp.Range(1, 10)) +func Range(start, end interface{}) exp.RangeVal { + return exp.NewRangeVal(start, end) +} + +// Creates a literal * +func Star() exp.LiteralExpression { return exp.Star() } + +// Returns a literal for DEFAULT sql keyword +func Default() exp.LiteralExpression { + return exp.Default() +} + +func Lateral(table exp.AppendableExpression) exp.LateralExpression { + return exp.NewLateralExpression(table) +} + +// Create a new ANY comparison +func Any(val interface{}) exp.SQLFunctionExpression { + return Func("ANY ", val) +} + +// Create a new ALL comparison +func All(val interface{}) exp.SQLFunctionExpression { + return Func("ALL ", val) +} + +func Case() exp.CaseExpression { + return exp.NewCaseExpression() +} diff --git a/expressions_example_test.go b/expressions_example_test.go new file mode 100644 index 0000000..3c526e9 --- /dev/null +++ b/expressions_example_test.go @@ -0,0 +1,1902 @@ +//nolint:lll // sql statements are long +package db_test + +import ( + "fmt" + "regexp" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" +) + +func ExampleAVG() { + ds := dbv2.From("test").Select(dbv2.AVG("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT AVG("col") FROM "test" [] + // SELECT AVG("col") FROM "test" [] +} + +func ExampleAVG_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.AVG("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT AVG("a") AS "a" FROM "test" +} + +func ExampleAVG_havingClause() { + ds := dbv2. + From("test"). + Select(dbv2.AVG("a").As("avg")). + GroupBy("a"). + Having(dbv2.AVG("a").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT AVG("a") AS "avg" FROM "test" GROUP BY "a" HAVING (AVG("a") > 10) [] + // SELECT AVG("a") AS "avg" FROM "test" GROUP BY "a" HAVING (AVG("a") > ?) [10] +} + +func ExampleAnd() { + ds := dbv2.From("test").Where( + dbv2.And( + dbv2.C("col").Gt(10), + dbv2.C("col").Lt(20), + ), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("col" > 10) AND ("col" < 20)) [] + // SELECT * FROM "test" WHERE (("col" > ?) AND ("col" < ?)) [10 20] +} + +// You can use And with Or to create more complex queries +func ExampleAnd_withOr() { + ds := dbv2.From("test").Where( + dbv2.And( + dbv2.C("col1").IsTrue(), + dbv2.Or( + dbv2.C("col2").Gt(10), + dbv2.C("col2").Lt(20), + ), + ), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // by default expressions are anded together + ds = dbv2.From("test").Where( + dbv2.C("col1").IsTrue(), + dbv2.Or( + dbv2.C("col2").Gt(10), + dbv2.C("col2").Lt(20), + ), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > 10) OR ("col2" < 20))) [] + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > ?) OR ("col2" < ?))) [10 20] + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > 10) OR ("col2" < 20))) [] + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > ?) OR ("col2" < ?))) [10 20] +} + +// You can use ExOr inside of And expression lists. +func ExampleAnd_withExOr() { + // by default expressions are anded together + ds := dbv2.From("test").Where( + dbv2.C("col1").IsTrue(), + dbv2.ExOr{ + "col2": dbv2.Op{"gt": 10}, + "col3": dbv2.Op{"lt": 20}, + }, + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > 10) OR ("col3" < 20))) [] + // SELECT * FROM "test" WHERE (("col1" IS TRUE) AND (("col2" > ?) OR ("col3" < ?))) [10 20] +} + +func ExampleC() { + sql, args, _ := dbv2.From("test"). + Select(dbv2.C("*")). + ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test"). + Select(dbv2.C("col1")). + ToSQL() + fmt.Println(sql, args) + + ds := dbv2.From("test").Where( + dbv2.C("col1").Eq(10), + dbv2.C("col2").In([]int64{1, 2, 3, 4}), + dbv2.C("col3").Like(regexp.MustCompile("^[ab]")), + dbv2.C("col4").IsNull(), + ) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" [] + // SELECT "col1" FROM "test" [] + // SELECT * FROM "test" WHERE (("col1" = 10) AND ("col2" IN (1, 2, 3, 4)) AND ("col3" ~ '^[ab]') AND ("col4" IS NULL)) [] + // SELECT * FROM "test" WHERE (("col1" = ?) AND ("col2" IN (?, ?, ?, ?)) AND ("col3" ~ ?) AND ("col4" IS NULL)) [10 1 2 3 4 ^[ab]] +} + +func ExampleC_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.C("a").As("as_a")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Select(dbv2.C("a").As(dbv2.C("as_a"))).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT "a" AS "as_a" FROM "test" + // SELECT "a" AS "as_a" FROM "test" +} + +func ExampleC_ordering() { + sql, args, _ := dbv2.From("test").Order(dbv2.C("a").Asc()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Order(dbv2.C("a").Asc().NullsFirst()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Order(dbv2.C("a").Asc().NullsLast()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Order(dbv2.C("a").Desc()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Order(dbv2.C("a").Desc().NullsFirst()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Order(dbv2.C("a").Desc().NullsLast()).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" ORDER BY "a" ASC [] + // SELECT * FROM "test" ORDER BY "a" ASC NULLS FIRST [] + // SELECT * FROM "test" ORDER BY "a" ASC NULLS LAST [] + // SELECT * FROM "test" ORDER BY "a" DESC [] + // SELECT * FROM "test" ORDER BY "a" DESC NULLS FIRST [] + // SELECT * FROM "test" ORDER BY "a" DESC NULLS LAST [] +} + +func ExampleC_cast() { + sql, _, _ := dbv2.From("test"). + Select(dbv2.C("json1").Cast("TEXT").As("json_text")). + ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.C("json1").Cast("TEXT").Neq( + dbv2.C("json2").Cast("TEXT"), + ), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT CAST("json1" AS TEXT) AS "json_text" FROM "test" + // SELECT * FROM "test" WHERE (CAST("json1" AS TEXT) != CAST("json2" AS TEXT)) +} + +func ExampleC_comparisons() { + // used from an identifier + sql, _, _ := dbv2.From("test").Where(dbv2.C("a").Eq(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Neq(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Gt(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Gte(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Lt(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Lte(10)).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ("a" = 10) + // SELECT * FROM "test" WHERE ("a" != 10) + // SELECT * FROM "test" WHERE ("a" > 10) + // SELECT * FROM "test" WHERE ("a" >= 10) + // SELECT * FROM "test" WHERE ("a" < 10) + // SELECT * FROM "test" WHERE ("a" <= 10) +} + +func ExampleC_inOperators() { + // using identifiers + sql, _, _ := dbv2.From("test").Where(dbv2.C("a").In("a", "b", "c")).ToSQL() + fmt.Println(sql) + // with a slice + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").In([]string{"a", "b", "c"})).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotIn("a", "b", "c")).ToSQL() + fmt.Println(sql) + // with a slice + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotIn([]string{"a", "b", "c"})).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ("a" IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE ("a" IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE ("a" NOT IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE ("a" NOT IN ('a', 'b', 'c')) +} + +func ExampleC_likeComparisons() { + // using identifiers + sql, _, _ := dbv2.From("test").Where(dbv2.C("a").Like("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").Like(regexp.MustCompile(`[ab]`))).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").ILike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").ILike(regexp.MustCompile("[ab]"))).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotLike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotLike(regexp.MustCompile("[ab]"))).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotILike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.C("a").NotILike(regexp.MustCompile(`[ab]`))).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ("a" LIKE '%a%') + // SELECT * FROM "test" WHERE ("a" ~ '[ab]') + // SELECT * FROM "test" WHERE ("a" ILIKE '%a%') + // SELECT * FROM "test" WHERE ("a" ~* '[ab]') + // SELECT * FROM "test" WHERE ("a" NOT LIKE '%a%') + // SELECT * FROM "test" WHERE ("a" !~ '[ab]') + // SELECT * FROM "test" WHERE ("a" NOT ILIKE '%a%') + // SELECT * FROM "test" WHERE ("a" !~* '[ab]') +} + +func ExampleC_isComparisons() { + sql, args, _ := dbv2.From("test").Where(dbv2.C("a").Is(nil)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").Is(true)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").Is(false)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNull()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsTrue()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsFalse()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNot(nil)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNot(true)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNot(false)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNotNull()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNotTrue()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.C("a").IsNotFalse()).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NOT TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NOT TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT FALSE) [] +} + +func ExampleC_betweenComparisons() { + ds := dbv2.From("test").Where( + dbv2.C("a").Between(dbv2.Range(1, 10)), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where( + dbv2.C("a").NotBetween(dbv2.Range(1, 10)), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("a" BETWEEN ? AND ?) [1 10] + // SELECT * FROM "test" WHERE ("a" NOT BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("a" NOT BETWEEN ? AND ?) [1 10] +} + +func ExampleCOALESCE() { + ds := dbv2.From("test").Select( + dbv2.COALESCE(dbv2.C("a"), "a"), + dbv2.COALESCE(dbv2.C("a"), dbv2.C("b"), nil), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT COALESCE("a", 'a'), COALESCE("a", "b", NULL) FROM "test" [] + // SELECT COALESCE("a", ?), COALESCE("a", "b", ?) FROM "test" [a ] +} + +func ExampleCOALESCE_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.COALESCE(dbv2.C("a"), "a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT COALESCE("a", 'a') AS "a" FROM "test" +} + +func ExampleCOUNT() { + ds := dbv2.From("test").Select(dbv2.COUNT("*")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT COUNT(*) FROM "test" [] + // SELECT COUNT(*) FROM "test" [] +} + +func ExampleCOUNT_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.COUNT("*").As("count")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT COUNT(*) AS "count" FROM "test" +} + +func ExampleCOUNT_havingClause() { + ds := dbv2. + From("test"). + Select(dbv2.COUNT("a").As("COUNT")). + GroupBy("a"). + Having(dbv2.COUNT("a").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT COUNT("a") AS "COUNT" FROM "test" GROUP BY "a" HAVING (COUNT("a") > 10) [] + // SELECT COUNT("a") AS "COUNT" FROM "test" GROUP BY "a" HAVING (COUNT("a") > ?) [10] +} + +func ExampleCast() { + sql, _, _ := dbv2.From("test"). + Select(dbv2.Cast(dbv2.C("json1"), "TEXT").As("json_text")). + ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.Cast(dbv2.C("json1"), "TEXT").Neq( + dbv2.Cast(dbv2.C("json2"), "TEXT"), + ), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT CAST("json1" AS TEXT) AS "json_text" FROM "test" + // SELECT * FROM "test" WHERE (CAST("json1" AS TEXT) != CAST("json2" AS TEXT)) +} + +func ExampleDISTINCT() { + ds := dbv2.From("test").Select(dbv2.DISTINCT("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT DISTINCT("col") FROM "test" [] + // SELECT DISTINCT("col") FROM "test" [] +} + +func ExampleDISTINCT_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.DISTINCT("a").As("distinct_a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT DISTINCT("a") AS "distinct_a" FROM "test" +} + +func ExampleDefault() { + ds := dbv2.Insert("items") + + sql, args, _ := ds.Rows(dbv2.Record{ + "name": dbv2.Default(), + "address": dbv2.Default(), + }).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).Rows(dbv2.Record{ + "name": dbv2.Default(), + "address": dbv2.Default(), + }).ToSQL() + fmt.Println(sql, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES (DEFAULT, DEFAULT) [] + // INSERT INTO "items" ("address", "name") VALUES (DEFAULT, DEFAULT) [] +} + +func ExampleDoNothing() { + ds := dbv2.Insert("items") + + sql, args, _ := ds.Rows(dbv2.Record{ + "address": "111 Address", + "name": "bob", + }).OnConflict(dbv2.DoNothing()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).Rows(dbv2.Record{ + "address": "111 Address", + "name": "bob", + }).OnConflict(dbv2.DoNothing()).ToSQL() + fmt.Println(sql, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Address', 'bob') ON CONFLICT DO NOTHING [] + // INSERT INTO "items" ("address", "name") VALUES (?, ?) ON CONFLICT DO NOTHING [111 Address bob] +} + +func ExampleDoUpdate() { + ds := dbv2.Insert("items") + + sql, args, _ := ds. + Rows(dbv2.Record{"address": "111 Address"}). + OnConflict(dbv2.DoUpdate("address", dbv2.C("address").Set(dbv2.I("excluded.address")))). + ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true). + Rows(dbv2.Record{"address": "111 Address"}). + OnConflict(dbv2.DoUpdate("address", dbv2.C("address").Set(dbv2.I("excluded.address")))). + ToSQL() + fmt.Println(sql, args) + + // Output: + // INSERT INTO "items" ("address") VALUES ('111 Address') ON CONFLICT (address) DO UPDATE SET "address"="excluded"."address" [] + // INSERT INTO "items" ("address") VALUES (?) ON CONFLICT (address) DO UPDATE SET "address"="excluded"."address" [111 Address] +} + +func ExampleDoUpdate_where() { + ds := dbv2.Insert("items") + + sql, args, _ := ds. + Rows(dbv2.Record{"address": "111 Address"}). + OnConflict(dbv2.DoUpdate( + "address", + dbv2.C("address").Set(dbv2.I("excluded.address"))).Where(dbv2.I("items.updated").IsNull()), + ). + ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true). + Rows(dbv2.Record{"address": "111 Address"}). + OnConflict(dbv2.DoUpdate( + "address", + dbv2.C("address").Set(dbv2.I("excluded.address"))).Where(dbv2.I("items.updated").IsNull()), + ). + ToSQL() + fmt.Println(sql, args) + + // Output: + // INSERT INTO "items" ("address") VALUES ('111 Address') ON CONFLICT (address) DO UPDATE SET "address"="excluded"."address" WHERE ("items"."updated" IS NULL) [] + // INSERT INTO "items" ("address") VALUES (?) ON CONFLICT (address) DO UPDATE SET "address"="excluded"."address" WHERE ("items"."updated" IS NULL) [111 Address] +} + +func ExampleFIRST() { + ds := dbv2.From("test").Select(dbv2.FIRST("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT FIRST("col") FROM "test" [] + // SELECT FIRST("col") FROM "test" [] +} + +func ExampleFIRST_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.FIRST("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT FIRST("a") AS "a" FROM "test" +} + +// This example shows how to create custom SQL Functions +func ExampleFunc() { + stragg := func(expression exp.Expression, delimiter string) exp.SQLFunctionExpression { + return dbv2.Func("str_agg", expression, dbv2.L(delimiter)) + } + sql, _, _ := dbv2.From("test").Select(stragg(dbv2.C("col"), "|")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT str_agg("col", |) FROM "test" +} + +func ExampleI() { + ds := dbv2.From("test"). + Select( + dbv2.I("my_schema.table.col1"), + dbv2.I("table.col2"), + dbv2.I("col3"), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Select(dbv2.I("test.*")) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT "my_schema"."table"."col1", "table"."col2", "col3" FROM "test" [] + // SELECT "my_schema"."table"."col1", "table"."col2", "col3" FROM "test" [] + // SELECT "test".* FROM "test" [] + // SELECT "test".* FROM "test" [] +} + +func ExampleL() { + ds := dbv2.From("test").Where( + // literal with no args + dbv2.L(`"col"::TEXT = ""other_col"::text`), + // literal with args they will be interpolated into the sql by default + dbv2.L("col IN (?, ?, ?)", "a", "b", "c"), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" WHERE ("col"::TEXT = ""other_col"::text AND col IN ('a', 'b', 'c')) [] + // SELECT * FROM "test" WHERE ("col"::TEXT = ""other_col"::text AND col IN (?, ?, ?)) [a b c] +} + +func ExampleL_withArgs() { + ds := dbv2.From("test").Where( + dbv2.L( + "(? AND ?) OR ?", + dbv2.C("a").Eq(1), + dbv2.C("b").Eq("b"), + dbv2.C("c").In([]string{"a", "b", "c"}), + ), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" WHERE (("a" = 1) AND ("b" = 'b')) OR ("c" IN ('a', 'b', 'c')) [] + // SELECT * FROM "test" WHERE (("a" = ?) AND ("b" = ?)) OR ("c" IN (?, ?, ?)) [1 b a b c] +} + +func ExampleL_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.L("json_col->>'totalAmount'").As("total_amount")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT json_col->>'totalAmount' AS "total_amount" FROM "test" +} + +func ExampleL_comparisons() { + // used from a literal expression + sql, _, _ := dbv2.From("test").Where(dbv2.L("(a + b)").Eq(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a + b)").Neq(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a + b)").Gt(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a + b)").Gte(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a + b)").Lt(10)).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a + b)").Lte(10)).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ((a + b) = 10) + // SELECT * FROM "test" WHERE ((a + b) != 10) + // SELECT * FROM "test" WHERE ((a + b) > 10) + // SELECT * FROM "test" WHERE ((a + b) >= 10) + // SELECT * FROM "test" WHERE ((a + b) < 10) + // SELECT * FROM "test" WHERE ((a + b) <= 10) +} + +func ExampleL_inOperators() { + // using identifiers + sql, _, _ := dbv2.From("test").Where(dbv2.L("json_col->>'val'").In("a", "b", "c")).ToSQL() + fmt.Println(sql) + // with a slice + sql, _, _ = dbv2.From("test").Where(dbv2.L("json_col->>'val'").In([]string{"a", "b", "c"})).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("json_col->>'val'").NotIn("a", "b", "c")).ToSQL() + fmt.Println(sql) + // with a slice + sql, _, _ = dbv2.From("test").Where(dbv2.L("json_col->>'val'").NotIn([]string{"a", "b", "c"})).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE (json_col->>'val' IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE (json_col->>'val' IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE (json_col->>'val' NOT IN ('a', 'b', 'c')) + // SELECT * FROM "test" WHERE (json_col->>'val' NOT IN ('a', 'b', 'c')) +} + +func ExampleL_likeComparisons() { + // using identifiers + sql, _, _ := dbv2.From("test").Where(dbv2.L("(a::text || 'bar')").Like("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.L("(a::text || 'bar')").Like(regexp.MustCompile("[ab]")), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a::text || 'bar')").ILike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.L("(a::text || 'bar')").ILike(regexp.MustCompile("[ab]")), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a::text || 'bar')").NotLike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.L("(a::text || 'bar')").NotLike(regexp.MustCompile("[ab]")), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where(dbv2.L("(a::text || 'bar')").NotILike("%a%")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Where( + dbv2.L("(a::text || 'bar')").NotILike(regexp.MustCompile("[ab]")), + ).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ((a::text || 'bar') LIKE '%a%') + // SELECT * FROM "test" WHERE ((a::text || 'bar') ~ '[ab]') + // SELECT * FROM "test" WHERE ((a::text || 'bar') ILIKE '%a%') + // SELECT * FROM "test" WHERE ((a::text || 'bar') ~* '[ab]') + // SELECT * FROM "test" WHERE ((a::text || 'bar') NOT LIKE '%a%') + // SELECT * FROM "test" WHERE ((a::text || 'bar') !~ '[ab]') + // SELECT * FROM "test" WHERE ((a::text || 'bar') NOT ILIKE '%a%') + // SELECT * FROM "test" WHERE ((a::text || 'bar') !~* '[ab]') +} + +func ExampleL_isComparisons() { + sql, args, _ := dbv2.From("test").Where(dbv2.L("a").Is(nil)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").Is(true)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").Is(false)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNull()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsTrue()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsFalse()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNot(nil)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNot(true)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNot(false)).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNotNull()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNotTrue()).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("test").Where(dbv2.L("a").IsNotFalse()).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (a IS NULL) [] + // SELECT * FROM "test" WHERE (a IS TRUE) [] + // SELECT * FROM "test" WHERE (a IS FALSE) [] + // SELECT * FROM "test" WHERE (a IS NULL) [] + // SELECT * FROM "test" WHERE (a IS TRUE) [] + // SELECT * FROM "test" WHERE (a IS FALSE) [] + // SELECT * FROM "test" WHERE (a IS NOT NULL) [] + // SELECT * FROM "test" WHERE (a IS NOT TRUE) [] + // SELECT * FROM "test" WHERE (a IS NOT FALSE) [] + // SELECT * FROM "test" WHERE (a IS NOT NULL) [] + // SELECT * FROM "test" WHERE (a IS NOT TRUE) [] + // SELECT * FROM "test" WHERE (a IS NOT FALSE) [] +} + +func ExampleL_betweenComparisons() { + ds := dbv2.From("test").Where( + dbv2.L("(a + b)").Between(dbv2.Range(1, 10)), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where( + dbv2.L("(a + b)").NotBetween(dbv2.Range(1, 10)), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ((a + b) BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ((a + b) BETWEEN ? AND ?) [1 10] + // SELECT * FROM "test" WHERE ((a + b) NOT BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ((a + b) NOT BETWEEN ? AND ?) [1 10] +} + +func ExampleLAST() { + ds := dbv2.From("test").Select(dbv2.LAST("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT LAST("col") FROM "test" [] + // SELECT LAST("col") FROM "test" [] +} + +func ExampleLAST_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.LAST("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT LAST("a") AS "a" FROM "test" +} + +func ExampleMAX() { + ds := dbv2.From("test").Select(dbv2.MAX("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT MAX("col") FROM "test" [] + // SELECT MAX("col") FROM "test" [] +} + +func ExampleMAX_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.MAX("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT MAX("a") AS "a" FROM "test" +} + +func ExampleMAX_havingClause() { + ds := dbv2. + From("test"). + Select(dbv2.MAX("a").As("MAX")). + GroupBy("a"). + Having(dbv2.MAX("a").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT MAX("a") AS "MAX" FROM "test" GROUP BY "a" HAVING (MAX("a") > 10) [] + // SELECT MAX("a") AS "MAX" FROM "test" GROUP BY "a" HAVING (MAX("a") > ?) [10] +} + +func ExampleMIN() { + ds := dbv2.From("test").Select(dbv2.MIN("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT MIN("col") FROM "test" [] + // SELECT MIN("col") FROM "test" [] +} + +func ExampleMIN_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.MIN("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT MIN("a") AS "a" FROM "test" +} + +func ExampleMIN_havingClause() { + ds := dbv2. + From("test"). + Select(dbv2.MIN("a").As("MIN")). + GroupBy("a"). + Having(dbv2.MIN("a").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT MIN("a") AS "MIN" FROM "test" GROUP BY "a" HAVING (MIN("a") > 10) [] + // SELECT MIN("a") AS "MIN" FROM "test" GROUP BY "a" HAVING (MIN("a") > ?) [10] +} + +func ExampleOn() { + ds := dbv2.From("test").Join( + dbv2.T("my_table"), + dbv2.On(dbv2.I("my_table.fkey").Eq(dbv2.I("other_table.id"))), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" INNER JOIN "my_table" ON ("my_table"."fkey" = "other_table"."id") [] + // SELECT * FROM "test" INNER JOIN "my_table" ON ("my_table"."fkey" = "other_table"."id") [] +} + +func ExampleOn_withEx() { + ds := dbv2.From("test").Join( + dbv2.T("my_table"), + dbv2.On(dbv2.Ex{"my_table.fkey": dbv2.I("other_table.id")}), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" INNER JOIN "my_table" ON ("my_table"."fkey" = "other_table"."id") [] + // SELECT * FROM "test" INNER JOIN "my_table" ON ("my_table"."fkey" = "other_table"."id") [] +} + +func ExampleOr() { + ds := dbv2.From("test").Where( + dbv2.Or( + dbv2.C("col").Eq(10), + dbv2.C("col").Eq(20), + ), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("col" = 10) OR ("col" = 20)) [] + // SELECT * FROM "test" WHERE (("col" = ?) OR ("col" = ?)) [10 20] +} + +func ExampleOr_withAnd() { + ds := dbv2.From("items").Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Eq(100), + dbv2.C("c").Neq("test"), + ), + ), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "items" WHERE (("a" > 10) OR (("b" = 100) AND ("c" != 'test'))) [] + // SELECT * FROM "items" WHERE (("a" > ?) OR (("b" = ?) AND ("c" != ?))) [10 100 test] +} + +func ExampleOr_withExMap() { + ds := dbv2.From("test").Where( + dbv2.Or( + // Ex will be anded together + dbv2.Ex{ + "col1": 1, + "col2": true, + }, + dbv2.Ex{ + "col3": nil, + "col4": "foo", + }, + ), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ((("col1" = 1) AND ("col2" IS TRUE)) OR (("col3" IS NULL) AND ("col4" = 'foo'))) [] + // SELECT * FROM "test" WHERE ((("col1" = ?) AND ("col2" IS TRUE)) OR (("col3" IS NULL) AND ("col4" = ?))) [1 foo] +} + +func ExampleRange_numbers() { + ds := dbv2.From("test").Where( + dbv2.C("col").Between(dbv2.Range(1, 10)), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where( + dbv2.C("col").NotBetween(dbv2.Range(1, 10)), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("col" BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("col" BETWEEN ? AND ?) [1 10] + // SELECT * FROM "test" WHERE ("col" NOT BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("col" NOT BETWEEN ? AND ?) [1 10] +} + +func ExampleRange_strings() { + ds := dbv2.From("test").Where( + dbv2.C("col").Between(dbv2.Range("a", "z")), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where( + dbv2.C("col").NotBetween(dbv2.Range("a", "z")), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("col" BETWEEN 'a' AND 'z') [] + // SELECT * FROM "test" WHERE ("col" BETWEEN ? AND ?) [a z] + // SELECT * FROM "test" WHERE ("col" NOT BETWEEN 'a' AND 'z') [] + // SELECT * FROM "test" WHERE ("col" NOT BETWEEN ? AND ?) [a z] +} + +func ExampleRange_identifiers() { + ds := dbv2.From("test").Where( + dbv2.C("col1").Between(dbv2.Range(dbv2.C("col2"), dbv2.C("col3"))), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where( + dbv2.C("col1").NotBetween(dbv2.Range(dbv2.C("col2"), dbv2.C("col3"))), + ) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("col1" BETWEEN "col2" AND "col3") [] + // SELECT * FROM "test" WHERE ("col1" BETWEEN "col2" AND "col3") [] + // SELECT * FROM "test" WHERE ("col1" NOT BETWEEN "col2" AND "col3") [] + // SELECT * FROM "test" WHERE ("col1" NOT BETWEEN "col2" AND "col3") [] +} + +func ExampleS() { + s := dbv2.S("test_schema") + t := s.Table("test") + sql, args, _ := dbv2. + From(t). + Select( + t.Col("col1"), + t.Col("col2"), + t.Col("col3"), + ). + ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT "test_schema"."test"."col1", "test_schema"."test"."col2", "test_schema"."test"."col3" FROM "test_schema"."test" [] +} + +func ExampleSUM() { + ds := dbv2.From("test").Select(dbv2.SUM("col")) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT SUM("col") FROM "test" [] + // SELECT SUM("col") FROM "test" [] +} + +func ExampleSUM_as() { + sql, _, _ := dbv2.From("test").Select(dbv2.SUM("a").As("a")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT SUM("a") AS "a" FROM "test" +} + +func ExampleSUM_havingClause() { + ds := dbv2. + From("test"). + Select(dbv2.SUM("a").As("SUM")). + GroupBy("a"). + Having(dbv2.SUM("a").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT SUM("a") AS "SUM" FROM "test" GROUP BY "a" HAVING (SUM("a") > 10) [] + // SELECT SUM("a") AS "SUM" FROM "test" GROUP BY "a" HAVING (SUM("a") > ?) [10] +} + +func ExampleStar() { + ds := dbv2.From("test").Select(dbv2.Star()) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" [] + // SELECT * FROM "test" [] +} + +func ExampleT() { + t := dbv2.T("test") + sql, args, _ := dbv2. + From(t). + Select( + t.Col("col1"), + t.Col("col2"), + t.Col("col3"), + ). + ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT "test"."col1", "test"."col2", "test"."col3" FROM "test" [] +} + +func ExampleUsing() { + ds := dbv2.From("test").Join( + dbv2.T("my_table"), + dbv2.Using("fkey"), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" INNER JOIN "my_table" USING ("fkey") [] + // SELECT * FROM "test" INNER JOIN "my_table" USING ("fkey") [] +} + +func ExampleUsing_withIdentifier() { + ds := dbv2.From("test").Join( + dbv2.T("my_table"), + dbv2.Using(dbv2.C("fkey")), + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" INNER JOIN "my_table" USING ("fkey") [] + // SELECT * FROM "test" INNER JOIN "my_table" USING ("fkey") [] +} + +func ExampleEx() { + ds := dbv2.From("items").Where( + dbv2.Ex{ + "col1": "a", + "col2": 1, + "col3": true, + "col4": false, + "col5": nil, + "col6": []string{"a", "b", "c"}, + }, + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "items" WHERE (("col1" = 'a') AND ("col2" = 1) AND ("col3" IS TRUE) AND ("col4" IS FALSE) AND ("col5" IS NULL) AND ("col6" IN ('a', 'b', 'c'))) [] + // SELECT * FROM "items" WHERE (("col1" = ?) AND ("col2" = ?) AND ("col3" IS TRUE) AND ("col4" IS FALSE) AND ("col5" IS NULL) AND ("col6" IN (?, ?, ?))) [a 1 a b c] +} + +func ExampleEx_withOp() { + sql, args, _ := dbv2.From("items").Where( + dbv2.Ex{ + "col1": dbv2.Op{"neq": "a"}, + "col3": dbv2.Op{"isNot": true}, + "col6": dbv2.Op{"notIn": []string{"a", "b", "c"}}, + }, + ).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "items" WHERE (("col1" != 'a') AND ("col3" IS NOT TRUE) AND ("col6" NOT IN ('a', 'b', 'c'))) [] +} + +func ExampleEx_in() { + // using an Ex expression map + sql, _, _ := dbv2.From("test").Where(dbv2.Ex{ + "a": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "test" WHERE ("a" IN ('a', 'b', 'c')) +} + +func ExampleExOr() { + sql, args, _ := dbv2.From("items").Where( + dbv2.ExOr{ + "col1": "a", + "col2": 1, + "col3": true, + "col4": false, + "col5": nil, + "col6": []string{"a", "b", "c"}, + }, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "items" WHERE (("col1" = 'a') OR ("col2" = 1) OR ("col3" IS TRUE) OR ("col4" IS FALSE) OR ("col5" IS NULL) OR ("col6" IN ('a', 'b', 'c'))) [] +} + +func ExampleExOr_withOp() { + sql, _, _ := dbv2.From("items").Where(dbv2.ExOr{ + "col1": dbv2.Op{"neq": "a"}, + "col3": dbv2.Op{"isNot": true}, + "col6": dbv2.Op{"notIn": []string{"a", "b", "c"}}, + }).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("items").Where(dbv2.ExOr{ + "col1": dbv2.Op{"gt": 1}, + "col2": dbv2.Op{"gte": 1}, + "col3": dbv2.Op{"lt": 1}, + "col4": dbv2.Op{"lte": 1}, + }).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("items").Where(dbv2.ExOr{ + "col1": dbv2.Op{"like": "a%"}, + "col2": dbv2.Op{"notLike": "a%"}, + "col3": dbv2.Op{"iLike": "a%"}, + "col4": dbv2.Op{"notILike": "a%"}, + }).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("items").Where(dbv2.ExOr{ + "col1": dbv2.Op{"like": regexp.MustCompile("^[ab]")}, + "col2": dbv2.Op{"notLike": regexp.MustCompile("^[ab]")}, + "col3": dbv2.Op{"iLike": regexp.MustCompile("^[ab]")}, + "col4": dbv2.Op{"notILike": regexp.MustCompile("^[ab]")}, + }).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT * FROM "items" WHERE (("col1" != 'a') OR ("col3" IS NOT TRUE) OR ("col6" NOT IN ('a', 'b', 'c'))) + // SELECT * FROM "items" WHERE (("col1" > 1) OR ("col2" >= 1) OR ("col3" < 1) OR ("col4" <= 1)) + // SELECT * FROM "items" WHERE (("col1" LIKE 'a%') OR ("col2" NOT LIKE 'a%') OR ("col3" ILIKE 'a%') OR ("col4" NOT ILIKE 'a%')) + // SELECT * FROM "items" WHERE (("col1" ~ '^[ab]') OR ("col2" !~ '^[ab]') OR ("col3" ~* '^[ab]') OR ("col4" !~* '^[ab]')) +} + +func ExampleOp_comparisons() { + ds := dbv2.From("test").Where(dbv2.Ex{ + "a": 10, + "b": dbv2.Op{"neq": 10}, + "c": dbv2.Op{"gte": 10}, + "d": dbv2.Op{"lt": 10}, + "e": dbv2.Op{"lte": 10}, + }) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE (("a" = 10) AND ("b" != 10) AND ("c" >= 10) AND ("d" < 10) AND ("e" <= 10)) [] + // SELECT * FROM "test" WHERE (("a" = ?) AND ("b" != ?) AND ("c" >= ?) AND ("d" < ?) AND ("e" <= ?)) [10 10 10 10 10] +} + +func ExampleOp_inComparisons() { + // using an Ex expression map + ds := dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"in": []string{"a", "b", "c"}}, + }) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notIn": []string{"a", "b", "c"}}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" IN ('a', 'b', 'c')) [] + // SELECT * FROM "test" WHERE ("a" IN (?, ?, ?)) [a b c] + // SELECT * FROM "test" WHERE ("a" NOT IN ('a', 'b', 'c')) [] + // SELECT * FROM "test" WHERE ("a" NOT IN (?, ?, ?)) [a b c] +} + +func ExampleOp_likeComparisons() { + // using an Ex expression map + ds := dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"like": "%a%"}, + }) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"like": regexp.MustCompile("[ab]")}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"iLike": "%a%"}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"iLike": regexp.MustCompile("[ab]")}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notLike": "%a%"}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notLike": regexp.MustCompile("[ab]")}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notILike": "%a%"}, + }) + + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notILike": regexp.MustCompile("[ab]")}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" LIKE '%a%') [] + // SELECT * FROM "test" WHERE ("a" LIKE ?) [%a%] + // SELECT * FROM "test" WHERE ("a" ~ '[ab]') [] + // SELECT * FROM "test" WHERE ("a" ~ ?) [[ab]] + // SELECT * FROM "test" WHERE ("a" ILIKE '%a%') [] + // SELECT * FROM "test" WHERE ("a" ILIKE ?) [%a%] + // SELECT * FROM "test" WHERE ("a" ~* '[ab]') [] + // SELECT * FROM "test" WHERE ("a" ~* ?) [[ab]] + // SELECT * FROM "test" WHERE ("a" NOT LIKE '%a%') [] + // SELECT * FROM "test" WHERE ("a" NOT LIKE ?) [%a%] + // SELECT * FROM "test" WHERE ("a" !~ '[ab]') [] + // SELECT * FROM "test" WHERE ("a" !~ ?) [[ab]] + // SELECT * FROM "test" WHERE ("a" NOT ILIKE '%a%') [] + // SELECT * FROM "test" WHERE ("a" NOT ILIKE ?) [%a%] + // SELECT * FROM "test" WHERE ("a" !~* '[ab]') [] + // SELECT * FROM "test" WHERE ("a" !~* ?) [[ab]] +} + +func ExampleOp_isComparisons() { + // using an Ex expression map + ds := dbv2.From("test").Where(dbv2.Ex{ + "a": true, + }) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"is": true}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": false, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"is": false}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": nil, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"is": nil}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"isNot": true}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"isNot": false}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"isNot": nil}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NOT TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT TRUE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT FALSE) [] + // SELECT * FROM "test" WHERE ("a" IS NOT NULL) [] + // SELECT * FROM "test" WHERE ("a" IS NOT NULL) [] +} + +func ExampleOp_betweenComparisons() { + ds := dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"between": dbv2.Range(1, 10)}, + }) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"notBetween": dbv2.Range(1, 10)}, + }) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" WHERE ("a" BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("a" BETWEEN ? AND ?) [1 10] + // SELECT * FROM "test" WHERE ("a" NOT BETWEEN 1 AND 10) [] + // SELECT * FROM "test" WHERE ("a" NOT BETWEEN ? AND ?) [1 10] +} + +// When using a single op with multiple keys they are ORed together +func ExampleOp_withMultipleKeys() { + ds := dbv2.From("items").Where(dbv2.Ex{ + "col1": dbv2.Op{"is": nil, "eq": 10}, + }) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "items" WHERE (("col1" = 10) OR ("col1" IS NULL)) [] + // SELECT * FROM "items" WHERE (("col1" = ?) OR ("col1" IS NULL)) [10] +} + +func ExampleRecord_insert() { + ds := dbv2.Insert("test") + + records := []dbv2.Record{ + {"col1": 1, "col2": "foo"}, + {"col1": 2, "col2": "bar"}, + } + + sql, args, _ := ds.Rows(records).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).Rows(records).ToSQL() + fmt.Println(sql, args) + // Output: + // INSERT INTO "test" ("col1", "col2") VALUES (1, 'foo'), (2, 'bar') [] + // INSERT INTO "test" ("col1", "col2") VALUES (?, ?), (?, ?) [1 foo 2 bar] +} + +func ExampleRecord_update() { + ds := dbv2.Update("test") + update := dbv2.Record{"col1": 1, "col2": "foo"} + + sql, args, _ := ds.Set(update).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).Set(update).ToSQL() + fmt.Println(sql, args) + // Output: + // UPDATE "test" SET "col1"=1,"col2"='foo' [] + // UPDATE "test" SET "col1"=?,"col2"=? [1 foo] +} + +func ExampleV() { + ds := dbv2.From("user").Select( + dbv2.V(true).As("is_verified"), + dbv2.V(1.2).As("version"), + "first_name", + "last_name", + ) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("user").Where(dbv2.V(1).Neq(1)) + sql, args, _ = ds.ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT TRUE AS "is_verified", 1.2 AS "version", "first_name", "last_name" FROM "user" [] + // SELECT * FROM "user" WHERE (1 != 1) [] +} + +func ExampleV_prepared() { + ds := dbv2.From("user").Select( + dbv2.V(true).As("is_verified"), + dbv2.V(1.2).As("version"), + "first_name", + "last_name", + ) + + sql, args, _ := ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + ds = dbv2.From("user").Where(dbv2.V(1).Neq(1)) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT ? AS "is_verified", ? AS "version", "first_name", "last_name" FROM "user" [true 1.2] + // SELECT * FROM "user" WHERE (? != ?) [1 1] +} + +func ExampleVals() { + ds := dbv2.Insert("user"). + Cols("first_name", "last_name", "is_verified"). + Vals( + dbv2.Vals{"Greg", "Farley", true}, + dbv2.Vals{"Jimmy", "Stewart", true}, + dbv2.Vals{"Jeff", "Jeffers", false}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name", "is_verified") VALUES ('Greg', 'Farley', TRUE), ('Jimmy', 'Stewart', TRUE), ('Jeff', 'Jeffers', FALSE) [] +} + +func ExampleW() { + ds := dbv2.From("test"). + Select(dbv2.ROW_NUMBER().Over(dbv2.W().PartitionBy("a").OrderBy(dbv2.I("b").Asc()))) + query, args, _ := ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().OverName(dbv2.I("w"))). + Window(dbv2.W("w").PartitionBy("a").OrderBy(dbv2.I("b").Asc())) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().OverName(dbv2.I("w1"))). + Window( + dbv2.W("w1").PartitionBy("a"), + dbv2.W("w").Inherit("w1").OrderBy(dbv2.I("b").Asc()), + ) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().Over(dbv2.W().Inherit("w").OrderBy("b"))). + Window(dbv2.W("w").PartitionBy("a")) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + // Output: + // SELECT ROW_NUMBER() OVER (PARTITION BY "a" ORDER BY "b" ASC) FROM "test" [] + // SELECT ROW_NUMBER() OVER "w" FROM "test" WINDOW "w" AS (PARTITION BY "a" ORDER BY "b" ASC) [] + // SELECT ROW_NUMBER() OVER "w1" FROM "test" WINDOW "w1" AS (PARTITION BY "a"), "w" AS ("w1" ORDER BY "b" ASC) [] + // SELECT ROW_NUMBER() OVER ("w" ORDER BY "b") FROM "test" WINDOW "w" AS (PARTITION BY "a") [] +} + +func ExampleLateral() { + maxEntry := dbv2.From("entry"). + Select(dbv2.MAX("int").As("max_int")). + Where(dbv2.Ex{"time": dbv2.Op{"lt": dbv2.I("e.time")}}). + As("max_entry") + + maxID := dbv2.From("entry"). + Select("id"). + Where(dbv2.Ex{"int": dbv2.I("max_entry.max_int")}). + As("max_id") + + ds := dbv2. + Select("e.id", "max_entry.max_int", "max_id.id"). + From( + dbv2.T("entry").As("e"), + dbv2.Lateral(maxEntry), + dbv2.Lateral(maxID), + ) + query, args, _ := ds.ToSQL() + fmt.Println(query, args) + + query, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(query, args) + + // Output: + // SELECT "e"."id", "max_entry"."max_int", "max_id"."id" FROM "entry" AS "e", LATERAL (SELECT MAX("int") AS "max_int" FROM "entry" WHERE ("time" < "e"."time")) AS "max_entry", LATERAL (SELECT "id" FROM "entry" WHERE ("int" = "max_entry"."max_int")) AS "max_id" [] + // SELECT "e"."id", "max_entry"."max_int", "max_id"."id" FROM "entry" AS "e", LATERAL (SELECT MAX("int") AS "max_int" FROM "entry" WHERE ("time" < "e"."time")) AS "max_entry", LATERAL (SELECT "id" FROM "entry" WHERE ("int" = "max_entry"."max_int")) AS "max_id" [] +} + +func ExampleLateral_join() { + maxEntry := dbv2.From("entry"). + Select(dbv2.MAX("int").As("max_int")). + Where(dbv2.Ex{"time": dbv2.Op{"lt": dbv2.I("e.time")}}). + As("max_entry") + + maxID := dbv2.From("entry"). + Select("id"). + Where(dbv2.Ex{"int": dbv2.I("max_entry.max_int")}). + As("max_id") + + ds := dbv2. + Select("e.id", "max_entry.max_int", "max_id.id"). + From(dbv2.T("entry").As("e")). + Join(dbv2.Lateral(maxEntry), dbv2.On(dbv2.V(true))). + Join(dbv2.Lateral(maxID), dbv2.On(dbv2.V(true))) + query, args, _ := ds.ToSQL() + fmt.Println(query, args) + + query, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(query, args) + + // Output: + // SELECT "e"."id", "max_entry"."max_int", "max_id"."id" FROM "entry" AS "e" INNER JOIN LATERAL (SELECT MAX("int") AS "max_int" FROM "entry" WHERE ("time" < "e"."time")) AS "max_entry" ON TRUE INNER JOIN LATERAL (SELECT "id" FROM "entry" WHERE ("int" = "max_entry"."max_int")) AS "max_id" ON TRUE [] + // SELECT "e"."id", "max_entry"."max_int", "max_id"."id" FROM "entry" AS "e" INNER JOIN LATERAL (SELECT MAX("int") AS "max_int" FROM "entry" WHERE ("time" < "e"."time")) AS "max_entry" ON ? INNER JOIN LATERAL (SELECT "id" FROM "entry" WHERE ("int" = "max_entry"."max_int")) AS "max_id" ON ? [true true] +} + +func ExampleAny() { + ds := dbv2.From("test").Where(dbv2.Ex{ + "id": dbv2.Any(dbv2.From("other").Select("test_id")), + }) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" WHERE ("id" = ANY ((SELECT "test_id" FROM "other"))) [] + // SELECT * FROM "test" WHERE ("id" = ANY ((SELECT "test_id" FROM "other"))) [] +} + +func ExampleAll() { + ds := dbv2.From("test").Where(dbv2.Ex{ + "id": dbv2.All(dbv2.From("other").Select("test_id")), + }) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" WHERE ("id" = ALL ((SELECT "test_id" FROM "other"))) [] + // SELECT * FROM "test" WHERE ("id" = ALL ((SELECT "test_id" FROM "other"))) [] +} + +func ExampleCase_search() { + ds := dbv2.From("test"). + Select( + dbv2.C("col"), + dbv2.Case(). + When(dbv2.C("col").Gt(0), true). + When(dbv2.C("col").Lte(0), false). + As("is_gt_zero"), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT "col", CASE WHEN ("col" > 0) THEN TRUE WHEN ("col" <= 0) THEN FALSE END AS "is_gt_zero" FROM "test" [] + // SELECT "col", CASE WHEN ("col" > ?) THEN ? WHEN ("col" <= ?) THEN ? END AS "is_gt_zero" FROM "test" [0 true 0 false] +} + +func ExampleCase_searchElse() { + ds := dbv2.From("test"). + Select( + dbv2.C("col"), + dbv2.Case(). + When(dbv2.C("col").Gt(10), "Gt 10"). + When(dbv2.C("col").Gt(20), "Gt 20"). + Else("Bad Val"). + As("str_val"), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT "col", CASE WHEN ("col" > 10) THEN 'Gt 10' WHEN ("col" > 20) THEN 'Gt 20' ELSE 'Bad Val' END AS "str_val" FROM "test" [] + // SELECT "col", CASE WHEN ("col" > ?) THEN ? WHEN ("col" > ?) THEN ? ELSE ? END AS "str_val" FROM "test" [10 Gt 10 20 Gt 20 Bad Val] +} + +func ExampleCase_value() { + ds := dbv2.From("test"). + Select( + dbv2.C("col"), + dbv2.Case(). + Value(dbv2.C("str")). + When("foo", "FOO"). + When("bar", "BAR"). + As("foo_bar_upper"), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT "col", CASE "str" WHEN 'foo' THEN 'FOO' WHEN 'bar' THEN 'BAR' END AS "foo_bar_upper" FROM "test" [] + // SELECT "col", CASE "str" WHEN ? THEN ? WHEN ? THEN ? END AS "foo_bar_upper" FROM "test" [foo FOO bar BAR] +} + +func ExampleCase_valueElse() { + ds := dbv2.From("test"). + Select( + dbv2.C("col"), + dbv2.Case(). + Value(dbv2.C("str")). + When("foo", "FOO"). + When("bar", "BAR"). + Else("Baz"). + As("foo_bar_upper"), + ) + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT "col", CASE "str" WHEN 'foo' THEN 'FOO' WHEN 'bar' THEN 'BAR' ELSE 'Baz' END AS "foo_bar_upper" FROM "test" [] + // SELECT "col", CASE "str" WHEN ? THEN ? WHEN ? THEN ? ELSE ? END AS "foo_bar_upper" FROM "test" [foo FOO bar BAR Baz] +} diff --git a/expressions_test.go b/expressions_test.go new file mode 100644 index 0000000..2d052cd --- /dev/null +++ b/expressions_test.go @@ -0,0 +1,190 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/stretchr/testify/suite" +) + +type ( + dbv2ExpressionsSuite struct { + suite.Suite + } +) + +func (ges *dbv2ExpressionsSuite) TestCast() { + ges.Equal(exp.NewCastExpression(dbv2.C("test"), "string"), dbv2.Cast(dbv2.C("test"), "string")) +} + +func (ges *dbv2ExpressionsSuite) TestDoNothing() { + ges.Equal(exp.NewDoNothingConflictExpression(), dbv2.DoNothing()) +} + +func (ges *dbv2ExpressionsSuite) TestDoUpdate() { + ges.Equal(exp.NewDoUpdateConflictExpression("test", dbv2.Record{"a": "b"}), dbv2.DoUpdate("test", dbv2.Record{"a": "b"})) +} + +func (ges *dbv2ExpressionsSuite) TestOr() { + e1 := dbv2.C("a").Eq("b") + e2 := dbv2.C("b").Eq(2) + ges.Equal(exp.NewExpressionList(exp.OrType, e1, e2), dbv2.Or(e1, e2)) +} + +func (ges *dbv2ExpressionsSuite) TestAnd() { + e1 := dbv2.C("a").Eq("b") + e2 := dbv2.C("b").Eq(2) + ges.Equal(exp.NewExpressionList(exp.AndType, e1, e2), dbv2.And(e1, e2)) +} + +func (ges *dbv2ExpressionsSuite) TestFunc() { + ges.Equal(exp.NewSQLFunctionExpression("count", dbv2.L("*")), dbv2.Func("count", dbv2.L("*"))) +} + +func (ges *dbv2ExpressionsSuite) TestDISTINCT() { + ges.Equal(exp.NewSQLFunctionExpression("DISTINCT", dbv2.I("col")), dbv2.DISTINCT("col")) +} + +func (ges *dbv2ExpressionsSuite) TestCOUNT() { + ges.Equal(exp.NewSQLFunctionExpression("COUNT", dbv2.I("col")), dbv2.COUNT("col")) +} + +func (ges *dbv2ExpressionsSuite) TestMIN() { + ges.Equal(exp.NewSQLFunctionExpression("MIN", dbv2.I("col")), dbv2.MIN("col")) +} + +func (ges *dbv2ExpressionsSuite) TestMAX() { + ges.Equal(exp.NewSQLFunctionExpression("MAX", dbv2.I("col")), dbv2.MAX("col")) +} + +func (ges *dbv2ExpressionsSuite) TestAVG() { + ges.Equal(exp.NewSQLFunctionExpression("AVG", dbv2.I("col")), dbv2.AVG("col")) +} + +func (ges *dbv2ExpressionsSuite) TestFIRST() { + ges.Equal(exp.NewSQLFunctionExpression("FIRST", dbv2.I("col")), dbv2.FIRST("col")) +} + +func (ges *dbv2ExpressionsSuite) TestLAST() { + ges.Equal(exp.NewSQLFunctionExpression("LAST", dbv2.I("col")), dbv2.LAST("col")) +} + +func (ges *dbv2ExpressionsSuite) TestSUM() { + ges.Equal(exp.NewSQLFunctionExpression("SUM", dbv2.I("col")), dbv2.SUM("col")) +} + +func (ges *dbv2ExpressionsSuite) TestCOALESCE() { + ges.Equal(exp.NewSQLFunctionExpression("COALESCE", dbv2.I("col"), nil), dbv2.COALESCE(dbv2.I("col"), nil)) +} + +func (ges *dbv2ExpressionsSuite) TestROW_NUMBER() { + ges.Equal(exp.NewSQLFunctionExpression("ROW_NUMBER"), dbv2.ROW_NUMBER()) +} + +func (ges *dbv2ExpressionsSuite) TestRANK() { + ges.Equal(exp.NewSQLFunctionExpression("RANK"), dbv2.RANK()) +} + +func (ges *dbv2ExpressionsSuite) TestDENSE_RANK() { + ges.Equal(exp.NewSQLFunctionExpression("DENSE_RANK"), dbv2.DENSE_RANK()) +} + +func (ges *dbv2ExpressionsSuite) TestPERCENT_RANK() { + ges.Equal(exp.NewSQLFunctionExpression("PERCENT_RANK"), dbv2.PERCENT_RANK()) +} + +func (ges *dbv2ExpressionsSuite) TestCUME_DIST() { + ges.Equal(exp.NewSQLFunctionExpression("CUME_DIST"), dbv2.CUME_DIST()) +} + +func (ges *dbv2ExpressionsSuite) TestNTILE() { + ges.Equal(exp.NewSQLFunctionExpression("NTILE", 1), dbv2.NTILE(1)) +} + +func (ges *dbv2ExpressionsSuite) TestFIRST_VALUE() { + ges.Equal(exp.NewSQLFunctionExpression("FIRST_VALUE", dbv2.I("col")), dbv2.FIRST_VALUE("col")) +} + +func (ges *dbv2ExpressionsSuite) TestLAST_VALUE() { + ges.Equal(exp.NewSQLFunctionExpression("LAST_VALUE", dbv2.I("col")), dbv2.LAST_VALUE("col")) +} + +func (ges *dbv2ExpressionsSuite) TestNTH_VALUE() { + ges.Equal(exp.NewSQLFunctionExpression("NTH_VALUE", dbv2.I("col"), 1), dbv2.NTH_VALUE("col", 1)) + ges.Equal(exp.NewSQLFunctionExpression("NTH_VALUE", dbv2.I("col"), 1), dbv2.NTH_VALUE(dbv2.C("col"), 1)) +} + +func (ges *dbv2ExpressionsSuite) TestI() { + ges.Equal(exp.NewIdentifierExpression("s", "t", "c"), dbv2.I("s.t.c")) +} + +func (ges *dbv2ExpressionsSuite) TestC() { + ges.Equal(exp.NewIdentifierExpression("", "", "c"), dbv2.C("c")) +} + +func (ges *dbv2ExpressionsSuite) TestS() { + ges.Equal(exp.NewIdentifierExpression("s", "", ""), dbv2.S("s")) +} + +func (ges *dbv2ExpressionsSuite) TestT() { + ges.Equal(exp.NewIdentifierExpression("", "t", ""), dbv2.T("t")) +} + +func (ges *dbv2ExpressionsSuite) TestW() { + ges.Equal(exp.NewWindowExpression(nil, nil, nil, nil), dbv2.W()) + ges.Equal(exp.NewWindowExpression(dbv2.I("a"), nil, nil, nil), dbv2.W("a")) + ges.Equal(exp.NewWindowExpression(dbv2.I("a"), dbv2.I("b"), nil, nil), dbv2.W("a", "b")) + ges.Equal(exp.NewWindowExpression(dbv2.I("a"), dbv2.I("b"), nil, nil), dbv2.W("a", "b", "c")) +} + +func (ges *dbv2ExpressionsSuite) TestOn() { + ges.Equal(exp.NewJoinOnCondition(dbv2.Ex{"a": "b"}), dbv2.On(dbv2.Ex{"a": "b"})) +} + +func (ges *dbv2ExpressionsSuite) TestUsing() { + ges.Equal(exp.NewJoinUsingCondition("a", "b"), dbv2.Using("a", "b")) +} + +func (ges *dbv2ExpressionsSuite) TestL() { + ges.Equal(exp.NewLiteralExpression("? + ?", 1, 2), dbv2.L("? + ?", 1, 2)) +} + +func (ges *dbv2ExpressionsSuite) TestLiteral() { + ges.Equal(exp.NewLiteralExpression("? + ?", 1, 2), dbv2.Literal("? + ?", 1, 2)) +} + +func (ges *dbv2ExpressionsSuite) TestV() { + ges.Equal(exp.NewLiteralExpression("?", "a"), dbv2.V("a")) +} + +func (ges *dbv2ExpressionsSuite) TestRange() { + ges.Equal(exp.NewRangeVal("a", "b"), dbv2.Range("a", "b")) +} + +func (ges *dbv2ExpressionsSuite) TestStar() { + ges.Equal(exp.NewLiteralExpression("*"), dbv2.Star()) +} + +func (ges *dbv2ExpressionsSuite) TestDefault() { + ges.Equal(exp.Default(), dbv2.Default()) +} + +func (ges *dbv2ExpressionsSuite) TestLateral() { + ds := dbv2.From("test") + ges.Equal(exp.NewLateralExpression(ds), dbv2.Lateral(ds)) +} + +func (ges *dbv2ExpressionsSuite) TestAny() { + ds := dbv2.From("test").Select("id") + ges.Equal(exp.NewSQLFunctionExpression("ANY ", ds), dbv2.Any(ds)) +} + +func (ges *dbv2ExpressionsSuite) TestAll() { + ds := dbv2.From("test").Select("id") + ges.Equal(exp.NewSQLFunctionExpression("ALL ", ds), dbv2.All(ds)) +} + +func TestDbv2Expressions(t *testing.T) { + suite.Run(t, new(dbv2ExpressionsSuite)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cbfe8c --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module git.fsdpf.net/go/db/v2 + +go 1.19 + +require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/denisenkom/go-mssqldb v0.12.3 + github.com/go-sql-driver/mysql v1.7.1 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.17 + github.com/samber/lo v1.49.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c1b87f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/insert_dataset.go b/insert_dataset.go new file mode 100644 index 0000000..3698c54 --- /dev/null +++ b/insert_dataset.go @@ -0,0 +1,273 @@ +package db + +import ( + "fmt" + + "git.fsdpf.net/go/db/v2/exec" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type InsertDataset struct { + dialect SQLDialect + clauses exp.InsertClauses + isPrepared prepared + queryFactory exec.QueryFactory + err error +} + +var ErrUnsupportedIntoType = errors.New("unsupported table type, a string or identifier expression is required") + +// used internally by database to create a database with a specific adapter +func newInsertDataset(d string, queryFactory exec.QueryFactory) *InsertDataset { + return &InsertDataset{ + clauses: exp.NewInsertClauses(), + dialect: GetDialect(d), + queryFactory: queryFactory, + } +} + +// Creates a new InsertDataset for the provided table. Using this method will only allow you +// to create SQL user Database#From to create an InsertDataset with query capabilities +func Insert(table interface{}) *InsertDataset { + return newInsertDataset("default", nil).Into(table) +} + +// Set the parameter interpolation behavior. See examples +// +// prepared: If true the dataset WILL NOT interpolate the parameters. +func (id *InsertDataset) Prepared(prepared bool) *InsertDataset { + ret := id.copy(id.clauses) + ret.isPrepared = preparedFromBool(prepared) + return ret +} + +func (id *InsertDataset) IsPrepared() bool { + return id.isPrepared.Bool() +} + +// Sets the adapter used to serialize values and create the SQL statement +func (id *InsertDataset) WithDialect(dl string) *InsertDataset { + ds := id.copy(id.GetClauses()) + ds.dialect = GetDialect(dl) + return ds +} + +// Returns the current adapter on the dataset +func (id *InsertDataset) Dialect() SQLDialect { + return id.dialect +} + +// Returns the current adapter on the dataset +func (id *InsertDataset) SetDialect(dialect SQLDialect) *InsertDataset { + cd := id.copy(id.GetClauses()) + cd.dialect = dialect + return cd +} + +func (id *InsertDataset) Expression() exp.Expression { + return id +} + +// Clones the dataset +func (id *InsertDataset) Clone() exp.Expression { + return id.copy(id.clauses) +} + +// Returns the current clauses on the dataset. +func (id *InsertDataset) GetClauses() exp.InsertClauses { + return id.clauses +} + +// used interally to copy the dataset +func (id *InsertDataset) copy(clauses exp.InsertClauses) *InsertDataset { + return &InsertDataset{ + dialect: id.dialect, + clauses: clauses, + isPrepared: id.isPrepared, + queryFactory: id.queryFactory, + err: id.err, + } +} + +// Creates a WITH clause for a common table expression (CTE). +// +// The name will be available to SELECT from in the associated query; and can optionally +// contain a list of column names "name(col1, col2, col3)". +// +// The name will refer to the results of the specified subquery. +func (id *InsertDataset) With(name string, subquery exp.Expression) *InsertDataset { + return id.copy(id.clauses.CommonTablesAppend(exp.NewCommonTableExpression(false, name, subquery))) +} + +// Creates a WITH RECURSIVE clause for a common table expression (CTE) +// +// The name will be available to SELECT from in the associated query; and must +// contain a list of column names "name(col1, col2, col3)" for a recursive clause. +// +// The name will refer to the results of the specified subquery. The subquery for +// a recursive query will always end with a UNION or UNION ALL with a clause that +// refers to the CTE by name. +func (id *InsertDataset) WithRecursive(name string, subquery exp.Expression) *InsertDataset { + return id.copy(id.clauses.CommonTablesAppend(exp.NewCommonTableExpression(true, name, subquery))) +} + +// Sets the table to insert INTO. This return a new dataset with the original table replaced. See examples. +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Expression: Any valid expression (IdentifierExpression, AliasedExpression, Literal, etc.) +func (id *InsertDataset) Into(into interface{}) *InsertDataset { + switch t := into.(type) { + case exp.Expression: + return id.copy(id.clauses.SetInto(t)) + case string: + return id.copy(id.clauses.SetInto(exp.ParseIdentifier(t))) + default: + panic(ErrUnsupportedIntoType) + } +} + +// Sets the Columns to insert into +func (id *InsertDataset) Cols(cols ...interface{}) *InsertDataset { + return id.copy(id.clauses.SetCols(exp.NewColumnListExpression(cols...))) +} + +// Clears the Columns to insert into +func (id *InsertDataset) ClearCols() *InsertDataset { + return id.copy(id.clauses.SetCols(nil)) +} + +// Adds columns to the current list of columns clause. See examples +func (id *InsertDataset) ColsAppend(cols ...interface{}) *InsertDataset { + return id.copy(id.clauses.ColsAppend(exp.NewColumnListExpression(cols...))) +} + +// Adds a subquery to the insert. See examples. +func (id *InsertDataset) FromQuery(from exp.AppendableExpression) *InsertDataset { + if sds, ok := from.(*SelectDataset); ok { + if sds.dialect != GetDialect("default") && id.Dialect() != sds.dialect { + panic( + fmt.Errorf( + "incompatible dialects for INSERT (%q) and SELECT (%q)", + id.dialect.Dialect(), sds.dialect.Dialect(), + ), + ) + } + sds.dialect = id.dialect + } + return id.copy(id.clauses.SetFrom(from)) +} + +// Manually set values to insert See examples. +func (id *InsertDataset) Vals(vals ...[]interface{}) *InsertDataset { + return id.copy(id.clauses.ValsAppend(vals)) +} + +// Clears the values. See examples. +func (id *InsertDataset) ClearVals() *InsertDataset { + return id.copy(id.clauses.SetVals(nil)) +} + +// Insert rows. Rows can be a map, db.Record or struct. See examples. +func (id *InsertDataset) Rows(rows ...interface{}) *InsertDataset { + return id.copy(id.clauses.SetRows(rows)) +} + +// Clears the rows for this insert dataset. See examples. +func (id *InsertDataset) ClearRows() *InsertDataset { + return id.copy(id.clauses.SetRows(nil)) +} + +// Adds a RETURNING clause to the dataset if the adapter supports it See examples. +func (id *InsertDataset) Returning(returning ...interface{}) *InsertDataset { + return id.copy(id.clauses.SetReturning(exp.NewColumnListExpression(returning...))) +} + +// Adds an (ON CONFLICT/ON DUPLICATE KEY) clause to the dataset if the dialect supports it. See examples. +func (id *InsertDataset) OnConflict(conflict exp.ConflictExpression) *InsertDataset { + return id.copy(id.clauses.SetOnConflict(conflict)) +} + +// Clears the on conflict clause. See example +func (id *InsertDataset) ClearOnConflict() *InsertDataset { + return id.OnConflict(nil) +} + +// Get any error that has been set or nil if no error has been set. +func (id *InsertDataset) Error() error { + return id.err +} + +// Set an error on the dataset if one has not already been set. This error will be returned by a future call to Error +// or as part of ToSQL. This can be used by end users to record errors while building up queries without having to +// track those separately. +func (id *InsertDataset) SetError(err error) *InsertDataset { + if id.err == nil { + id.err = err + } + + return id +} + +// Generates the default INSERT statement. If Prepared has been called with true then the statement will not be +// interpolated. See examples. When using structs you may specify a column to be skipped in the insert, (e.g. id) by +// specifying a ff tag with `skipinsert` +// +// type Item struct{ +// Id uint32 `db:"id" ff:"skipinsert"` +// Name string `db:"name"` +// } +// +// rows: variable number arguments of either map[string]interface, Record, struct, or a single slice argument of the +// accepted types. +// +// Errors: +// - There is no INTO clause +// - Different row types passed in, all rows must be of the same type +// - Maps with different numbers of K/V pairs +// - Rows of different lengths, (i.e. (Record{"name": "a"}, Record{"name": "a", "age": 10}) +// - Error generating SQL +func (id *InsertDataset) ToSQL() (sql string, params []interface{}, err error) { + return id.insertSQLBuilder().ToSQL() +} + +// Appends this Dataset's INSERT statement to the SQLBuilder +// This is used internally when using inserts in CTEs +func (id *InsertDataset) AppendSQL(b sb.SQLBuilder) { + if id.err != nil { + b.SetError(id.err) + return + } + id.dialect.ToInsertSQL(b, id.GetClauses()) +} + +func (id *InsertDataset) GetAs() exp.IdentifierExpression { + return id.clauses.Alias() +} + +// Sets the alias for this dataset. This is typically used when using a Dataset as MySQL upsert +func (id *InsertDataset) As(alias string) *InsertDataset { + return id.copy(id.clauses.SetAlias(T(alias))) +} + +func (id *InsertDataset) ReturnsColumns() bool { + return id.clauses.HasReturning() +} + +// Generates the INSERT sql, and returns an QueryExecutor struct with the sql set to the INSERT statement +// +// db.Insert("test").Rows(Record{"name":"Bob"}).Executor().Exec() +func (id *InsertDataset) Executor() exec.QueryExecutor { + return id.queryFactory.FromSQLBuilder(id.insertSQLBuilder()) +} + +func (id *InsertDataset) insertSQLBuilder() sb.SQLBuilder { + buf := sb.NewSQLBuilder(id.isPrepared.Bool()) + if id.err != nil { + return buf.SetError(id.err) + } + id.dialect.ToInsertSQL(buf, id.clauses) + return buf +} diff --git a/insert_dataset_example_test.go b/insert_dataset_example_test.go new file mode 100644 index 0000000..97648d2 --- /dev/null +++ b/insert_dataset_example_test.go @@ -0,0 +1,779 @@ +//nolint:lll // SQL statements are long +package db_test + +import ( + "database/sql" + "fmt" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + _ "git.fsdpf.net/go/db/v2/dialect/postgres" +) + +func ExampleInsert_dbv2Record() { + ds := dbv2.Insert("user").Rows( + dbv2.Record{"first_name": "Greg", "last_name": "Farley"}, + dbv2.Record{"first_name": "Jimmy", "last_name": "Stewart"}, + dbv2.Record{"first_name": "Jeff", "last_name": "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsert_map() { + ds := dbv2.Insert("user").Rows( + map[string]interface{}{"first_name": "Greg", "last_name": "Farley"}, + map[string]interface{}{"first_name": "Jimmy", "last_name": "Stewart"}, + map[string]interface{}{"first_name": "Jeff", "last_name": "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsert_struct() { + type User struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + ds := dbv2.Insert("user").Rows( + User{FirstName: "Greg", LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsert_prepared() { + ds := dbv2.Insert("user").Prepared(true).Rows( + dbv2.Record{"first_name": "Greg", "last_name": "Farley"}, + dbv2.Record{"first_name": "Jimmy", "last_name": "Stewart"}, + dbv2.Record{"first_name": "Jeff", "last_name": "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") VALUES (?, ?), (?, ?), (?, ?) [Greg Farley Jimmy Stewart Jeff Jeffers] +} + +func ExampleInsert_fromQuery() { + ds := dbv2.Insert("user").Prepared(true). + FromQuery(dbv2.From("other_table")) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" SELECT * FROM "other_table" [] +} + +func ExampleInsert_fromQueryWithCols() { + ds := dbv2.Insert("user").Prepared(true). + Cols("first_name", "last_name"). + FromQuery(dbv2.From("other_table").Select("fn", "ln")) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") SELECT "fn", "ln" FROM "other_table" [] +} + +func ExampleInsert_colsAndVals() { + ds := dbv2.Insert("user"). + Cols("first_name", "last_name"). + Vals( + dbv2.Vals{"Greg", "Farley"}, + dbv2.Vals{"Jimmy", "Stewart"}, + dbv2.Vals{"Jeff", "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("first_name", "last_name") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_Executor_withRecord() { + db := getDB() + insert := db.Insert("test_user").Rows( + dbv2.Record{"first_name": "Jed", "last_name": "Riley", "created": time.Now()}, + ).Executor() + if _, err := insert.Exec(); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Println("Inserted 1 user") + } + + users := []dbv2.Record{ + {"first_name": "Greg", "last_name": "Farley", "created": time.Now()}, + {"first_name": "Jimmy", "last_name": "Stewart", "created": time.Now()}, + {"first_name": "Jeff", "last_name": "Jeffers", "created": time.Now()}, + } + if _, err := db.Insert("test_user").Rows(users).Executor().Exec(); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("Inserted %d users", len(users)) + } + + // Output: + // Inserted 1 user + // Inserted 3 users +} + +func ExampleInsertDataset_Executor_recordReturning() { + db := getDB() + + type User struct { + ID sql.NullInt64 `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Created time.Time `db:"created"` + } + + insert := db.Insert("test_user").Returning(dbv2.C("id")).Rows( + dbv2.Record{"first_name": "Jed", "last_name": "Riley", "created": time.Now()}, + ).Executor() + var id int64 + if _, err := insert.ScanVal(&id); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("Inserted 1 user id:=%d\n", id) + } + + insert = db.Insert("test_user").Returning(dbv2.Star()).Rows([]dbv2.Record{ + {"first_name": "Greg", "last_name": "Farley", "created": time.Now()}, + {"first_name": "Jimmy", "last_name": "Stewart", "created": time.Now()}, + {"first_name": "Jeff", "last_name": "Jeffers", "created": time.Now()}, + }).Executor() + var insertedUsers []User + if err := insert.ScanStructs(&insertedUsers); err != nil { + fmt.Println(err.Error()) + } else { + for _, u := range insertedUsers { + fmt.Printf("Inserted user: [ID=%d], [FirstName=%+s] [LastName=%s]\n", u.ID.Int64, u.FirstName, u.LastName) + } + } + + // Output: + // Inserted 1 user id:=5 + // Inserted user: [ID=6], [FirstName=Greg] [LastName=Farley] + // Inserted user: [ID=7], [FirstName=Jimmy] [LastName=Stewart] + // Inserted user: [ID=8], [FirstName=Jeff] [LastName=Jeffers] +} + +func ExampleInsertDataset_Executor_scanStructs() { + db := getDB() + + type User struct { + ID sql.NullInt64 `db:"id" ff:"skipinsert"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Created time.Time `db:"created"` + } + + insert := db.Insert("test_user").Returning("id").Rows( + User{FirstName: "Jed", LastName: "Riley"}, + ).Executor() + var id int64 + if _, err := insert.ScanVal(&id); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("Inserted 1 user id:=%d\n", id) + } + + insert = db.Insert("test_user").Returning(dbv2.Star()).Rows([]User{ + {FirstName: "Greg", LastName: "Farley", Created: time.Now()}, + {FirstName: "Jimmy", LastName: "Stewart", Created: time.Now()}, + {FirstName: "Jeff", LastName: "Jeffers", Created: time.Now()}, + }).Executor() + var insertedUsers []User + if err := insert.ScanStructs(&insertedUsers); err != nil { + fmt.Println(err.Error()) + } else { + for _, u := range insertedUsers { + fmt.Printf("Inserted user: [ID=%d], [FirstName=%+s] [LastName=%s]\n", u.ID.Int64, u.FirstName, u.LastName) + } + } + + // Output: + // Inserted 1 user id:=5 + // Inserted user: [ID=6], [FirstName=Greg] [LastName=Farley] + // Inserted user: [ID=7], [FirstName=Jimmy] [LastName=Stewart] + // Inserted user: [ID=8], [FirstName=Jeff] [LastName=Jeffers] +} + +func ExampleInsertDataset_FromQuery() { + insertSQL, _, _ := dbv2.Insert("test"). + FromQuery(dbv2.From("test2").Where(dbv2.C("age").Gt(10))). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" SELECT * FROM "test2" WHERE ("age" > 10) +} + +func ExampleInsertDataset_ToSQL() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + insertSQL, args, _ := dbv2.Insert("items").Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items").Rows( + dbv2.Record{"name": "Test1", "address": "111 Test Addr"}, + dbv2.Record{"name": "Test2", "address": "112 Test Addr"}, + ).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items").Rows( + []item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.From("items").Insert().Rows( + []dbv2.Record{ + {"name": "Test1", "address": "111 Test Addr"}, + {"name": "Test2", "address": "112 Test Addr"}, + }).ToSQL() + fmt.Println(insertSQL, args) + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] +} + +func ExampleInsertDataset_Prepared() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + + insertSQL, args, _ := dbv2.Insert("items").Prepared(true).Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items").Prepared(true).Rows( + dbv2.Record{"name": "Test1", "address": "111 Test Addr"}, + dbv2.Record{"name": "Test2", "address": "112 Test Addr"}, + ).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items").Prepared(true).Rows( + []item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }).ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items").Prepared(true).Rows( + []dbv2.Record{ + {"name": "Test1", "address": "111 Test Addr"}, + {"name": "Test2", "address": "112 Test Addr"}, + }).ToSQL() + fmt.Println(insertSQL, args) + // Output: + // INSERT INTO "items" ("address", "name") VALUES (?, ?), (?, ?) [111 Test Addr Test1 112 Test Addr Test2] + // INSERT INTO "items" ("address", "name") VALUES (?, ?), (?, ?) [111 Test Addr Test1 112 Test Addr Test2] + // INSERT INTO "items" ("address", "name") VALUES (?, ?), (?, ?) [111 Test Addr Test1 112 Test Addr Test2] + // INSERT INTO "items" ("address", "name") VALUES (?, ?), (?, ?) [111 Test Addr Test1 112 Test Addr Test2] +} + +func ExampleInsertDataset_ClearRows() { + type item struct { + ID uint32 `ff:"skipinsert"` + Address string + Name string + } + ds := dbv2.Insert("items").Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ) + insertSQL, args, _ := ds.ClearRows().ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" DEFAULT VALUES [] +} + +func ExampleInsertDataset_Rows_withNoDbTag() { + type item struct { + ID uint32 `ff:"skipinsert"` + Address string + Name string + } + insertSQL, args, _ := dbv2.Insert("items"). + Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ). + ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items"). + Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ). + ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items"). + Rows([]item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] +} + +func ExampleInsertDataset_Rows_withDbv2SkipInsertTag() { + type item struct { + ID uint32 `ff:"skipinsert"` + Address string + Name string `ff:"skipinsert"` + } + insertSQL, args, _ := dbv2.Insert("items"). + Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ). + ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items"). + Rows([]item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] + // INSERT INTO "items" ("address") VALUES ('111 Test Addr'), ('112 Test Addr') [] +} + +func ExampleInsertDataset_Rows_withOmitNilTag() { + type item struct { + FirstName string `db:"first_name" ff:"omitnil"` + LastName string `db:"last_name" ff:"omitnil"` + Address1 *string `db:"address1" ff:"omitnil"` + Address2 *string `db:"address2" ff:"omitnil"` + Address3 *string `db:"address3" ff:"omitnil"` + } + address1 := "111 Test Addr" + var emptyString string + i := item{ + FirstName: "Test First Name", + LastName: "", + Address1: &address1, + Address2: &emptyString, + Address3: nil, // will omit nil pointer + } + + insertSQL, args, _ := dbv2.Insert("items").Rows(i).ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address1", "address2", "first_name", "last_name") VALUES ('111 Test Addr', '', 'Test First Name', '') [] +} + +func ExampleInsertDataset_Rows_withOmitEmptyTag() { + type item struct { + FirstName string `db:"first_name" ff:"omitempty"` + LastName string `db:"last_name" ff:"omitempty"` + Address1 *string `db:"address1" ff:"omitempty"` + Address2 *string `db:"address2" ff:"omitempty"` + Address3 *string `db:"address3" ff:"omitempty"` + } + address1 := "112 Test Addr" + var emptyString string + i := item{ + FirstName: "Test First Name", + LastName: "", // will omit zero field + Address1: &address1, + Address2: &emptyString, + Address3: nil, // will omit nil pointer + } + insertSQL, args, _ := dbv2.Insert("items").Rows(i).ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address1", "address2", "first_name") VALUES ('112 Test Addr', '', 'Test First Name') [] +} + +func ExampleInsertDataset_Rows_withOmitEmptyTag_Valuer() { + type item struct { + FirstName sql.NullString `db:"first_name" ff:"omitempty"` + MiddleName sql.NullString `db:"middle_name" ff:"omitempty"` + LastName sql.NullString `db:"last_name" ff:"omitempty"` + Address1 *sql.NullString `db:"address1" ff:"omitempty"` + Address2 *sql.NullString `db:"address2" ff:"omitempty"` + Address3 *sql.NullString `db:"address3" ff:"omitempty"` + Address4 *sql.NullString `db:"address4" ff:"omitempty"` + } + i := item{ + FirstName: sql.NullString{Valid: true, String: "Test First Name"}, + MiddleName: sql.NullString{Valid: true, String: ""}, + LastName: sql.NullString{}, // will omit zero valuer struct + Address1: &sql.NullString{Valid: true, String: "Test Address 1"}, + Address2: &sql.NullString{Valid: true, String: ""}, + Address3: &sql.NullString{}, + Address4: nil, // will omit nil pointer + } + insertSQL, args, _ := dbv2.Insert("items").Rows(i).ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address1", "address2", "address3", "first_name", "middle_name") VALUES ('Test Address 1', '', NULL, 'Test First Name', '') [] +} + +func ExampleInsertDataset_Rows_withDbv2DefaultIfEmptyTag() { + type item struct { + ID uint32 `ff:"skipinsert"` + Address string + Name string `ff:"defaultifempty"` + } + insertSQL, args, _ := dbv2.Insert("items"). + Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Address: "112 Test Addr"}, + ). + ToSQL() + fmt.Println(insertSQL, args) + + insertSQL, args, _ = dbv2.Insert("items"). + Rows([]item{ + {Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', DEFAULT) [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', DEFAULT), ('112 Test Addr', 'Test2') [] +} + +func ExampleInsertDataset_Rows_withEmbeddedStruct() { + type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` + } + type User struct { + Address + FirstName string + LastName string + } + ds := dbv2.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("address_state", "address_street", "firstname", "lastname") VALUES ('NY', '111 Street', 'Greg', 'Farley'), ('NY', '211 Street', 'Jimmy', 'Stewart'), ('NY', '311 Street', 'Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_Rows_withIgnoredEmbedded() { + type Address struct { + Street string + State string + } + type User struct { + Address `db:"-"` + FirstName string + LastName string + } + ds := dbv2.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_Rows_withNilEmbeddedPointer() { + type Address struct { + Street string + State string + } + type User struct { + *Address + FirstName string + LastName string + } + ds := dbv2.Insert("user").Rows( + User{FirstName: "Greg", LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_ClearOnConflict() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + ds := dbv2.Insert("items").OnConflict(dbv2.DoNothing()) + insertSQL, args, _ := ds.ClearOnConflict().Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ).ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] +} + +func ExampleInsertDataset_OnConflict_doNothing() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + insertSQL, args, _ := dbv2.Insert("items").Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ).OnConflict(dbv2.DoNothing()).ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') ON CONFLICT DO NOTHING [] +} + +func ExampleInsertDataset_OnConflict_doUpdate() { + insertSQL, args, _ := dbv2.Insert("items"). + Rows( + dbv2.Record{"name": "Test1", "address": "111 Test Addr"}, + dbv2.Record{"name": "Test2", "address": "112 Test Addr"}, + ). + OnConflict(dbv2.DoUpdate("key", dbv2.Record{"updated": dbv2.L("NOW()")})). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') ON CONFLICT (key) DO UPDATE SET "updated"=NOW() [] +} + +func ExampleInsertDataset_OnConflict_doUpdateWithWhere() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + insertSQL, args, _ := dbv2.Insert("items"). + Rows([]item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }). + OnConflict(dbv2.DoUpdate( + "key", + dbv2.Record{"updated": dbv2.L("NOW()")}).Where(dbv2.C("allow_update").IsTrue()), + ). + ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') ON CONFLICT (key) DO UPDATE SET "updated"=NOW() WHERE ("allow_update" IS TRUE) [] +} + +func ExampleInsertDataset_Returning() { + insertSQL, _, _ := dbv2.Insert("test"). + Returning("id"). + Rows(dbv2.Record{"a": "a", "b": "b"}). + ToSQL() + fmt.Println(insertSQL) + insertSQL, _, _ = dbv2.Insert("test"). + Returning(dbv2.T("test").All()). + Rows(dbv2.Record{"a": "a", "b": "b"}). + ToSQL() + fmt.Println(insertSQL) + insertSQL, _, _ = dbv2.Insert("test"). + Returning("a", "b"). + Rows(dbv2.Record{"a": "a", "b": "b"}). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" ("a", "b") VALUES ('a', 'b') RETURNING "id" + // INSERT INTO "test" ("a", "b") VALUES ('a', 'b') RETURNING "test".* + // INSERT INTO "test" ("a", "b") VALUES ('a', 'b') RETURNING "a", "b" +} + +func ExampleInsertDataset_With() { + insertSQL, _, _ := dbv2.Insert("foo"). + With("other", dbv2.From("bar").Where(dbv2.C("id").Gt(10))). + FromQuery(dbv2.From("other")). + ToSQL() + fmt.Println(insertSQL) + + // Output: + // WITH other AS (SELECT * FROM "bar" WHERE ("id" > 10)) INSERT INTO "foo" SELECT * FROM "other" +} + +func ExampleInsertDataset_WithRecursive() { + insertSQL, _, _ := dbv2.Insert("num_count"). + WithRecursive("nums(x)", + dbv2.From().Select(dbv2.L("1")). + UnionAll(dbv2.From("nums"). + Select(dbv2.L("x+1")).Where(dbv2.C("x").Lt(5))), + ). + FromQuery(dbv2.From("nums")). + ToSQL() + fmt.Println(insertSQL) + // Output: + // WITH RECURSIVE nums(x) AS (SELECT 1 UNION ALL (SELECT x+1 FROM "nums" WHERE ("x" < 5))) INSERT INTO "num_count" SELECT * FROM "nums" +} + +func ExampleInsertDataset_Into() { + ds := dbv2.Insert("test") + insertSQL, _, _ := ds.Into("test2").Rows(dbv2.Record{"first_name": "bob", "last_name": "yukon"}).ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test2" ("first_name", "last_name") VALUES ('bob', 'yukon') +} + +func ExampleInsertDataset_Into_aliased() { + ds := dbv2.Insert("test") + insertSQL, _, _ := ds. + Into(dbv2.T("test").As("t")). + Rows(dbv2.Record{"first_name": "bob", "last_name": "yukon"}). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" AS "t" ("first_name", "last_name") VALUES ('bob', 'yukon') +} + +func ExampleInsertDataset_Cols() { + insertSQL, _, _ := dbv2.Insert("test"). + Cols("a", "b", "c"). + Vals( + []interface{}{"a1", "b1", "c1"}, + []interface{}{"a2", "b1", "c1"}, + []interface{}{"a3", "b1", "c1"}, + ). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" ("a", "b", "c") VALUES ('a1', 'b1', 'c1'), ('a2', 'b1', 'c1'), ('a3', 'b1', 'c1') +} + +func ExampleInsertDataset_Cols_withFromQuery() { + insertSQL, _, _ := dbv2.Insert("test"). + Cols("a", "b", "c"). + FromQuery(dbv2.From("foo").Select("d", "e", "f")). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" ("a", "b", "c") SELECT "d", "e", "f" FROM "foo" +} + +func ExampleInsertDataset_ColsAppend() { + insertSQL, _, _ := dbv2.Insert("test"). + Cols("a", "b"). + ColsAppend("c"). + Vals( + []interface{}{"a1", "b1", "c1"}, + []interface{}{"a2", "b1", "c1"}, + []interface{}{"a3", "b1", "c1"}, + ). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" ("a", "b", "c") VALUES ('a1', 'b1', 'c1'), ('a2', 'b1', 'c1'), ('a3', 'b1', 'c1') +} + +func ExampleInsertDataset_ClearCols() { + ds := dbv2.Insert("test").Cols("a", "b", "c") + insertSQL, _, _ := ds.ClearCols().Cols("other_a", "other_b", "other_c"). + FromQuery(dbv2.From("foo").Select("d", "e", "f")). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" ("other_a", "other_b", "other_c") SELECT "d", "e", "f" FROM "foo" +} + +func ExampleInsertDataset_Vals() { + insertSQL, _, _ := dbv2.Insert("test"). + Cols("a", "b", "c"). + Vals( + []interface{}{"a1", "b1", "c1"}, + []interface{}{"a2", "b2", "c2"}, + []interface{}{"a3", "b3", "c3"}, + ). + ToSQL() + fmt.Println(insertSQL) + + insertSQL, _, _ = dbv2.Insert("test"). + Cols("a", "b", "c"). + Vals([]interface{}{"a1", "b1", "c1"}). + Vals([]interface{}{"a2", "b2", "c2"}). + Vals([]interface{}{"a3", "b3", "c3"}). + ToSQL() + fmt.Println(insertSQL) + + // Output: + // INSERT INTO "test" ("a", "b", "c") VALUES ('a1', 'b1', 'c1'), ('a2', 'b2', 'c2'), ('a3', 'b3', 'c3') + // INSERT INTO "test" ("a", "b", "c") VALUES ('a1', 'b1', 'c1'), ('a2', 'b2', 'c2'), ('a3', 'b3', 'c3') +} + +func ExampleInsertDataset_ClearVals() { + insertSQL, _, _ := dbv2.Insert("test"). + Cols("a", "b", "c"). + Vals( + []interface{}{"a1", "b1", "c1"}, + []interface{}{"a2", "b1", "c1"}, + []interface{}{"a3", "b1", "c1"}, + ). + ClearVals(). + ToSQL() + fmt.Println(insertSQL) + + insertSQL, _, _ = dbv2.Insert("test"). + Cols("a", "b", "c"). + Vals([]interface{}{"a1", "b1", "c1"}). + Vals([]interface{}{"a2", "b2", "c2"}). + Vals([]interface{}{"a3", "b3", "c3"}). + ClearVals(). + ToSQL() + fmt.Println(insertSQL) + // Output: + // INSERT INTO "test" DEFAULT VALUES + // INSERT INTO "test" DEFAULT VALUES +} diff --git a/insert_dataset_test.go b/insert_dataset_test.go new file mode 100644 index 0000000..a0f1da1 --- /dev/null +++ b/insert_dataset_test.go @@ -0,0 +1,618 @@ +package db_test + +import ( + "testing" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/mocks" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ( + insertTestCase struct { + ds *dbv2.InsertDataset + clauses exp.InsertClauses + } + insertDatasetSuite struct { + suite.Suite + } +) + +func (ids *insertDatasetSuite) assertCases(cases ...insertTestCase) { + for _, s := range cases { + ids.Equal(s.clauses, s.ds.GetClauses()) + } +} + +func (ids *insertDatasetSuite) TestInsert() { + ds := dbv2.Insert("test") + ids.IsType(&dbv2.InsertDataset{}, ds) + ids.Implements((*exp.Expression)(nil), ds) + ids.Implements((*exp.AppendableExpression)(nil), ds) +} + +func (ids *insertDatasetSuite) TestClone() { + ds := dbv2.Insert("test") + ids.Equal(ds.Clone(), ds) +} + +func (ids *insertDatasetSuite) TestExpression() { + ds := dbv2.Insert("test") + ids.Equal(ds.Expression(), ds) +} + +func (ids *insertDatasetSuite) TestDialect() { + ds := dbv2.Insert("test") + ids.NotNil(ds.Dialect()) +} + +func (ids *insertDatasetSuite) TestWithDialect() { + ds := dbv2.Insert("test") + md := new(mocks.SQLDialect) + ds = ds.SetDialect(md) + + dialect := dbv2.GetDialect("default") + dialectDs := ds.WithDialect("default") + ids.Equal(md, ds.Dialect()) + ids.Equal(dialect, dialectDs.Dialect()) +} + +func (ids *insertDatasetSuite) TestPrepared() { + ds := dbv2.Insert("test") + preparedDs := ds.Prepared(true) + ids.True(preparedDs.IsPrepared()) + ids.False(ds.IsPrepared()) + // should apply the prepared to any datasets created from the root + ids.True(preparedDs.Returning(dbv2.C("col")).IsPrepared()) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + // should be prepared by default + ds = dbv2.Insert("test") + ids.True(ds.IsPrepared()) +} + +func (ids *insertDatasetSuite) TestGetClauses() { + ds := dbv2.Insert("test") + ce := exp.NewInsertClauses().SetInto(dbv2.I("test")) + ids.Equal(ce, ds.GetClauses()) +} + +func (ids *insertDatasetSuite) TestWith() { + from := dbv2.From("cte") + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.With("test-cte", from), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestWithRecursive() { + from := dbv2.From("cte") + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.WithRecursive("test-cte", from), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(true, "test-cte", from)), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestInto() { + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.Into("items2"), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items2")), + }, + insertTestCase{ + ds: bd.Into(dbv2.L("items2")), + clauses: exp.NewInsertClauses().SetInto(dbv2.L("items2")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) + + ids.PanicsWithValue(dbv2.ErrUnsupportedIntoType, func() { + bd.Into(true) + }) +} + +func (ids *insertDatasetSuite) TestCols() { + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.Cols("a", "b"), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetCols(exp.NewColumnListExpression("a", "b")), + }, + insertTestCase{ + ds: bd.Cols("a", "b").Cols("c", "d"), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetCols(exp.NewColumnListExpression("c", "d")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestClearCols() { + bd := dbv2.Insert("items").Cols("a", "b") + ids.assertCases( + insertTestCase{ + ds: bd.ClearCols(), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetCols(exp.NewColumnListExpression("a", "b")), + }, + ) +} + +func (ids *insertDatasetSuite) TestColsAppend() { + bd := dbv2.Insert("items").Cols("a") + ids.assertCases( + insertTestCase{ + ds: bd.ColsAppend("b"), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetCols(exp.NewColumnListExpression("a", "b")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetCols(exp.NewColumnListExpression("a")), + }, + ) +} + +func (ids *insertDatasetSuite) TestFromQuery() { + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.FromQuery(dbv2.From("other_items").Where(dbv2.C("b").Gt(10))), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetFrom(dbv2.From("other_items").Where(dbv2.C("b").Gt(10))), + }, + insertTestCase{ + ds: bd.FromQuery(dbv2.From("other_items").Where(dbv2.C("b").Gt(10))).Cols("a", "b"), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetCols(exp.NewColumnListExpression("a", "b")). + SetFrom(dbv2.From("other_items").Where(dbv2.C("b").Gt(10))), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestFromQueryDialectInheritance() { + md := new(mocks.SQLDialect) + md.On("Dialect").Return("dialect") + + ids.Run("ok, default dialect is replaced with insert dialect", func() { + bd := dbv2.Insert("items").SetDialect(md).FromQuery(dbv2.From("other_items")) + ids.Require().Equal(md, bd.GetClauses().From().(*dbv2.SelectDataset).Dialect()) + }) + + ids.Run("ok, insert and select dialects coincide", func() { + bd := dbv2.Insert("items").SetDialect(md).FromQuery(dbv2.From("other_items").SetDialect(md)) + ids.Require().Equal(md, bd.GetClauses().From().(*dbv2.SelectDataset).Dialect()) + }) + + ids.Run("ok, insert and select dialects are default", func() { + bd := dbv2.Insert("items").FromQuery(dbv2.From("other_items")) + ids.Require().Equal(dbv2.GetDialect("default"), bd.GetClauses().From().(*dbv2.SelectDataset).Dialect()) + }) + + ids.Run("panic, insert and select dialects are different", func() { + defer func() { + r := recover() + if r == nil { + ids.Fail("there should be a panic") + } + ids.Require().Equal( + "incompatible dialects for INSERT (\"dialect\") and SELECT (\"other_dialect\")", + r.(error).Error(), + ) + }() + + otherDialect := new(mocks.SQLDialect) + otherDialect.On("Dialect").Return("other_dialect") + dbv2.Insert("items").SetDialect(md).FromQuery(dbv2.From("otherItems").SetDialect(otherDialect)) + }) +} + +func (ids *insertDatasetSuite) TestVals() { + val1 := []interface{}{ + "a", "b", + } + val2 := []interface{}{ + "c", "d", + } + + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.Vals(val1), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetVals([][]interface{}{val1}), + }, + insertTestCase{ + ds: bd.Vals(val1, val2), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetVals([][]interface{}{val1, val2}), + }, + insertTestCase{ + ds: bd.Vals(val1).Vals(val2), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetVals([][]interface{}{val1, val2}), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestClearVals() { + val := []interface{}{ + "a", "b", + } + bd := dbv2.Insert("items").Vals(val) + ids.assertCases( + insertTestCase{ + ds: bd.ClearVals(), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetVals([][]interface{}{val}), + }, + ) +} + +func (ids *insertDatasetSuite) TestRows() { + type item struct { + CreatedAt *time.Time `db:"created_at"` + } + n := time.Now() + r := item{CreatedAt: nil} + r2 := item{CreatedAt: &n} + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.Rows(r), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetRows([]interface{}{r}), + }, + insertTestCase{ + ds: bd.Rows(r).Rows(r2), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetRows([]interface{}{r2}), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestClearRows() { + type item struct { + CreatedAt *time.Time `db:"created_at"` + } + r := item{CreatedAt: nil} + bd := dbv2.Insert("items").Rows(r) + ids.assertCases( + insertTestCase{ + ds: bd.ClearRows(), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetRows([]interface{}{r}), + }, + ) +} + +func (ids *insertDatasetSuite) TestOnConflict() { + du := dbv2.DoUpdate("other_items", dbv2.Record{"a": 1}) + + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.OnConflict(nil), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + insertTestCase{ + ds: bd.OnConflict(dbv2.DoNothing()), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetOnConflict(dbv2.DoNothing()), + }, + insertTestCase{ + ds: bd.OnConflict(du), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetOnConflict(du), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestAs() { + du := dbv2.DoUpdate("other_items", dbv2.Record{"new.a": 1}) + + bd := dbv2.Insert("items").As("new") + ids.assertCases( + insertTestCase{ + ds: bd.OnConflict(nil), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")). + SetAlias(exp.NewIdentifierExpression("", "new", "")), + }, + insertTestCase{ + ds: bd.OnConflict(dbv2.DoNothing()), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")). + SetAlias(exp.NewIdentifierExpression("", "new", "")). + SetOnConflict(dbv2.DoNothing()), + }, + insertTestCase{ + ds: bd.OnConflict(du), + clauses: exp.NewInsertClauses(). + SetAlias(exp.NewIdentifierExpression("", "new", "")). + SetInto(dbv2.C("items")).SetOnConflict(du), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses(). + SetAlias(exp.NewIdentifierExpression("", "new", "")). + SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestClearOnConflict() { + du := dbv2.DoUpdate("other_items", dbv2.Record{"a": 1}) + + bd := dbv2.Insert("items").OnConflict(du) + ids.assertCases( + insertTestCase{ + ds: bd.ClearOnConflict(), + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")).SetOnConflict(du), + }, + ) +} + +func (ids *insertDatasetSuite) TestReturning() { + bd := dbv2.Insert("items") + ids.assertCases( + insertTestCase{ + ds: bd.Returning("a"), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("a")), + }, + insertTestCase{ + ds: bd.Returning(), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + insertTestCase{ + ds: bd.Returning(nil), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + insertTestCase{ + ds: bd.Returning(), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + insertTestCase{ + ds: bd.Returning("a").Returning("b"), + clauses: exp.NewInsertClauses(). + SetInto(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("b")), + }, + insertTestCase{ + ds: bd, + clauses: exp.NewInsertClauses().SetInto(dbv2.C("items")), + }, + ) +} + +func (ids *insertDatasetSuite) TestReturnsColumns() { + ds := dbv2.Insert("test") + ids.False(ds.ReturnsColumns()) + ids.True(ds.Returning("foo", "bar").ReturnsColumns()) +} + +func (ids *insertDatasetSuite) TestExecutor() { + mDB, _, err := sqlmock.New() + ids.NoError(err) + + ds := dbv2.New("mock", mDB).Insert("items"). + Rows(dbv2.Record{"address": "111 Test Addr", "name": "Test1"}) + + isql, args, err := ds.Executor().ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(`INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1')`, isql) + + isql, args, err = ds.Prepared(true).Executor().ToSQL() + ids.NoError(err) + ids.Equal([]interface{}{"111 Test Addr", "Test1"}, args) + ids.Equal(`INSERT INTO "items" ("address", "name") VALUES (?, ?)`, isql) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + isql, args, err = ds.Executor().ToSQL() + ids.NoError(err) + ids.Equal([]interface{}{"111 Test Addr", "Test1"}, args) + ids.Equal(`INSERT INTO "items" ("address", "name") VALUES (?, ?)`, isql) +} + +func (ids *insertDatasetSuite) TestInsertStruct() { + defer dbv2.SetIgnoreUntaggedFields(false) + + mDB, _, err := sqlmock.New() + ids.NoError(err) + + item := dsUntaggedTestActionItem{ + Address: "111 Test Addr", + Name: "Test1", + Untagged: "Test2", + } + + ds := dbv2.New("mock", mDB).Insert("items"). + Rows(item) + + isql, args, err := ds.Executor().ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(`INSERT INTO "items" ("address", "name", "untagged") VALUES ('111 Test Addr', 'Test1', 'Test2')`, isql) + + isql, args, err = ds.Prepared(true).Executor().ToSQL() + ids.NoError(err) + ids.Equal([]interface{}{"111 Test Addr", "Test1", "Test2"}, args) + ids.Equal(`INSERT INTO "items" ("address", "name", "untagged") VALUES (?, ?, ?)`, isql) + + dbv2.SetIgnoreUntaggedFields(true) + + isql, args, err = ds.Executor().ToSQL() + ids.NoError(err) + ids.Empty(args) + ids.Equal(`INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1')`, isql) + + isql, args, err = ds.Prepared(true).Executor().ToSQL() + ids.NoError(err) + ids.Equal([]interface{}{"111 Test Addr", "Test1"}, args) + ids.Equal(`INSERT INTO "items" ("address", "name") VALUES (?, ?)`, isql) +} + +func (ids *insertDatasetSuite) TestToSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.Insert("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToInsertSQL", sqlB, c).Return(nil).Once() + insertSQL, args, err := ds.ToSQL() + ids.Empty(insertSQL) + ids.Empty(args) + ids.Nil(err) + md.AssertExpectations(ids.T()) +} + +func (ids *insertDatasetSuite) TestToSQL_Prepared() { + md := new(mocks.SQLDialect) + ds := dbv2.Insert("test").SetDialect(md).Prepared(true) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(true) + md.On("ToInsertSQL", sqlB, c).Return(nil).Once() + insertSQL, args, err := ds.ToSQL() + ids.Empty(insertSQL) + ids.Empty(args) + ids.Nil(err) + md.AssertExpectations(ids.T()) +} + +func (ids *insertDatasetSuite) TestToSQL_ReturnedError() { + md := new(mocks.SQLDialect) + ds := dbv2.Insert("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + ee := errors.New("expected error") + md.On("ToInsertSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(ee) + }).Once() + + insertSQL, args, err := ds.ToSQL() + ids.Empty(insertSQL) + ids.Empty(args) + ids.Equal(ee, err) + md.AssertExpectations(ids.T()) +} + +func (ids *insertDatasetSuite) TestSetError() { + err1 := errors.New("error #1") + err2 := errors.New("error #2") + err3 := errors.New("error #3") + + // Verify initial error set/get works properly + md := new(mocks.SQLDialect) + ds := dbv2.Insert("test").SetDialect(md) + ds = ds.SetError(err1) + ids.Equal(err1, ds.Error()) + sql, args, err := ds.ToSQL() + ids.Empty(sql) + ids.Empty(args) + ids.Equal(err1, err) + + // Repeated SetError calls on Dataset should not overwrite the original error + ds = ds.SetError(err2) + ids.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + ids.Empty(sql) + ids.Empty(args) + ids.Equal(err1, err) + + // Builder functions should not lose the error + ds = ds.Cols("a", "b") + ids.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + ids.Empty(sql) + ids.Empty(args) + ids.Equal(err1, err) + + // Deeper errors inside SQL generation should still return original error + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToInsertSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(err3) + }).Once() + + sql, args, err = ds.ToSQL() + ids.Empty(sql) + ids.Empty(args) + ids.Equal(err1, err) +} + +func TestInsertDataset(t *testing.T) { + suite.Run(t, new(insertDatasetSuite)) +} diff --git a/internal/errors/error.go b/internal/errors/error.go new file mode 100644 index 0000000..2ad55e0 --- /dev/null +++ b/internal/errors/error.go @@ -0,0 +1,19 @@ +package errors + +import "fmt" + +type Error struct { + err string +} + +func New(message string, args ...interface{}) error { + return Error{err: "db: " + fmt.Sprintf(message, args...)} +} + +func NewEncodeError(t interface{}) error { + return Error{err: "db_encode_error: " + fmt.Sprintf("Unable to encode value %+v", t)} +} + +func (e Error) Error() string { + return e.err +} diff --git a/internal/sb/sql_builder.go b/internal/sb/sql_builder.go new file mode 100644 index 0000000..e60e457 --- /dev/null +++ b/internal/sb/sql_builder.go @@ -0,0 +1,101 @@ +package sb + +import ( + "bytes" +) + +// Builder that is composed of a bytes.Buffer. It is used internally and by adapters to build SQL statements +type ( + SQLBuilder interface { + Error() error + SetError(err error) SQLBuilder + WriteArg(i ...interface{}) SQLBuilder + Write(p []byte) SQLBuilder + WriteStrings(ss ...string) SQLBuilder + WriteRunes(r ...rune) SQLBuilder + IsPrepared() bool + CurrentArgPosition() int + ToSQL() (sql string, args []interface{}, err error) + } + sqlBuilder struct { + buf *bytes.Buffer + // True if the sql should not be interpolated + isPrepared bool + // Current Number of arguments, used by adapters that need positional placeholders + currentArgPosition int + args []interface{} + err error + } +) + +func NewSQLBuilder(isPrepared bool) SQLBuilder { + return &sqlBuilder{ + buf: &bytes.Buffer{}, + isPrepared: isPrepared, + args: make([]interface{}, 0), + currentArgPosition: 1, + } +} + +func (b *sqlBuilder) Error() error { + return b.err +} + +func (b *sqlBuilder) SetError(err error) SQLBuilder { + if b.err == nil { + b.err = err + } + return b +} + +func (b *sqlBuilder) Write(bs []byte) SQLBuilder { + if b.err == nil { + b.buf.Write(bs) + } + return b +} + +func (b *sqlBuilder) WriteStrings(ss ...string) SQLBuilder { + if b.err == nil { + for _, s := range ss { + b.buf.WriteString(s) + } + } + return b +} + +func (b *sqlBuilder) WriteRunes(rs ...rune) SQLBuilder { + if b.err == nil { + for _, r := range rs { + b.buf.WriteRune(r) + } + } + return b +} + +// Returns true if the sql is a prepared statement +func (b *sqlBuilder) IsPrepared() bool { + return b.isPrepared +} + +// Returns true if the sql is a prepared statement +func (b *sqlBuilder) CurrentArgPosition() int { + return b.currentArgPosition +} + +// Adds an argument to the builder, used when IsPrepared is false +func (b *sqlBuilder) WriteArg(i ...interface{}) SQLBuilder { + if b.err == nil { + b.currentArgPosition += len(i) + b.args = append(b.args, i...) + } + return b +} + +// Returns the sql string, and arguments. +func (b *sqlBuilder) ToSQL() (sql string, args []interface{}, err error) { + if b.err != nil { + return sql, args, b.err + } + return b.buf.String(), b.args, nil +} diff --git a/internal/tag/tags.go b/internal/tag/tags.go new file mode 100644 index 0000000..03652e9 --- /dev/null +++ b/internal/tag/tags.go @@ -0,0 +1,51 @@ +package tag + +import ( + "reflect" + "strings" +) + +// tagOptions is the string following a comma in a struct field's "json" +// tag, or the empty string. It does not include the leading comma. +type Options string + +func New(tagName string, st reflect.StructTag) Options { + return Options(st.Get(tagName)) +} + +func (o Options) Values() []string { + if string(o) == "" { + return []string{} + } + return strings.Split(string(o), ",") +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o Options) Contains(optionName string) bool { + if o.IsEmpty() { + return false + } + values := o.Values() + for _, s := range values { + if s == optionName { + return true + } + } + return false +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o Options) Equals(val string) bool { + if len(o) == 0 { + return false + } + return string(o) == val +} + +func (o Options) IsEmpty() bool { + return len(o) == 0 +} diff --git a/internal/util/column_map.go b/internal/util/column_map.go new file mode 100644 index 0000000..23cf2f6 --- /dev/null +++ b/internal/util/column_map.go @@ -0,0 +1,135 @@ +package util + +import ( + "reflect" + "sort" + "strings" + + "git.fsdpf.net/go/db/v2/internal/tag" +) + +type ( + ColumnData struct { + ColumnName string + FieldIndex []int + ShouldInsert bool + ShouldUpdate bool + DefaultIfEmpty bool + OmitNil bool + OmitEmpty bool + GoType reflect.Type + } + ColumnMap map[string]ColumnData +) + +func newColumnMap(t reflect.Type, fieldIndex []int, prefixes []string) ColumnMap { + cm, n := ColumnMap{}, t.NumField() + var subColMaps []ColumnMap + for i := 0; i < n; i++ { + f := t.Field(i) + if f.Anonymous && (f.Type.Kind() == reflect.Struct || f.Type.Kind() == reflect.Ptr) { + dbTag := tag.New("db", f.Tag) + if !dbTag.Contains("-") { + subColMaps = append(subColMaps, getStructColumnMap(&f, fieldIndex, dbTag.Values(), prefixes)) + } + } else if f.PkgPath == "" { + dbTag := tag.New("db", f.Tag) + // if PkgPath is empty then it is an exported field + columnName := getColumnName(&f, dbTag) + if !shouldIgnoreField(dbTag) { + if !implementsScanner(f.Type) { + subCm := getStructColumnMap(&f, fieldIndex, []string{columnName}, prefixes) + if len(subCm) != 0 { + subColMaps = append(subColMaps, subCm) + continue + } + } + ffTag := tag.New("ff", f.Tag) + columnName = strings.Join(append(prefixes, columnName), ".") + cm[columnName] = newColumnData(&f, columnName, fieldIndex, ffTag) + } + } + } + return cm.Merge(subColMaps) +} + +func (cm ColumnMap) Cols() []string { + structCols := make([]string, 0, len(cm)) + for key := range cm { + structCols = append(structCols, key) + } + sort.Strings(structCols) + return structCols +} + +func (cm ColumnMap) Merge(colMaps []ColumnMap) ColumnMap { + for _, subCm := range colMaps { + for key, val := range subCm { + if _, ok := cm[key]; !ok { + cm[key] = val + } + } + } + return cm +} + +func implementsScanner(t reflect.Type) bool { + if IsPointer(t.Kind()) { + t = t.Elem() + } + if reflect.PtrTo(t).Implements(scannerType) { + return true + } + if !IsStruct(t.Kind()) { + return true + } + + return false +} + +func newColumnData(f *reflect.StructField, columnName string, fieldIndex []int, ffTag tag.Options) ColumnData { + return ColumnData{ + ColumnName: columnName, + ShouldInsert: !ffTag.Contains(skipInsertTagName), + ShouldUpdate: !ffTag.Contains(skipUpdateTagName), + DefaultIfEmpty: ffTag.Contains(defaultIfEmptyTagName), + OmitNil: ffTag.Contains(omitNilTagName), + OmitEmpty: ffTag.Contains(omitEmptyTagName), + FieldIndex: concatFieldIndexes(fieldIndex, f.Index), + GoType: f.Type, + } +} + +func getStructColumnMap(f *reflect.StructField, fieldIndex []int, fieldNames, prefixes []string) ColumnMap { + subFieldIndexes := concatFieldIndexes(fieldIndex, f.Index) + subPrefixes := prefixes + subPrefixes = append(subPrefixes, fieldNames...) + if f.Type.Kind() == reflect.Ptr { + return newColumnMap(f.Type.Elem(), subFieldIndexes, subPrefixes) + } + return newColumnMap(f.Type, subFieldIndexes, subPrefixes) +} + +func getColumnName(f *reflect.StructField, dbTag tag.Options) string { + if dbTag.IsEmpty() { + return columnRenameFunction(f.Name) + } + return dbTag.Values()[0] +} + +func shouldIgnoreField(dbTag tag.Options) bool { + if dbTag.Equals("-") { + return true + } else if dbTag.IsEmpty() && ignoreUntaggedFields { + return true + } + + return false +} + +// safely concat two fieldIndex slices into one. +func concatFieldIndexes(fieldIndexPath, fieldIndex []int) []int { + fieldIndexes := make([]int, 0, len(fieldIndexPath)+len(fieldIndex)) + fieldIndexes = append(fieldIndexes, fieldIndexPath...) + return append(fieldIndexes, fieldIndex...) +} diff --git a/internal/util/reflect.go b/internal/util/reflect.go new file mode 100644 index 0000000..6fd97d2 --- /dev/null +++ b/internal/util/reflect.go @@ -0,0 +1,217 @@ +package util + +import ( + "database/sql" + "reflect" + "strings" + "sync" + + "git.fsdpf.net/go/db/v2/internal/errors" +) + +const ( + skipUpdateTagName = "skipupdate" + skipInsertTagName = "skipinsert" + defaultIfEmptyTagName = "defaultifempty" + omitNilTagName = "omitnil" + omitEmptyTagName = "omitempty" +) + +var scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem() + +func IsUint(k reflect.Kind) bool { + return (k == reflect.Uint) || + (k == reflect.Uint8) || + (k == reflect.Uint16) || + (k == reflect.Uint32) || + (k == reflect.Uint64) +} + +func IsInt(k reflect.Kind) bool { + return (k == reflect.Int) || + (k == reflect.Int8) || + (k == reflect.Int16) || + (k == reflect.Int32) || + (k == reflect.Int64) +} + +func IsFloat(k reflect.Kind) bool { + return (k == reflect.Float32) || + (k == reflect.Float64) +} + +func IsString(k reflect.Kind) bool { + return k == reflect.String +} + +func IsBool(k reflect.Kind) bool { + return k == reflect.Bool +} + +func IsSlice(k reflect.Kind) bool { + return k == reflect.Slice +} + +func IsStruct(k reflect.Kind) bool { + return k == reflect.Struct +} + +func IsInvalid(k reflect.Kind) bool { + return k == reflect.Invalid +} + +func IsPointer(k reflect.Kind) bool { + return k == reflect.Ptr +} + +func IsNil(v reflect.Value) bool { + if !v.IsValid() { + return true + } + switch v.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func: + return v.IsNil() + default: + return false + } +} + +func IsEmptyValue(v reflect.Value) bool { + return !v.IsValid() || v.IsZero() +} + +var ( + structMapCache = make(map[interface{}]ColumnMap) + structMapCacheLock = sync.Mutex{} +) + +var ( + DefaultColumnRenameFunction = strings.ToLower + columnRenameFunction = DefaultColumnRenameFunction + ignoreUntaggedFields = false +) + +func SetIgnoreUntaggedFields(ignore bool) { + // If the value here is changing, reset the struct map cache + if ignore != ignoreUntaggedFields { + ignoreUntaggedFields = ignore + + structMapCacheLock.Lock() + defer structMapCacheLock.Unlock() + + structMapCache = make(map[interface{}]ColumnMap) + } +} + +func SetColumnRenameFunction(newFunction func(string) string) { + columnRenameFunction = newFunction +} + +// GetSliceElementType returns the type for a slices elements. +func GetSliceElementType(val reflect.Value) reflect.Type { + elemType := val.Type().Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + + return elemType +} + +// AppendSliceElement will append val to slice. Handles slice of pointers and +// not pointers. Val needs to be a pointer. +func AppendSliceElement(slice, val reflect.Value) { + if slice.Type().Elem().Kind() == reflect.Ptr { + slice.Set(reflect.Append(slice, val)) + } else { + slice.Set(reflect.Append(slice, reflect.Indirect(val))) + } +} + +func GetTypeInfo(i interface{}, val reflect.Value) (reflect.Type, reflect.Kind) { + var t reflect.Type + valKind := val.Kind() + if valKind == reflect.Slice { + if reflect.ValueOf(i).Kind() == reflect.Ptr { + t = reflect.TypeOf(i).Elem().Elem() + } else { + t = reflect.TypeOf(i).Elem() + } + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + valKind = t.Kind() + } else { + t = val.Type() + } + return t, valKind +} + +func SafeGetFieldByIndex(v reflect.Value, fieldIndex []int) (result reflect.Value, isAvailable bool) { + switch len(fieldIndex) { + case 0: + return v, true + case 1: + return v.FieldByIndex(fieldIndex), true + default: + if f := reflect.Indirect(v.Field(fieldIndex[0])); f.IsValid() { + return SafeGetFieldByIndex(f, fieldIndex[1:]) + } + } + return reflect.ValueOf(nil), false +} + +func SafeSetFieldByIndex(v reflect.Value, fieldIndex []int, src interface{}) (result reflect.Value) { + v = reflect.Indirect(v) + switch len(fieldIndex) { + case 0: + return v + case 1: + f := v.FieldByIndex(fieldIndex) + srcVal := reflect.ValueOf(src) + f.Set(reflect.Indirect(srcVal)) + default: + f := v.Field(fieldIndex[0]) + switch f.Kind() { + case reflect.Ptr: + s := f + if f.IsNil() || !f.IsValid() { + s = reflect.New(f.Type().Elem()) + f.Set(s) + } + SafeSetFieldByIndex(reflect.Indirect(s), fieldIndex[1:], src) + case reflect.Struct: + SafeSetFieldByIndex(f, fieldIndex[1:], src) + default: // use the original value + } + } + return v +} + +type rowData = map[string]interface{} + +// AssignStructVals will assign the data from rd to i. +func AssignStructVals(i interface{}, rd rowData, cm ColumnMap) { + val := reflect.Indirect(reflect.ValueOf(i)) + + for name, data := range cm { + src, ok := rd[name] + if ok { + SafeSetFieldByIndex(val, data.FieldIndex, src) + } + } +} + +func GetColumnMap(i interface{}) (ColumnMap, error) { + val := reflect.Indirect(reflect.ValueOf(i)) + t, valKind := GetTypeInfo(i, val) + if valKind != reflect.Struct { + return nil, errors.New("cannot scan into this type: %v", t) // #nosec + } + + structMapCacheLock.Lock() + defer structMapCacheLock.Unlock() + if _, ok := structMapCache[t]; !ok { + structMapCache[t] = newColumnMap(t, []int{}, []string{}) + } + return structMapCache[t], nil +} diff --git a/internal/util/reflect_test.go b/internal/util/reflect_test.go new file mode 100644 index 0000000..f3eb10e --- /dev/null +++ b/internal/util/reflect_test.go @@ -0,0 +1,1346 @@ +package util_test + +import ( + "database/sql" + "reflect" + "strings" + "sync" + "testing" + "time" + + "git.fsdpf.net/go/db/v2/internal/util" + "github.com/stretchr/testify/suite" +) + +var ( + uints = []interface{}{ + uint(10), + uint8(10), + uint16(10), + uint32(10), + uint64(10), + } + ints = []interface{}{ + int(10), + int8(10), + int16(10), + int32(10), + int64(10), + } + floats = []interface{}{ + float32(3.14), + float64(3.14), + } + strs = []interface{}{ + "abc", + "", + } + bools = []interface{}{ + true, + false, + } + structs = []interface{}{ + sql.NullString{}, + } + invalids = []interface{}{ + nil, + } + pointers = []interface{}{ + &sql.NullString{}, + } +) + +type ( + TestInterface interface { + A() string + } + TestInterfaceImpl struct { + str string + } + TestStruct struct { + arr [0]string + slc []string + mp map[string]interface{} + str string + bl bool + i int + i8 int8 + i16 int16 + i32 int32 + i64 int64 + ui uint + ui8 uint8 + ui16 uint16 + ui32 uint32 + ui64 uint64 + f32 float32 + f64 float64 + intr TestInterface + ptr *sql.NullString + } +) + +func (t TestInterfaceImpl) A() string { + return t.str +} + +type reflectTest struct { + suite.Suite +} + +func (rt *reflectTest) TestIsUint() { + for _, v := range uints { + rt.True(util.IsUint(reflect.ValueOf(v).Kind())) + } + + for _, v := range ints { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsUint(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsInt() { + for _, v := range ints { + rt.True(util.IsInt(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsInt(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsFloat() { + for _, v := range floats { + rt.True(util.IsFloat(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsFloat(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsString() { + for _, v := range strs { + rt.True(util.IsString(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsString(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsBool() { + for _, v := range bools { + rt.True(util.IsBool(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsBool(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsStruct() { + for _, v := range structs { + rt.True(util.IsStruct(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsStruct(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsSlice() { + rt.True(util.IsSlice(reflect.ValueOf(uints).Kind())) + rt.True(util.IsSlice(reflect.ValueOf(ints).Kind())) + rt.True(util.IsSlice(reflect.ValueOf(floats).Kind())) + rt.True(util.IsSlice(reflect.ValueOf(structs).Kind())) + + rt.False(util.IsSlice(reflect.ValueOf(structs[0]).Kind())) +} + +func (rt *reflectTest) TestIsInvalid() { + for _, v := range invalids { + rt.True(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } + for _, v := range pointers { + rt.False(util.IsInvalid(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsPointer() { + for _, v := range pointers { + rt.True(util.IsPointer(reflect.ValueOf(v).Kind())) + } + + for _, v := range uints { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range ints { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range floats { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range bools { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range strs { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range structs { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } + for _, v := range invalids { + rt.False(util.IsPointer(reflect.ValueOf(v).Kind())) + } +} + +func (rt *reflectTest) TestIsEmptyValue_emptyValues() { + ts := TestStruct{} + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.arr))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.slc))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.mp))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.str))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.bl))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.i))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.i8))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.i16))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.i32))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.i64))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ui))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ui8))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ui16))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ui32))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ui64))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.f32))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.f64))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.intr))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts.ptr))) + rt.True(util.IsEmptyValue(reflect.ValueOf(ts))) + rt.True(util.IsNil(reflect.ValueOf(nil))) +} + +func (rt *reflectTest) TestIsEmptyValue_validValues() { + ts := TestStruct{intr: TestInterfaceImpl{"hello"}} + rt.False(util.IsEmptyValue(reflect.ValueOf([1]string{"a"}))) + rt.False(util.IsEmptyValue(reflect.ValueOf([]string{"a"}))) + rt.False(util.IsEmptyValue(reflect.ValueOf(map[string]interface{}{"a": true}))) + rt.False(util.IsEmptyValue(reflect.ValueOf("str"))) + rt.False(util.IsEmptyValue(reflect.ValueOf(true))) + rt.False(util.IsEmptyValue(reflect.ValueOf(int(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(int8(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(int16(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(int32(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(int64(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(uint(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(uint8(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(uint16(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(uint32(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(uint64(1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(float32(0.1)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(float64(0.2)))) + rt.False(util.IsEmptyValue(reflect.ValueOf(ts.intr))) + rt.False(util.IsEmptyValue(reflect.ValueOf(&TestStruct{str: "a"}))) + rt.False(util.IsEmptyValue(reflect.ValueOf(ts))) +} + +func (rt *reflectTest) TestIsNil() { + ts := TestStruct{} + rt.False(util.IsNil(reflect.ValueOf(ts.arr))) + rt.True(util.IsNil(reflect.ValueOf(ts.slc))) + rt.False(util.IsEmptyValue(reflect.ValueOf([]string{"a"}))) + rt.True(util.IsNil(reflect.ValueOf(ts.mp))) + rt.False(util.IsEmptyValue(reflect.ValueOf(map[string]interface{}{"a": true}))) + rt.False(util.IsNil(reflect.ValueOf(ts.str))) + rt.False(util.IsNil(reflect.ValueOf(ts.bl))) + rt.False(util.IsNil(reflect.ValueOf(ts.i))) + rt.False(util.IsNil(reflect.ValueOf(ts.i8))) + rt.False(util.IsNil(reflect.ValueOf(ts.i16))) + rt.False(util.IsNil(reflect.ValueOf(ts.i32))) + rt.False(util.IsNil(reflect.ValueOf(ts.i64))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui8))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui16))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui32))) + rt.False(util.IsNil(reflect.ValueOf(ts.ui64))) + rt.False(util.IsNil(reflect.ValueOf(ts.f32))) + rt.False(util.IsNil(reflect.ValueOf(ts.f64))) + rt.True(util.IsNil(reflect.ValueOf(ts.intr))) + rt.True(util.IsNil(reflect.ValueOf(ts.ptr))) + rt.False(util.IsNil(reflect.ValueOf(ts))) + rt.True(util.IsNil(reflect.ValueOf(nil))) +} + +func (rt *reflectTest) TestColumnRename() { + // different key names are used each time to circumvent the caching that happens + // it seems like a solid assumption that when people use this feature, + // they would simply set a renaming function once at startup, + // and not change between requests like this + lowerAnon := struct { + FirstLower string + LastLower string + }{} + lowerColumnMap, lowerErr := util.GetColumnMap(&lowerAnon) + rt.NoError(lowerErr) + + lowerKeys := make([]string, 0, len(lowerColumnMap)) + for key := range lowerColumnMap { + lowerKeys = append(lowerKeys, key) + } + rt.Contains(lowerKeys, "firstlower") + rt.Contains(lowerKeys, "lastlower") + + // changing rename function + util.SetColumnRenameFunction(strings.ToUpper) + + upperAnon := struct { + FirstUpper string + LastUpper string + }{} + upperColumnMap, upperErr := util.GetColumnMap(&upperAnon) + rt.NoError(upperErr) + + upperKeys := make([]string, 0, len(upperColumnMap)) + for key := range upperColumnMap { + upperKeys = append(upperKeys, key) + } + rt.Contains(upperKeys, "FIRSTUPPER") + rt.Contains(upperKeys, "LASTUPPER") + + util.SetColumnRenameFunction(util.DefaultColumnRenameFunction) +} + +func (rt *reflectTest) TestParallelGetColumnMap() { + type item struct { + id uint + name string + } + + wg := sync.WaitGroup{} + + wg.Add(1) + go func() { + i := item{id: 1, name: "bob"} + m, err := util.GetColumnMap(i) + rt.NoError(err) + rt.NotNil(m) + wg.Done() + }() + + wg.Add(1) + go func() { + i := item{id: 2, name: "sally"} + m, err := util.GetColumnMap(i) + rt.NoError(err) + rt.NotNil(m) + wg.Done() + }() + + wg.Wait() +} + +func (rt *reflectTest) TestAssignStructVals_withStruct() { + type TestStruct struct { + Str string + Int int64 + Bool bool + Valuer sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + data := map[string]interface{}{ + "str": "string", + "int": int64(10), + "bool": true, + "valuer": sql.NullString{String: "null_str", Valid: true}, + } + + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + Str: "string", + Int: 10, + Bool: true, + Valuer: sql.NullString{String: "null_str", Valid: true}, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithPointerVals() { + type TestStruct struct { + Str string + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + Str: "string", + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithEmbeddedStruct() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + EmbeddedStruct + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + EmbeddedStruct: EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithEmbeddedStructPointer() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + *EmbeddedStruct + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + EmbeddedStruct: &EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithTaggedEmbeddedStruct() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + EmbeddedStruct `db:"embedded"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "embedded.str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + EmbeddedStruct: EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithTaggedEmbeddedPointer() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + *EmbeddedStruct `db:"embedded"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "embedded.str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + EmbeddedStruct: &EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithTaggedStructField() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + Embedded EmbeddedStruct `db:"embedded"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "embedded.str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + Embedded: EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestAssignStructVals_withStructWithTaggedPointerField() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + Embedded *EmbeddedStruct `db:"embedded"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + ns := &sql.NullString{String: "null_str1", Valid: true} + data := map[string]interface{}{ + "embedded.str": "string", + "int": int64(10), + "bool": true, + "valuer": &ns, + } + util.AssignStructVals(&ts, data, cm) + rt.Equal(ts, TestStruct{ + Embedded: &EmbeddedStruct{Str: "string"}, + Int: 10, + Bool: true, + Valuer: ns, + }) +} + +func (rt *reflectTest) TestGetColumnMap_withStruct() { + type TestStruct struct { + Str string + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructDbv2Tags() { + type TestStruct struct { + Str string `ff:"skipinsert,skipupdate"` + Int int64 `ff:"skipinsert"` + Bool bool `ff:"skipupdate"` + Empty bool `ff:"defaultifempty"` + OmitNil bool `ff:"omitnil"` + OmitEmpty bool `ff:"omitempty"` + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: false, ShouldUpdate: false, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: false, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: false, GoType: reflect.TypeOf(true)}, + "empty": { + ColumnName: "empty", + FieldIndex: []int{3}, + ShouldInsert: true, + ShouldUpdate: true, + DefaultIfEmpty: true, + GoType: reflect.TypeOf(true), + }, + "omitnil": { + ColumnName: "omitnil", + FieldIndex: []int{4}, + ShouldInsert: true, + ShouldUpdate: true, + OmitNil: true, + GoType: reflect.TypeOf(true), + }, + "omitempty": { + ColumnName: "omitempty", + FieldIndex: []int{5}, + ShouldInsert: true, + ShouldUpdate: true, + OmitEmpty: true, + GoType: reflect.TypeOf(true), + }, + "valuer": {ColumnName: "valuer", FieldIndex: []int{6}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithIgnoreUntagged() { + defer util.SetIgnoreUntaggedFields(false) + util.SetIgnoreUntaggedFields(true) + + type EmbeddedStruct struct { + Float float64 `db:"f"` + Rune rune // Ignored + } + + type TestStruct struct { + EmbeddedStruct + Str string `db:"s"` + Int int64 `db:"i"` + Bool bool // Ignored + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "f": {ColumnName: "f", FieldIndex: []int{0, 0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(float64(1))}, + "s": {ColumnName: "s", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "i": {ColumnName: "i", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithTag() { + type TestStruct struct { + Str string `db:"s"` + Int int64 `db:"i"` + Bool bool `db:"b"` + Valuer *sql.NullString `db:"v"` + Ignored string `db:"-"` + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "s": {ColumnName: "s", FieldIndex: []int{0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "i": {ColumnName: "i", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "b": {ColumnName: "b", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "v": {ColumnName: "v", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithTagAndDbv2Tag() { + type TestStruct struct { + Str string `db:"s" ff:"skipinsert,skipupdate"` + Int int64 `db:"i" ff:"skipinsert"` + Bool bool `db:"b" ff:"skipupdate"` + Valuer *sql.NullString `db:"v"` + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "s": {ColumnName: "s", FieldIndex: []int{0}, ShouldInsert: false, ShouldUpdate: false, GoType: reflect.TypeOf("")}, + "i": {ColumnName: "i", FieldIndex: []int{1}, ShouldInsert: false, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "b": {ColumnName: "b", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: false, GoType: reflect.TypeOf(true)}, + "v": {ColumnName: "v", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithTransientFields() { + type TestStruct struct { + Str string + Int int64 + Bool bool + Valuer *sql.NullString `db:"-"` + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withSliceOfStructs() { + type TestStruct struct { + Str string + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts []TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withNonStruct() { + var v int64 + _, err := util.GetColumnMap(&v) + rt.EqualError(err, "db: cannot scan into this type: int64") +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithEmbeddedStruct() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + EmbeddedStruct + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0, 0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withStructWithEmbeddedStructPointer() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + *EmbeddedStruct + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "str": {ColumnName: "str", FieldIndex: []int{0, 0}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf("")}, + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withIgnoredEmbeddedStruct() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + EmbeddedStruct `db:"-"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withIgnoredEmbeddedPointerStruct() { + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + *EmbeddedStruct `db:"-"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withPrivateFields() { + type TestStruct struct { + str string //nolint:structcheck,unused // not used directly but needed for test + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withPrivateEmbeddedFields() { + type TestEmbedded struct { + str string //nolint:structcheck,unused // not used directly but need for test + Int int64 + } + + type TestStruct struct { + TestEmbedded + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{0, 1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withEmbeddedTaggedStruct() { + type TestEmbedded struct { + Bool bool + Valuer *sql.NullString + } + + type TestStruct struct { + TestEmbedded `db:"test_embedded"` + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "test_embedded.bool": { + ColumnName: "test_embedded.bool", + FieldIndex: []int{0, 0}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "test_embedded.valuer": { + ColumnName: "test_embedded.valuer", + FieldIndex: []int{0, 1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + "bool": { + ColumnName: "bool", + FieldIndex: []int{1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "valuer": { + ColumnName: "valuer", + FieldIndex: []int{2}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withEmbeddedTaggedStructPointer() { + type TestEmbedded struct { + Bool bool + Valuer *sql.NullString + } + + type TestStruct struct { + *TestEmbedded `db:"test_embedded"` + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "test_embedded.bool": { + ColumnName: "test_embedded.bool", + FieldIndex: []int{0, 0}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "test_embedded.valuer": { + ColumnName: "test_embedded.valuer", + FieldIndex: []int{0, 1}, + ShouldInsert: true, ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + "bool": { + ColumnName: "bool", + FieldIndex: []int{1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "valuer": { + ColumnName: "valuer", + FieldIndex: []int{2}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withTaggedStructField() { + type TestEmbedded struct { + Bool bool + Valuer *sql.NullString + } + + type TestStruct struct { + Embedded TestEmbedded `db:"test_embedded"` + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "test_embedded.bool": { + ColumnName: "test_embedded.bool", + FieldIndex: []int{0, 0}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "test_embedded.valuer": { + ColumnName: "test_embedded.valuer", + FieldIndex: []int{0, 1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + "bool": { + ColumnName: "bool", + FieldIndex: []int{1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "valuer": { + ColumnName: "valuer", + FieldIndex: []int{2}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withTaggedStructPointerField() { + type TestEmbedded struct { + Bool bool + Valuer *sql.NullString + } + + type TestStruct struct { + Embedded *TestEmbedded `db:"test_embedded"` + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := util.GetColumnMap(&ts) + rt.NoError(err) + rt.Equal(util.ColumnMap{ + "test_embedded.bool": { + ColumnName: "test_embedded.bool", + FieldIndex: []int{0, 0}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "test_embedded.valuer": { + ColumnName: "test_embedded.valuer", + FieldIndex: []int{0, 1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + "bool": { + ColumnName: "bool", + FieldIndex: []int{1}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(true), + }, + "valuer": { + ColumnName: "valuer", + FieldIndex: []int{2}, + ShouldInsert: true, + ShouldUpdate: true, + GoType: reflect.TypeOf(&sql.NullString{}), + }, + }, cm) +} + +func (rt *reflectTest) TestGetTypeInfo() { + var a int64 + var b []int64 + var c []*time.Time + + t, k := util.GetTypeInfo(&a, reflect.ValueOf(a)) + rt.Equal(reflect.TypeOf(a), t) + rt.Equal(reflect.Int64, k) + + t, k = util.GetTypeInfo(&b, reflect.ValueOf(a)) + rt.Equal(reflect.TypeOf(a), t) + rt.Equal(reflect.Int64, k) + + t, k = util.GetTypeInfo(c, reflect.ValueOf(c)) + rt.Equal(reflect.TypeOf(time.Time{}), t) + rt.Equal(reflect.Struct, k) +} + +func (rt *reflectTest) TestSafeGetFieldByIndex() { + type TestEmbedded struct { + FieldA int + } + type TestEmbeddedPointerStruct struct { + *TestEmbedded + FieldB string + } + type TestEmbeddedStruct struct { + TestEmbedded + FieldB string + } + v := reflect.ValueOf(TestEmbeddedPointerStruct{}) + f, isAvailable := util.SafeGetFieldByIndex(v, []int{0, 0}) + rt.False(isAvailable) + rt.False(f.IsValid()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedPointerStruct{TestEmbedded: &TestEmbedded{}}) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.Int, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedStruct{}) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.Int, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedStruct{TestEmbedded: TestEmbedded{}}) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = util.SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) +} + +func (rt *reflectTest) TestSafeSetFieldByIndex() { + type TestEmbedded struct { + FieldA int + } + type TestEmbeddedPointerStruct struct { + *TestEmbedded + FieldB string + } + type TestEmbeddedStruct struct { + TestEmbedded + FieldB string + } + var teps TestEmbeddedPointerStruct + v := reflect.ValueOf(&teps) + f := util.SafeSetFieldByIndex(v, []int{}, nil) + rt.Equal(TestEmbeddedPointerStruct{}, f.Interface()) + + f = util.SafeSetFieldByIndex(v, []int{0, 0}, 1) + rt.Equal(TestEmbeddedPointerStruct{ + TestEmbedded: &TestEmbedded{FieldA: 1}, + }, f.Interface()) + + f = util.SafeSetFieldByIndex(v, []int{1}, "hello") + rt.Equal(TestEmbeddedPointerStruct{ + TestEmbedded: &TestEmbedded{FieldA: 1}, + FieldB: "hello", + }, f.Interface()) + rt.Equal(TestEmbeddedPointerStruct{ + TestEmbedded: &TestEmbedded{FieldA: 1}, + FieldB: "hello", + }, teps) + + var tes TestEmbeddedStruct + v = reflect.ValueOf(&tes) + f = util.SafeSetFieldByIndex(v, []int{}, nil) + rt.Equal(TestEmbeddedStruct{}, f.Interface()) + + f = util.SafeSetFieldByIndex(v, []int{0, 0}, 1) + rt.Equal(TestEmbeddedStruct{ + TestEmbedded: TestEmbedded{FieldA: 1}, + }, f.Interface()) + + f = util.SafeSetFieldByIndex(v, []int{1}, "hello") + rt.Equal(TestEmbeddedStruct{ + TestEmbedded: TestEmbedded{FieldA: 1}, + FieldB: "hello", + }, f.Interface()) + rt.Equal(TestEmbeddedStruct{ + TestEmbedded: TestEmbedded{FieldA: 1}, + FieldB: "hello", + }, tes) +} + +func (rt *reflectTest) TestGetSliceElementType() { + type MyStruct struct{} + + tests := []struct { + slice interface{} + want reflect.Type + }{ + { + slice: []int{}, + want: reflect.TypeOf(1), + }, + { + slice: []*int{}, + want: reflect.TypeOf(1), + }, + { + slice: []MyStruct{}, + want: reflect.TypeOf(MyStruct{}), + }, + { + slice: []*MyStruct{}, + want: reflect.TypeOf(MyStruct{}), + }, + } + + for _, tt := range tests { + sliceVal := reflect.ValueOf(tt.slice) + elementType := util.GetSliceElementType(sliceVal) + + rt.Equal(tt.want, elementType) + } +} + +func (rt *reflectTest) TestAppendSliceElement() { + type MyStruct struct{} + + sliceVal := reflect.Indirect(reflect.ValueOf(&[]MyStruct{})) + util.AppendSliceElement(sliceVal, reflect.ValueOf(&MyStruct{})) + + rt.Equal([]MyStruct{{}}, sliceVal.Interface()) + + sliceVal = reflect.Indirect(reflect.ValueOf(&[]*MyStruct{})) + util.AppendSliceElement(sliceVal, reflect.ValueOf(&MyStruct{})) + + rt.Equal([]*MyStruct{{}}, sliceVal.Interface()) +} + +func TestReflectSuite(t *testing.T) { + suite.Run(t, new(reflectTest)) +} diff --git a/internal/util/value_slice.go b/internal/util/value_slice.go new file mode 100644 index 0000000..b1d3697 --- /dev/null +++ b/internal/util/value_slice.go @@ -0,0 +1,33 @@ +package util + +import ( + "fmt" + "reflect" + "sort" + "strings" +) + +type ValueSlice []reflect.Value + +func (vs ValueSlice) Len() int { return len(vs) } +func (vs ValueSlice) Less(i, j int) bool { return vs[i].String() < vs[j].String() } +func (vs ValueSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } + +func (vs ValueSlice) Equal(other ValueSlice) bool { + sort.Sort(other) + for i, key := range vs { + if other[i].String() != key.String() { + return false + } + } + return true +} + +func (vs ValueSlice) String() string { + vals := make([]string, vs.Len()) + for i, key := range vs { + vals[i] = fmt.Sprintf("%q", key.String()) + } + sort.Strings(vals) + return fmt.Sprintf("[%s]", strings.Join(vals, ",")) +} diff --git a/issues_test.go b/issues_test.go new file mode 100644 index 0000000..bed1d8f --- /dev/null +++ b/issues_test.go @@ -0,0 +1,496 @@ +package db_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/suite" +) + +type githubIssuesSuite struct { + suite.Suite +} + +func (gis *githubIssuesSuite) AfterTest(_, _ string) { + dbv2.SetColumnRenameFunction(strings.ToLower) +} + +// Test for https://github.com/doug-martin/dbv2/issues/49 +func (gis *githubIssuesSuite) TestIssue49() { + dialect := dbv2.Dialect("default") + + filters := dbv2.Or() + sql, args, err := dialect.From("table").Where(filters).ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal(`SELECT * FROM "table"`, sql) + + sql, args, err = dialect.From("table").Where(dbv2.Ex{}).ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal(`SELECT * FROM "table"`, sql) + + sql, args, err = dialect.From("table").Where(dbv2.ExOr{}).ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal(`SELECT * FROM "table"`, sql) +} + +// Test for https://github.com/doug-martin/dbv2/issues/115 +func (gis *githubIssuesSuite) TestIssue115() { + type TestStruct struct { + Field string + } + dbv2.SetColumnRenameFunction(func(col string) string { + return "" + }) + + _, _, err := dbv2.Insert("test").Rows(TestStruct{Field: "hello"}).ToSQL() + gis.EqualError(err, `db: a empty identifier was encountered, please specify a "schema", "table" or "column"`) +} + +// Test for https://github.com/doug-martin/dbv2/issues/118 +func (gis *githubIssuesSuite) TestIssue118_withEmbeddedStructWithoutExportedFields() { + // struct is in a custom package + type SimpleRole struct { + permissions []string //nolint:structcheck,unused //needed for test + } + + // ..... + + type Role struct { + *SimpleRole + + ID string `json:"id" db:"id" ff:"skipinsert"` + Key string `json:"key" db:"key"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"-" db:"created_at" ff:"skipinsert"` + } + + rUser := &Role{ + Key: `user`, + Name: `User role`, + } + + sql, arg, err := dbv2.Insert(`rbac_roles`). + Returning(dbv2.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = dbv2.Update(`rbac_roles`). + Returning(dbv2.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "rbac_roles" SET "created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + + rUser = &Role{ + SimpleRole: &SimpleRole{}, + Key: `user`, + Name: `User role`, + } + + sql, arg, err = dbv2.Insert(`rbac_roles`). + Returning(dbv2.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = dbv2.Update(`rbac_roles`). + Returning(dbv2.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "rbac_roles" SET `+ + `"created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) +} + +// Test for https://github.com/doug-martin/dbv2/issues/118 +func (gis *githubIssuesSuite) TestIssue118_withNilEmbeddedStructWithExportedFields() { + // struct is in a custom package + type SimpleRole struct { + permissions []string //nolint:structcheck,unused // needed for test + IDStr string + } + + // ..... + + type Role struct { + *SimpleRole + + ID string `json:"id" db:"id" ff:"skipinsert"` + Key string `json:"key" db:"key"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"-" db:"created_at" ff:"skipinsert"` + } + + rUser := &Role{ + Key: `user`, + Name: `User role`, + } + sql, arg, err := dbv2.Insert(`rbac_roles`). + Returning(dbv2.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = dbv2.Update(`rbac_roles`). + Returning(dbv2.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `UPDATE "rbac_roles" SET "created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + + rUser = &Role{ + SimpleRole: &SimpleRole{}, + Key: `user`, + Name: `User role`, + } + sql, arg, err = dbv2.Insert(`rbac_roles`). + Returning(dbv2.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `INSERT INTO "rbac_roles" ("idstr", "key", "name") VALUES ('', 'user', 'User role') RETURNING "id"`, + sql, + ) + + sql, arg, err = dbv2.Update(`rbac_roles`). + Returning(dbv2.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `UPDATE "rbac_roles" SET `+ + `"created_at"='0001-01-01T00:00:00Z',"id"='',"idstr"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) +} + +// Test for https://github.com/doug-martin/dbv2/issues/118 +func (gis *githubIssuesSuite) TestIssue140() { + sql, arg, err := dbv2.Insert(`test`).Returning().ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "test" DEFAULT VALUES`, sql) + + sql, arg, err = dbv2.Update(`test`).Set(dbv2.Record{"a": "b"}).Returning().ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "test" SET "a"='b'`, + sql, + ) + + sql, arg, err = dbv2.Delete(`test`).Returning().ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `DELETE FROM "test"`, + sql, + ) + + sql, arg, err = dbv2.Insert(`test`).Returning(nil).ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "test" DEFAULT VALUES`, sql) + + sql, arg, err = dbv2.Update(`test`).Set(dbv2.Record{"a": "b"}).Returning(nil).ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "test" SET "a"='b'`, + sql, + ) + + sql, arg, err = dbv2.Delete(`test`).Returning(nil).ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `DELETE FROM "test"`, + sql, + ) +} + +// Test for https://github.com/doug-martin/dbv2/issues/164 +func (gis *githubIssuesSuite) TestIssue164() { + insertDs := dbv2.Insert("foo").Rows(dbv2.Record{"user_id": 10}).Returning("id") + + ds := dbv2.From("bar"). + With("ins", insertDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("ins.user_id")}) + + sql, args, err := ds.ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal( + `WITH ins AS (INSERT INTO "foo" ("user_id") VALUES (10) RETURNING "id") `+ + `SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "ins"."user_id")`, + sql, + ) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.NoError(err) + gis.Equal([]interface{}{int64(10)}, args) + gis.Equal( + `WITH ins AS (INSERT INTO "foo" ("user_id") VALUES (?) RETURNING "id")`+ + ` SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "ins"."user_id")`, + sql, + ) + + updateDs := dbv2.Update("foo").Set(dbv2.Record{"bar": "baz"}).Returning("id") + + ds = dbv2.From("bar"). + With("upd", updateDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("upd.user_id")}) + + sql, args, err = ds.ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal( + `WITH upd AS (UPDATE "foo" SET "bar"='baz' RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "upd"."user_id")`, + sql, + ) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.NoError(err) + gis.Equal([]interface{}{"baz"}, args) + gis.Equal( + `WITH upd AS (UPDATE "foo" SET "bar"=? RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "upd"."user_id")`, + sql, + ) + + deleteDs := dbv2.Delete("foo").Where(dbv2.Ex{"bar": "baz"}).Returning("id") + + ds = dbv2.From("bar"). + With("del", deleteDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("del.user_id")}) + + sql, args, err = ds.ToSQL() + gis.NoError(err) + gis.Empty(args) + gis.Equal( + `WITH del AS (DELETE FROM "foo" WHERE ("bar" = 'baz') RETURNING "id")`+ + ` SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "del"."user_id")`, + sql, + ) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.NoError(err) + gis.Equal([]interface{}{"baz"}, args) + gis.Equal( + `WITH del AS (DELETE FROM "foo" WHERE ("bar" = ?) RETURNING "id")`+ + ` SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "del"."user_id")`, + sql, + ) +} + +// Test for https://github.com/doug-martin/dbv2/issues/177 +func (gis *githubIssuesSuite) TestIssue177() { + ds := dbv2.Dialect("postgres"). + From("ins1"). + With("ins1", + dbv2.Dialect("postgres"). + Insert("account"). + Rows(dbv2.Record{"email": "email@email.com", "status": "active", "uuid": "XXX-XXX-XXXX"}). + Returning("*"), + ). + With("ins2", + dbv2.Dialect("postgres"). + Insert("account_user"). + Cols("account_id", "user_id"). + FromQuery(dbv2.Dialect("postgres"). + From("ins1"). + Select( + "id", + dbv2.V(1001), + ), + ), + ). + Select("*") + sql, args, err := ds.ToSQL() + gis.NoError(err) + gis.Equal(`WITH ins1 AS (`+ + `INSERT INTO "account" ("email", "status", "uuid") VALUES ('email@email.com', 'active', 'XXX-XXX-XXXX') RETURNING *),`+ + ` ins2 AS (INSERT INTO "account_user" ("account_id", "user_id") SELECT "id", 1001 FROM "ins1")`+ + ` SELECT * FROM "ins1"`, sql) + gis.Len(args, 0) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.NoError(err) + gis.Equal(`WITH ins1 AS (INSERT INTO "account" ("email", "status", "uuid") VALUES ($1, $2, $3) RETURNING *), ins2`+ + ` AS (INSERT INTO "account_user" ("account_id", "user_id") SELECT "id", $4 FROM "ins1") SELECT * FROM "ins1"`, sql) + gis.Equal(args, []interface{}{"email@email.com", "active", "XXX-XXX-XXXX", int64(1001)}) +} + +// Test for https://github.com/doug-martin/dbv2/issues/183 +func (gis *githubIssuesSuite) TestIssue184() { + expectedErr := fmt.Errorf("an error") + testCases := []struct { + ds exp.AppendableExpression + }{ + {ds: dbv2.From("test").As("t").SetError(expectedErr)}, + {ds: dbv2.Insert("test").Rows(dbv2.Record{"foo": "bar"}).Returning("foo").SetError(expectedErr)}, + {ds: dbv2.Update("test").Set(dbv2.Record{"foo": "bar"}).Returning("foo").SetError(expectedErr)}, + {ds: dbv2.Update("test").Set(dbv2.Record{"foo": "bar"}).Returning("foo").SetError(expectedErr)}, + {ds: dbv2.Delete("test").Returning("foo").SetError(expectedErr)}, + } + + for _, tc := range testCases { + ds := dbv2.From(tc.ds) + sql, args, err := ds.ToSQL() + gis.Equal(expectedErr, err) + gis.Empty(sql) + gis.Empty(args) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.Equal(expectedErr, err) + gis.Empty(sql) + gis.Empty(args) + + ds = dbv2.From("test2").Where(dbv2.Ex{"foo": tc.ds}) + + sql, args, err = ds.ToSQL() + gis.Equal(expectedErr, err) + gis.Empty(sql) + gis.Empty(args) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.Equal(expectedErr, err) + gis.Empty(sql) + gis.Empty(args) + } +} + +// Test for https://github.com/doug-martin/dbv2/issues/185 +func (gis *githubIssuesSuite) TestIssue185() { + mDB, sqlMock, err := sqlmock.New() + gis.NoError(err) + sqlMock.ExpectQuery( + `SELECT \* FROM \(SELECT "id" FROM "table" ORDER BY "id" ASC\) AS "t1" UNION +\(SELECT \* FROM \(SELECT "id" FROM "table" ORDER BY "id" ASC\) AS "t1"\)`, + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n")) + db := dbv2.New("mock", mDB) + + ds := db.Select("id").From("table").Order(dbv2.C("id").Asc()). + Union( + db.Select("id").From("table").Order(dbv2.C("id").Asc()), + ) + + ctx := context.Background() + var i []int + gis.NoError(ds.ScanValsContext(ctx, &i)) + gis.Equal([]int{1, 2, 3, 4}, i) +} + +// Test for https://github.com/doug-martin/dbv2/issues/203 +func (gis *githubIssuesSuite) TestIssue203() { + // Schema definitions. + authSchema := dbv2.S("company_auth") + + // Table definitions + usersTable := authSchema.Table("users") + + u := usersTable.As("u") + + ds := dbv2.From(u).Select( + u.Col("id"), + u.Col("name"), + u.Col("created_at"), + u.Col("updated_at"), + ) + + sql, args, err := ds.ToSQL() + gis.NoError(err) + gis.Equal(`SELECT "u"."id", "u"."name", "u"."created_at", "u"."updated_at" FROM "company_auth"."users" AS "u"`, sql) + gis.Empty(args, []interface{}{}) + + sql, args, err = ds.Prepared(true).ToSQL() + gis.NoError(err) + gis.Equal(`SELECT "u"."id", "u"."name", "u"."created_at", "u"."updated_at" FROM "company_auth"."users" AS "u"`, sql) + gis.Empty(args, []interface{}{}) +} + +func (gis *githubIssuesSuite) TestIssue290() { + type OcomModel struct { + ID uint `json:"id" db:"id" ff:"skipinsert"` + CreatedDate time.Time `json:"created_date" db:"created_date" ff:"skipupdate"` + ModifiedDate time.Time `json:"modified_date" db:"modified_date"` + } + + type ActiveModel struct { + OcomModel + ActiveStartDate time.Time `json:"active_start_date" db:"active_start_date"` + ActiveEndDate *time.Time `json:"active_end_date" db:"active_end_date"` + } + + type CodeModel struct { + ActiveModel + + Code string `json:"code" db:"code"` + Description string `json:"description" binding:"required" db:"description"` + } + + type CodeExample struct { + CodeModel + } + + var item CodeExample + item.Code = "Code" + item.Description = "Description" + item.ID = 1 // Value set HERE! + item.CreatedDate = time.Date( + 2021, 1, 1, 1, 1, 1, 1, time.UTC) + item.ModifiedDate = time.Date( + 2021, 2, 2, 2, 2, 2, 2, time.UTC) // The Value we Get! + item.ActiveStartDate = time.Date( + 2021, 3, 3, 3, 3, 3, 3, time.UTC) + + updateQuery := dbv2.From("example").Update().Set(item).Where(dbv2.C("id").Eq(1)) + + sql, params, err := updateQuery.ToSQL() + + gis.NoError(err) + gis.Empty(params) + gis.Equal(`UPDATE "example" SET "active_end_date"=NULL,"active_start_date"='2021-03-03T03:03:03.000000003Z',"code"='Code',"description"='Description',"id"=1,"modified_date"='2021-02-02T02:02:02.000000002Z' WHERE ("id" = 1)`, sql) //nolint:lll +} + +func TestGithubIssuesSuite(t *testing.T) { + suite.Run(t, new(githubIssuesSuite)) +} diff --git a/mocks/SQLDialect.go b/mocks/SQLDialect.go new file mode 100644 index 0000000..f191e61 --- /dev/null +++ b/mocks/SQLDialect.go @@ -0,0 +1,52 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" + +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// SQLDialect is an autogenerated mock type for the SQLDialect type +type SQLDialect struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *SQLDialect) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ToDeleteSQL provides a mock function with given fields: b, clauses +func (_m *SQLDialect) ToDeleteSQL(b sb.SQLBuilder, clauses exp.DeleteClauses) { + _m.Called(b, clauses) +} + +// ToInsertSQL provides a mock function with given fields: b, clauses +func (_m *SQLDialect) ToInsertSQL(b sb.SQLBuilder, clauses exp.InsertClauses) { + _m.Called(b, clauses) +} + +// ToSelectSQL provides a mock function with given fields: b, clauses +func (_m *SQLDialect) ToSelectSQL(b sb.SQLBuilder, clauses exp.SelectClauses) { + _m.Called(b, clauses) +} + +// ToTruncateSQL provides a mock function with given fields: b, clauses +func (_m *SQLDialect) ToTruncateSQL(b sb.SQLBuilder, clauses exp.TruncateClauses) { + _m.Called(b, clauses) +} + +// ToUpdateSQL provides a mock function with given fields: b, clauses +func (_m *SQLDialect) ToUpdateSQL(b sb.SQLBuilder, clauses exp.UpdateClauses) { + _m.Called(b, clauses) +} diff --git a/prepared.go b/prepared.go new file mode 100644 index 0000000..6671415 --- /dev/null +++ b/prepared.go @@ -0,0 +1,48 @@ +package db + +var ( + // defaultPrepared is controlled by SetDefaultPrepared + defaultPrepared bool +) + +type prepared int + +const ( + // zero value that defers to defaultPrepared + preparedNoPreference prepared = iota + + // explicitly enabled via Prepared(true) on a dataset + preparedEnabled + + // explicitly disabled via Prepared(false) on a dataset + preparedDisabled +) + +// Bool converts the ternary prepared state into a boolean. If the prepared +// state is preparedNoPreference, the value depends on the last value that +// SetDefaultPrepared was called with which is false by default. +func (p prepared) Bool() bool { + if p == preparedNoPreference { + return defaultPrepared + } else if p == preparedEnabled { + return true + } + + return false +} + +// preparedFromBool converts a bool from e.g. Prepared(true) into a prepared +// const. +func preparedFromBool(prepared bool) prepared { + if prepared { + return preparedEnabled + } + + return preparedDisabled +} + +// SetDefaultPrepared controls the default Prepared state of all datasets. If +// set to true, any new dataset will use prepared queries by default. +func SetDefaultPrepared(prepared bool) { + defaultPrepared = prepared +} diff --git a/select_dataset.go b/select_dataset.go new file mode 100644 index 0000000..b2c8ec6 --- /dev/null +++ b/select_dataset.go @@ -0,0 +1,701 @@ +package db + +import ( + "context" + "fmt" + + "git.fsdpf.net/go/db/v2/exec" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +// Dataset for creating and/or executing SELECT SQL statements. +type SelectDataset struct { + dialect SQLDialect + clauses exp.SelectClauses + isPrepared prepared + queryFactory exec.QueryFactory + err error +} + +var ErrQueryFactoryNotFoundError = errors.New( + "unable to execute query did you use db.Database#From to create the dataset", +) + +// used internally by database to create a database with a specific adapter +func newDataset(d string, queryFactory exec.QueryFactory) *SelectDataset { + return &SelectDataset{ + clauses: exp.NewSelectClauses(), + dialect: GetDialect(d), + queryFactory: queryFactory, + } +} + +func From(table ...interface{}) *SelectDataset { + return newDataset("default", nil).From(table...) +} + +func Select(cols ...interface{}) *SelectDataset { + return newDataset("default", nil).Select(cols...) +} + +// Sets the adapter used to serialize values and create the SQL statement +func (sd *SelectDataset) WithDialect(dl string) *SelectDataset { + ds := sd.copy(sd.GetClauses()) + ds.dialect = GetDialect(dl) + return ds +} + +// Set the parameter interpolation behavior. See examples +// +// prepared: If true the dataset WILL NOT interpolate the parameters. +func (sd *SelectDataset) Prepared(prepared bool) *SelectDataset { + ret := sd.copy(sd.clauses) + ret.isPrepared = preparedFromBool(prepared) + return ret +} + +func (sd *SelectDataset) IsPrepared() bool { + return sd.isPrepared.Bool() +} + +// Returns the current adapter on the dataset +func (sd *SelectDataset) Dialect() SQLDialect { + return sd.dialect +} + +// Returns the current adapter on the dataset +func (sd *SelectDataset) SetDialect(dialect SQLDialect) *SelectDataset { + cd := sd.copy(sd.GetClauses()) + cd.dialect = dialect + return cd +} + +func (sd *SelectDataset) Expression() exp.Expression { + return sd +} + +// Clones the dataset +func (sd *SelectDataset) Clone() exp.Expression { + return sd.copy(sd.clauses) +} + +// Returns the current clauses on the dataset. +func (sd *SelectDataset) GetClauses() exp.SelectClauses { + return sd.clauses +} + +// used interally to copy the dataset +func (sd *SelectDataset) copy(clauses exp.SelectClauses) *SelectDataset { + return &SelectDataset{ + dialect: sd.dialect, + clauses: clauses, + isPrepared: sd.isPrepared, + queryFactory: sd.queryFactory, + err: sd.err, + } +} + +// Creates a new UpdateDataset using the FROM of this dataset. This method will also copy over the `WITH`, `WHERE`, +// `ORDER , and `LIMIT` +func (sd *SelectDataset) Update() *UpdateDataset { + u := newUpdateDataset(sd.dialect.Dialect(), sd.queryFactory). + Prepared(sd.isPrepared.Bool()) + if sd.clauses.HasSources() { + u = u.Table(sd.GetClauses().From().Columns()[0]) + } + c := u.clauses + for _, ce := range sd.clauses.CommonTables() { + c = c.CommonTablesAppend(ce) + } + if sd.clauses.Where() != nil { + c = c.WhereAppend(sd.clauses.Where()) + } + if sd.clauses.HasLimit() { + c = c.SetLimit(sd.clauses.Limit()) + } + if sd.clauses.HasOrder() { + for _, oe := range sd.clauses.Order().Columns() { + c = c.OrderAppend(oe.(exp.OrderedExpression)) + } + } + u.clauses = c + return u +} + +// Creates a new InsertDataset using the FROM of this dataset. This method will also copy over the `WITH` clause to the +// insert. +func (sd *SelectDataset) Insert() *InsertDataset { + i := newInsertDataset(sd.dialect.Dialect(), sd.queryFactory). + Prepared(sd.isPrepared.Bool()) + if sd.clauses.HasSources() { + i = i.Into(sd.GetClauses().From().Columns()[0]) + } + c := i.clauses + for _, ce := range sd.clauses.CommonTables() { + c = c.CommonTablesAppend(ce) + } + i.clauses = c + return i +} + +// Creates a new DeleteDataset using the FROM of this dataset. This method will also copy over the `WITH`, `WHERE`, +// `ORDER , and `LIMIT` +func (sd *SelectDataset) Delete() *DeleteDataset { + d := newDeleteDataset(sd.dialect.Dialect(), sd.queryFactory). + Prepared(sd.isPrepared.Bool()) + if sd.clauses.HasSources() { + d = d.From(sd.clauses.From().Columns()[0]) + } + c := d.clauses + for _, ce := range sd.clauses.CommonTables() { + c = c.CommonTablesAppend(ce) + } + if sd.clauses.Where() != nil { + c = c.WhereAppend(sd.clauses.Where()) + } + if sd.clauses.HasLimit() { + c = c.SetLimit(sd.clauses.Limit()) + } + if sd.clauses.HasOrder() { + for _, oe := range sd.clauses.Order().Columns() { + c = c.OrderAppend(oe.(exp.OrderedExpression)) + } + } + d.clauses = c + return d +} + +// Creates a new TruncateDataset using the FROM of this dataset. +func (sd *SelectDataset) Truncate() *TruncateDataset { + td := newTruncateDataset(sd.dialect.Dialect(), sd.queryFactory) + if sd.clauses.HasSources() { + td = td.Table(sd.clauses.From()) + } + return td +} + +// Creates a WITH clause for a common table expression (CTE). +// +// The name will be available to SELECT from in the associated query; and can optionally +// contain a list of column names "name(col1, col2, col3)". +// +// The name will refer to the results of the specified subquery. +func (sd *SelectDataset) With(name string, subquery exp.Expression) *SelectDataset { + return sd.copy(sd.clauses.CommonTablesAppend(exp.NewCommonTableExpression(false, name, subquery))) +} + +// Creates a WITH RECURSIVE clause for a common table expression (CTE) +// +// The name will be available to SELECT from in the associated query; and must +// contain a list of column names "name(col1, col2, col3)" for a recursive clause. +// +// The name will refer to the results of the specified subquery. The subquery for +// a recursive query will always end with a UNION or UNION ALL with a clause that +// refers to the CTE by name. +func (sd *SelectDataset) WithRecursive(name string, subquery exp.Expression) *SelectDataset { + return sd.copy(sd.clauses.CommonTablesAppend(exp.NewCommonTableExpression(true, name, subquery))) +} + +// Replaces columns of the SELECT clause. Empty list resets the clause. See examples +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Dataset: Will use the SQL generated from that Dataset. If the dataset is aliased it will use that alias as the +// column name. +// LiteralExpression: (See Literal) Will use the literal SQL +// SQLFunction: (See Func, MIN, MAX, COUNT....) +// Struct: If passing in an instance of a struct, we will parse the struct for the column names to select. +// See examples +func (sd *SelectDataset) Select(selects ...interface{}) *SelectDataset { + if len(selects) == 0 { + return sd.ClearSelect() + } + return sd.copy(sd.clauses.SetSelect(exp.NewColumnListExpression(selects...))) +} + +// Replaces columns of the SELECT DISTINCT clause. Empty list resets the clause. See examples +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Dataset: Will use the SQL generated from that Dataset. If the dataset is aliased it will use that alias as the +// column name. +// LiteralExpression: (See Literal) Will use the literal SQL +// SQLFunction: (See Func, MIN, MAX, COUNT....) +// Struct: If passing in an instance of a struct, we will parse the struct for the column names to select. +// See examples +// +// Deprecated: Use Distinct() instead. +func (sd *SelectDataset) SelectDistinct(selects ...interface{}) *SelectDataset { + if len(selects) == 0 { + cleared := sd.ClearSelect() + return cleared.copy(cleared.clauses.SetDistinct(nil)) + } + return sd.copy(sd.clauses.SetSelect(exp.NewColumnListExpression(selects...)).SetDistinct(exp.NewColumnListExpression())) +} + +// Resets to SELECT *. If the SelectDistinct or Distinct was used the returned Dataset will have the the dataset set to SELECT *. +// See examples. +func (sd *SelectDataset) ClearSelect() *SelectDataset { + return sd.copy(sd.clauses.SetSelect(exp.NewColumnListExpression(exp.Star())).SetDistinct(nil)) +} + +// Adds columns to the SELECT clause. See examples +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Dataset: Will use the SQL generated from that Dataset. If the dataset is aliased it will use that alias as the +// column name. +// LiteralExpression: (See Literal) Will use the literal SQL +// SQLFunction: (See Func, MIN, MAX, COUNT....) +func (sd *SelectDataset) SelectAppend(selects ...interface{}) *SelectDataset { + return sd.copy(sd.clauses.SelectAppend(exp.NewColumnListExpression(selects...))) +} + +func (sd *SelectDataset) Distinct(on ...interface{}) *SelectDataset { + return sd.copy(sd.clauses.SetDistinct(exp.NewColumnListExpression(on...))) +} + +// Adds a FROM clause. This return a new dataset with the original sources replaced. See examples. +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// Dataset: Will be added as a sub select. If the Dataset is not aliased it will automatically be aliased +// LiteralExpression: (See Literal) Will use the literal SQL +func (sd *SelectDataset) From(from ...interface{}) *SelectDataset { + var sources []interface{} + numSources := 0 + for _, source := range from { + if ds, ok := source.(*SelectDataset); ok && !ds.clauses.HasAlias() { + numSources++ + sources = append(sources, ds.As(fmt.Sprintf("t%d", numSources))) + } else { + sources = append(sources, source) + } + } + return sd.copy(sd.clauses.SetFrom(exp.NewColumnListExpression(sources...))) +} + +// Returns a new Dataset with the current one as an source. If the current Dataset is not aliased (See Dataset#As) then +// it will automatically be aliased. See examples. +func (sd *SelectDataset) FromSelf() *SelectDataset { + return sd.copy(exp.NewSelectClauses()).From(sd) +} + +// Alias to InnerJoin. See examples. +func (sd *SelectDataset) Join(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.InnerJoin(table, condition) +} + +// Adds an INNER JOIN clause. See examples. +func (sd *SelectDataset) InnerJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.InnerJoinType, table, condition)) +} + +// Adds a FULL OUTER JOIN clause. See examples. +func (sd *SelectDataset) FullOuterJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.FullOuterJoinType, table, condition)) +} + +// Adds a RIGHT OUTER JOIN clause. See examples. +func (sd *SelectDataset) RightOuterJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.RightOuterJoinType, table, condition)) +} + +// Adds a LEFT OUTER JOIN clause. See examples. +func (sd *SelectDataset) LeftOuterJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.LeftOuterJoinType, table, condition)) +} + +// Adds a FULL JOIN clause. See examples. +func (sd *SelectDataset) FullJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.FullJoinType, table, condition)) +} + +// Adds a RIGHT JOIN clause. See examples. +func (sd *SelectDataset) RightJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.RightJoinType, table, condition)) +} + +// Adds a LEFT JOIN clause. See examples. +func (sd *SelectDataset) LeftJoin(table exp.Expression, condition exp.JoinCondition) *SelectDataset { + return sd.joinTable(exp.NewConditionedJoinExpression(exp.LeftJoinType, table, condition)) +} + +// Adds a NATURAL JOIN clause. See examples. +func (sd *SelectDataset) NaturalJoin(table exp.Expression) *SelectDataset { + return sd.joinTable(exp.NewUnConditionedJoinExpression(exp.NaturalJoinType, table)) +} + +// Adds a NATURAL LEFT JOIN clause. See examples. +func (sd *SelectDataset) NaturalLeftJoin(table exp.Expression) *SelectDataset { + return sd.joinTable(exp.NewUnConditionedJoinExpression(exp.NaturalLeftJoinType, table)) +} + +// Adds a NATURAL RIGHT JOIN clause. See examples. +func (sd *SelectDataset) NaturalRightJoin(table exp.Expression) *SelectDataset { + return sd.joinTable(exp.NewUnConditionedJoinExpression(exp.NaturalRightJoinType, table)) +} + +// Adds a NATURAL FULL JOIN clause. See examples. +func (sd *SelectDataset) NaturalFullJoin(table exp.Expression) *SelectDataset { + return sd.joinTable(exp.NewUnConditionedJoinExpression(exp.NaturalFullJoinType, table)) +} + +// Adds a CROSS JOIN clause. See examples. +func (sd *SelectDataset) CrossJoin(table exp.Expression) *SelectDataset { + return sd.joinTable(exp.NewUnConditionedJoinExpression(exp.CrossJoinType, table)) +} + +// Joins this Datasets table with another +func (sd *SelectDataset) joinTable(join exp.JoinExpression) *SelectDataset { + return sd.copy(sd.clauses.JoinsAppend(join)) +} + +// Adds a WHERE clause. See examples. +func (sd *SelectDataset) Where(expressions ...exp.Expression) *SelectDataset { + return sd.copy(sd.clauses.WhereAppend(expressions...)) +} + +// Removes the WHERE clause. See examples. +func (sd *SelectDataset) ClearWhere() *SelectDataset { + return sd.copy(sd.clauses.ClearWhere()) +} + +// Adds a FOR UPDATE clause. See examples. +func (sd *SelectDataset) ForUpdate(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset { + return sd.withLock(exp.ForUpdate, waitOption, of...) +} + +// Adds a FOR NO KEY UPDATE clause. See examples. +func (sd *SelectDataset) ForNoKeyUpdate(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset { + return sd.withLock(exp.ForNoKeyUpdate, waitOption, of...) +} + +// Adds a FOR KEY SHARE clause. See examples. +func (sd *SelectDataset) ForKeyShare(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset { + return sd.withLock(exp.ForKeyShare, waitOption, of...) +} + +// Adds a FOR SHARE clause. See examples. +func (sd *SelectDataset) ForShare(waitOption exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset { + return sd.withLock(exp.ForShare, waitOption, of...) +} + +func (sd *SelectDataset) withLock(strength exp.LockStrength, option exp.WaitOption, of ...exp.IdentifierExpression) *SelectDataset { + return sd.copy(sd.clauses.SetLock(exp.NewLock(strength, option, of...))) +} + +// Adds a GROUP BY clause. See examples. +func (sd *SelectDataset) GroupBy(groupBy ...interface{}) *SelectDataset { + return sd.copy(sd.clauses.SetGroupBy(exp.NewColumnListExpression(groupBy...))) +} + +// Adds more columns to the current GROUP BY clause. See examples. +func (sd *SelectDataset) GroupByAppend(groupBy ...interface{}) *SelectDataset { + return sd.copy(sd.clauses.GroupByAppend(exp.NewColumnListExpression(groupBy...))) +} + +// Adds a HAVING clause. See examples. +func (sd *SelectDataset) Having(expressions ...exp.Expression) *SelectDataset { + return sd.copy(sd.clauses.HavingAppend(expressions...)) +} + +// Adds a ORDER clause. If the ORDER is currently set it replaces it. See examples. +func (sd *SelectDataset) Order(order ...exp.OrderedExpression) *SelectDataset { + return sd.copy(sd.clauses.SetOrder(order...)) +} + +// Adds a more columns to the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (sd *SelectDataset) OrderAppend(order ...exp.OrderedExpression) *SelectDataset { + return sd.copy(sd.clauses.OrderAppend(order...)) +} + +// Adds a more columns to the beginning of the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (sd *SelectDataset) OrderPrepend(order ...exp.OrderedExpression) *SelectDataset { + return sd.copy(sd.clauses.OrderPrepend(order...)) +} + +// Removes the ORDER BY clause. See examples. +func (sd *SelectDataset) ClearOrder() *SelectDataset { + return sd.copy(sd.clauses.ClearOrder()) +} + +// Adds a LIMIT clause. If the LIMIT is currently set it replaces it. See examples. +func (sd *SelectDataset) Limit(limit uint) *SelectDataset { + if limit > 0 { + return sd.copy(sd.clauses.SetLimit(limit)) + } + return sd.copy(sd.clauses.ClearLimit()) +} + +// Adds a LIMIT ALL clause. If the LIMIT is currently set it replaces it. See examples. +func (sd *SelectDataset) LimitAll() *SelectDataset { + return sd.copy(sd.clauses.SetLimit(L("ALL"))) +} + +// Removes the LIMIT clause. +func (sd *SelectDataset) ClearLimit() *SelectDataset { + return sd.copy(sd.clauses.ClearLimit()) +} + +// Adds an OFFSET clause. If the OFFSET is currently set it replaces it. See examples. +func (sd *SelectDataset) Offset(offset uint) *SelectDataset { + return sd.copy(sd.clauses.SetOffset(offset)) +} + +// Removes the OFFSET clause from the Dataset +func (sd *SelectDataset) ClearOffset() *SelectDataset { + return sd.copy(sd.clauses.ClearOffset()) +} + +// Creates an UNION statement with another dataset. +// If this or the other dataset has a limit or offset it will use that dataset as a subselect in the FROM clause. +// See examples. +func (sd *SelectDataset) Union(other *SelectDataset) *SelectDataset { + return sd.withCompound(exp.UnionCompoundType, other.CompoundFromSelf()) +} + +// Creates an UNION ALL statement with another dataset. +// If this or the other dataset has a limit or offset it will use that dataset as a subselect in the FROM clause. +// See examples. +func (sd *SelectDataset) UnionAll(other *SelectDataset) *SelectDataset { + return sd.withCompound(exp.UnionAllCompoundType, other.CompoundFromSelf()) +} + +// Creates an INTERSECT statement with another dataset. +// If this or the other dataset has a limit or offset it will use that dataset as a subselect in the FROM clause. +// See examples. +func (sd *SelectDataset) Intersect(other *SelectDataset) *SelectDataset { + return sd.withCompound(exp.IntersectCompoundType, other.CompoundFromSelf()) +} + +// Creates an INTERSECT ALL statement with another dataset. +// If this or the other dataset has a limit or offset it will use that dataset as a subselect in the FROM clause. +// See examples. +func (sd *SelectDataset) IntersectAll(other *SelectDataset) *SelectDataset { + return sd.withCompound(exp.IntersectAllCompoundType, other.CompoundFromSelf()) +} + +func (sd *SelectDataset) withCompound(ct exp.CompoundType, other exp.AppendableExpression) *SelectDataset { + ce := exp.NewCompoundExpression(ct, other) + ret := sd.CompoundFromSelf() + ret.clauses = ret.clauses.CompoundsAppend(ce) + return ret +} + +// Used internally to determine if the dataset needs to use iteself as a source. +// If the dataset has an order or limit it will select from itself +func (sd *SelectDataset) CompoundFromSelf() *SelectDataset { + if sd.clauses.HasOrder() || sd.clauses.HasLimit() { + return sd.FromSelf() + } + return sd.copy(sd.clauses) +} + +// Sets the alias for this dataset. This is typically used when using a Dataset as a subselect. See examples. +func (sd *SelectDataset) As(alias string) *SelectDataset { + return sd.copy(sd.clauses.SetAlias(T(alias))) +} + +// Returns the alias value as an identiier expression +func (sd *SelectDataset) GetAs() exp.IdentifierExpression { + return sd.clauses.Alias() +} + +// Sets the WINDOW clauses +func (sd *SelectDataset) Window(ws ...exp.WindowExpression) *SelectDataset { + return sd.copy(sd.clauses.SetWindows(ws)) +} + +// Sets the WINDOW clauses +func (sd *SelectDataset) WindowAppend(ws ...exp.WindowExpression) *SelectDataset { + return sd.copy(sd.clauses.WindowsAppend(ws...)) +} + +// Sets the WINDOW clauses +func (sd *SelectDataset) ClearWindow() *SelectDataset { + return sd.copy(sd.clauses.ClearWindows()) +} + +// Get any error that has been set or nil if no error has been set. +func (sd *SelectDataset) Error() error { + return sd.err +} + +// Set an error on the dataset if one has not already been set. This error will be returned by a future call to Error +// or as part of ToSQL. This can be used by end users to record errors while building up queries without having to +// track those separately. +func (sd *SelectDataset) SetError(err error) *SelectDataset { + if sd.err == nil { + sd.err = err + } + + return sd +} + +// Generates a SELECT sql statement, if Prepared has been called with true then the parameters will not be interpolated. +// See examples. +// +// Errors: +// - There is an error generating the SQL +func (sd *SelectDataset) ToSQL() (sql string, params []interface{}, err error) { + return sd.selectSQLBuilder().ToSQL() +} + +// Generates the SELECT sql, and returns an Exec struct with the sql set to the SELECT statement +// +// db.From("test").Select("col").Executor() +// +// See Dataset#ToUpdateSQL for arguments +func (sd *SelectDataset) Executor() exec.QueryExecutor { + return sd.queryFactory.FromSQLBuilder(sd.selectSQLBuilder()) +} + +// Appends this Dataset's SELECT statement to the SQLBuilder +// This is used internally for sub-selects by the dialect +func (sd *SelectDataset) AppendSQL(b sb.SQLBuilder) { + if sd.err != nil { + b.SetError(sd.err) + return + } + sd.dialect.ToSelectSQL(b, sd.GetClauses()) +} + +func (sd *SelectDataset) ReturnsColumns() bool { + return true +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanStructs to scan the results into a slice of structs. +// +// ScanStructs will only select the columns that can be scanned in to the struct unless you have explicitly selected +// certain columns. See examples. +// +// i: A pointer to a slice of structs +func (sd *SelectDataset) ScanStructs(i interface{}) error { + return sd.ScanStructsContext(context.Background(), i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanStructsContext to scan the results into a slice of +// structs. +// +// ScanStructsContext will only select the columns that can be scanned in to the struct unless you have explicitly +// selected certain columns. See examples. +// +// i: A pointer to a slice of structs +func (sd *SelectDataset) ScanStructsContext(ctx context.Context, i interface{}) error { + if sd.queryFactory == nil { + return ErrQueryFactoryNotFoundError + } + ds := sd + if sd.GetClauses().IsDefaultSelect() { + ds = sd.Select(i) + } + return ds.Executor().ScanStructsContext(ctx, i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanStruct to scan the result into a slice of structs +// +// ScanStruct will only select the columns that can be scanned in to the struct unless you have explicitly selected +// certain columns. See examples. +// +// i: A pointer to a structs +func (sd *SelectDataset) ScanStruct(i interface{}) (bool, error) { + return sd.ScanStructContext(context.Background(), i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanStructContext to scan the result into a slice of structs +// +// ScanStructContext will only select the columns that can be scanned in to the struct unless you have explicitly +// selected certain columns. See examples. +// +// i: A pointer to a structs +func (sd *SelectDataset) ScanStructContext(ctx context.Context, i interface{}) (bool, error) { + if sd.queryFactory == nil { + return false, ErrQueryFactoryNotFoundError + } + ds := sd + if sd.GetClauses().IsDefaultSelect() { + ds = sd.Select(i) + } + return ds.Limit(1).Executor().ScanStructContext(ctx, i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanVals to scan the results into a slice of primitive values +// +// i: A pointer to a slice of primitive values +func (sd *SelectDataset) ScanVals(i interface{}) error { + return sd.ScanValsContext(context.Background(), i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanValsContext to scan the results into a slice of primitive +// values +// +// i: A pointer to a slice of primitive values +func (sd *SelectDataset) ScanValsContext(ctx context.Context, i interface{}) error { + if sd.queryFactory == nil { + return ErrQueryFactoryNotFoundError + } + return sd.Executor().ScanValsContext(ctx, i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanVal to scan the result into a primitive value +// +// i: A pointer to a primitive value +func (sd *SelectDataset) ScanVal(i interface{}) (bool, error) { + return sd.ScanValContext(context.Background(), i) +} + +// Generates the SELECT sql for this dataset and uses Exec#ScanValContext to scan the result into a primitive value +// +// i: A pointer to a primitive value +func (sd *SelectDataset) ScanValContext(ctx context.Context, i interface{}) (bool, error) { + if sd.queryFactory == nil { + return false, ErrQueryFactoryNotFoundError + } + return sd.Limit(1).Executor().ScanValContext(ctx, i) +} + +// Generates the SELECT COUNT(*) sql for this dataset and uses Exec#ScanVal to scan the result into an int64. +func (sd *SelectDataset) Count() (int64, error) { + return sd.CountContext(context.Background()) +} + +// Generates the SELECT COUNT(*) sql for this dataset and uses Exec#ScanValContext to scan the result into an int64. +func (sd *SelectDataset) CountContext(ctx context.Context) (int64, error) { + var count int64 + _, err := sd.Select(COUNT(Star()).As("count")).ScanValContext(ctx, &count) + return count, err +} + +// Generates the SELECT sql only selecting the passed in column and uses Exec#ScanVals to scan the result into a slice +// of primitive values. +// +// i: A slice of primitive values +// +// col: The column to select when generative the SQL +func (sd *SelectDataset) Pluck(i interface{}, col string) error { + return sd.PluckContext(context.Background(), i, col) +} + +// Generates the SELECT sql only selecting the passed in column and uses Exec#ScanValsContext to scan the result into a +// slice of primitive values. +// +// i: A slice of primitive values +// +// col: The column to select when generative the SQL +func (sd *SelectDataset) PluckContext(ctx context.Context, i interface{}, col string) error { + return sd.Select(col).ScanValsContext(ctx, i) +} + +func (sd *SelectDataset) selectSQLBuilder() sb.SQLBuilder { + buf := sb.NewSQLBuilder(sd.isPrepared.Bool()) + if sd.err != nil { + return buf.SetError(sd.err) + } + sd.dialect.ToSelectSQL(buf, sd.GetClauses()) + return buf +} diff --git a/select_dataset_example_test.go b/select_dataset_example_test.go new file mode 100644 index 0000000..839c62d --- /dev/null +++ b/select_dataset_example_test.go @@ -0,0 +1,1685 @@ +//nolint:lll // sql statements are long +package db_test + +import ( + goSQL "database/sql" + "fmt" + "os" + "regexp" + "time" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "github.com/lib/pq" +) + +const schema = ` + DROP TABLE IF EXISTS "user_role"; + DROP TABLE IF EXISTS "test_user"; + CREATE TABLE "test_user" ( + "id" SERIAL PRIMARY KEY NOT NULL, + "first_name" VARCHAR(45) NOT NULL, + "last_name" VARCHAR(45) NOT NULL, + "created" TIMESTAMP NOT NULL DEFAULT now() + ); + CREATE TABLE "user_role" ( + "id" SERIAL PRIMARY KEY NOT NULL, + "user_id" BIGINT NOT NULL REFERENCES test_user(id) ON DELETE CASCADE, + "name" VARCHAR(45) NOT NULL, + "created" TIMESTAMP NOT NULL DEFAULT now() + ); + ` + +const defaultDBURI = "postgres://postgres:@localhost:5435/dbv2postgres?sslmode=disable" + +var dbv2DB *dbv2.Database + +func getDB() *dbv2.Database { + if dbv2DB == nil { + dbURI := os.Getenv("PG_URI") + if dbURI == "" { + dbURI = defaultDBURI + } + uri, err := pq.ParseURL(dbURI) + if err != nil { + panic(err) + } + pdb, err := goSQL.Open("postgres", uri) + if err != nil { + panic(err) + } + dbv2DB = dbv2.New("postgres", pdb) + } + // reset the db + if _, err := dbv2DB.Exec(schema); err != nil { + panic(err) + } + type dbv2User struct { + ID int64 `db:"id" ff:"skipinsert"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Created time.Time `db:"created" ff:"skipupdate"` + } + + users := []dbv2User{ + {FirstName: "Bob", LastName: "Yukon"}, + {FirstName: "Sally", LastName: "Yukon"}, + {FirstName: "Vinita", LastName: "Yukon"}, + {FirstName: "John", LastName: "Doe"}, + } + var userIds []int64 + err := dbv2DB.Insert("test_user").Rows(users).Returning("id").Executor().ScanVals(&userIds) + if err != nil { + panic(err) + } + type userRole struct { + ID int64 `db:"id" ff:"skipinsert"` + UserID int64 `db:"user_id"` + Name string `db:"name"` + Created time.Time `db:"created" ff:"skipupdate"` + } + + roles := []userRole{ + {UserID: userIds[0], Name: "Admin"}, + {UserID: userIds[1], Name: "Manager"}, + {UserID: userIds[2], Name: "Manager"}, + {UserID: userIds[3], Name: "User"}, + } + _, err = dbv2DB.Insert("user_role").Rows(roles).Executor().Exec() + if err != nil { + panic(err) + } + return dbv2DB +} + +func ExampleSelectDataset() { + ds := dbv2.From("test"). + Select(dbv2.COUNT("*")). + InnerJoin(dbv2.T("test2"), dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.id")))). + LeftJoin(dbv2.T("test3"), dbv2.On(dbv2.I("test2.fkey").Eq(dbv2.I("test3.id")))). + Where( + dbv2.Ex{ + "test.name": dbv2.Op{ + "like": regexp.MustCompile("^[ab]"), + }, + "test2.amount": dbv2.Op{ + "isNot": nil, + }, + }, + dbv2.ExOr{ + "test3.id": nil, + "test3.status": []string{"passed", "active", "registered"}, + }). + Order(dbv2.I("test.created").Desc().NullsLast()). + GroupBy(dbv2.I("test.user_id")). + Having(dbv2.AVG("test3.age").Gt(10)) + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + sql, args, _ = ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + //nolint:lll // SQL statements are long + // Output: + // SELECT COUNT(*) FROM "test" INNER JOIN "test2" ON ("test"."fkey" = "test2"."id") LEFT JOIN "test3" ON ("test2"."fkey" = "test3"."id") WHERE ((("test"."name" ~ '^[ab]') AND ("test2"."amount" IS NOT NULL)) AND (("test3"."id" IS NULL) OR ("test3"."status" IN ('passed', 'active', 'registered')))) GROUP BY "test"."user_id" HAVING (AVG("test3"."age") > 10) ORDER BY "test"."created" DESC NULLS LAST [] + // SELECT COUNT(*) FROM "test" INNER JOIN "test2" ON ("test"."fkey" = "test2"."id") LEFT JOIN "test3" ON ("test2"."fkey" = "test3"."id") WHERE ((("test"."name" ~ ?) AND ("test2"."amount" IS NOT NULL)) AND (("test3"."id" IS NULL) OR ("test3"."status" IN (?, ?, ?)))) GROUP BY "test"."user_id" HAVING (AVG("test3"."age") > ?) ORDER BY "test"."created" DESC NULLS LAST [^[ab] passed active registered 10] +} + +func ExampleSelect() { + sql, _, _ := dbv2.Select(dbv2.L("NOW()")).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT NOW() +} + +func ExampleFrom() { + sql, args, _ := dbv2.From("test").ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" [] +} + +func ExampleSelectDataset_As() { + ds := dbv2.From("test").As("t") + sql, _, _ := dbv2.From(ds).ToSQL() + fmt.Println(sql) + // Output: SELECT * FROM (SELECT * FROM "test") AS "t" +} + +func ExampleSelectDataset_Union() { + sql, _, _ := dbv2.From("test"). + Union(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test"). + Limit(1). + Union(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test"). + Limit(1). + Union(dbv2.From("test2"). + Order(dbv2.C("id").Desc())). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" UNION (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" UNION (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" UNION (SELECT * FROM (SELECT * FROM "test2" ORDER BY "id" DESC) AS "t1") +} + +func ExampleSelectDataset_UnionAll() { + sql, _, _ := dbv2.From("test"). + UnionAll(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + UnionAll(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + UnionAll(dbv2.From("test2"). + Order(dbv2.C("id").Desc())). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" UNION ALL (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" UNION ALL (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" UNION ALL (SELECT * FROM (SELECT * FROM "test2" ORDER BY "id" DESC) AS "t1") +} + +func ExampleSelectDataset_With() { + sql, _, _ := dbv2.From("one"). + With("one", dbv2.From().Select(dbv2.L("1"))). + Select(dbv2.Star()). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("derived"). + With("intermed", dbv2.From("test").Select(dbv2.Star()).Where(dbv2.C("x").Gte(5))). + With("derived", dbv2.From("intermed").Select(dbv2.Star()).Where(dbv2.C("x").Lt(10))). + Select(dbv2.Star()). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("multi"). + With("multi(x,y)", dbv2.From().Select(dbv2.L("1"), dbv2.L("2"))). + Select(dbv2.C("x"), dbv2.C("y")). + ToSQL() + fmt.Println(sql) + + // Output: + // WITH one AS (SELECT 1) SELECT * FROM "one" + // WITH intermed AS (SELECT * FROM "test" WHERE ("x" >= 5)), derived AS (SELECT * FROM "intermed" WHERE ("x" < 10)) SELECT * FROM "derived" + // WITH multi(x,y) AS (SELECT 1, 2) SELECT "x", "y" FROM "multi" +} + +func ExampleSelectDataset_With_insertDataset() { + insertDs := dbv2.Insert("foo").Rows(dbv2.Record{"user_id": 10}).Returning("id") + + ds := dbv2.From("bar"). + With("ins", insertDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("ins.user_id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + + sql, args, _ := ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // WITH ins AS (INSERT INTO "foo" ("user_id") VALUES (10) RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "ins"."user_id") + // WITH ins AS (INSERT INTO "foo" ("user_id") VALUES (?) RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "ins"."user_id") [10] +} + +func ExampleSelectDataset_With_updateDataset() { + updateDs := dbv2.Update("foo").Set(dbv2.Record{"bar": "baz"}).Returning("id") + + ds := dbv2.From("bar"). + With("upd", updateDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("upd.user_id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + + sql, args, _ := ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + + // Output: + // WITH upd AS (UPDATE "foo" SET "bar"='baz' RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "upd"."user_id") + // WITH upd AS (UPDATE "foo" SET "bar"=? RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "upd"."user_id") [baz] +} + +func ExampleSelectDataset_With_deleteDataset() { + deleteDs := dbv2.Delete("foo").Where(dbv2.Ex{"bar": "baz"}).Returning("id") + + ds := dbv2.From("bar"). + With("del", deleteDs). + Select("bar_name"). + Where(dbv2.Ex{"bar.user_id": dbv2.I("del.user_id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + + sql, args, _ := ds.Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // WITH del AS (DELETE FROM "foo" WHERE ("bar" = 'baz') RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "del"."user_id") + // WITH del AS (DELETE FROM "foo" WHERE ("bar" = ?) RETURNING "id") SELECT "bar_name" FROM "bar" WHERE ("bar"."user_id" = "del"."user_id") [baz] +} + +func ExampleSelectDataset_WithRecursive() { + sql, _, _ := dbv2.From("nums"). + WithRecursive("nums(x)", + dbv2.From().Select(dbv2.L("1")). + UnionAll(dbv2.From("nums"). + Select(dbv2.L("x+1")).Where(dbv2.C("x").Lt(5)))). + ToSQL() + fmt.Println(sql) + // Output: + // WITH RECURSIVE nums(x) AS (SELECT 1 UNION ALL (SELECT x+1 FROM "nums" WHERE ("x" < 5))) SELECT * FROM "nums" +} + +func ExampleSelectDataset_Intersect() { + sql, _, _ := dbv2.From("test"). + Intersect(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + Intersect(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + Intersect(dbv2.From("test2"). + Order(dbv2.C("id").Desc())). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" INTERSECT (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" INTERSECT (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" INTERSECT (SELECT * FROM (SELECT * FROM "test2" ORDER BY "id" DESC) AS "t1") +} + +func ExampleSelectDataset_IntersectAll() { + sql, _, _ := dbv2.From("test"). + IntersectAll(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + IntersectAll(dbv2.From("test2")). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test"). + Limit(1). + IntersectAll(dbv2.From("test2"). + Order(dbv2.C("id").Desc())). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" INTERSECT ALL (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" INTERSECT ALL (SELECT * FROM "test2") + // SELECT * FROM (SELECT * FROM "test" LIMIT 1) AS "t1" INTERSECT ALL (SELECT * FROM (SELECT * FROM "test2" ORDER BY "id" DESC) AS "t1") +} + +func ExampleSelectDataset_ClearOffset() { + ds := dbv2.From("test"). + Offset(2) + sql, _, _ := ds. + ClearOffset(). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" +} + +func ExampleSelectDataset_Offset() { + ds := dbv2.From("test").Offset(2) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" OFFSET 2 +} + +func ExampleSelectDataset_Limit() { + ds := dbv2.From("test").Limit(10) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" LIMIT 10 +} + +func ExampleSelectDataset_LimitAll() { + ds := dbv2.From("test").LimitAll() + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" LIMIT ALL +} + +func ExampleSelectDataset_ClearLimit() { + ds := dbv2.From("test").Limit(10) + sql, _, _ := ds.ClearLimit().ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" +} + +func ExampleSelectDataset_Order() { + ds := dbv2.From("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" ORDER BY "a" ASC +} + +func ExampleSelectDataset_Order_caseExpression() { + ds := dbv2.From("test").Order(dbv2.Case().When(dbv2.C("num").Gt(10), 0).Else(1).Asc()) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" ORDER BY CASE WHEN ("num" > 10) THEN 0 ELSE 1 END ASC +} + +func ExampleSelectDataset_OrderAppend() { + ds := dbv2.From("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.OrderAppend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" ORDER BY "a" ASC, "b" DESC NULLS LAST +} + +func ExampleSelectDataset_OrderPrepend() { + ds := dbv2.From("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.OrderPrepend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" ORDER BY "b" DESC NULLS LAST, "a" ASC +} + +func ExampleSelectDataset_ClearOrder() { + ds := dbv2.From("test").Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ClearOrder().ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" +} + +func ExampleSelectDataset_GroupBy() { + sql, _, _ := dbv2.From("test"). + Select(dbv2.SUM("income").As("income_sum")). + GroupBy("age"). + ToSQL() + fmt.Println(sql) + // Output: + // SELECT SUM("income") AS "income_sum" FROM "test" GROUP BY "age" +} + +func ExampleSelectDataset_GroupByAppend() { + ds := dbv2.From("test"). + Select(dbv2.SUM("income").As("income_sum")). + GroupBy("age") + sql, _, _ := ds. + GroupByAppend("job"). + ToSQL() + fmt.Println(sql) + // the original dataset group by does not change + sql, _, _ = ds.ToSQL() + fmt.Println(sql) + // Output: + // SELECT SUM("income") AS "income_sum" FROM "test" GROUP BY "age", "job" + // SELECT SUM("income") AS "income_sum" FROM "test" GROUP BY "age" +} + +func ExampleSelectDataset_Having() { + sql, _, _ := dbv2.From("test").Having(dbv2.SUM("income").Gt(1000)).ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test").GroupBy("age").Having(dbv2.SUM("income").Gt(1000)).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" HAVING (SUM("income") > 1000) + // SELECT * FROM "test" GROUP BY "age" HAVING (SUM("income") > 1000) +} + +func ExampleSelectDataset_Window() { + ds := dbv2.From("test"). + Select(dbv2.ROW_NUMBER().Over(dbv2.W().PartitionBy("a").OrderBy(dbv2.I("b").Asc()))) + query, args, _ := ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().OverName(dbv2.I("w"))). + Window(dbv2.W("w").PartitionBy("a").OrderBy(dbv2.I("b").Asc())) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().OverName(dbv2.I("w1"))). + Window( + dbv2.W("w1").PartitionBy("a"), + dbv2.W("w").Inherit("w1").OrderBy(dbv2.I("b").Asc()), + ) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + + ds = dbv2.From("test"). + Select(dbv2.ROW_NUMBER().Over(dbv2.W().Inherit("w").OrderBy("b"))). + Window(dbv2.W("w").PartitionBy("a")) + query, args, _ = ds.ToSQL() + fmt.Println(query, args) + // Output + // SELECT ROW_NUMBER() OVER (PARTITION BY "a" ORDER BY "b" ASC) FROM "test" [] + // SELECT ROW_NUMBER() OVER "w" FROM "test" WINDOW "w" AS (PARTITION BY "a" ORDER BY "b" ASC) [] + // SELECT ROW_NUMBER() OVER "w" FROM "test" WINDOW "w1" AS (PARTITION BY "a"), "w" AS ("w1" ORDER BY "b" ASC) [] + // SELECT ROW_NUMBER() OVER ("w" ORDER BY "b") FROM "test" WINDOW "w" AS (PARTITION BY "a") [] +} + +func ExampleSelectDataset_Where() { + // By default everything is anded together + sql, _, _ := dbv2.From("test").Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use ExOr to get ORed expressions together + sql, _, _ = dbv2.From("test").Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use Or with Ex to Or multiple Ex maps together + sql, _, _ = dbv2.From("test").Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql) + // By default everything is anded together + sql, _, _ = dbv2.From("test").Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql) + // You can use a combination of Ors and Ands + sql, _, _ = dbv2.From("test").Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // SELECT * FROM "test" WHERE (("a" > 10) OR ("b" < 10) OR ("c" IS NULL) OR ("d" IN ('a', 'b', 'c'))) + // SELECT * FROM "test" WHERE ((("a" > 10) AND ("b" < 10)) OR (("c" IS NULL) AND ("d" IN ('a', 'b', 'c')))) + // SELECT * FROM "test" WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // SELECT * FROM "test" WHERE (("a" > 10) OR (("b" < 10) AND ("c" IS NULL))) +} + +func ExampleSelectDataset_Where_prepared() { + // By default everything is anded together + sql, args, _ := dbv2.From("test").Prepared(true).Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use ExOr to get ORed expressions together + sql, args, _ = dbv2.From("test").Prepared(true).Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use Or with Ex to Or multiple Ex maps together + sql, args, _ = dbv2.From("test").Prepared(true).Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql, args) + // By default everything is anded together + sql, args, _ = dbv2.From("test").Prepared(true).Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql, args) + // You can use a combination of Ors and Ands + sql, args, _ = dbv2.From("test").Prepared(true).Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "test" WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [10 10 a b c] + // SELECT * FROM "test" WHERE (("a" > ?) OR ("b" < ?) OR ("c" IS NULL) OR ("d" IN (?, ?, ?))) [10 10 a b c] + // SELECT * FROM "test" WHERE ((("a" > ?) AND ("b" < ?)) OR (("c" IS NULL) AND ("d" IN (?, ?, ?)))) [10 10 a b c] + // SELECT * FROM "test" WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [10 10 a b c] + // SELECT * FROM "test" WHERE (("a" > ?) OR (("b" < ?) AND ("c" IS NULL))) [10 10] +} + +func ExampleSelectDataset_ClearWhere() { + ds := dbv2.From("test").Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ) + sql, _, _ := ds.ClearWhere().ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" +} + +func ExampleSelectDataset_Join() { + sql, _, _ := dbv2.From("test").Join( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{"test.fkey": dbv2.I("test2.Id")}), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Join(dbv2.T("test2"), dbv2.Using("common_column")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Join( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.T("test2").Col("Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").Join( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.T("test").Col("fkey").Eq(dbv2.T("t").Col("Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" INNER JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" INNER JOIN "test2" USING ("common_column") + // SELECT * FROM "test" INNER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" INNER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_InnerJoin() { + sql, _, _ := dbv2.From("test").InnerJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").InnerJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").InnerJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").InnerJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" INNER JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" INNER JOIN "test2" USING ("common_column") + // SELECT * FROM "test" INNER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" INNER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_FullOuterJoin() { + sql, _, _ := dbv2.From("test").FullOuterJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullOuterJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" FULL OUTER JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" FULL OUTER JOIN "test2" USING ("common_column") + // SELECT * FROM "test" FULL OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" FULL OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_RightOuterJoin() { + sql, _, _ := dbv2.From("test").RightOuterJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightOuterJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" RIGHT OUTER JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" RIGHT OUTER JOIN "test2" USING ("common_column") + // SELECT * FROM "test" RIGHT OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" RIGHT OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_LeftOuterJoin() { + sql, _, _ := dbv2.From("test").LeftOuterJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftOuterJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftOuterJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" LEFT OUTER JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" LEFT OUTER JOIN "test2" USING ("common_column") + // SELECT * FROM "test" LEFT OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" LEFT OUTER JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_FullJoin() { + sql, _, _ := dbv2.From("test").FullJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").FullJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" FULL JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" FULL JOIN "test2" USING ("common_column") + // SELECT * FROM "test" FULL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" FULL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_RightJoin() { + sql, _, _ := dbv2.From("test").RightJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").RightJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" RIGHT JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" RIGHT JOIN "test2" USING ("common_column") + // SELECT * FROM "test" RIGHT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" RIGHT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_LeftJoin() { + sql, _, _ := dbv2.From("test").LeftJoin( + dbv2.T("test2"), + dbv2.On(dbv2.Ex{ + "test.fkey": dbv2.I("test2.Id"), + }), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftJoin( + dbv2.T("test2"), + dbv2.Using("common_column"), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("test2.Id"))), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").LeftJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + dbv2.On(dbv2.I("test.fkey").Eq(dbv2.I("t.Id"))), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" LEFT JOIN "test2" ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" LEFT JOIN "test2" USING ("common_column") + // SELECT * FROM "test" LEFT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) ON ("test"."fkey" = "test2"."Id") + // SELECT * FROM "test" LEFT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" ON ("test"."fkey" = "t"."Id") +} + +func ExampleSelectDataset_NaturalJoin() { + sql, _, _ := dbv2.From("test").NaturalJoin(dbv2.T("test2")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" NATURAL JOIN "test2" + // SELECT * FROM "test" NATURAL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) + // SELECT * FROM "test" NATURAL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" +} + +func ExampleSelectDataset_NaturalLeftJoin() { + sql, _, _ := dbv2.From("test").NaturalLeftJoin(dbv2.T("test2")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalLeftJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalLeftJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" NATURAL LEFT JOIN "test2" + // SELECT * FROM "test" NATURAL LEFT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) + // SELECT * FROM "test" NATURAL LEFT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" +} + +func ExampleSelectDataset_NaturalRightJoin() { + sql, _, _ := dbv2.From("test").NaturalRightJoin(dbv2.T("test2")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalRightJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalRightJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" NATURAL RIGHT JOIN "test2" + // SELECT * FROM "test" NATURAL RIGHT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) + // SELECT * FROM "test" NATURAL RIGHT JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" +} + +func ExampleSelectDataset_NaturalFullJoin() { + sql, _, _ := dbv2.From("test").NaturalFullJoin(dbv2.T("test2")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalFullJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").NaturalFullJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" NATURAL FULL JOIN "test2" + // SELECT * FROM "test" NATURAL FULL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) + // SELECT * FROM "test" NATURAL FULL JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" +} + +func ExampleSelectDataset_CrossJoin() { + sql, _, _ := dbv2.From("test").CrossJoin(dbv2.T("test2")).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").CrossJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)), + ).ToSQL() + fmt.Println(sql) + + sql, _, _ = dbv2.From("test").CrossJoin( + dbv2.From("test2").Where(dbv2.C("amount").Gt(0)).As("t"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" CROSS JOIN "test2" + // SELECT * FROM "test" CROSS JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) + // SELECT * FROM "test" CROSS JOIN (SELECT * FROM "test2" WHERE ("amount" > 0)) AS "t" +} + +func ExampleSelectDataset_FromSelf() { + sql, _, _ := dbv2.From("test").FromSelf().ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.From("test").As("my_test_table").FromSelf().ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM (SELECT * FROM "test") AS "t1" + // SELECT * FROM (SELECT * FROM "test") AS "my_test_table" +} + +func ExampleSelectDataset_From() { + ds := dbv2.From("test") + sql, _, _ := ds.From("test2").ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test2" +} + +func ExampleSelectDataset_From_withDataset() { + ds := dbv2.From("test") + fromDs := ds.Where(dbv2.C("age").Gt(10)) + sql, _, _ := ds.From(fromDs).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM (SELECT * FROM "test" WHERE ("age" > 10)) AS "t1" +} + +func ExampleSelectDataset_From_withAliasedDataset() { + ds := dbv2.From("test") + fromDs := ds.Where(dbv2.C("age").Gt(10)) + sql, _, _ := ds.From(fromDs.As("test2")).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM (SELECT * FROM "test" WHERE ("age" > 10)) AS "test2" +} + +func ExampleSelectDataset_Select() { + sql, _, _ := dbv2.From("test").Select("a", "b", "c").ToSQL() + fmt.Println(sql) + // Output: + // SELECT "a", "b", "c" FROM "test" +} + +func ExampleSelectDataset_Select_withDataset() { + ds := dbv2.From("test") + fromDs := ds.Select("age").Where(dbv2.C("age").Gt(10)) + sql, _, _ := ds.From().Select(fromDs).ToSQL() + fmt.Println(sql) + // Output: + // SELECT (SELECT "age" FROM "test" WHERE ("age" > 10)) +} + +func ExampleSelectDataset_Select_withAliasedDataset() { + ds := dbv2.From("test") + fromDs := ds.Select("age").Where(dbv2.C("age").Gt(10)) + sql, _, _ := ds.From().Select(fromDs.As("ages")).ToSQL() + fmt.Println(sql) + // Output: + // SELECT (SELECT "age" FROM "test" WHERE ("age" > 10)) AS "ages" +} + +func ExampleSelectDataset_Select_withLiteral() { + sql, _, _ := dbv2.From("test").Select(dbv2.L("a + b").As("sum")).ToSQL() + fmt.Println(sql) + // Output: + // SELECT a + b AS "sum" FROM "test" +} + +func ExampleSelectDataset_Select_withSQLFunctionExpression() { + sql, _, _ := dbv2.From("test").Select( + dbv2.COUNT("*").As("age_count"), + dbv2.MAX("age").As("max_age"), + dbv2.AVG("age").As("avg_age"), + ).ToSQL() + fmt.Println(sql) + // Output: + // SELECT COUNT(*) AS "age_count", MAX("age") AS "max_age", AVG("age") AS "avg_age" FROM "test" +} + +func ExampleSelectDataset_Select_withStruct() { + ds := dbv2.From("test") + + type myStruct struct { + Name string + Address string `db:"address"` + EmailAddress string `db:"email_address"` + } + + // Pass with pointer + sql, _, _ := ds.Select(&myStruct{}).ToSQL() + fmt.Println(sql) + + // Pass instance of + sql, _, _ = ds.Select(myStruct{}).ToSQL() + fmt.Println(sql) + + type myStruct2 struct { + myStruct + Zipcode string `db:"zipcode"` + } + + // Pass pointer to struct with embedded struct + sql, _, _ = ds.Select(&myStruct2{}).ToSQL() + fmt.Println(sql) + + // Pass instance of struct with embedded struct + sql, _, _ = ds.Select(myStruct2{}).ToSQL() + fmt.Println(sql) + + var myStructs []myStruct + + // Pass slice of structs, will only select columns from underlying type + sql, _, _ = ds.Select(myStructs).ToSQL() + fmt.Println(sql) + + // Output: + // SELECT "address", "email_address", "name" FROM "test" + // SELECT "address", "email_address", "name" FROM "test" + // SELECT "address", "email_address", "name", "zipcode" FROM "test" + // SELECT "address", "email_address", "name", "zipcode" FROM "test" + // SELECT "address", "email_address", "name" FROM "test" +} + +func ExampleSelectDataset_Distinct() { + sql, _, _ := dbv2.From("test").Select("a", "b").Distinct().ToSQL() + fmt.Println(sql) + // Output: + // SELECT DISTINCT "a", "b" FROM "test" +} + +func ExampleSelectDataset_Distinct_on() { + sql, _, _ := dbv2.From("test").Distinct("a").ToSQL() + fmt.Println(sql) + // Output: + // SELECT DISTINCT ON ("a") * FROM "test" +} + +func ExampleSelectDataset_Distinct_onWithLiteral() { + sql, _, _ := dbv2.From("test").Distinct(dbv2.L("COALESCE(?, ?)", dbv2.C("a"), "empty")).ToSQL() + fmt.Println(sql) + // Output: + // SELECT DISTINCT ON (COALESCE("a", 'empty')) * FROM "test" +} + +func ExampleSelectDataset_Distinct_onCoalesce() { + sql, _, _ := dbv2.From("test").Distinct(dbv2.COALESCE(dbv2.C("a"), "empty")).ToSQL() + fmt.Println(sql) + // Output: + // SELECT DISTINCT ON (COALESCE("a", 'empty')) * FROM "test" +} + +func ExampleSelectDataset_SelectAppend() { + ds := dbv2.From("test").Select("a", "b") + sql, _, _ := ds.SelectAppend("c").ToSQL() + fmt.Println(sql) + ds = dbv2.From("test").Select("a", "b").Distinct() + sql, _, _ = ds.SelectAppend("c").ToSQL() + fmt.Println(sql) + // Output: + // SELECT "a", "b", "c" FROM "test" + // SELECT DISTINCT "a", "b", "c" FROM "test" +} + +func ExampleSelectDataset_ClearSelect() { + ds := dbv2.From("test").Select("a", "b") + sql, _, _ := ds.ClearSelect().ToSQL() + fmt.Println(sql) + ds = dbv2.From("test").Select("a", "b").Distinct() + sql, _, _ = ds.ClearSelect().ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" + // SELECT * FROM "test" +} + +func ExampleSelectDataset_ToSQL() { + sql, args, _ := dbv2.From("items").Where(dbv2.Ex{"a": 1}).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "items" WHERE ("a" = 1) [] +} + +func ExampleSelectDataset_ToSQL_prepared() { + sql, args, _ := dbv2.From("items").Where(dbv2.Ex{"a": 1}).Prepared(true).ToSQL() + fmt.Println(sql, args) + // Output: + // SELECT * FROM "items" WHERE ("a" = ?) [1] +} + +func ExampleSelectDataset_Update() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + sql, args, _ := dbv2.From("items").Update().Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Update().Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Update().Set( + map[string]interface{}{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleSelectDataset_Insert() { + type item struct { + ID uint32 `db:"id" ff:"skipinsert"` + Address string `db:"address"` + Name string `db:"name"` + } + sql, args, _ := dbv2.From("items").Insert().Rows( + item{Name: "Test1", Address: "111 Test Addr"}, + item{Name: "Test2", Address: "112 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Insert().Rows( + dbv2.Record{"name": "Test1", "address": "111 Test Addr"}, + dbv2.Record{"name": "Test2", "address": "112 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Insert().Rows( + []item{ + {Name: "Test1", Address: "111 Test Addr"}, + {Name: "Test2", Address: "112 Test Addr"}, + }).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Insert().Rows( + []dbv2.Record{ + {"name": "Test1", "address": "111 Test Addr"}, + {"name": "Test2", "address": "112 Test Addr"}, + }).ToSQL() + fmt.Println(sql, args) + // Output: + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] + // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', 'Test1'), ('112 Test Addr', 'Test2') [] +} + +func ExampleSelectDataset_Delete() { + sql, args, _ := dbv2.From("items").Delete().ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items"). + Where(dbv2.Ex{"id": dbv2.Op{"gt": 10}}). + Delete(). + ToSQL() + fmt.Println(sql, args) + + // Output: + // DELETE FROM "items" [] + // DELETE FROM "items" WHERE ("id" > 10) [] +} + +func ExampleSelectDataset_Truncate() { + sql, args, _ := dbv2.From("items").Truncate().ToSQL() + fmt.Println(sql, args) + // Output: + // TRUNCATE "items" [] +} + +func ExampleSelectDataset_Prepared() { + sql, args, _ := dbv2.From("items").Prepared(true).Where(dbv2.Ex{ + "col1": "a", + "col2": 1, + "col3": true, + "col4": false, + "col5": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + //nolint:lll // sql statements are long + // Output: + // SELECT * FROM "items" WHERE (("col1" = ?) AND ("col2" = ?) AND ("col3" IS TRUE) AND ("col4" IS FALSE) AND ("col5" IN (?, ?, ?))) [a 1 a b c] +} + +func ExampleSelectDataset_ScanStructs() { + type User struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + db := getDB() + var users []User + if err := db.From("test_user").ScanStructs(&users); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("\n%+v", users) + + users = users[0:0] + if err := db.From("test_user").Select("first_name").ScanStructs(&users); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("\n%+v", users) + + // Output: + // [{FirstName:Bob LastName:Yukon} {FirstName:Sally LastName:Yukon} {FirstName:Vinita LastName:Yukon} {FirstName:John LastName:Doe}] + // [{FirstName:Bob LastName:} {FirstName:Sally LastName:} {FirstName:Vinita LastName:} {FirstName:John LastName:}] +} + +func ExampleSelectDataset_ScanStructs_prepared() { + type User struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + db := getDB() + + ds := db.From("test_user"). + Prepared(true). + Where(dbv2.Ex{ + "last_name": "Yukon", + }) + + var users []User + if err := ds.ScanStructs(&users); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("\n%+v", users) + + // Output: + // [{FirstName:Bob LastName:Yukon} {FirstName:Sally LastName:Yukon} {FirstName:Vinita LastName:Yukon}] +} + +// In this example we create a new struct that has two structs that represent two table +// the User and Role fields are tagged with the table name +func ExampleSelectDataset_ScanStructs_withJoinAutoSelect() { + type Role struct { + UserID uint64 `db:"user_id"` + Name string `db:"name"` + } + type User struct { + ID uint64 `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + type UserAndRole struct { + User User `db:"test_user"` // tag as the "test_user" table + Role Role `db:"user_role"` // tag as "user_role" table + } + db := getDB() + + ds := db. + From("test_user"). + Join(dbv2.T("user_role"), dbv2.On(dbv2.I("test_user.id").Eq(dbv2.I("user_role.user_id")))) + var users []UserAndRole + // Scan structs will auto build the + if err := ds.ScanStructs(&users); err != nil { + fmt.Println(err.Error()) + return + } + for _, u := range users { + fmt.Printf("\n%+v", u) + } + // Output: + // {User:{ID:1 FirstName:Bob LastName:Yukon} Role:{UserID:1 Name:Admin}} + // {User:{ID:2 FirstName:Sally LastName:Yukon} Role:{UserID:2 Name:Manager}} + // {User:{ID:3 FirstName:Vinita LastName:Yukon} Role:{UserID:3 Name:Manager}} + // {User:{ID:4 FirstName:John LastName:Doe} Role:{UserID:4 Name:User}} +} + +// In this example we create a new struct that has the user properties as well as a nested +// Role struct from the join table +func ExampleSelectDataset_ScanStructs_withJoinManualSelect() { + type Role struct { + UserID uint64 `db:"user_id"` + Name string `db:"name"` + } + type User struct { + ID uint64 `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Role Role `db:"user_role"` // tag as "user_role" table + } + db := getDB() + + ds := db. + Select( + "test_user.id", + "test_user.first_name", + "test_user.last_name", + // alias the fully qualified identifier `C` is important here so it doesnt parse it + dbv2.I("user_role.user_id").As(dbv2.C("user_role.user_id")), + dbv2.I("user_role.name").As(dbv2.C("user_role.name")), + ). + From("test_user"). + Join(dbv2.T("user_role"), dbv2.On(dbv2.I("test_user.id").Eq(dbv2.I("user_role.user_id")))) + var users []User + if err := ds.ScanStructs(&users); err != nil { + fmt.Println(err.Error()) + return + } + for _, u := range users { + fmt.Printf("\n%+v", u) + } + + // Output: + // {ID:1 FirstName:Bob LastName:Yukon Role:{UserID:1 Name:Admin}} + // {ID:2 FirstName:Sally LastName:Yukon Role:{UserID:2 Name:Manager}} + // {ID:3 FirstName:Vinita LastName:Yukon Role:{UserID:3 Name:Manager}} + // {ID:4 FirstName:John LastName:Doe Role:{UserID:4 Name:User}} +} + +func ExampleSelectDataset_ScanStruct() { + type User struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + db := getDB() + findUserByName := func(name string) { + var user User + ds := db.From("test_user").Where(dbv2.C("first_name").Eq(name)) + found, err := ds.ScanStruct(&user) + switch { + case err != nil: + fmt.Println(err.Error()) + case !found: + fmt.Printf("No user found for first_name %s\n", name) + default: + fmt.Printf("Found user: %+v\n", user) + } + } + + findUserByName("Bob") + findUserByName("Zeb") + + // Output: + // Found user: {FirstName:Bob LastName:Yukon} + // No user found for first_name Zeb +} + +// In this example we create a new struct that has two structs that represent two table +// the User and Role fields are tagged with the table name +func ExampleSelectDataset_ScanStruct_withJoinAutoSelect() { + type Role struct { + UserID uint64 `db:"user_id"` + Name string `db:"name"` + } + type User struct { + ID uint64 `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + type UserAndRole struct { + User User `db:"test_user"` // tag as the "test_user" table + Role Role `db:"user_role"` // tag as "user_role" table + } + db := getDB() + findUserAndRoleByName := func(name string) { + var userAndRole UserAndRole + ds := db. + From("test_user"). + Join( + dbv2.T("user_role"), + dbv2.On(dbv2.I("test_user.id").Eq(dbv2.I("user_role.user_id"))), + ). + Where(dbv2.C("first_name").Eq(name)) + found, err := ds.ScanStruct(&userAndRole) + switch { + case err != nil: + fmt.Println(err.Error()) + case !found: + fmt.Printf("No user found for first_name %s\n", name) + default: + fmt.Printf("Found user and role: %+v\n", userAndRole) + } + } + + findUserAndRoleByName("Bob") + findUserAndRoleByName("Zeb") + // Output: + // Found user and role: {User:{ID:1 FirstName:Bob LastName:Yukon} Role:{UserID:1 Name:Admin}} + // No user found for first_name Zeb +} + +// In this example we create a new struct that has the user properties as well as a nested +// Role struct from the join table +func ExampleSelectDataset_ScanStruct_withJoinManualSelect() { + type Role struct { + UserID uint64 `db:"user_id"` + Name string `db:"name"` + } + type User struct { + ID uint64 `db:"id"` + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Role Role `db:"user_role"` // tag as "user_role" table + } + db := getDB() + findUserByName := func(name string) { + var userAndRole User + ds := db. + Select( + "test_user.id", + "test_user.first_name", + "test_user.last_name", + // alias the fully qualified identifier `C` is important here so it doesnt parse it + dbv2.I("user_role.user_id").As(dbv2.C("user_role.user_id")), + dbv2.I("user_role.name").As(dbv2.C("user_role.name")), + ). + From("test_user"). + Join( + dbv2.T("user_role"), + dbv2.On(dbv2.I("test_user.id").Eq(dbv2.I("user_role.user_id"))), + ). + Where(dbv2.C("first_name").Eq(name)) + found, err := ds.ScanStruct(&userAndRole) + switch { + case err != nil: + fmt.Println(err.Error()) + case !found: + fmt.Printf("No user found for first_name %s\n", name) + default: + fmt.Printf("Found user and role: %+v\n", userAndRole) + } + } + + findUserByName("Bob") + findUserByName("Zeb") + + // Output: + // Found user and role: {ID:1 FirstName:Bob LastName:Yukon Role:{UserID:1 Name:Admin}} + // No user found for first_name Zeb +} + +func ExampleSelectDataset_ScanVals() { + var ids []int64 + if err := getDB().From("test_user").Select("id").ScanVals(&ids); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("UserIds = %+v", ids) + + // Output: + // UserIds = [1 2 3 4] +} + +func ExampleSelectDataset_ScanVal() { + db := getDB() + findUserIDByName := func(name string) { + var id int64 + ds := db.From("test_user"). + Select("id"). + Where(dbv2.C("first_name").Eq(name)) + + found, err := ds.ScanVal(&id) + switch { + case err != nil: + fmt.Println(err.Error()) + case !found: + fmt.Printf("No id found for user %s", name) + default: + fmt.Printf("\nFound userId: %+v\n", id) + } + } + + findUserIDByName("Bob") + findUserIDByName("Zeb") + // Output: + // Found userId: 1 + // No id found for user Zeb +} + +func ExampleSelectDataset_Count() { + count, err := getDB().From("test_user").Count() + if err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("Count is %d", count) + + // Output: + // Count is 4 +} + +func ExampleSelectDataset_Pluck() { + var lastNames []string + if err := getDB().From("test_user").Pluck(&lastNames, "last_name"); err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("LastNames = %+v", lastNames) + + // Output: + // LastNames = [Yukon Yukon Yukon Doe] +} + +func ExampleSelectDataset_Executor_scannerScanStruct() { + type User struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + } + db := getDB() + + scanner, err := db. + From("test_user"). + Select("first_name", "last_name"). + Where(dbv2.Ex{ + "last_name": "Yukon", + }). + Executor(). + Scanner() + if err != nil { + fmt.Println(err.Error()) + return + } + + defer scanner.Close() + + for scanner.Next() { + u := User{} + + err = scanner.ScanStruct(&u) + if err != nil { + fmt.Println(err.Error()) + return + } + + fmt.Printf("\n%+v", u) + } + + if scanner.Err() != nil { + fmt.Println(scanner.Err().Error()) + } + + // Output: + // {FirstName:Bob LastName:Yukon} + // {FirstName:Sally LastName:Yukon} + // {FirstName:Vinita LastName:Yukon} +} + +func ExampleSelectDataset_Executor_scannerScanVal() { + db := getDB() + + scanner, err := db. + From("test_user"). + Select("first_name"). + Where(dbv2.Ex{ + "last_name": "Yukon", + }). + Executor(). + Scanner() + if err != nil { + fmt.Println(err.Error()) + return + } + + defer scanner.Close() + + for scanner.Next() { + name := "" + + err = scanner.ScanVal(&name) + if err != nil { + fmt.Println(err.Error()) + return + } + + fmt.Println(name) + } + + if scanner.Err() != nil { + fmt.Println(scanner.Err().Error()) + } + + // Output: + // Bob + // Sally + // Vinita +} + +func ExampleForUpdate() { + sql, args, _ := dbv2.From("test").ForUpdate(exp.Wait).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" FOR UPDATE [] +} + +func ExampleForUpdate_of() { + sql, args, _ := dbv2.From("test").ForUpdate(exp.Wait, dbv2.T("test")).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "test" FOR UPDATE OF "test" [] +} + +func ExampleForUpdate_ofMultiple() { + sql, args, _ := dbv2.From("table1").Join( + dbv2.T("table2"), + dbv2.On(dbv2.I("table2.id").Eq(dbv2.I("table1.id"))), + ).ForUpdate( + exp.Wait, + dbv2.T("table1"), + dbv2.T("table2"), + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM "table1" INNER JOIN "table2" ON ("table2"."id" = "table1"."id") FOR UPDATE OF "table1", "table2" [] +} diff --git a/select_dataset_test.go b/select_dataset_test.go new file mode 100644 index 0000000..8c7cc1f --- /dev/null +++ b/select_dataset_test.go @@ -0,0 +1,1623 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/mocks" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ( + selectTestCase struct { + ds *dbv2.SelectDataset + clauses exp.SelectClauses + } + dsTestActionItem struct { + Address string `db:"address"` + Name string `db:"name"` + } + dsUntaggedTestActionItem struct { + Address string `db:"address"` + Name string `db:"name"` + Untagged string + } + selectDatasetSuite struct { + suite.Suite + } +) + +func (sds *selectDatasetSuite) assertCases(cases ...selectTestCase) { + for _, s := range cases { + sds.Equal(s.clauses, s.ds.GetClauses()) + } +} + +func (sds *selectDatasetSuite) TestReturnsColumns() { + ds := dbv2.Select(dbv2.L("NOW()")) + sds.True(ds.ReturnsColumns()) +} + +func (sds *selectDatasetSuite) TestClone() { + ds := dbv2.From("test") + sds.Equal(ds, ds.Clone()) +} + +func (sds *selectDatasetSuite) TestExpression() { + ds := dbv2.From("test") + sds.Equal(ds, ds.Expression()) +} + +func (sds *selectDatasetSuite) TestDialect() { + ds := dbv2.From("test") + sds.NotNil(ds.Dialect()) +} + +func (sds *selectDatasetSuite) TestWithDialect() { + ds := dbv2.From("test") + md := new(mocks.SQLDialect) + ds = ds.SetDialect(md) + + dialect := dbv2.GetDialect("default") + dialectDs := ds.WithDialect("default") + sds.Equal(md, ds.Dialect()) + sds.Equal(dialect, dialectDs.Dialect()) +} + +func (sds *selectDatasetSuite) TestPrepared() { + ds := dbv2.From("test") + preparedDs := ds.Prepared(true) + sds.True(preparedDs.IsPrepared()) + sds.False(ds.IsPrepared()) + // should apply the prepared to any datasets created from the root + sds.True(preparedDs.Where(dbv2.Ex{"a": 1}).IsPrepared()) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + // should be prepared by default + ds = dbv2.From("test") + sds.True(ds.IsPrepared()) +} + +func (sds *selectDatasetSuite) TestGetClauses() { + ds := dbv2.From("test") + ce := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression(dbv2.I("test"))) + sds.Equal(ce, ds.GetClauses()) +} + +func (sds *selectDatasetSuite) TestUpdate() { + where := dbv2.Ex{"a": 1} + from := dbv2.From("cte") + limit := uint(1) + order := []exp.OrderedExpression{dbv2.C("a").Asc(), dbv2.C("b").Desc()} + ds := dbv2.From("test"). + With("test-cte", from). + Where(where). + Limit(limit). + Order(order...) + ec := exp.NewUpdateClauses(). + SetTable(dbv2.C("test")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)). + WhereAppend(ds.GetClauses().Where()). + SetLimit(limit). + SetOrder(order...) + sds.Equal(ec, ds.Update().GetClauses()) +} + +func (sds *selectDatasetSuite) TestInsert() { + where := dbv2.Ex{"a": 1} + from := dbv2.From("cte") + limit := uint(1) + order := []exp.OrderedExpression{dbv2.C("a").Asc(), dbv2.C("b").Desc()} + ds := dbv2.From("test"). + With("test-cte", from). + Where(where). + Limit(limit). + Order(order...) + ec := exp.NewInsertClauses(). + SetInto(dbv2.C("test")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)) + sds.Equal(ec, ds.Insert().GetClauses()) +} + +func (sds *selectDatasetSuite) TestDelete() { + where := dbv2.Ex{"a": 1} + from := dbv2.From("cte") + limit := uint(1) + order := []exp.OrderedExpression{dbv2.C("a").Asc(), dbv2.C("b").Desc()} + ds := dbv2.From("test"). + With("test-cte", from). + Where(where). + Limit(limit). + Order(order...) + ec := exp.NewDeleteClauses(). + SetFrom(dbv2.C("test")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)). + WhereAppend(ds.GetClauses().Where()). + SetLimit(limit). + SetOrder(order...) + sds.Equal(ec, ds.Delete().GetClauses()) +} + +func (sds *selectDatasetSuite) TestTruncate() { + where := dbv2.Ex{"a": 1} + from := dbv2.From("cte") + limit := uint(1) + order := []exp.OrderedExpression{dbv2.C("a").Asc(), dbv2.C("b").Desc()} + ds := dbv2.From("test"). + With("test-cte", from). + Where(where). + Limit(limit). + Order(order...) + ec := exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")) + sds.Equal(ec, ds.Truncate().GetClauses()) +} + +func (sds *selectDatasetSuite) TestWith() { + from := dbv2.From("cte") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.With("test-cte", from), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestWithRecursive() { + from := dbv2.From("cte") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.WithRecursive("test-cte", from), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + CommonTablesAppend(exp.NewCommonTableExpression(true, "test-cte", from)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestSelect() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Select("a", "b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("a", "b")), + }, + selectTestCase{ + ds: bd.Select("a").Select("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("b")), + }, + selectTestCase{ + ds: bd.Select("a").Select(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestSelectDistinct() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.SelectDistinct("a", "b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("a", "b")). + SetDistinct(exp.NewColumnListExpression()), + }, + selectTestCase{ + ds: bd.SelectDistinct("a").SelectDistinct("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("b")). + SetDistinct(exp.NewColumnListExpression()), + }, + selectTestCase{ + ds: bd.Select("a").SelectDistinct("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("b")). + SetDistinct(exp.NewColumnListExpression()), + }, + selectTestCase{ + ds: bd.Select("a").SelectDistinct(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression(dbv2.Star())). + SetDistinct(nil), + }, + selectTestCase{ + ds: bd.SelectDistinct("a").SelectDistinct(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression(dbv2.Star())). + SetDistinct(nil), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearSelect() { + bd := dbv2.From("test").Select("a") + sds.assertCases( + selectTestCase{ + ds: bd.ClearSelect(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("a")), + }, + ) +} + +func (sds *selectDatasetSuite) TestSelectAppend() { + bd := dbv2.From("test").Select("a") + sds.assertCases( + selectTestCase{ + ds: bd.SelectAppend("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("a", "b")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetSelect(exp.NewColumnListExpression("a")), + }, + ) +} + +func (sds *selectDatasetSuite) TestDistinct() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Distinct("a", "b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetDistinct(exp.NewColumnListExpression("a", "b")), + }, + selectTestCase{ + ds: bd.Distinct("a").Distinct("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetDistinct(exp.NewColumnListExpression("b")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestFrom() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.From(dbv2.T("test2")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(dbv2.T("test2"))), + }, + selectTestCase{ + ds: bd.From(dbv2.From("test")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(dbv2.From("test").As("t1"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestFromSelf() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.FromSelf(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(bd.As("t1"))), + }, + selectTestCase{ + ds: bd.As("alias").FromSelf(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(bd.As("alias"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestCompoundFromSelf() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.CompoundFromSelf(), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd.Limit(10).CompoundFromSelf(), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression(bd.Limit(10).As("t1"))), + }, + selectTestCase{ + ds: bd.Order(dbv2.C("a").Asc()).CompoundFromSelf(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(bd.Order(dbv2.C("a").Asc()).As("t1"))), + }, + selectTestCase{ + ds: bd.As("alias").FromSelf(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression(bd.As("alias"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Join(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.InnerJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestInnerJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.InnerJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.InnerJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestFullOuterJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.FullOuterJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.FullOuterJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestRightOuterJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.RightOuterJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.RightOuterJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestLeftOuterJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.LeftOuterJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.LeftOuterJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestFullJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.FullJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.FullJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestRightJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.RightJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.RightJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestLeftJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.LeftJoin(dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewConditionedJoinExpression(exp.LeftJoinType, dbv2.T("foo"), dbv2.On(dbv2.C("a").IsNull())), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestNaturalJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.NaturalJoin(dbv2.T("foo")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewUnConditionedJoinExpression(exp.NaturalJoinType, dbv2.T("foo")), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestNaturalLeftJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.NaturalLeftJoin(dbv2.T("foo")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewUnConditionedJoinExpression(exp.NaturalLeftJoinType, dbv2.T("foo")), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestNaturalRightJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.NaturalRightJoin(dbv2.T("foo")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewUnConditionedJoinExpression(exp.NaturalRightJoinType, dbv2.T("foo")), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestNaturalFullJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.NaturalFullJoin(dbv2.T("foo")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewUnConditionedJoinExpression(exp.NaturalFullJoinType, dbv2.T("foo")), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestCrossJoin() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.CrossJoin(dbv2.T("foo")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + JoinsAppend( + exp.NewUnConditionedJoinExpression(exp.CrossJoinType, dbv2.T("foo")), + ), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestWhere() { + w := dbv2.Ex{"a": 1} + w2 := dbv2.Ex{"b": "c"} + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Where(w), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WhereAppend(w), + }, + selectTestCase{ + ds: bd.Where(w).Where(w2), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WhereAppend(w).WhereAppend(w2), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearWhere() { + w := dbv2.Ex{"a": 1} + bd := dbv2.From("test").Where(w) + sds.assertCases( + selectTestCase{ + ds: bd.ClearWhere(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).WhereAppend(w), + }, + ) +} + +func (sds *selectDatasetSuite) TestForUpdate() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.ForUpdate(dbv2.NoWait), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForUpdate, dbv2.NoWait)), + }, + selectTestCase{ + ds: bd.ForUpdate(dbv2.NoWait, dbv2.T("table1")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForUpdate, dbv2.NoWait, dbv2.T("table1"))), + }, + selectTestCase{ + ds: bd.ForUpdate(dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForUpdate, dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestForNoKeyUpdate() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.ForNoKeyUpdate(dbv2.NoWait), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForNoKeyUpdate, dbv2.NoWait)), + }, + selectTestCase{ + ds: bd.ForNoKeyUpdate(dbv2.NoWait, dbv2.T("table1")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForNoKeyUpdate, dbv2.NoWait, dbv2.T("table1"))), + }, + selectTestCase{ + ds: bd.ForNoKeyUpdate(dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForNoKeyUpdate, dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestForKeyShare() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.ForKeyShare(dbv2.NoWait), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForKeyShare, dbv2.NoWait)), + }, + selectTestCase{ + ds: bd.ForKeyShare(dbv2.NoWait, dbv2.T("table1")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForKeyShare, dbv2.NoWait, dbv2.T("table1"))), + }, + selectTestCase{ + ds: bd.ForKeyShare(dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForKeyShare, dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestForShare() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.ForShare(dbv2.NoWait), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForShare, dbv2.NoWait)), + }, + selectTestCase{ + ds: bd.ForShare(dbv2.NoWait, dbv2.T("table1")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForShare, dbv2.NoWait, dbv2.T("table1"))), + }, + selectTestCase{ + ds: bd.ForShare(dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2")), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLock(exp.NewLock(exp.ForShare, dbv2.NoWait, dbv2.T("table1"), dbv2.T("table2"))), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestGroupBy() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.GroupBy("a"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetGroupBy(exp.NewColumnListExpression("a")), + }, + selectTestCase{ + ds: bd.GroupBy("a").GroupBy("b"), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetGroupBy(exp.NewColumnListExpression("b")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestWindow() { + w1 := dbv2.W("w1").PartitionBy("a").OrderBy("b") + w2 := dbv2.W("w2").PartitionBy("a").OrderBy("b") + + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Window(w1), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w1), + }, + selectTestCase{ + ds: bd.Window(w1).Window(w2), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w2), + }, + selectTestCase{ + ds: bd.Window(w1, w2), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w1, w2), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestWindowAppend() { + w1 := dbv2.W("w1").PartitionBy("a").OrderBy("b") + w2 := dbv2.W("w2").PartitionBy("a").OrderBy("b") + + bd := dbv2.From("test").Window(w1) + sds.assertCases( + selectTestCase{ + ds: bd.WindowAppend(w2), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w1, w2), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w1), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearWindow() { + w1 := dbv2.W("w1").PartitionBy("a").OrderBy("b") + + bd := dbv2.From("test").Window(w1) + sds.assertCases( + selectTestCase{ + ds: bd.ClearWindow(), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + WindowsAppend(w1), + }, + ) +} + +func (sds *selectDatasetSuite) TestHaving() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Having(dbv2.C("a").Gt(1)), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + HavingAppend(dbv2.C("a").Gt(1)), + }, + selectTestCase{ + ds: bd.Having(dbv2.C("a").Gt(1)).Having(dbv2.Ex{"b": "c"}), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + HavingAppend(dbv2.C("a").Gt(1)).HavingAppend(dbv2.Ex{"b": "c"}), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestOrder() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Order(dbv2.C("a").Asc()), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("a").Asc()), + }, + selectTestCase{ + ds: bd.Order(dbv2.C("a").Asc()).Order(dbv2.C("b").Asc()), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("b").Asc()), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestOrderAppend() { + bd := dbv2.From("test").Order(dbv2.C("a").Asc()) + sds.assertCases( + selectTestCase{ + ds: bd.OrderAppend(dbv2.C("b").Asc()), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("a").Asc(), dbv2.C("b").Asc()), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (sds *selectDatasetSuite) TestOrderPrepend() { + bd := dbv2.From("test").Order(dbv2.C("a").Asc()) + sds.assertCases( + selectTestCase{ + ds: bd.OrderPrepend(dbv2.C("b").Asc()), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("b").Asc(), dbv2.C("a").Asc()), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearOrder() { + bd := dbv2.From("test").Order(dbv2.C("a").Asc()) + sds.assertCases( + selectTestCase{ + ds: bd.ClearOrder(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetOrder(dbv2.C("a").Asc()), + }, + ) +} + +func (sds *selectDatasetSuite) TestLimit() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Limit(10), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLimit(uint(10)), + }, + selectTestCase{ + ds: bd.Limit(0), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd.Limit(10).Limit(2), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLimit(uint(2)), + }, + selectTestCase{ + ds: bd.Limit(10).Limit(0), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestLimitAll() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.LimitAll(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLimit(dbv2.L("ALL")), + }, + selectTestCase{ + ds: bd.Limit(10).LimitAll(), + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLimit(dbv2.L("ALL")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearLimit() { + bd := dbv2.From("test").Limit(10) + sds.assertCases( + selectTestCase{ + ds: bd.ClearLimit(), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses(). + SetFrom(exp.NewColumnListExpression("test")). + SetLimit(uint(10)), + }, + ) +} + +func (sds *selectDatasetSuite) TestOffset() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Offset(10), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).SetOffset(10), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestClearOffset() { + bd := dbv2.From("test").Offset(10) + sds.assertCases( + selectTestCase{ + ds: bd.ClearOffset(), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).SetOffset(10), + }, + ) +} + +func (sds *selectDatasetSuite) TestUnion() { + uds := dbv2.From("union_test") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Union(uds), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + CompoundsAppend(exp.NewCompoundExpression(exp.UnionCompoundType, uds)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestUnionAll() { + uds := dbv2.From("union_test") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.UnionAll(uds), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + CompoundsAppend(exp.NewCompoundExpression(exp.UnionAllCompoundType, uds)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestIntersect() { + uds := dbv2.From("union_test") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.Intersect(uds), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + CompoundsAppend(exp.NewCompoundExpression(exp.IntersectCompoundType, uds)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestIntersectAll() { + uds := dbv2.From("union_test") + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.IntersectAll(uds), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + CompoundsAppend(exp.NewCompoundExpression(exp.IntersectAllCompoundType, uds)), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestAs() { + bd := dbv2.From("test") + sds.assertCases( + selectTestCase{ + ds: bd.As("t"), + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetAlias(dbv2.T("t")), + }, + selectTestCase{ + ds: bd, + clauses: exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")), + }, + ) +} + +func (sds *selectDatasetSuite) TestToSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.From("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToSelectSQL", sqlB, c).Return(nil).Once() + sql, args, err := ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Nil(err) + md.AssertExpectations(sds.T()) +} + +func (sds *selectDatasetSuite) TestToSQL_prepared() { + md := new(mocks.SQLDialect) + ds := dbv2.From("test").Prepared(true).SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(true) + md.On("ToSelectSQL", sqlB, c).Return(nil).Once() + sql, args, err := ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Nil(err) + md.AssertExpectations(sds.T()) +} + +func (sds *selectDatasetSuite) TestToSQL_ReturnedError() { + md := new(mocks.SQLDialect) + ds := dbv2.From("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + ee := errors.New("expected error") + md.On("ToSelectSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(ee) + }).Once() + + sql, args, err := ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Equal(ee, err) + md.AssertExpectations(sds.T()) +} + +func (sds *selectDatasetSuite) TestAppendSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.From("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToSelectSQL", sqlB, c).Return(nil).Once() + ds.AppendSQL(sqlB) + sds.NoError(sqlB.Error()) + md.AssertExpectations(sds.T()) +} + +func (sds *selectDatasetSuite) TestScanStructs() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + sqlMock.ExpectQuery(`SELECT DISTINCT "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + sqlMock.ExpectQuery(`SELECT "test" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("mock", mDB) + var items []dsTestActionItem + sds.NoError(db.From("items").ScanStructs(&items)) + sds.Equal([]dsTestActionItem{ + {Address: "111 Test Addr", Name: "Test1"}, + {Address: "211 Test Addr", Name: "Test2"}, + }, items) + + items = items[0:0] + sds.NoError(db.From("items").Select("name").Distinct().ScanStructs(&items)) + sds.Equal([]dsTestActionItem{ + {Address: "111 Test Addr", Name: "Test1"}, + {Address: "211 Test Addr", Name: "Test2"}, + }, items) + + items = items[0:0] + sds.EqualError(db.From("items").ScanStructs(items), + "db: type must be a pointer to a slice when scanning into structs") + sds.EqualError(db.From("items").ScanStructs(&dsTestActionItem{}), + "db: type must be a pointer to a slice when scanning into structs") + sds.EqualError(db.From("items").Select("test").ScanStructs(&items), + `db: unable to find corresponding field to column "test" returned by query`) + + sds.Equal(dbv2.ErrQueryFactoryNotFoundError, dbv2.From("items").ScanStructs(items)) +} + +func (sds *selectDatasetSuite) TestScanStructs_WithPreparedStatements() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT "address", "name" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\)`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy"). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + sqlMock.ExpectQuery( + `SELECT "test" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\)`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy"). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("mock", mDB) + var items []dsTestActionItem + sds.NoError(db.From("items").Prepared(true).Where(dbv2.Ex{ + "name": []string{"Bob", "Sally", "Billy"}, + "address": "111 Test Addr", + }).ScanStructs(&items)) + sds.Equal(items, []dsTestActionItem{ + {Address: "111 Test Addr", Name: "Test1"}, + {Address: "211 Test Addr", Name: "Test2"}, + }) + + items = items[0:0] + sds.EqualError(db.From("items").ScanStructs(items), + "db: type must be a pointer to a slice when scanning into structs") + sds.EqualError(db.From("items").ScanStructs(&dsTestActionItem{}), + "db: type must be a pointer to a slice when scanning into structs") + sds.EqualError(db.From("items"). + Prepared(true). + Select("test"). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + ScanStructs(&items), `db: unable to find corresponding field to column "test" returned by query`) +} + +func (sds *selectDatasetSuite) TestScanStruct() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + sqlMock.ExpectQuery(`SELECT DISTINCT "name" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + sqlMock.ExpectQuery(`SELECT "test" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("mock", mDB) + var item dsTestActionItem + found, err := db.From("items").ScanStruct(&item) + sds.NoError(err) + sds.True(found) + sds.Equal("111 Test Addr", item.Address) + sds.Equal("Test1", item.Name) + + item = dsTestActionItem{} + found, err = db.From("items").Select("name").Distinct().ScanStruct(&item) + sds.NoError(err) + sds.True(found) + sds.Equal("111 Test Addr", item.Address) + sds.Equal("Test1", item.Name) + + _, err = db.From("items").ScanStruct(item) + sds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.From("items").ScanStruct([]dsTestActionItem{}) + sds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.From("items").Select("test").ScanStruct(&item) + sds.EqualError(err, `db: unable to find corresponding field to column "test" returned by query`) + + _, err = dbv2.From("items").ScanStruct(item) + sds.Equal(dbv2.ErrQueryFactoryNotFoundError, err) +} + +func (sds *selectDatasetSuite) TestScanStruct_WithPreparedStatements() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT "address", "name" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\) LIMIT \?`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy", 1). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + sqlMock.ExpectQuery(`SELECT "test" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\) LIMIT \?`). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy", 1). + WillReturnRows(sqlmock.NewRows([]string{"test"}).FromCSVString("test1\ntest2")) + + db := dbv2.New("mock", mDB) + var item dsTestActionItem + found, err := db.From("items").Prepared(true).Where(dbv2.Ex{ + "name": []string{"Bob", "Sally", "Billy"}, + "address": "111 Test Addr", + }).ScanStruct(&item) + sds.NoError(err) + sds.True(found) + sds.Equal("111 Test Addr", item.Address) + sds.Equal("Test1", item.Name) + + _, err = db.From("items").ScanStruct(item) + sds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.From("items").ScanStruct([]dsTestActionItem{}) + sds.EqualError(err, "db: type must be a pointer to a struct when scanning into a struct") + _, err = db.From("items"). + Prepared(true). + Select("test"). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + ScanStruct(&item) + sds.EqualError(err, `db: unable to find corresponding field to column "test" returned by query`) +} + +func (sds *selectDatasetSuite) TestScanStructUntagged() { + defer dbv2.SetIgnoreUntaggedFields(false) + + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "address", "name", "untagged" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name", "untagged"}).FromCSVString("111 Test Addr,Test1,Test2")) + + sqlMock.ExpectQuery(`SELECT "address", "name" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).FromCSVString("111 Test Addr,Test1")) + + db := dbv2.New("mock", mDB) + var item dsUntaggedTestActionItem + + found, err := db.From("items").ScanStruct(&item) + sds.NoError(err) + sds.True(found) + sds.Equal("111 Test Addr", item.Address) + sds.Equal("Test1", item.Name) + sds.Equal("Test2", item.Untagged) + + // Ignore untagged fields, which will suppress the "untagged" column + dbv2.SetIgnoreUntaggedFields(true) + + item = dsUntaggedTestActionItem{} + found, err = db.From("items").ScanStruct(&item) + sds.NoError(err) + sds.True(found) + sds.Equal("111 Test Addr", item.Address) + sds.Equal("Test1", item.Name) + sds.Equal("", item.Untagged) +} + +func (sds *selectDatasetSuite) TestScanVals() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "id" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + sqlMock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + sqlMock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + + db := dbv2.New("mock", mDB) + var ids []uint32 + sds.NoError(db.From("items").Select("id").ScanVals(&ids)) + sds.Equal(ids, []uint32{1, 2, 3, 4, 5}) + + sds.EqualError(db.From("items").ScanVals([]uint32{}), + "db: type must be a pointer to a slice when scanning into vals") + sds.EqualError(db.From("items").ScanVals(dsTestActionItem{}), + "db: type must be a pointer to a slice when scanning into vals") + + err = dbv2.From("items").ScanVals(&ids) + sds.Equal(dbv2.ErrQueryFactoryNotFoundError, err) +} + +func (sds *selectDatasetSuite) TestScanVals_WithPreparedStatment() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT "id" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\)`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy"). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + + sqlMock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + sqlMock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("1\n2\n3\n4\n5")) + + db := dbv2.New("mock", mDB) + var ids []uint32 + sds.NoError(db.From("items"). + Prepared(true). + Select("id"). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + ScanVals(&ids)) + sds.Equal([]uint32{1, 2, 3, 4, 5}, ids) + + sds.EqualError(db.From("items").ScanVals([]uint32{}), + "db: type must be a pointer to a slice when scanning into vals") + + sds.EqualError(db.From("items").ScanVals(dsTestActionItem{}), + "db: type must be a pointer to a slice when scanning into vals") +} + +func (sds *selectDatasetSuite) TestScanVal() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "id" FROM "items" LIMIT 1`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("10")) + + db := dbv2.New("mock", mDB) + var id int64 + found, err := db.From("items").Select("id").ScanVal(&id) + sds.NoError(err) + sds.Equal(id, int64(10)) + sds.True(found) + + found, err = db.From("items").ScanVal([]int64{}) + sds.False(found) + sds.EqualError(err, "db: type must be a pointer when scanning into val") + found, err = db.From("items").ScanVal(10) + sds.False(found) + sds.EqualError(err, "db: type must be a pointer when scanning into val") + + _, err = dbv2.From("items").ScanVal(&id) + sds.Equal(dbv2.ErrQueryFactoryNotFoundError, err) +} + +func (sds *selectDatasetSuite) TestScanVal_WithPreparedStatement() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT "id" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\) LIMIT ?`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy", 1). + WillReturnRows(sqlmock.NewRows([]string{"id"}).FromCSVString("10")) + + db := dbv2.New("mock", mDB) + var id int64 + found, err := db.From("items"). + Prepared(true). + Select("id"). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + ScanVal(&id) + sds.NoError(err) + sds.Equal(int64(10), id) + sds.True(found) + + found, err = db.From("items").ScanVal([]int64{}) + sds.False(found) + sds.EqualError(err, "db: type must be a pointer when scanning into val") + found, err = db.From("items").ScanVal(10) + sds.False(found) + sds.EqualError(err, "db: type must be a pointer when scanning into val") +} + +func (sds *selectDatasetSuite) TestCount() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT COUNT\(\*\) AS "count" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"count"}).FromCSVString("10")) + + db := dbv2.New("mock", mDB) + count, err := db.From("items").Count() + sds.NoError(err) + sds.Equal(count, int64(10)) +} + +func (sds *selectDatasetSuite) TestCount_WithPreparedStatement() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT COUNT\(\*\) AS "count" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\)`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy", 1). + WillReturnRows(sqlmock.NewRows([]string{"count"}).FromCSVString("10")) + + ds := dbv2.New("mock", mDB) + count, err := ds.From("items"). + Prepared(true). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + Count() + sds.NoError(err) + sds.Equal(int64(10), count) +} + +func (sds *selectDatasetSuite) TestPluck() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery(`SELECT "name" FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"name"}).FromCSVString("test1\ntest2\ntest3\ntest4\ntest5")) + + db := dbv2.New("mock", mDB) + var names []string + sds.NoError(db.From("items").Pluck(&names, "name")) + sds.Equal([]string{"test1", "test2", "test3", "test4", "test5"}, names) +} + +func (sds *selectDatasetSuite) TestPluck_WithPreparedStatement() { + mDB, sqlMock, err := sqlmock.New() + sds.NoError(err) + sqlMock.ExpectQuery( + `SELECT "name" FROM "items" WHERE \(\("address" = \?\) AND \("name" IN \(\?, \?, \?\)\)\)`, + ). + WithArgs("111 Test Addr", "Bob", "Sally", "Billy"). + WillReturnRows(sqlmock.NewRows([]string{"name"}).FromCSVString("Bob\nSally\nBilly")) + + db := dbv2.New("mock", mDB) + var names []string + sds.NoError(db.From("items"). + Prepared(true). + Where(dbv2.Ex{"name": []string{"Bob", "Sally", "Billy"}, "address": "111 Test Addr"}). + Pluck(&names, "name")) + sds.Equal([]string{"Bob", "Sally", "Billy"}, names) +} + +func (sds *selectDatasetSuite) TestSetError() { + err1 := errors.New("error #1") + err2 := errors.New("error #2") + err3 := errors.New("error #3") + + // Verify initial error set/get works properly + md := new(mocks.SQLDialect) + ds := dbv2.From("test").SetDialect(md) + ds = ds.SetError(err1) + sds.Equal(err1, ds.Error()) + sql, args, err := ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Equal(err1, err) + + // Repeated SetError calls on Dataset should not overwrite the original error + ds = ds.SetError(err2) + sds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Equal(err1, err) + + // Builder functions should not lose the error + ds = ds.ClearWindow() + sds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Equal(err1, err) + + // Deeper errors inside SQL generation should still return original error + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToInsertSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(err3) + }).Once() + + sql, args, err = ds.ToSQL() + sds.Empty(sql) + sds.Empty(args) + sds.Equal(err1, err) +} + +func TestSelectDataset(t *testing.T) { + suite.Run(t, new(selectDatasetSuite)) +} diff --git a/sql_dialect.go b/sql_dialect.go new file mode 100644 index 0000000..5c6560b --- /dev/null +++ b/sql_dialect.go @@ -0,0 +1,103 @@ +package db + +import ( + "strings" + "sync" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" +) + +type ( + SQLDialectOptions = sqlgen.SQLDialectOptions + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + SQLDialect interface { + Dialect() string + ToSelectSQL(b sb.SQLBuilder, clauses exp.SelectClauses) + ToUpdateSQL(b sb.SQLBuilder, clauses exp.UpdateClauses) + ToInsertSQL(b sb.SQLBuilder, clauses exp.InsertClauses) + ToDeleteSQL(b sb.SQLBuilder, clauses exp.DeleteClauses) + ToTruncateSQL(b sb.SQLBuilder, clauses exp.TruncateClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + sqlDialect struct { + dialect string + dialectOptions *SQLDialectOptions + selectGen sqlgen.SelectSQLGenerator + updateGen sqlgen.UpdateSQLGenerator + insertGen sqlgen.InsertSQLGenerator + deleteGen sqlgen.DeleteSQLGenerator + truncateGen sqlgen.TruncateSQLGenerator + } +) + +var ( + dialects = make(map[string]SQLDialect) + DefaultDialectOptions = sqlgen.DefaultDialectOptions + dialectsMu sync.RWMutex +) + +func init() { + RegisterDialect("default", DefaultDialectOptions()) +} + +func RegisterDialect(name string, do *SQLDialectOptions) { + dialectsMu.Lock() + defer dialectsMu.Unlock() + lowerName := strings.ToLower(name) + dialects[lowerName] = newDialect(lowerName, do) +} + +func DeregisterDialect(name string) { + dialectsMu.Lock() + defer dialectsMu.Unlock() + delete(dialects, strings.ToLower(name)) +} + +func GetDialect(name string) SQLDialect { + name = strings.ToLower(name) + if d, ok := dialects[name]; ok { + return d + } + return newDialect("default", DefaultDialectOptions()) +} + +func newDialect(dialect string, do *SQLDialectOptions) SQLDialect { + return &sqlDialect{ + dialect: dialect, + dialectOptions: do, + selectGen: sqlgen.NewSelectSQLGenerator(dialect, do), + updateGen: sqlgen.NewUpdateSQLGenerator(dialect, do), + insertGen: sqlgen.NewInsertSQLGenerator(dialect, do), + deleteGen: sqlgen.NewDeleteSQLGenerator(dialect, do), + truncateGen: sqlgen.NewTruncateSQLGenerator(dialect, do), + } +} + +func (d *sqlDialect) Dialect() string { + return d.dialect +} + +func (d *sqlDialect) ToSelectSQL(b sb.SQLBuilder, clauses exp.SelectClauses) { + d.selectGen.Generate(b, clauses) +} + +func (d *sqlDialect) ToUpdateSQL(b sb.SQLBuilder, clauses exp.UpdateClauses) { + d.updateGen.Generate(b, clauses) +} + +func (d *sqlDialect) ToInsertSQL(b sb.SQLBuilder, clauses exp.InsertClauses) { + d.insertGen.Generate(b, clauses) +} + +func (d *sqlDialect) ToDeleteSQL(b sb.SQLBuilder, clauses exp.DeleteClauses) { + d.deleteGen.Generate(b, clauses) +} + +func (d *sqlDialect) ToTruncateSQL(b sb.SQLBuilder, clauses exp.TruncateClauses) { + d.truncateGen.Generate(b, clauses) +} diff --git a/sql_dialect_example_test.go b/sql_dialect_example_test.go new file mode 100644 index 0000000..e105216 --- /dev/null +++ b/sql_dialect_example_test.go @@ -0,0 +1,23 @@ +package db_test + +import ( + "fmt" + + dbv2 "git.fsdpf.net/go/db/v2" +) + +func ExampleRegisterDialect() { + opts := dbv2.DefaultDialectOptions() + opts.QuoteRune = '`' + dbv2.RegisterDialect("custom-dialect", opts) + + dialect := dbv2.Dialect("custom-dialect") + + ds := dialect.From("test") + + sql, args, _ := ds.ToSQL() + fmt.Println(sql, args) + + // Output: + // SELECT * FROM `test` [] +} diff --git a/sql_dialect_internal_test.go b/sql_dialect_internal_test.go new file mode 100644 index 0000000..f985b4a --- /dev/null +++ b/sql_dialect_internal_test.go @@ -0,0 +1,91 @@ +package db + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen/mocks" + "github.com/stretchr/testify/suite" +) + +type dialectTestSuite struct { + suite.Suite +} + +func (dts *dialectTestSuite) TestDialect() { + opts := DefaultDialectOptions() + sm := new(mocks.SelectSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, selectGen: sm} + + dts.Equal("test", d.Dialect()) +} + +func (dts *dialectTestSuite) TestToSelectSQL() { + opts := DefaultDialectOptions() + sm := new(mocks.SelectSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, selectGen: sm} + + b := sb.NewSQLBuilder(true) + sc := exp.NewSelectClauses() + sm.On("Generate", b, sc).Return(nil).Once() + + d.ToSelectSQL(b, sc) + sm.AssertExpectations(dts.T()) +} + +func (dts *dialectTestSuite) TestToUpdateSQL() { + opts := DefaultDialectOptions() + um := new(mocks.UpdateSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, updateGen: um} + + b := sb.NewSQLBuilder(true) + uc := exp.NewUpdateClauses() + um.On("Generate", b, uc).Return(nil).Once() + + d.ToUpdateSQL(b, uc) + um.AssertExpectations(dts.T()) +} + +func (dts *dialectTestSuite) TestToInsertSQL() { + opts := DefaultDialectOptions() + im := new(mocks.InsertSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, insertGen: im} + + b := sb.NewSQLBuilder(true) + ic := exp.NewInsertClauses() + im.On("Generate", b, ic).Return(nil).Once() + + d.ToInsertSQL(b, ic) + im.AssertExpectations(dts.T()) +} + +func (dts *dialectTestSuite) TestToDeleteSQL() { + opts := DefaultDialectOptions() + dm := new(mocks.DeleteSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, deleteGen: dm} + + b := sb.NewSQLBuilder(true) + dc := exp.NewDeleteClauses() + dm.On("Generate", b, dc).Return(nil).Once() + + d.ToDeleteSQL(b, dc) + dm.AssertExpectations(dts.T()) +} + +func (dts *dialectTestSuite) TestToTruncateSQL() { + opts := DefaultDialectOptions() + tm := new(mocks.TruncateSQLGenerator) + d := sqlDialect{dialect: "test", dialectOptions: opts, truncateGen: tm} + + b := sb.NewSQLBuilder(true) + tc := exp.NewTruncateClauses() + tm.On("Generate", b, tc).Return(nil).Once() + + d.ToTruncateSQL(b, tc) + tm.AssertExpectations(dts.T()) +} + +func TestSQLDialect(t *testing.T) { + suite.Run(t, new(dialectTestSuite)) +} diff --git a/sqlgen/base_test.go b/sqlgen/base_test.go new file mode 100644 index 0000000..8f1b7bb --- /dev/null +++ b/sqlgen/base_test.go @@ -0,0 +1,39 @@ +package sqlgen_test + +import ( + "git.fsdpf.net/go/db/v2/internal/sb" + "github.com/stretchr/testify/suite" +) + +type baseSQLGeneratorSuite struct { + suite.Suite +} + +func (bsgs *baseSQLGeneratorSuite) assertNotPreparedSQL(b sb.SQLBuilder, expectedSQL string) { + actualSQL, actualArgs, err := b.ToSQL() + bsgs.NoError(err) + bsgs.Equal(expectedSQL, actualSQL) + bsgs.Empty(actualArgs) +} + +func (bsgs *baseSQLGeneratorSuite) assertPreparedSQL( + b sb.SQLBuilder, + expectedSQL string, + expectedArgs []interface{}, +) { + actualSQL, actualArgs, err := b.ToSQL() + bsgs.NoError(err) + bsgs.Equal(expectedSQL, actualSQL) + if len(actualArgs) == 0 { + bsgs.Empty(expectedArgs) + } else { + bsgs.Equal(expectedArgs, actualArgs) + } +} + +func (bsgs *baseSQLGeneratorSuite) assertErrorSQL(b sb.SQLBuilder, errMsg string) { + actualSQL, actualArgs, err := b.ToSQL() + bsgs.EqualError(err, errMsg) + bsgs.Empty(actualSQL) + bsgs.Empty(actualArgs) +} diff --git a/sqlgen/common_sql_generator.go b/sqlgen/common_sql_generator.go new file mode 100644 index 0000000..95c3e4e --- /dev/null +++ b/sqlgen/common_sql_generator.go @@ -0,0 +1,155 @@ +package sqlgen + +import ( + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +var ErrNoUpdatedValuesProvided = errors.New("no update values provided") + +func ErrCTENotSupported(dialect string) error { + return errors.New("dialect does not support CTE WITH clause [dialect=%s]", dialect) +} + +func ErrRecursiveCTENotSupported(dialect string) error { + return errors.New("dialect does not support CTE WITH RECURSIVE clause [dialect=%s]", dialect) +} + +func ErrReturnNotSupported(dialect string) error { + return errors.New("dialect does not support RETURNING clause [dialect=%s]", dialect) +} + +func ErrNotSupportedFragment(sqlType string, f SQLFragmentType) error { + return errors.New("unsupported %s SQL fragment %s", sqlType, f) +} + +type ( + CommonSQLGenerator interface { + Dialect() string + DialectOptions() *SQLDialectOptions + ExpressionSQLGenerator() ExpressionSQLGenerator + ReturningSQL(b sb.SQLBuilder, returns exp.ColumnListExpression) + FromSQL(b sb.SQLBuilder, from exp.ColumnListExpression) + SourcesSQL(b sb.SQLBuilder, from exp.ColumnListExpression) + WhereSQL(b sb.SQLBuilder, where exp.ExpressionList) + OrderSQL(b sb.SQLBuilder, order exp.ColumnListExpression) + OrderWithOffsetFetchSQL(b sb.SQLBuilder, order exp.ColumnListExpression, offset uint, limit interface{}) + LimitSQL(b sb.SQLBuilder, limit interface{}) + UpdateExpressionSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression) + } + commonSQLGenerator struct { + dialect string + esg ExpressionSQLGenerator + dialectOptions *SQLDialectOptions + } +) + +func NewCommonSQLGenerator(dialect string, do *SQLDialectOptions) CommonSQLGenerator { + return &commonSQLGenerator{dialect: dialect, esg: NewExpressionSQLGenerator(dialect, do), dialectOptions: do} +} + +func (csg *commonSQLGenerator) Dialect() string { + return csg.dialect +} + +func (csg *commonSQLGenerator) DialectOptions() *SQLDialectOptions { + return csg.dialectOptions +} + +func (csg *commonSQLGenerator) ExpressionSQLGenerator() ExpressionSQLGenerator { + return csg.esg +} + +func (csg *commonSQLGenerator) ReturningSQL(b sb.SQLBuilder, returns exp.ColumnListExpression) { + if returns != nil && len(returns.Columns()) > 0 { + if csg.dialectOptions.SupportsReturn { + b.Write(csg.dialectOptions.ReturningFragment) + csg.esg.Generate(b, returns) + } else { + b.SetError(ErrReturnNotSupported(csg.dialect)) + } + } +} + +// Adds the FROM clause and tables to an sql statement +func (csg *commonSQLGenerator) FromSQL(b sb.SQLBuilder, from exp.ColumnListExpression) { + if from != nil && !from.IsEmpty() { + b.Write(csg.dialectOptions.FromFragment) + csg.SourcesSQL(b, from) + } +} + +// Adds the generates the SQL for a column list +func (csg *commonSQLGenerator) SourcesSQL(b sb.SQLBuilder, from exp.ColumnListExpression) { + b.WriteRunes(csg.dialectOptions.SpaceRune) + csg.esg.Generate(b, from) +} + +// Generates the WHERE clause for an SQL statement +func (csg *commonSQLGenerator) WhereSQL(b sb.SQLBuilder, where exp.ExpressionList) { + if where != nil && !where.IsEmpty() { + b.Write(csg.dialectOptions.WhereFragment) + csg.esg.Generate(b, where) + } +} + +// Generates the ORDER BY clause for an SQL statement +func (csg *commonSQLGenerator) OrderSQL(b sb.SQLBuilder, order exp.ColumnListExpression) { + if order != nil && len(order.Columns()) > 0 { + b.Write(csg.dialectOptions.OrderByFragment) + csg.esg.Generate(b, order) + } +} + +func (csg *commonSQLGenerator) OrderWithOffsetFetchSQL( + b sb.SQLBuilder, + order exp.ColumnListExpression, + offset uint, + limit interface{}, +) { + if order == nil { + return + } + + csg.OrderSQL(b, order) + if offset > 0 { + b.Write(csg.dialectOptions.OffsetFragment) + csg.esg.Generate(b, offset) + b.Write([]byte(" ROWS")) + + if limit != nil { + b.Write(csg.dialectOptions.FetchFragment) + csg.esg.Generate(b, limit) + b.Write([]byte(" ROWS ONLY")) + } + } +} + +// Generates the LIMIT clause for an SQL statement +func (csg *commonSQLGenerator) LimitSQL(b sb.SQLBuilder, limit interface{}) { + if limit != nil { + b.Write(csg.dialectOptions.LimitFragment) + if csg.dialectOptions.SurroundLimitWithParentheses { + b.WriteRunes(csg.dialectOptions.LeftParenRune) + } + csg.esg.Generate(b, limit) + if csg.dialectOptions.SurroundLimitWithParentheses { + b.WriteRunes(csg.dialectOptions.RightParenRune) + } + } +} + +func (csg *commonSQLGenerator) UpdateExpressionSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression) { + if len(updates) == 0 { + b.SetError(ErrNoUpdatedValuesProvided) + return + } + updateLen := len(updates) + for i, update := range updates { + csg.esg.Generate(b, update) + if i < updateLen-1 { + b.WriteRunes(csg.dialectOptions.CommaRune) + } + } +} diff --git a/sqlgen/common_sql_generator_test.go b/sqlgen/common_sql_generator_test.go new file mode 100644 index 0000000..78ed1d9 --- /dev/null +++ b/sqlgen/common_sql_generator_test.go @@ -0,0 +1,339 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + commonSQLTestCase struct { + gen func(builder sb.SQLBuilder) + sql string + isPrepared bool + err string + args []interface{} + } + commonSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (csgs *commonSQLGeneratorSuite) assertCases(testCases ...commonSQLTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + tc.gen(b) + switch { + case len(tc.err) > 0: + csgs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + csgs.assertPreparedSQL(b, tc.sql, tc.args) + default: + csgs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (csgs *commonSQLGeneratorSuite) TestReturningSQL() { + returningGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.ReturningSQL(sb, exp.NewColumnListExpression("a", "b")) + } + } + + returningNoColsGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.ReturningSQL(sb, exp.NewColumnListExpression()) + } + } + + returningNilExpGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.ReturningSQL(sb, nil) + } + } + + opts := sqlgen.DefaultDialectOptions() + opts.SupportsReturn = true + csgs1 := sqlgen.NewCommonSQLGenerator("test", opts) + + opts2 := sqlgen.DefaultDialectOptions() + opts2.SupportsReturn = false + csgs2 := sqlgen.NewCommonSQLGenerator("test", opts2) + + csgs.assertCases( + commonSQLTestCase{gen: returningGen(csgs1), sql: ` RETURNING "a", "b"`}, + commonSQLTestCase{gen: returningGen(csgs1), sql: ` RETURNING "a", "b"`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: returningNoColsGen(csgs1), sql: ``}, + commonSQLTestCase{gen: returningNoColsGen(csgs1), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: returningNilExpGen(csgs1), sql: ``}, + commonSQLTestCase{gen: returningNilExpGen(csgs1), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: returningGen(csgs2), err: `db: dialect does not support RETURNING clause [dialect=test]`}, + commonSQLTestCase{gen: returningGen(csgs2), err: `db: dialect does not support RETURNING clause [dialect=test]`}, + ) +} + +func (csgs *commonSQLGeneratorSuite) TestFromSQL() { + fromGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.FromSQL(sb, exp.NewColumnListExpression("a", "b")) + } + } + + fromNoColsGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.FromSQL(sb, exp.NewColumnListExpression()) + } + } + + fromNilExpGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.FromSQL(sb, nil) + } + } + + csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions()) + + opts := sqlgen.DefaultDialectOptions() + opts.FromFragment = []byte(" from") + csgFromFrag := sqlgen.NewCommonSQLGenerator("test", opts) + + csgs.assertCases( + commonSQLTestCase{gen: fromGen(csg), sql: ` FROM "a", "b"`}, + commonSQLTestCase{gen: fromGen(csg), sql: ` FROM "a", "b"`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: fromNoColsGen(csg), sql: ``}, + commonSQLTestCase{gen: fromNoColsGen(csg), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: fromNilExpGen(csg), sql: ``}, + commonSQLTestCase{gen: fromNilExpGen(csg), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: fromGen(csgFromFrag), sql: ` from "a", "b"`}, + commonSQLTestCase{gen: fromGen(csgFromFrag), sql: ` from "a", "b"`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: fromNoColsGen(csgFromFrag), sql: ``}, + commonSQLTestCase{gen: fromNoColsGen(csgFromFrag), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: fromNilExpGen(csgFromFrag), sql: ``}, + commonSQLTestCase{gen: fromNilExpGen(csgFromFrag), sql: ``, isPrepared: true, args: emptyArgs}, + ) +} + +func (csgs *commonSQLGeneratorSuite) TestWhereSQL() { + whereAndGen := func(csgs sqlgen.CommonSQLGenerator, exps ...exp.Expression) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.WhereSQL(sb, exp.NewExpressionList(exp.AndType, exps...)) + } + } + + whereOrGen := func(csgs sqlgen.CommonSQLGenerator, exps ...exp.Expression) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.WhereSQL(sb, exp.NewExpressionList(exp.OrType, exps...)) + } + } + + csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions()) + + opts := sqlgen.DefaultDialectOptions() + opts.WhereFragment = []byte(" where ") + csgWhereFrag := sqlgen.NewCommonSQLGenerator("test", opts) + + w := exp.Ex{"a": "b"} + w2 := exp.Ex{"b": "c"} + + csgs.assertCases( + commonSQLTestCase{gen: whereAndGen(csg), sql: ``}, + commonSQLTestCase{gen: whereAndGen(csg), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: whereAndGen(csg, w), sql: ` WHERE ("a" = 'b')`}, + commonSQLTestCase{gen: whereAndGen(csg, w), sql: ` WHERE ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + commonSQLTestCase{gen: whereAndGen(csg, w, w2), sql: ` WHERE (("a" = 'b') AND ("b" = 'c'))`}, + commonSQLTestCase{gen: whereAndGen(csg, w, w2), sql: ` WHERE (("a" = ?) AND ("b" = ?))`, isPrepared: true, args: []interface{}{"b", "c"}}, + + commonSQLTestCase{gen: whereOrGen(csg), sql: ``}, + commonSQLTestCase{gen: whereOrGen(csg), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: whereOrGen(csg, w), sql: ` WHERE ("a" = 'b')`}, + commonSQLTestCase{gen: whereOrGen(csg, w), sql: ` WHERE ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + commonSQLTestCase{gen: whereOrGen(csg, w, w2), sql: ` WHERE (("a" = 'b') OR ("b" = 'c'))`}, + commonSQLTestCase{gen: whereOrGen(csg, w, w2), sql: ` WHERE (("a" = ?) OR ("b" = ?))`, isPrepared: true, args: []interface{}{"b", "c"}}, + + commonSQLTestCase{gen: whereAndGen(csgWhereFrag), sql: ``}, + commonSQLTestCase{gen: whereAndGen(csgWhereFrag), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w), sql: ` where ("a" = 'b')`}, + commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w), sql: ` where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w, w2), sql: ` where (("a" = 'b') AND ("b" = 'c'))`}, + commonSQLTestCase{ + gen: whereAndGen(csgWhereFrag, w, w2), + sql: ` where (("a" = ?) AND ("b" = ?))`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + + commonSQLTestCase{gen: whereOrGen(csgWhereFrag), sql: ``}, + commonSQLTestCase{gen: whereOrGen(csgWhereFrag), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w), sql: ` where ("a" = 'b')`}, + commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w), sql: ` where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w, w2), sql: ` where (("a" = 'b') OR ("b" = 'c'))`}, + commonSQLTestCase{ + gen: whereOrGen(csgWhereFrag, w, w2), + sql: ` where (("a" = ?) OR ("b" = ?))`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + ) +} + +func (csgs *commonSQLGeneratorSuite) TestOrderSQL() { + orderGen := func(csgs sqlgen.CommonSQLGenerator, o ...exp.OrderedExpression) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.OrderSQL(sb, exp.NewOrderedColumnList(o...)) + } + } + + csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions()) + + opts := sqlgen.DefaultDialectOptions() + // override fragments to ensure they are used + opts.OrderByFragment = []byte(" order by ") + opts.AscFragment = []byte(" asc") + opts.DescFragment = []byte(" desc") + opts.NullsFirstFragment = []byte(" nulls first") + opts.NullsLastFragment = []byte(" nulls last") + csgCustom := sqlgen.NewCommonSQLGenerator("test", opts) + + ident := exp.NewIdentifierExpression("", "", "a") + oa := ident.Asc() + oanf := ident.Asc().NullsFirst() + oanl := ident.Asc().NullsLast() + + od := ident.Desc() + odnf := ident.Desc().NullsFirst() + odnl := ident.Desc().NullsLast() + + csgs.assertCases( + commonSQLTestCase{gen: orderGen(csg), sql: ``}, + commonSQLTestCase{gen: orderGen(csg), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, oa), sql: ` ORDER BY "a" ASC`}, + commonSQLTestCase{gen: orderGen(csg, oa), sql: ` ORDER BY "a" ASC`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, oanf), sql: ` ORDER BY "a" ASC NULLS FIRST`}, + commonSQLTestCase{gen: orderGen(csg, oanf), sql: ` ORDER BY "a" ASC NULLS FIRST`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, oanl), sql: ` ORDER BY "a" ASC NULLS LAST`}, + commonSQLTestCase{gen: orderGen(csg, oanl), sql: ` ORDER BY "a" ASC NULLS LAST`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, od), sql: ` ORDER BY "a" DESC`}, + commonSQLTestCase{gen: orderGen(csg, od), sql: ` ORDER BY "a" DESC`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, odnf), sql: ` ORDER BY "a" DESC NULLS FIRST`}, + commonSQLTestCase{gen: orderGen(csg, odnf), sql: ` ORDER BY "a" DESC NULLS FIRST`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, odnl), sql: ` ORDER BY "a" DESC NULLS LAST`}, + commonSQLTestCase{gen: orderGen(csg, odnl), sql: ` ORDER BY "a" DESC NULLS LAST`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csg, oa, od), sql: ` ORDER BY "a" ASC, "a" DESC`}, + commonSQLTestCase{gen: orderGen(csg, oa, od), sql: ` ORDER BY "a" ASC, "a" DESC`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom), sql: ``}, + commonSQLTestCase{gen: orderGen(csgCustom), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, oa), sql: ` order by "a" asc`}, + commonSQLTestCase{gen: orderGen(csgCustom, oa), sql: ` order by "a" asc`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, oanf), sql: ` order by "a" asc nulls first`}, + commonSQLTestCase{gen: orderGen(csgCustom, oanf), sql: ` order by "a" asc nulls first`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, oanl), sql: ` order by "a" asc nulls last`}, + commonSQLTestCase{gen: orderGen(csgCustom, oanl), sql: ` order by "a" asc nulls last`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, od), sql: ` order by "a" desc`}, + commonSQLTestCase{gen: orderGen(csgCustom, od), sql: ` order by "a" desc`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, odnf), sql: ` order by "a" desc nulls first`}, + commonSQLTestCase{gen: orderGen(csgCustom, odnf), sql: ` order by "a" desc nulls first`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, odnl), sql: ` order by "a" desc nulls last`}, + commonSQLTestCase{gen: orderGen(csgCustom, odnl), sql: ` order by "a" desc nulls last`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: orderGen(csgCustom, oa, od), sql: ` order by "a" asc, "a" desc`}, + commonSQLTestCase{gen: orderGen(csgCustom, oa, od), sql: ` order by "a" asc, "a" desc`, isPrepared: true, args: emptyArgs}, + ) +} + +func (csgs *commonSQLGeneratorSuite) TestLimitSQL() { + limitGen := func(csgs sqlgen.CommonSQLGenerator, l interface{}) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.LimitSQL(sb, l) + } + } + + csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions()) + + opts := sqlgen.DefaultDialectOptions() + opts.LimitFragment = []byte(" limit ") + csgCustom := sqlgen.NewCommonSQLGenerator("test", opts) + + l := int64(10) + la := exp.NewLiteralExpression("ALL") + + csgs.assertCases( + commonSQLTestCase{gen: limitGen(csg, nil), sql: ``}, + commonSQLTestCase{gen: limitGen(csg, nil), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: limitGen(csg, l), sql: ` LIMIT 10`}, + commonSQLTestCase{gen: limitGen(csg, l), sql: ` LIMIT ?`, isPrepared: true, args: []interface{}{l}}, + + commonSQLTestCase{gen: limitGen(csg, la), sql: ` LIMIT ALL`}, + commonSQLTestCase{gen: limitGen(csg, la), sql: ` LIMIT ALL`, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: limitGen(csgCustom, nil), sql: ``}, + commonSQLTestCase{gen: limitGen(csgCustom, nil), sql: ``, isPrepared: true, args: emptyArgs}, + + commonSQLTestCase{gen: limitGen(csgCustom, l), sql: ` limit 10`}, + commonSQLTestCase{gen: limitGen(csgCustom, l), sql: ` limit ?`, isPrepared: true, args: []interface{}{l}}, + + commonSQLTestCase{gen: limitGen(csgCustom, la), sql: ` limit ALL`}, + commonSQLTestCase{gen: limitGen(csgCustom, la), sql: ` limit ALL`, isPrepared: true, args: emptyArgs}, + ) +} + +func (csgs *commonSQLGeneratorSuite) TestUpdateExpressionSQL() { + updateGen := func(csgs sqlgen.CommonSQLGenerator, ues ...exp.UpdateExpression) func(sb.SQLBuilder) { + return func(sb sb.SQLBuilder) { + csgs.UpdateExpressionSQL(sb, ues...) + } + } + + csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions()) + ue := exp.NewIdentifierExpression("", "", "col").Set("a") + ue2 := exp.NewIdentifierExpression("", "", "col2").Set("b") + + csgs.assertCases( + commonSQLTestCase{gen: updateGen(csg), err: sqlgen.ErrNoUpdatedValuesProvided.Error()}, + commonSQLTestCase{gen: updateGen(csg), err: sqlgen.ErrNoUpdatedValuesProvided.Error()}, + + commonSQLTestCase{gen: updateGen(csg, ue), sql: `"col"='a'`}, + commonSQLTestCase{gen: updateGen(csg, ue), sql: `"col"=?`, isPrepared: true, args: []interface{}{"a"}}, + + commonSQLTestCase{gen: updateGen(csg, ue, ue2), sql: `"col"='a',"col2"='b'`}, + commonSQLTestCase{gen: updateGen(csg, ue, ue2), sql: `"col"=?,"col2"=?`, isPrepared: true, args: []interface{}{"a", "b"}}, + ) +} + +func TestCommonSQLGenerator(t *testing.T) { + suite.Run(t, new(commonSQLGeneratorSuite)) +} diff --git a/sqlgen/delete_sql_generator.go b/sqlgen/delete_sql_generator.go new file mode 100644 index 0000000..81ab235 --- /dev/null +++ b/sqlgen/delete_sql_generator.go @@ -0,0 +1,72 @@ +package sqlgen + +import ( + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + DeleteSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, clauses exp.DeleteClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + deleteSQLGenerator struct { + CommonSQLGenerator + } +) + +var ErrNoSourceForDelete = errors.New("no source found when generating delete sql") + +func NewDeleteSQLGenerator(dialect string, do *SQLDialectOptions) DeleteSQLGenerator { + return &deleteSQLGenerator{NewCommonSQLGenerator(dialect, do)} +} + +func (dsg *deleteSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.DeleteClauses) { + if !clauses.HasFrom() { + b.SetError(ErrNoSourceForDelete) + return + } + for _, f := range dsg.DialectOptions().DeleteSQLOrder { + if b.Error() != nil { + return + } + switch f { + case CommonTableSQLFragment: + dsg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables()) + case DeleteBeginSQLFragment: + dsg.DeleteBeginSQL( + b, exp.NewColumnListExpression(clauses.From()), !(clauses.HasLimit() || clauses.HasOrder()), + ) + case FromSQLFragment: + dsg.FromSQL(b, exp.NewColumnListExpression(clauses.From())) + case WhereSQLFragment: + dsg.WhereSQL(b, clauses.Where()) + case OrderSQLFragment: + if dsg.DialectOptions().SupportsOrderByOnDelete { + dsg.OrderSQL(b, clauses.Order()) + } + case LimitSQLFragment: + if dsg.DialectOptions().SupportsLimitOnDelete { + dsg.LimitSQL(b, clauses.Limit()) + } + case ReturningSQLFragment: + dsg.ReturningSQL(b, clauses.Returning()) + default: + b.SetError(ErrNotSupportedFragment("DELETE", f)) + } + } +} + +// Adds the correct fragment to being an DELETE statement +func (dsg *deleteSQLGenerator) DeleteBeginSQL(b sb.SQLBuilder, from exp.ColumnListExpression, multiTable bool) { + b.Write(dsg.DialectOptions().DeleteClause) + if multiTable && dsg.DialectOptions().SupportsDeleteTableHint { + dsg.SourcesSQL(b, from) + } +} diff --git a/sqlgen/delete_sql_generator_test.go b/sqlgen/delete_sql_generator_test.go new file mode 100644 index 0000000..a5caa69 --- /dev/null +++ b/sqlgen/delete_sql_generator_test.go @@ -0,0 +1,233 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + deleteTestCase struct { + clause exp.DeleteClauses + sql string + isPrepared bool + args []interface{} + err string + } + deleteSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (dsgs *deleteSQLGeneratorSuite) assertCases(dsg sqlgen.DeleteSQLGenerator, testCases ...deleteTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + dsg.Generate(b, tc.clause) + switch { + case len(tc.err) > 0: + dsgs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + dsgs.assertPreparedSQL(b, tc.sql, tc.args) + default: + dsgs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (dsgs *deleteSQLGeneratorSuite) TestDialect() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewDeleteSQLGenerator("test", opts) + dsgs.Equal("test", d.Dialect()) + + opts2 := sqlgen.DefaultDialectOptions() + d2 := sqlgen.NewDeleteSQLGenerator("test2", opts2) + dsgs.Equal("test2", d2.Dialect()) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate() { + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()), + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true}, + ) + + opts2 := sqlgen.DefaultDialectOptions() + opts2.DeleteClause = []byte("delete") + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts2), + deleteTestCase{clause: dc, sql: `delete FROM "test"`}, + deleteTestCase{clause: dc, sql: `delete FROM "test"`, isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withUnsupportedFragment() { + opts := sqlgen.DefaultDialectOptions() + opts.DeleteSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment} + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, err: `db: unsupported DELETE SQL fragment InsertBeingSQLFragment`}, + deleteTestCase{clause: dc, err: `db: unsupported DELETE SQL fragment InsertBeingSQLFragment`, isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_noFrom() { + dc := exp.NewDeleteClauses() + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()), + deleteTestCase{clause: dc, err: sqlgen.ErrNoSourceForDelete.Error()}, + deleteTestCase{clause: dc, err: sqlgen.ErrNoSourceForDelete.Error(), isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withErroredBuilder() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewDeleteSQLGenerator("test", opts) + + dc := exp.NewDeleteClauses().SetFrom(exp.NewIdentifierExpression("", "test", "")) + b := sb.NewSQLBuilder(false).SetError(errors.New("expected error")) + d.Generate(b, dc) + dsgs.assertErrorSQL(b, "db: expected error") + + b = sb.NewSQLBuilder(true).SetError(errors.New("expected error")) + d.Generate(b, dc) + dsgs.assertErrorSQL(b, "db: expected error") +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withCommonTables() { + opts := sqlgen.DefaultDialectOptions() + opts.WithFragment = []byte("with ") + opts.RecursiveFragment = []byte("recursive ") + + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + + dc := exp.NewDeleteClauses().SetFrom(exp.NewIdentifierExpression("", "test_cte", "")) + dcCte1 := dc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) + dcCte2 := dc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse)) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`}, + deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true}, + + deleteTestCase{clause: dcCte2, sql: `with recursive test_cte AS (select * from foo) DELETE FROM "test_cte"`}, + deleteTestCase{clause: dcCte2, sql: `with recursive test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true}, + ) + + opts.SupportsWithCTE = false + expectedErr := sqlgen.ErrCTENotSupported("test") + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dcCte1, err: expectedErr.Error()}, + deleteTestCase{clause: dcCte1, err: expectedErr.Error(), isPrepared: true}, + + deleteTestCase{clause: dcCte2, err: expectedErr.Error()}, + deleteTestCase{clause: dcCte2, err: expectedErr.Error(), isPrepared: true}, + ) + + opts.SupportsWithCTE = true + opts.SupportsWithCTERecursive = false + expectedErr = sqlgen.ErrRecursiveCTENotSupported("test") + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`}, + deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true}, + + deleteTestCase{clause: dcCte2, err: expectedErr.Error()}, + deleteTestCase{clause: dcCte2, err: expectedErr.Error(), isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withWhere() { + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")). + WhereAppend(exp.NewLiteralExpression(`"a"=?`, 1)) + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()), + deleteTestCase{clause: dc, sql: `DELETE FROM "test" WHERE "a"=1`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test" WHERE "a"=?`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withOrder() { + opts := sqlgen.DefaultDialectOptions() + opts.SupportsOrderByOnDelete = true + + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")). + SetOrder(exp.NewIdentifierExpression("", "", "c").Desc()) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, sql: `DELETE FROM "test" ORDER BY "c" DESC`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test" ORDER BY "c" DESC`, isPrepared: true}, + ) + + opts.SupportsOrderByOnDelete = false + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withLimit() { + opts := sqlgen.DefaultDialectOptions() + opts.SupportsLimitOnDelete = true + + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")). + SetLimit(1) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, sql: `DELETE FROM "test" LIMIT 1`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test" LIMIT ?`, isPrepared: true, args: []interface{}{int64(1)}}, + ) + + opts.SupportsLimitOnDelete = false + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true}, + ) +} + +func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withReturning() { + opts := sqlgen.DefaultDialectOptions() + opts.SupportsReturn = true + + dc := exp.NewDeleteClauses(). + SetFrom(exp.NewIdentifierExpression("", "test", "")). + SetReturning(exp.NewColumnListExpression("a", "b")) + + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, sql: `DELETE FROM "test" RETURNING "a", "b"`}, + deleteTestCase{clause: dc, sql: `DELETE FROM "test" RETURNING "a", "b"`, isPrepared: true}, + ) + + opts.SupportsReturn = false + expectedErr := `db: dialect does not support RETURNING clause [dialect=test]` + dsgs.assertCases( + sqlgen.NewDeleteSQLGenerator("test", opts), + deleteTestCase{clause: dc, err: expectedErr}, + deleteTestCase{clause: dc, err: expectedErr, isPrepared: true}, + ) +} + +func TestDeleteSQLGenerator(t *testing.T) { + suite.Run(t, new(deleteSQLGeneratorSuite)) +} diff --git a/sqlgen/expression_sql_generator.go b/sqlgen/expression_sql_generator.go new file mode 100644 index 0000000..a7f6a4f --- /dev/null +++ b/sqlgen/expression_sql_generator.go @@ -0,0 +1,736 @@ +package sqlgen + +import ( + "database/sql/driver" + "reflect" + "strconv" + "time" + "unicode/utf8" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/internal/util" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + ExpressionSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, val interface{}) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + expressionSQLGenerator struct { + dialect string + dialectOptions *SQLDialectOptions + } +) + +var ( + replacementRune = '?' + TrueLiteral = exp.NewLiteralExpression("TRUE") + FalseLiteral = exp.NewLiteralExpression("FALSE") + + ErrEmptyIdentifier = errors.New( + `a empty identifier was encountered, please specify a "schema", "table" or "column"`, + ) + ErrUnexpectedNamedWindow = errors.New(`unexpected named window function`) + ErrEmptyCaseWhens = errors.New(`when conditions not found for case statement`) +) + +func errUnsupportedExpressionType(e exp.Expression) error { + return errors.New("unsupported expression type %T", e) +} + +func errUnsupportedIdentifierExpression(t interface{}) error { + return errors.New("unexpected col type must be string or LiteralExpression received %T", t) +} + +func errUnsupportedBooleanExpressionOperator(op exp.BooleanOperation) error { + return errors.New("boolean operator '%+v' not supported", op) +} + +func errUnsupportedBitwiseExpressionOperator(op exp.BitwiseOperation) error { + return errors.New("bitwise operator '%+v' not supported", op) +} + +func errUnsupportedRangeExpressionOperator(op exp.RangeOperation) error { + return errors.New("range operator %+v not supported", op) +} + +func errLateralNotSupported(dialect string) error { + return errors.New("dialect does not support lateral expressions [dialect=%s]", dialect) +} + +func NewExpressionSQLGenerator(dialect string, do *SQLDialectOptions) ExpressionSQLGenerator { + return &expressionSQLGenerator{dialect: dialect, dialectOptions: do} +} + +func (esg *expressionSQLGenerator) Dialect() string { + return esg.dialect +} + +var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem() + +func (esg *expressionSQLGenerator) Generate(b sb.SQLBuilder, val interface{}) { + if b.Error() != nil { + return + } + if val == nil { + esg.literalNil(b) + return + } + switch v := val.(type) { + case exp.Expression: + esg.expressionSQL(b, v) + case int: + esg.literalInt(b, int64(v)) + case int32: + esg.literalInt(b, int64(v)) + case int64: + esg.literalInt(b, v) + case float32: + esg.literalFloat(b, float64(v)) + case float64: + esg.literalFloat(b, v) + case string: + esg.literalString(b, v) + case bool: + esg.literalBool(b, v) + case time.Time: + esg.literalTime(b, v) + case *time.Time: + if v == nil { + esg.literalNil(b) + return + } + esg.literalTime(b, *v) + case driver.Valuer: + // See https://github.com/golang/go/commit/0ce1d79a6a771f7449ec493b993ed2a720917870 + if rv := reflect.ValueOf(val); rv.Kind() == reflect.Ptr && + rv.IsNil() && + rv.Type().Elem().Implements(valuerReflectType) { + esg.literalNil(b) + return + } + dVal, err := v.Value() + if err != nil { + b.SetError(err) + return + } + esg.Generate(b, dVal) + default: + esg.reflectSQL(b, val) + } +} + +func (esg *expressionSQLGenerator) reflectSQL(b sb.SQLBuilder, val interface{}) { + v := reflect.Indirect(reflect.ValueOf(val)) + valKind := v.Kind() + switch { + case util.IsInvalid(valKind): + esg.literalNil(b) + case util.IsSlice(valKind): + switch t := val.(type) { + case []byte: + esg.literalBytes(b, t) + case []exp.CommonTableExpression: + esg.commonTablesSliceSQL(b, t) + default: + esg.sliceValueSQL(b, v) + } + case util.IsInt(valKind): + esg.Generate(b, v.Int()) + case util.IsUint(valKind): + esg.Generate(b, int64(v.Uint())) + case util.IsFloat(valKind): + esg.Generate(b, v.Float()) + case util.IsString(valKind): + esg.Generate(b, v.String()) + case util.IsBool(valKind): + esg.Generate(b, v.Bool()) + default: + b.SetError(errors.NewEncodeError(val)) + } +} + +//nolint:gocyclo // not complex just long +func (esg *expressionSQLGenerator) expressionSQL(b sb.SQLBuilder, expression exp.Expression) { + switch e := expression.(type) { + case exp.ColumnListExpression: + esg.columnListSQL(b, e) + case exp.ExpressionList: + esg.expressionListSQL(b, e) + case exp.LiteralExpression: + esg.literalExpressionSQL(b, e) + case exp.IdentifierExpression: + esg.identifierExpressionSQL(b, e) + case exp.LateralExpression: + esg.lateralExpressionSQL(b, e) + case exp.AliasedExpression: + esg.aliasedExpressionSQL(b, e) + case exp.BooleanExpression: + esg.booleanExpressionSQL(b, e) + case exp.BitwiseExpression: + esg.bitwiseExpressionSQL(b, e) + case exp.RangeExpression: + esg.rangeExpressionSQL(b, e) + case exp.OrderedExpression: + esg.orderedExpressionSQL(b, e) + case exp.UpdateExpression: + esg.updateExpressionSQL(b, e) + case exp.SQLFunctionExpression: + esg.sqlFunctionExpressionSQL(b, e) + case exp.SQLWindowFunctionExpression: + esg.sqlWindowFunctionExpression(b, e) + case exp.WindowExpression: + esg.windowExpressionSQL(b, e) + case exp.CastExpression: + esg.castExpressionSQL(b, e) + case exp.AppendableExpression: + esg.appendableExpressionSQL(b, e) + case exp.CommonTableExpression: + esg.commonTableExpressionSQL(b, e) + case exp.CompoundExpression: + esg.compoundExpressionSQL(b, e) + case exp.CaseExpression: + esg.caseExpressionSQL(b, e) + case exp.Ex: + esg.expressionMapSQL(b, e) + case exp.ExOr: + esg.expressionOrMapSQL(b, e) + default: + b.SetError(errUnsupportedExpressionType(e)) + } +} + +// Generates a placeholder (e.g. ?, $1) +func (esg *expressionSQLGenerator) placeHolderSQL(b sb.SQLBuilder, i interface{}) { + b.Write(esg.dialectOptions.PlaceHolderFragment) + if esg.dialectOptions.IncludePlaceholderNum { + b.WriteStrings(strconv.FormatInt(int64(b.CurrentArgPosition()), 10)) + } + b.WriteArg(i) +} + +// Generates creates the sql for a sub select on a Dataset +func (esg *expressionSQLGenerator) appendableExpressionSQL(b sb.SQLBuilder, a exp.AppendableExpression) { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + a.AppendSQL(b) + b.WriteRunes(esg.dialectOptions.RightParenRune) + if a.GetAs() != nil { + b.Write(esg.dialectOptions.AsFragment) + esg.Generate(b, a.GetAs()) + } +} + +// Quotes an identifier (e.g. "col", "table"."col" +func (esg *expressionSQLGenerator) identifierExpressionSQL(b sb.SQLBuilder, ident exp.IdentifierExpression) { + if ident.IsEmpty() { + b.SetError(ErrEmptyIdentifier) + return + } + schema, table, col := ident.GetSchema(), ident.GetTable(), ident.GetCol() + if schema != esg.dialectOptions.EmptyString { + b.WriteRunes(esg.dialectOptions.QuoteRune). + WriteStrings(schema). + WriteRunes(esg.dialectOptions.QuoteRune) + } + if table != esg.dialectOptions.EmptyString { + if schema != esg.dialectOptions.EmptyString { + b.WriteRunes(esg.dialectOptions.PeriodRune) + } + b.WriteRunes(esg.dialectOptions.QuoteRune). + WriteStrings(table). + WriteRunes(esg.dialectOptions.QuoteRune) + } + switch t := col.(type) { + case nil: + case string: + if col != esg.dialectOptions.EmptyString { + if table != esg.dialectOptions.EmptyString || schema != esg.dialectOptions.EmptyString { + b.WriteRunes(esg.dialectOptions.PeriodRune) + } + b.WriteRunes(esg.dialectOptions.QuoteRune). + WriteStrings(t). + WriteRunes(esg.dialectOptions.QuoteRune) + } + case exp.LiteralExpression: + if table != esg.dialectOptions.EmptyString || schema != esg.dialectOptions.EmptyString { + b.WriteRunes(esg.dialectOptions.PeriodRune) + } + esg.Generate(b, t) + default: + b.SetError(errUnsupportedIdentifierExpression(col)) + } +} + +func (esg *expressionSQLGenerator) lateralExpressionSQL(b sb.SQLBuilder, le exp.LateralExpression) { + if !esg.dialectOptions.SupportsLateral { + b.SetError(errLateralNotSupported(esg.dialect)) + return + } + b.Write(esg.dialectOptions.LateralFragment) + esg.Generate(b, le.Table()) +} + +// Generates SQL NULL value +func (esg *expressionSQLGenerator) literalNil(b sb.SQLBuilder) { + if b.IsPrepared() { + esg.placeHolderSQL(b, nil) + return + } + b.Write(esg.dialectOptions.Null) +} + +// Generates SQL bool literal, (e.g. TRUE, FALSE, mysql 1, 0, sqlite3 1, 0) +func (esg *expressionSQLGenerator) literalBool(b sb.SQLBuilder, bl bool) { + if b.IsPrepared() { + esg.placeHolderSQL(b, bl) + return + } + if bl { + b.Write(esg.dialectOptions.True) + } else { + b.Write(esg.dialectOptions.False) + } +} + +// Generates SQL for a time.Time value +func (esg *expressionSQLGenerator) literalTime(b sb.SQLBuilder, t time.Time) { + if b.IsPrepared() { + esg.placeHolderSQL(b, t) + return + } + esg.Generate(b, t.In(timeLocation).Format(esg.dialectOptions.TimeFormat)) +} + +// Generates SQL for a Float Value +func (esg *expressionSQLGenerator) literalFloat(b sb.SQLBuilder, f float64) { + if b.IsPrepared() { + esg.placeHolderSQL(b, f) + return + } + b.WriteStrings(strconv.FormatFloat(f, 'f', -1, 64)) +} + +// Generates SQL for an int value +func (esg *expressionSQLGenerator) literalInt(b sb.SQLBuilder, i int64) { + if b.IsPrepared() { + esg.placeHolderSQL(b, i) + return + } + b.WriteStrings(strconv.FormatInt(i, 10)) +} + +// Generates SQL for a string +func (esg *expressionSQLGenerator) literalString(b sb.SQLBuilder, s string) { + if b.IsPrepared() { + esg.placeHolderSQL(b, s) + return + } + b.WriteRunes(esg.dialectOptions.StringQuote) + for _, char := range s { + if e, ok := esg.dialectOptions.EscapedRunes[char]; ok { + b.Write(e) + } else { + b.WriteRunes(char) + } + } + + b.WriteRunes(esg.dialectOptions.StringQuote) +} + +// Generates SQL for a slice of bytes +func (esg *expressionSQLGenerator) literalBytes(b sb.SQLBuilder, bs []byte) { + if b.IsPrepared() { + esg.placeHolderSQL(b, bs) + return + } + b.WriteRunes(esg.dialectOptions.StringQuote) + i := 0 + for len(bs) > 0 { + char, l := utf8.DecodeRune(bs) + if e, ok := esg.dialectOptions.EscapedRunes[char]; ok { + b.Write(e) + } else { + b.WriteRunes(char) + } + i++ + bs = bs[l:] + } + b.WriteRunes(esg.dialectOptions.StringQuote) +} + +// Generates SQL for a slice of values (e.g. []int64{1,2,3,4} -> (1,2,3,4) +func (esg *expressionSQLGenerator) sliceValueSQL(b sb.SQLBuilder, slice reflect.Value) { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + for i, l := 0, slice.Len(); i < l; i++ { + esg.Generate(b, slice.Index(i).Interface()) + if i < l-1 { + b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune) + } + } + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for an AliasedExpression (e.g. I("a").As("b") -> "a" AS "b") +func (esg *expressionSQLGenerator) aliasedExpressionSQL(b sb.SQLBuilder, aliased exp.AliasedExpression) { + esg.Generate(b, aliased.Aliased()) + b.Write(esg.dialectOptions.AsFragment) + esg.Generate(b, aliased.GetAs()) +} + +// Generates SQL for a BooleanExpresion (e.g. I("a").Eq(2) -> "a" = 2) +func (esg *expressionSQLGenerator) booleanExpressionSQL(b sb.SQLBuilder, operator exp.BooleanExpression) { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + esg.Generate(b, operator.LHS()) + b.WriteRunes(esg.dialectOptions.SpaceRune) + operatorOp := operator.Op() + if val, ok := esg.dialectOptions.BooleanOperatorLookup[operatorOp]; ok { + b.Write(val) + } else { + b.SetError(errUnsupportedBooleanExpressionOperator(operatorOp)) + return + } + rhs := operator.RHS() + + if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && rhs != nil && !esg.dialectOptions.BooleanDataTypeSupported { + b.SetError(errors.New("boolean data type is not supported by dialect %q", esg.dialect)) + return + } + + if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && esg.dialectOptions.UseLiteralIsBools { + // these values must be interpolated because preparing them generates invalid SQL + switch rhs { + case true: + rhs = TrueLiteral + case false: + rhs = FalseLiteral + case nil: + rhs = exp.NewLiteralExpression(string(esg.dialectOptions.Null)) + } + } + b.WriteRunes(esg.dialectOptions.SpaceRune) + + if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && rhs == nil && !esg.dialectOptions.BooleanDataTypeSupported { + // e.g. for SQL server dialect which does not support "IS @p1" for "IS NULL" + b.Write(esg.dialectOptions.Null) + } else { + esg.Generate(b, rhs) + } + + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for a BitwiseExpresion (e.g. I("a").BitwiseOr(2) - > "a" | 2) +func (esg *expressionSQLGenerator) bitwiseExpressionSQL(b sb.SQLBuilder, operator exp.BitwiseExpression) { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + + if operator.LHS() != nil { + esg.Generate(b, operator.LHS()) + b.WriteRunes(esg.dialectOptions.SpaceRune) + } + + operatorOp := operator.Op() + if val, ok := esg.dialectOptions.BitwiseOperatorLookup[operatorOp]; ok { + b.Write(val) + } else { + b.SetError(errUnsupportedBitwiseExpressionOperator(operatorOp)) + return + } + + b.WriteRunes(esg.dialectOptions.SpaceRune) + esg.Generate(b, operator.RHS()) + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for a RangeExpresion (e.g. I("a").Between(RangeVal{Start:2,End:5}) -> "a" BETWEEN 2 AND 5) +func (esg *expressionSQLGenerator) rangeExpressionSQL(b sb.SQLBuilder, operator exp.RangeExpression) { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + esg.Generate(b, operator.LHS()) + b.WriteRunes(esg.dialectOptions.SpaceRune) + operatorOp := operator.Op() + if val, ok := esg.dialectOptions.RangeOperatorLookup[operatorOp]; ok { + b.Write(val) + } else { + b.SetError(errUnsupportedRangeExpressionOperator(operatorOp)) + return + } + rhs := operator.RHS() + b.WriteRunes(esg.dialectOptions.SpaceRune) + esg.Generate(b, rhs.Start()) + b.Write(esg.dialectOptions.AndFragment) + esg.Generate(b, rhs.End()) + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for an OrderedExpression (e.g. I("a").Asc() -> "a" ASC) +func (esg *expressionSQLGenerator) orderedExpressionSQL(b sb.SQLBuilder, order exp.OrderedExpression) { + esg.Generate(b, order.SortExpression()) + if order.IsAsc() { + b.Write(esg.dialectOptions.AscFragment) + } else { + b.Write(esg.dialectOptions.DescFragment) + } + switch order.NullSortType() { + case exp.NoNullsSortType: + return + case exp.NullsFirstSortType: + b.Write(esg.dialectOptions.NullsFirstFragment) + case exp.NullsLastSortType: + b.Write(esg.dialectOptions.NullsLastFragment) + } +} + +// Generates SQL for an ExpressionList (e.g. And(I("a").Eq("a"), I("b").Eq("b")) -> (("a" = 'a') AND ("b" = 'b'))) +func (esg *expressionSQLGenerator) expressionListSQL(b sb.SQLBuilder, expressionList exp.ExpressionList) { + if expressionList.IsEmpty() { + return + } + var op []byte + if expressionList.Type() == exp.AndType { + op = esg.dialectOptions.AndFragment + } else { + op = esg.dialectOptions.OrFragment + } + exps := expressionList.Expressions() + expLen := len(exps) - 1 + if expLen > 0 { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + } else { + esg.Generate(b, exps[0]) + return + } + for i, e := range exps { + esg.Generate(b, e) + if i < expLen { + b.Write(op) + } + } + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for a ColumnListExpression +func (esg *expressionSQLGenerator) columnListSQL(b sb.SQLBuilder, columnList exp.ColumnListExpression) { + cols := columnList.Columns() + colLen := len(cols) + for i, col := range cols { + esg.Generate(b, col) + if i < colLen-1 { + b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune) + } + } +} + +// Generates SQL for an UpdateEpxresion +func (esg *expressionSQLGenerator) updateExpressionSQL(b sb.SQLBuilder, update exp.UpdateExpression) { + esg.Generate(b, update.Col()) + b.WriteRunes(esg.dialectOptions.SetOperatorRune) + esg.Generate(b, update.Val()) +} + +// Generates SQL for a LiteralExpression +// +// L("a + b") -> a + b +// L("a = ?", 1) -> a = 1 +func (esg *expressionSQLGenerator) literalExpressionSQL(b sb.SQLBuilder, literal exp.LiteralExpression) { + l := literal.Literal() + args := literal.Args() + if argsLen := len(args); argsLen > 0 { + currIndex := 0 + for _, char := range l { + if char == replacementRune && currIndex < argsLen { + esg.Generate(b, args[currIndex]) + currIndex++ + } else { + b.WriteRunes(char) + } + } + return + } + b.WriteStrings(l) +} + +// Generates SQL for a SQLFunctionExpression +// +// COUNT(I("a")) -> COUNT("a") +func (esg *expressionSQLGenerator) sqlFunctionExpressionSQL(b sb.SQLBuilder, sqlFunc exp.SQLFunctionExpression) { + b.WriteStrings(sqlFunc.Name()) + esg.Generate(b, sqlFunc.Args()) +} + +func (esg *expressionSQLGenerator) sqlWindowFunctionExpression(b sb.SQLBuilder, sqlWinFunc exp.SQLWindowFunctionExpression) { + if !esg.dialectOptions.SupportsWindowFunction { + b.SetError(ErrWindowNotSupported(esg.dialect)) + return + } + esg.Generate(b, sqlWinFunc.Func()) + b.Write(esg.dialectOptions.WindowOverFragment) + switch { + case sqlWinFunc.HasWindowName(): + esg.Generate(b, sqlWinFunc.WindowName()) + case sqlWinFunc.HasWindow(): + if sqlWinFunc.Window().HasName() { + b.SetError(ErrUnexpectedNamedWindow) + return + } + esg.Generate(b, sqlWinFunc.Window()) + default: + esg.Generate(b, exp.NewWindowExpression(nil, nil, nil, nil)) + } +} + +func (esg *expressionSQLGenerator) windowExpressionSQL(b sb.SQLBuilder, we exp.WindowExpression) { + if !esg.dialectOptions.SupportsWindowFunction { + b.SetError(ErrWindowNotSupported(esg.dialect)) + return + } + if we.HasName() { + esg.Generate(b, we.Name()) + b.Write(esg.dialectOptions.AsFragment) + } + b.WriteRunes(esg.dialectOptions.LeftParenRune) + + hasPartition := we.HasPartitionBy() + hasOrder := we.HasOrder() + + if we.HasParent() { + esg.Generate(b, we.Parent()) + if hasPartition || hasOrder { + b.WriteRunes(esg.dialectOptions.SpaceRune) + } + } + + if hasPartition { + b.Write(esg.dialectOptions.WindowPartitionByFragment) + esg.Generate(b, we.PartitionCols()) + if hasOrder { + b.WriteRunes(esg.dialectOptions.SpaceRune) + } + } + if hasOrder { + b.Write(esg.dialectOptions.WindowOrderByFragment) + esg.Generate(b, we.OrderCols()) + } + + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates SQL for a CastExpression +// +// I("a").Cast("NUMERIC") -> CAST("a" AS NUMERIC) +func (esg *expressionSQLGenerator) castExpressionSQL(b sb.SQLBuilder, cast exp.CastExpression) { + b.Write(esg.dialectOptions.CastFragment).WriteRunes(esg.dialectOptions.LeftParenRune) + esg.Generate(b, cast.Casted()) + b.Write(esg.dialectOptions.AsFragment) + esg.Generate(b, cast.Type()) + b.WriteRunes(esg.dialectOptions.RightParenRune) +} + +// Generates the sql for the WITH clauses for common table expressions (CTE) +func (esg *expressionSQLGenerator) commonTablesSliceSQL(b sb.SQLBuilder, ctes []exp.CommonTableExpression) { + l := len(ctes) + if l == 0 { + return + } + if !esg.dialectOptions.SupportsWithCTE { + b.SetError(ErrCTENotSupported(esg.dialect)) + return + } + b.Write(esg.dialectOptions.WithFragment) + anyRecursive := false + for _, cte := range ctes { + anyRecursive = anyRecursive || cte.IsRecursive() + } + if anyRecursive { + if !esg.dialectOptions.SupportsWithCTERecursive { + b.SetError(ErrRecursiveCTENotSupported(esg.dialect)) + return + } + b.Write(esg.dialectOptions.RecursiveFragment) + } + for i, cte := range ctes { + esg.Generate(b, cte) + if i < l-1 { + b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune) + } + } + b.WriteRunes(esg.dialectOptions.SpaceRune) +} + +// Generates SQL for a CommonTableExpression +func (esg *expressionSQLGenerator) commonTableExpressionSQL(b sb.SQLBuilder, cte exp.CommonTableExpression) { + esg.Generate(b, cte.Name()) + b.Write(esg.dialectOptions.AsFragment) + esg.Generate(b, cte.SubQuery()) +} + +// Generates SQL for a CompoundExpression +func (esg *expressionSQLGenerator) compoundExpressionSQL(b sb.SQLBuilder, compound exp.CompoundExpression) { + switch compound.Type() { + case exp.UnionCompoundType: + b.Write(esg.dialectOptions.UnionFragment) + case exp.UnionAllCompoundType: + b.Write(esg.dialectOptions.UnionAllFragment) + case exp.IntersectCompoundType: + b.Write(esg.dialectOptions.IntersectFragment) + case exp.IntersectAllCompoundType: + b.Write(esg.dialectOptions.IntersectAllFragment) + } + if esg.dialectOptions.WrapCompoundsInParens { + b.WriteRunes(esg.dialectOptions.LeftParenRune) + compound.RHS().AppendSQL(b) + b.WriteRunes(esg.dialectOptions.RightParenRune) + } else { + compound.RHS().AppendSQL(b) + } +} + +// Generates SQL for a CaseExpression +func (esg *expressionSQLGenerator) caseExpressionSQL(b sb.SQLBuilder, caseExpression exp.CaseExpression) { + caseVal := caseExpression.GetValue() + whens := caseExpression.GetWhens() + elseResult := caseExpression.GetElse() + + if len(whens) == 0 { + b.SetError(ErrEmptyCaseWhens) + return + } + b.Write(esg.dialectOptions.CaseFragment) + if caseVal != nil { + esg.Generate(b, caseVal) + } + for _, when := range whens { + b.Write(esg.dialectOptions.WhenFragment) + esg.Generate(b, when.Condition()) + b.Write(esg.dialectOptions.ThenFragment) + esg.Generate(b, when.Result()) + } + if elseResult != nil { + b.Write(esg.dialectOptions.ElseFragment) + esg.Generate(b, elseResult.Result()) + } + b.Write(esg.dialectOptions.EndFragment) +} + +func (esg *expressionSQLGenerator) expressionMapSQL(b sb.SQLBuilder, ex exp.Ex) { + expressionList, err := ex.ToExpressions() + if err != nil { + b.SetError(err) + return + } + esg.Generate(b, expressionList) +} + +func (esg *expressionSQLGenerator) expressionOrMapSQL(b sb.SQLBuilder, ex exp.ExOr) { + expressionList, err := ex.ToExpressions() + if err != nil { + b.SetError(err) + return + } + esg.Generate(b, expressionList) +} diff --git a/sqlgen/expression_sql_generator_test.go b/sqlgen/expression_sql_generator_test.go new file mode 100644 index 0000000..c370e9b --- /dev/null +++ b/sqlgen/expression_sql_generator_test.go @@ -0,0 +1,1670 @@ +package sqlgen_test + +import ( + "database/sql/driver" + "fmt" + "regexp" + "testing" + "time" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +var emptyArgs = make([]interface{}, 0) + +type testAppendableExpression struct { + sql string + args []interface{} + err error + alias exp.IdentifierExpression + returnsColumns bool +} + +func newTestAppendableExpression( + sql string, + args []interface{}, + err error, + alias exp.IdentifierExpression) exp.AppendableExpression { + return &testAppendableExpression{sql: sql, args: args, err: err, alias: alias} +} + +func (tae *testAppendableExpression) Expression() exp.Expression { + return tae +} + +func (tae *testAppendableExpression) Clone() exp.Expression { + return tae +} + +func (tae *testAppendableExpression) GetAs() exp.IdentifierExpression { + return tae.alias +} + +func (tae *testAppendableExpression) ReturnsColumns() bool { + return tae.returnsColumns +} + +func (tae *testAppendableExpression) AppendSQL(b sb.SQLBuilder) { + if tae.err != nil { + b.SetError(tae.err) + return + } + b.WriteStrings(tae.sql) + if len(tae.args) > 0 { + b.WriteArg(tae.args...) + } +} + +type ( + expressionTestCase struct { + val interface{} + sql string + err string + isPrepared bool + args []interface{} + } + expressionSQLGeneratorSuite struct { + suite.Suite + } +) + +func (esgs *expressionSQLGeneratorSuite) assertCases(esg sqlgen.ExpressionSQLGenerator, cases ...expressionTestCase) { + for i, c := range cases { + b := sb.NewSQLBuilder(c.isPrepared) + esg.Generate(b, c.val) + actualSQL, actualArgs, err := b.ToSQL() + if c.err == "" { + esgs.NoError(err, "test case %d failed", i) + } else { + esgs.EqualError(err, c.err, "test case %d failed", i) + } + esgs.Equal(c.sql, actualSQL, "test case %d failed", i) + if c.isPrepared && c.args != nil || len(c.args) > 0 { + esgs.Equal(c.args, actualArgs, "test case %d failed", i) + } else { + esgs.Empty(actualArgs, "test case %d failed", i) + } + } +} + +func (esgs *expressionSQLGeneratorSuite) TestDialect() { + esg := sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()) + esgs.Equal("test", esg.Dialect()) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ErroredBuilder() { + esg := sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()) + expectedErr := errors.New("test error") + b := sb.NewSQLBuilder(false).SetError(expectedErr) + esg.Generate(b, 1) + sql, args, err := b.ToSQL() + esgs.Equal(expectedErr, err) + esgs.Empty(sql) + esgs.Empty(args) + + b = sb.NewSQLBuilder(true).SetError(err) + esg.Generate(b, true) + sql, args, err = b.ToSQL() + esgs.Equal(expectedErr, err) + esgs.Empty(sql) + esgs.Empty(args) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_Invalid() { + var b *bool + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: b, sql: "NULL"}, + expressionTestCase{val: b, sql: "?", isPrepared: true, args: []interface{}{nil}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_UnsupportedType() { + type strct struct{} + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: strct{}, err: "dbv2_encode_error: Unable to encode value {}"}, + expressionTestCase{val: strct{}, err: "dbv2_encode_error: Unable to encode value {}", isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_IncludePlaceholderNum() { + opts := sqlgen.DefaultDialectOptions() + opts.IncludePlaceholderNum = true + opts.PlaceHolderFragment = []byte("$") + ex := exp.Ex{ + "a": 1, + "b": true, + "c": false, + "d": []string{"a", "b", "c"}, + } + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{ + val: ex, + sql: `(("a" = 1) AND ("b" IS TRUE) AND ("c" IS FALSE) AND ("d" IN ('a', 'b', 'c')))`, + }, + expressionTestCase{ + val: ex, + sql: `(("a" = $1) AND ("b" IS TRUE) AND ("c" IS FALSE) AND ("d" IN ($2, $3, $4)))`, + isPrepared: true, + args: []interface{}{int64(1), "a", "b", "c"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_FloatTypes() { + var float float64 + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: float32(10.01), sql: "10.010000228881836"}, + expressionTestCase{val: float32(10.01), sql: "?", isPrepared: true, args: []interface{}{float64(float32(10.01))}}, + + expressionTestCase{val: float64(10.01), sql: "10.01"}, + expressionTestCase{val: float64(10.01), sql: "?", isPrepared: true, args: []interface{}{float64(10.01)}}, + + expressionTestCase{val: &float, sql: "0"}, + expressionTestCase{val: &float, sql: "?", isPrepared: true, args: []interface{}{float}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_IntTypes() { + var i int64 + ints := []interface{}{ + int(10), + int16(10), + int32(10), + int64(10), + uint(10), + uint16(10), + uint32(10), + uint64(10), + } + for _, i := range ints { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: i, sql: "10"}, + expressionTestCase{val: i, sql: "?", isPrepared: true, args: []interface{}{int64(10)}}, + ) + } + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: &i, sql: "0"}, + expressionTestCase{val: &i, sql: "?", isPrepared: true, args: []interface{}{i}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_StringTypes() { + var str string + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: "Hello", sql: "'Hello'"}, + expressionTestCase{val: "Hello", sql: "?", isPrepared: true, args: []interface{}{"Hello"}}, + + expressionTestCase{val: "Hello'", sql: "'Hello'''"}, + expressionTestCase{val: "Hello'", sql: "?", isPrepared: true, args: []interface{}{"Hello'"}}, + + expressionTestCase{val: &str, sql: "''"}, + expressionTestCase{val: &str, sql: "?", isPrepared: true, args: []interface{}{str}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_BytesTypes() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: []byte("Hello"), sql: "'Hello'"}, + expressionTestCase{val: []byte("Hello"), sql: "?", isPrepared: true, args: []interface{}{[]byte("Hello")}}, + + expressionTestCase{val: []byte("Hello'"), sql: "'Hello'''"}, + expressionTestCase{val: []byte("Hello'"), sql: "?", isPrepared: true, args: []interface{}{[]byte("Hello'")}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_BoolTypes() { + var bl bool + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: true, sql: "TRUE"}, + expressionTestCase{val: true, sql: "?", isPrepared: true, args: []interface{}{true}}, + + expressionTestCase{val: false, sql: "FALSE"}, + expressionTestCase{val: false, sql: "?", isPrepared: true, args: []interface{}{false}}, + + expressionTestCase{val: &bl, sql: "FALSE"}, + expressionTestCase{val: &bl, sql: "?", isPrepared: true, args: []interface{}{bl}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_TimeTypes() { + var nt *time.Time + + ts, err := time.Parse(time.RFC3339, "2019-10-01T15:01:00Z") + esgs.Require().NoError(err) + originalLoc := sqlgen.GetTimeLocation() + + loc, err := time.LoadLocation("Asia/Shanghai") + esgs.Require().NoError(err) + + sqlgen.SetTimeLocation(loc) + // non time + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ts, sql: "'2019-10-01T23:01:00+08:00'"}, + expressionTestCase{val: ts, sql: "?", isPrepared: true, args: []interface{}{ts}}, + + expressionTestCase{val: &ts, sql: "'2019-10-01T23:01:00+08:00'"}, + expressionTestCase{val: &ts, sql: "?", isPrepared: true, args: []interface{}{ts}}, + ) + sqlgen.SetTimeLocation(time.UTC) + // utc time + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ts, sql: "'2019-10-01T15:01:00Z'"}, + expressionTestCase{val: ts, sql: "?", isPrepared: true, args: []interface{}{ts}}, + + expressionTestCase{val: &ts, sql: "'2019-10-01T15:01:00Z'"}, + expressionTestCase{val: &ts, sql: "?", isPrepared: true, args: []interface{}{ts}}, + ) + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: nt, sql: "NULL"}, + expressionTestCase{val: nt, sql: "?", isPrepared: true, args: []interface{}{nil}}, + ) + sqlgen.SetTimeLocation(originalLoc) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_NilTypes() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: nil, sql: "NULL"}, + expressionTestCase{val: nil, sql: "?", isPrepared: true, args: []interface{}{nil}}, + ) +} + +type datasetValuerType struct { + int int64 + err error +} + +func (j datasetValuerType) Value() (driver.Value, error) { + if j.err != nil { + return nil, j.err + } + return []byte(fmt.Sprintf("Hello World %d", j.int)), nil +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_Valuer() { + err := errors.New("valuer error") + var val *datasetValuerType + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: datasetValuerType{int: 10}, sql: "'Hello World 10'"}, + expressionTestCase{ + val: datasetValuerType{int: 10}, sql: "?", isPrepared: true, args: []interface{}{[]byte("Hello World 10")}, + }, + + expressionTestCase{val: datasetValuerType{err: err}, err: "db: valuer error"}, + expressionTestCase{ + val: datasetValuerType{err: err}, isPrepared: true, err: "db: valuer error", + }, + expressionTestCase{ + val: val, sql: "NULL", + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_Slice() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: []string{"a", "b", "c"}, sql: `('a', 'b', 'c')`}, + expressionTestCase{ + val: []string{"a", "b", "c"}, sql: "(?, ?, ?)", isPrepared: true, args: []interface{}{"a", "b", "c"}, + }, + + expressionTestCase{val: []byte{'a', 'b', 'c'}, sql: `'abc'`}, + expressionTestCase{ + val: []byte{'a', 'b', 'c'}, sql: "?", isPrepared: true, args: []interface{}{[]byte{'a', 'b', 'c'}}, + }, + ) +} + +type unknownExpression struct{} + +func (ue unknownExpression) Expression() exp.Expression { + return ue +} + +func (ue unknownExpression) Clone() exp.Expression { + return ue +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerateUnsupportedExpression() { + errMsg := "db: unsupported expression type sqlgen_test.unknownExpression" + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: unknownExpression{}, err: errMsg}, + expressionTestCase{ + val: unknownExpression{}, isPrepared: true, err: errMsg, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_AppendableExpression() { + ti := exp.NewIdentifierExpression("", "b", "") + a := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, nil) + aliasedA := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, ti) + argsA := newTestAppendableExpression(`select * from "a" where x=?`, []interface{}{true}, nil, ti) + ae := newTestAppendableExpression(`select * from "a"`, emptyArgs, errors.New("expected error"), nil) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: a, sql: `(select * from "a")`}, + expressionTestCase{val: a, sql: `(select * from "a")`, isPrepared: true}, + + expressionTestCase{val: aliasedA, sql: `(select * from "a") AS "b"`}, + expressionTestCase{val: aliasedA, sql: `(select * from "a") AS "b"`, isPrepared: true}, + + expressionTestCase{val: ae, err: "db: expected error"}, + expressionTestCase{val: ae, err: "db: expected error", isPrepared: true}, + + expressionTestCase{val: argsA, sql: `(select * from "a" where x=?) AS "b"`, args: []interface{}{true}}, + expressionTestCase{val: argsA, sql: `(select * from "a" where x=?) AS "b"`, isPrepared: true, args: []interface{}{true}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ColumnList() { + cl := exp.NewColumnListExpression("a", exp.NewLiteralExpression("true")) + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: cl, sql: `"a", true`}, + expressionTestCase{val: cl, sql: `"a", true`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionList() { + andEl := exp.NewExpressionList( + exp.AndType, + exp.NewIdentifierExpression("", "", "a").Eq("b"), + exp.NewIdentifierExpression("", "", "c").Neq(1), + ) + + orEl := exp.NewExpressionList( + exp.OrType, + exp.NewIdentifierExpression("", "", "a").Eq("b"), + exp.NewIdentifierExpression("", "", "c").Neq(1), + ) + + andOrEl := exp.NewExpressionList(exp.OrType, + exp.NewIdentifierExpression("", "", "a").Eq("b"), + exp.NewExpressionList(exp.AndType, + exp.NewIdentifierExpression("", "", "c").Neq(1), + exp.NewIdentifierExpression("", "", "d").Eq(exp.NewLiteralExpression("NOW()")), + ), + ) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: andEl, sql: `(("a" = 'b') AND ("c" != 1))`}, + expressionTestCase{ + val: andEl, sql: `(("a" = ?) AND ("c" != ?))`, isPrepared: true, args: []interface{}{"b", int64(1)}, + }, + + expressionTestCase{val: orEl, sql: `(("a" = 'b') OR ("c" != 1))`}, + expressionTestCase{ + val: orEl, sql: `(("a" = ?) OR ("c" != ?))`, isPrepared: true, args: []interface{}{"b", int64(1)}, + }, + + expressionTestCase{val: andOrEl, sql: `(("a" = 'b') OR (("c" != 1) AND ("d" = NOW())))`}, + expressionTestCase{ + val: andOrEl, + sql: `(("a" = ?) OR (("c" != ?) AND ("d" = NOW())))`, + isPrepared: true, + args: []interface{}{"b", int64(1)}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_LiteralExpression() { + noArgsL := exp.NewLiteralExpression(`"b"::DATE = '2010-09-02'`) + argsL := exp.NewLiteralExpression(`"b" = ? or "c" = ? or d IN ?`, "a", 1, []int{1, 2, 3, 4}) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: noArgsL, sql: `"b"::DATE = '2010-09-02'`}, + expressionTestCase{val: noArgsL, sql: `"b"::DATE = '2010-09-02'`, isPrepared: true}, + + expressionTestCase{val: argsL, sql: `"b" = 'a' or "c" = 1 or d IN (1, 2, 3, 4)`}, + expressionTestCase{ + val: argsL, + sql: `"b" = ? or "c" = ? or d IN (?, ?, ?, ?)`, + isPrepared: true, + args: []interface{}{ + "a", + int64(1), + int64(1), + int64(2), + int64(3), + int64(4), + }, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_AliasedExpression() { + aliasedI := exp.NewIdentifierExpression("", "", "a").As("b") + aliasedWithII := exp.NewIdentifierExpression("", "", "a"). + As(exp.NewIdentifierExpression("", "", "b")) + aliasedL := exp.NewLiteralExpression("count(*)").As("count") + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: aliasedI, sql: `"a" AS "b"`}, + expressionTestCase{val: aliasedI, sql: `"a" AS "b"`, isPrepared: true}, + + expressionTestCase{val: aliasedWithII, sql: `"a" AS "b"`}, + expressionTestCase{val: aliasedWithII, sql: `"a" AS "b"`, isPrepared: true}, + + expressionTestCase{val: aliasedL, sql: `count(*) AS "count"`}, + expressionTestCase{val: aliasedL, sql: `count(*) AS "count"`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_BooleanExpressionAliased() { + ident := exp.NewIdentifierExpression("", "", "a") + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ident.Eq(1).As("b"), sql: `("a" = 1) AS "b"`}, + expressionTestCase{val: ident.Eq(1).As("b"), sql: `("a" = ?) AS "b"`, + isPrepared: true, args: []interface{}{int64(1)}}, + ) +} +func (esgs *expressionSQLGeneratorSuite) TestGenerate_BooleanExpression() { + ae := newTestAppendableExpression(`SELECT "id" FROM "test2"`, emptyArgs, nil, nil) + re := regexp.MustCompile("[ab]") + ident := exp.NewIdentifierExpression("", "", "a") + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ident.Eq(1), sql: `("a" = 1)`}, + expressionTestCase{val: ident.Eq(1), sql: `("a" = ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.Eq(true), sql: `("a" IS TRUE)`}, + expressionTestCase{val: ident.Eq(true), sql: `("a" IS TRUE)`, isPrepared: true}, + + expressionTestCase{val: ident.Eq(false), sql: `("a" IS FALSE)`}, + expressionTestCase{val: ident.Eq(false), sql: `("a" IS FALSE)`, isPrepared: true}, + + expressionTestCase{val: ident.Eq(nil), sql: `("a" IS NULL)`}, + expressionTestCase{val: ident.Eq(nil), sql: `("a" IS NULL)`, isPrepared: true}, + + expressionTestCase{val: ident.Eq([]int64{1, 2, 3}), sql: `("a" IN (1, 2, 3))`}, + expressionTestCase{val: ident.Eq([]int64{1, 2, 3}), sql: `("a" IN (?, ?, ?))`, isPrepared: true, args: []interface{}{ + int64(1), int64(2), int64(3), + }}, + + expressionTestCase{val: ident.Eq(ae), sql: `("a" IN (SELECT "id" FROM "test2"))`}, + expressionTestCase{val: ident.Eq(ae), sql: `("a" IN (SELECT "id" FROM "test2"))`, isPrepared: true}, + + expressionTestCase{val: ident.Neq(1), sql: `("a" != 1)`}, + expressionTestCase{val: ident.Neq(1), sql: `("a" != ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.Neq(true), sql: `("a" IS NOT TRUE)`}, + expressionTestCase{val: ident.Neq(true), sql: `("a" IS NOT TRUE)`, isPrepared: true}, + + expressionTestCase{val: ident.Neq(false), sql: `("a" IS NOT FALSE)`}, + expressionTestCase{val: ident.Neq(false), sql: `("a" IS NOT FALSE)`, isPrepared: true}, + + expressionTestCase{val: ident.Neq(nil), sql: `("a" IS NOT NULL)`}, + expressionTestCase{val: ident.Neq(nil), sql: `("a" IS NOT NULL)`, isPrepared: true}, + + expressionTestCase{val: ident.Neq([]int64{1, 2, 3}), sql: `("a" NOT IN (1, 2, 3))`}, + expressionTestCase{val: ident.Neq([]int64{1, 2, 3}), sql: `("a" NOT IN (?, ?, ?))`, isPrepared: true, args: []interface{}{ + int64(1), int64(2), int64(3), + }}, + + expressionTestCase{val: ident.Neq(ae), sql: `("a" NOT IN (SELECT "id" FROM "test2"))`}, + expressionTestCase{val: ident.Neq(ae), sql: `("a" NOT IN (SELECT "id" FROM "test2"))`, isPrepared: true}, + + expressionTestCase{val: ident.Is(true), sql: `("a" IS TRUE)`}, + expressionTestCase{val: ident.Is(true), sql: `("a" IS TRUE)`, isPrepared: true}, + + expressionTestCase{val: ident.Is(false), sql: `("a" IS FALSE)`}, + expressionTestCase{val: ident.Is(false), sql: `("a" IS FALSE)`, isPrepared: true}, + + expressionTestCase{val: ident.Is(nil), sql: `("a" IS NULL)`}, + expressionTestCase{val: ident.Is(nil), sql: `("a" IS NULL)`, isPrepared: true}, + + expressionTestCase{val: ident.IsNot(true), sql: `("a" IS NOT TRUE)`}, + expressionTestCase{val: ident.IsNot(true), sql: `("a" IS NOT TRUE)`, isPrepared: true}, + + expressionTestCase{val: ident.IsNot(false), sql: `("a" IS NOT FALSE)`}, + expressionTestCase{val: ident.IsNot(false), sql: `("a" IS NOT FALSE)`, isPrepared: true}, + + expressionTestCase{val: ident.IsNot(nil), sql: `("a" IS NOT NULL)`}, + expressionTestCase{val: ident.IsNot(nil), sql: `("a" IS NOT NULL)`, isPrepared: true}, + + expressionTestCase{val: ident.Gt(1), sql: `("a" > 1)`}, + expressionTestCase{val: ident.Gt(1), sql: `("a" > ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.Gte(1), sql: `("a" >= 1)`}, + expressionTestCase{val: ident.Gte(1), sql: `("a" >= ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.Lt(1), sql: `("a" < 1)`}, + expressionTestCase{val: ident.Lt(1), sql: `("a" < ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.Lte(1), sql: `("a" <= 1)`}, + expressionTestCase{val: ident.Lte(1), sql: `("a" <= ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.In([]int64{1, 2, 3}), sql: `("a" IN (1, 2, 3))`}, + expressionTestCase{val: ident.In([]int64{1, 2, 3}), sql: `("a" IN (?, ?, ?))`, isPrepared: true, args: []interface{}{ + int64(1), int64(2), int64(3), + }}, + + expressionTestCase{val: ident.In(ae), sql: `("a" IN ((SELECT "id" FROM "test2")))`}, + expressionTestCase{val: ident.In(ae), sql: `("a" IN ((SELECT "id" FROM "test2")))`, isPrepared: true}, + + expressionTestCase{val: ident.NotIn([]int64{1, 2, 3}), sql: `("a" NOT IN (1, 2, 3))`}, + expressionTestCase{val: ident.NotIn([]int64{1, 2, 3}), sql: `("a" NOT IN (?, ?, ?))`, isPrepared: true, args: []interface{}{ + int64(1), int64(2), int64(3), + }}, + + expressionTestCase{val: ident.NotIn(ae), sql: `("a" NOT IN ((SELECT "id" FROM "test2")))`}, + expressionTestCase{val: ident.NotIn(ae), sql: `("a" NOT IN ((SELECT "id" FROM "test2")))`, isPrepared: true}, + + expressionTestCase{val: ident.Like("a%"), sql: `("a" LIKE 'a%')`}, + expressionTestCase{val: ident.Like("a%"), sql: `("a" LIKE ?)`, isPrepared: true, args: []interface{}{"a%"}}, + + expressionTestCase{val: ident.Like(re), sql: `("a" ~ '[ab]')`}, + expressionTestCase{val: ident.Like(re), sql: `("a" ~ ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + + expressionTestCase{val: ident.ILike("a%"), sql: `("a" ILIKE 'a%')`}, + expressionTestCase{val: ident.ILike("a%"), sql: `("a" ILIKE ?)`, isPrepared: true, args: []interface{}{"a%"}}, + + expressionTestCase{val: ident.ILike(re), sql: `("a" ~* '[ab]')`}, + expressionTestCase{val: ident.ILike(re), sql: `("a" ~* ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + + expressionTestCase{val: ident.NotLike("a%"), sql: `("a" NOT LIKE 'a%')`}, + expressionTestCase{val: ident.NotLike("a%"), sql: `("a" NOT LIKE ?)`, isPrepared: true, args: []interface{}{"a%"}}, + + expressionTestCase{val: ident.NotLike(re), sql: `("a" !~ '[ab]')`}, + expressionTestCase{val: ident.NotLike(re), sql: `("a" !~ ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + + expressionTestCase{val: ident.NotILike("a%"), sql: `("a" NOT ILIKE 'a%')`}, + expressionTestCase{val: ident.NotILike("a%"), sql: `("a" NOT ILIKE ?)`, isPrepared: true, args: []interface{}{"a%"}}, + + expressionTestCase{val: ident.NotILike(re), sql: `("a" !~* '[ab]')`}, + expressionTestCase{val: ident.NotILike(re), sql: `("a" !~* ?)`, isPrepared: true, args: []interface{}{"[ab]"}}, + ) + + opts := sqlgen.DefaultDialectOptions() + opts.BooleanOperatorLookup = map[exp.BooleanOperation][]byte{} + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: ident.Eq(1), err: "db: boolean operator 'eq' not supported"}, + expressionTestCase{val: ident.Neq(1), err: "db: boolean operator 'neq' not supported"}, + expressionTestCase{val: ident.Is(true), err: "db: boolean operator 'is' not supported"}, + expressionTestCase{val: ident.IsNot(true), err: "db: boolean operator 'isnot' not supported"}, + expressionTestCase{val: ident.Gt(1), err: "db: boolean operator 'gt' not supported"}, + expressionTestCase{val: ident.Gte(1), err: "db: boolean operator 'gte' not supported"}, + expressionTestCase{val: ident.Lt(1), err: "db: boolean operator 'lt' not supported"}, + expressionTestCase{val: ident.Lte(1), err: "db: boolean operator 'lte' not supported"}, + expressionTestCase{val: ident.In([]int64{1, 2, 3}), err: "db: boolean operator 'in' not supported"}, + expressionTestCase{val: ident.NotIn([]int64{1, 2, 3}), err: "db: boolean operator 'notin' not supported"}, + expressionTestCase{val: ident.Like("a%"), err: "db: boolean operator 'like' not supported"}, + expressionTestCase{val: ident.Like(re), err: "db: boolean operator 'regexplike' not supported"}, + expressionTestCase{val: ident.ILike("a%"), err: "db: boolean operator 'ilike' not supported"}, + expressionTestCase{val: ident.ILike(re), err: "db: boolean operator 'regexpilike' not supported"}, + expressionTestCase{val: ident.NotLike("a%"), err: "db: boolean operator 'notlike' not supported"}, + expressionTestCase{val: ident.NotLike(re), err: "db: boolean operator 'regexpnotlike' not supported"}, + expressionTestCase{val: ident.NotILike("a%"), err: "db: boolean operator 'notilike' not supported"}, + expressionTestCase{val: ident.NotILike(re), err: "db: boolean operator 'regexpnotilike' not supported"}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_BitwiseExpression() { + ident := exp.NewIdentifierExpression("", "", "a") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ident.BitwiseInversion(), sql: `(~ "a")`}, + expressionTestCase{val: ident.BitwiseInversion(), sql: `(~ "a")`, isPrepared: true}, + + expressionTestCase{val: ident.BitwiseAnd(1), sql: `("a" & 1)`}, + expressionTestCase{val: ident.BitwiseAnd(1), sql: `("a" & ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.BitwiseOr(1), sql: `("a" | 1)`}, + expressionTestCase{val: ident.BitwiseOr(1), sql: `("a" | ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.BitwiseXor(1), sql: `("a" # 1)`}, + expressionTestCase{val: ident.BitwiseXor(1), sql: `("a" # ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.BitwiseLeftShift(1), sql: `("a" << 1)`}, + expressionTestCase{val: ident.BitwiseLeftShift(1), sql: `("a" << ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: ident.BitwiseRightShift(1), sql: `("a" >> 1)`}, + expressionTestCase{val: ident.BitwiseRightShift(1), sql: `("a" >> ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + ) + + opts := sqlgen.DefaultDialectOptions() + opts.BitwiseOperatorLookup = map[exp.BitwiseOperation][]byte{} + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: ident.BitwiseInversion(), err: "db: bitwise operator 'Inversion' not supported"}, + expressionTestCase{val: ident.BitwiseAnd(1), err: "db: bitwise operator 'AND' not supported"}, + expressionTestCase{val: ident.BitwiseOr(1), err: "db: bitwise operator 'OR' not supported"}, + expressionTestCase{val: ident.BitwiseXor(1), err: "db: bitwise operator 'XOR' not supported"}, + expressionTestCase{val: ident.BitwiseLeftShift(1), err: "db: bitwise operator 'Left Shift' not supported"}, + expressionTestCase{val: ident.BitwiseRightShift(1), err: "db: bitwise operator 'Right Shift' not supported"}, + ) +} +func (esgs *expressionSQLGeneratorSuite) TestGenerate_RangeExpression() { + betweenNum := exp.NewIdentifierExpression("", "", "a"). + Between(exp.NewRangeVal(1, 2)) + notBetweenNum := exp.NewIdentifierExpression("", "", "a"). + NotBetween(exp.NewRangeVal(1, 2)) + + betweenStr := exp.NewIdentifierExpression("", "", "a"). + Between(exp.NewRangeVal("aaa", "zzz")) + notBetweenStr := exp.NewIdentifierExpression("", "", "a"). + NotBetween(exp.NewRangeVal("aaa", "zzz")) + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: betweenNum, sql: `("a" BETWEEN 1 AND 2)`}, + expressionTestCase{val: betweenNum, sql: `("a" BETWEEN ? AND ?)`, isPrepared: true, args: []interface{}{ + int64(1), + int64(2), + }}, + + expressionTestCase{val: notBetweenNum, sql: `("a" NOT BETWEEN 1 AND 2)`}, + expressionTestCase{val: notBetweenNum, sql: `("a" NOT BETWEEN ? AND ?)`, isPrepared: true, args: []interface{}{ + int64(1), + int64(2), + }}, + + expressionTestCase{val: betweenStr, sql: `("a" BETWEEN 'aaa' AND 'zzz')`}, + expressionTestCase{val: betweenStr, sql: `("a" BETWEEN ? AND ?)`, isPrepared: true, args: []interface{}{ + "aaa", + "zzz", + }}, + + expressionTestCase{val: notBetweenStr, sql: `("a" NOT BETWEEN 'aaa' AND 'zzz')`}, + expressionTestCase{val: notBetweenStr, sql: `("a" NOT BETWEEN ? AND ?)`, isPrepared: true, args: []interface{}{ + "aaa", + "zzz", + }}, + ) + + opts := sqlgen.DefaultDialectOptions() + opts.RangeOperatorLookup = map[exp.RangeOperation][]byte{} + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: betweenNum, err: "db: range operator between not supported"}, + expressionTestCase{val: betweenNum, err: "db: range operator between not supported"}, + + expressionTestCase{val: notBetweenNum, err: "db: range operator not between not supported"}, + expressionTestCase{val: notBetweenNum, err: "db: range operator not between not supported"}, + + expressionTestCase{val: betweenStr, err: "db: range operator between not supported"}, + expressionTestCase{val: betweenStr, err: "db: range operator between not supported"}, + + expressionTestCase{val: notBetweenStr, err: "db: range operator not between not supported"}, + expressionTestCase{val: notBetweenStr, err: "db: range operator not between not supported"}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_OrderedExpression() { + asc := exp.NewIdentifierExpression("", "", "a").Asc() + ascNf := exp.NewIdentifierExpression("", "", "a").Asc().NullsFirst() + ascNl := exp.NewIdentifierExpression("", "", "a").Asc().NullsLast() + + desc := exp.NewIdentifierExpression("", "", "a").Desc() + descNf := exp.NewIdentifierExpression("", "", "a").Desc().NullsFirst() + descNl := exp.NewIdentifierExpression("", "", "a").Desc().NullsLast() + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: asc, sql: `"a" ASC`}, + expressionTestCase{val: asc, sql: `"a" ASC`, isPrepared: true}, + + expressionTestCase{val: ascNf, sql: `"a" ASC NULLS FIRST`}, + expressionTestCase{val: ascNf, sql: `"a" ASC NULLS FIRST`, isPrepared: true}, + + expressionTestCase{val: ascNl, sql: `"a" ASC NULLS LAST`}, + expressionTestCase{val: ascNl, sql: `"a" ASC NULLS LAST`, isPrepared: true}, + + expressionTestCase{val: desc, sql: `"a" DESC`}, + expressionTestCase{val: desc, sql: `"a" DESC`, isPrepared: true}, + + expressionTestCase{val: descNf, sql: `"a" DESC NULLS FIRST`}, + expressionTestCase{val: descNf, sql: `"a" DESC NULLS FIRST`, isPrepared: true}, + + expressionTestCase{val: descNl, sql: `"a" DESC NULLS LAST`}, + expressionTestCase{val: descNl, sql: `"a" DESC NULLS LAST`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_UpdateExpression() { + ue := exp.NewIdentifierExpression("", "", "a").Set(1) + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: ue, sql: `"a"=1`}, + expressionTestCase{val: ue, sql: `"a"=?`, isPrepared: true, args: []interface{}{int64(1)}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_SQLFunctionExpression() { + min := exp.NewSQLFunctionExpression("MIN", exp.NewIdentifierExpression("", "", "a")) + coalesce := exp.NewSQLFunctionExpression("COALESCE", exp.NewIdentifierExpression("", "", "a"), "a") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: min, sql: `MIN("a")`}, + expressionTestCase{val: min, sql: `MIN("a")`, isPrepared: true}, + + expressionTestCase{val: coalesce, sql: `COALESCE("a", 'a')`}, + expressionTestCase{val: coalesce, sql: `COALESCE("a", ?)`, isPrepared: true, args: []interface{}{"a"}}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_SQLWindowFunctionExpression() { + sqlWinFunc := exp.NewSQLWindowFunctionExpression( + exp.NewSQLFunctionExpression("some_func"), + nil, + exp.NewWindowExpression( + nil, + exp.NewIdentifierExpression("", "", "win"), + nil, + nil, + ), + ) + sqlWinFuncFromWindow := exp.NewSQLWindowFunctionExpression( + exp.NewSQLFunctionExpression("some_func"), + exp.NewIdentifierExpression("", "", "win"), + nil, + ) + + emptyWinFunc := exp.NewSQLWindowFunctionExpression( + exp.NewSQLFunctionExpression("some_func"), + nil, + nil, + ) + badNamedSQLWinFuncInherit := exp.NewSQLWindowFunctionExpression( + exp.NewSQLFunctionExpression("some_func"), + nil, + exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w"), + nil, + nil, + nil, + ), + ) + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: sqlWinFunc, sql: `some_func() OVER ("win")`}, + expressionTestCase{val: sqlWinFunc, sql: `some_func() OVER ("win")`, isPrepared: true}, + + expressionTestCase{val: sqlWinFuncFromWindow, sql: `some_func() OVER "win"`}, + expressionTestCase{val: sqlWinFuncFromWindow, sql: `some_func() OVER "win"`, isPrepared: true}, + + expressionTestCase{val: emptyWinFunc, sql: `some_func() OVER ()`}, + expressionTestCase{val: emptyWinFunc, sql: `some_func() OVER ()`, isPrepared: true}, + + expressionTestCase{val: badNamedSQLWinFuncInherit, err: sqlgen.ErrUnexpectedNamedWindow.Error()}, + expressionTestCase{val: badNamedSQLWinFuncInherit, err: sqlgen.ErrUnexpectedNamedWindow.Error(), isPrepared: true}, + ) + opts := sqlgen.DefaultDialectOptions() + opts.SupportsWindowFunction = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: sqlWinFunc, err: sqlgen.ErrWindowNotSupported("test").Error()}, + expressionTestCase{val: sqlWinFunc, err: sqlgen.ErrWindowNotSupported("test").Error(), isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_WindowExpression() { + opts := sqlgen.DefaultDialectOptions() + opts.WindowPartitionByFragment = []byte("partition by ") + opts.WindowOrderByFragment = []byte("order by ") + + emptySQLWinFunc := exp.NewWindowExpression(nil, nil, nil, nil) + namedSQLWinFunc := exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w"), nil, nil, nil, + ) + inheritSQLWinFunc := exp.NewWindowExpression( + nil, exp.NewIdentifierExpression("", "", "w"), nil, nil, + ) + partitionBySQLWinFunc := exp.NewWindowExpression( + nil, nil, exp.NewColumnListExpression("a", "b"), nil, + ) + orderBySQLWinFunc := exp.NewWindowExpression( + nil, nil, nil, exp.NewOrderedColumnList( + exp.NewIdentifierExpression("", "", "a").Asc(), + exp.NewIdentifierExpression("", "", "b").Desc(), + ), + ) + + namedInheritPartitionOrderSQLWinFunc := exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w1"), + exp.NewIdentifierExpression("", "", "w2"), + exp.NewColumnListExpression("a", "b"), + exp.NewOrderedColumnList( + exp.NewIdentifierExpression("", "", "a").Asc(), + exp.NewIdentifierExpression("", "", "b").Desc(), + ), + ) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: emptySQLWinFunc, sql: `()`}, + expressionTestCase{val: emptySQLWinFunc, sql: `()`, isPrepared: true}, + + expressionTestCase{val: namedSQLWinFunc, sql: `"w" AS ()`}, + expressionTestCase{val: namedSQLWinFunc, sql: `"w" AS ()`, isPrepared: true}, + + expressionTestCase{val: inheritSQLWinFunc, sql: `("w")`}, + expressionTestCase{val: inheritSQLWinFunc, sql: `("w")`, isPrepared: true}, + + expressionTestCase{val: partitionBySQLWinFunc, sql: `(partition by "a", "b")`}, + expressionTestCase{val: partitionBySQLWinFunc, sql: `(partition by "a", "b")`, isPrepared: true}, + + expressionTestCase{val: orderBySQLWinFunc, sql: `(order by "a" ASC, "b" DESC)`}, + expressionTestCase{val: orderBySQLWinFunc, sql: `(order by "a" ASC, "b" DESC)`, isPrepared: true}, + + expressionTestCase{ + val: namedInheritPartitionOrderSQLWinFunc, + sql: `"w1" AS ("w2" partition by "a", "b" order by "a" ASC, "b" DESC)`, + }, + expressionTestCase{ + val: namedInheritPartitionOrderSQLWinFunc, + sql: `"w1" AS ("w2" partition by "a", "b" order by "a" ASC, "b" DESC)`, + isPrepared: true, + }, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsWindowFunction = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: emptySQLWinFunc, err: sqlgen.ErrWindowNotSupported("test").Error()}, + expressionTestCase{val: emptySQLWinFunc, err: sqlgen.ErrWindowNotSupported("test").Error(), isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_CastExpression() { + cast := exp.NewIdentifierExpression("", "", "a").Cast("DATE") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: cast, sql: `CAST("a" AS DATE)`}, + expressionTestCase{val: cast, sql: `CAST("a" AS DATE)`, isPrepared: true}, + ) +} + +// Generates the sql for the WITH clauses for common table expressions (CTE) +func (esgs *expressionSQLGeneratorSuite) TestGenerate_CommonTableExpressionSlice() { + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil) + + cteNoArgs := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(false, "a", ae), + } + cteArgs := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(false, "a(x,y)", ae), + } + + cteRecursiveNoArgs := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(true, "a", ae), + } + cteRecursiveArgs := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(true, "a(x,y)", ae), + } + + allCtes := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(false, "a", ae), + exp.NewCommonTableExpression(false, "a(x,y)", ae), + } + + allRecursiveCtes := []exp.CommonTableExpression{ + exp.NewCommonTableExpression(true, "a", ae), + exp.NewCommonTableExpression(true, "a(x,y)", ae), + } + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: cteNoArgs, sql: `WITH a AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteNoArgs, sql: `WITH a AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: cteArgs, sql: `WITH a(x,y) AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteArgs, sql: `WITH a(x,y) AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: cteRecursiveNoArgs, sql: `WITH RECURSIVE a AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteRecursiveNoArgs, sql: `WITH RECURSIVE a AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: cteRecursiveArgs, sql: `WITH RECURSIVE a(x,y) AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteRecursiveArgs, sql: `WITH RECURSIVE a(x,y) AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: allCtes, sql: `WITH a AS (SELECT * FROM "b"), a(x,y) AS (SELECT * FROM "b") `}, + expressionTestCase{val: allCtes, sql: `WITH a AS (SELECT * FROM "b"), a(x,y) AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: allRecursiveCtes, sql: `WITH RECURSIVE a AS (SELECT * FROM "b"), a(x,y) AS (SELECT * FROM "b") `}, + expressionTestCase{ + val: allRecursiveCtes, + sql: `WITH RECURSIVE a AS (SELECT * FROM "b"), a(x,y) AS (SELECT * FROM "b") `, + isPrepared: true, + }, + ) + opts := sqlgen.DefaultDialectOptions() + opts.SupportsWithCTE = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: cteNoArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]"}, + expressionTestCase{val: cteNoArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]", isPrepared: true}, + + expressionTestCase{val: cteArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]"}, + expressionTestCase{val: cteArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]", isPrepared: true}, + + expressionTestCase{val: cteRecursiveNoArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]"}, + expressionTestCase{val: cteRecursiveNoArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]", isPrepared: true}, + + expressionTestCase{val: cteRecursiveArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]"}, + expressionTestCase{val: cteRecursiveArgs, err: "db: dialect does not support CTE WITH clause [dialect=test]", isPrepared: true}, + ) + opts = sqlgen.DefaultDialectOptions() + opts.SupportsWithCTERecursive = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: cteNoArgs, sql: `WITH a AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteNoArgs, sql: `WITH a AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{val: cteArgs, sql: `WITH a(x,y) AS (SELECT * FROM "b") `}, + expressionTestCase{val: cteArgs, sql: `WITH a(x,y) AS (SELECT * FROM "b") `, isPrepared: true}, + + expressionTestCase{ + val: cteRecursiveNoArgs, + err: "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]", + }, + expressionTestCase{ + val: cteRecursiveNoArgs, + err: "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]", + isPrepared: true, + }, + + expressionTestCase{ + val: cteRecursiveArgs, + err: "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]", + }, + expressionTestCase{ + val: cteRecursiveArgs, + err: "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]", + isPrepared: true, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_CommonTableExpression() { + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil) + + cteNoArgs := exp.NewCommonTableExpression(false, "a", ae) + cteArgs := exp.NewCommonTableExpression(false, "a(x,y)", ae) + + cteRecursiveNoArgs := exp.NewCommonTableExpression(true, "a", ae) + cteRecursiveArgs := exp.NewCommonTableExpression(true, "a(x,y)", ae) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: cteNoArgs, sql: `a AS (SELECT * FROM "b")`}, + expressionTestCase{val: cteNoArgs, sql: `a AS (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: cteArgs, sql: `a(x,y) AS (SELECT * FROM "b")`}, + expressionTestCase{val: cteArgs, sql: `a(x,y) AS (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: cteRecursiveNoArgs, sql: `a AS (SELECT * FROM "b")`}, + expressionTestCase{val: cteRecursiveNoArgs, sql: `a AS (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: cteRecursiveArgs, sql: `a(x,y) AS (SELECT * FROM "b")`}, + expressionTestCase{val: cteRecursiveArgs, sql: `a(x,y) AS (SELECT * FROM "b")`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_CompoundExpression() { + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil) + + u := exp.NewCompoundExpression(exp.UnionCompoundType, ae) + ua := exp.NewCompoundExpression(exp.UnionAllCompoundType, ae) + + i := exp.NewCompoundExpression(exp.IntersectCompoundType, ae) + ia := exp.NewCompoundExpression(exp.IntersectAllCompoundType, ae) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: u, sql: ` UNION (SELECT * FROM "b")`}, + expressionTestCase{val: u, sql: ` UNION (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: ua, sql: ` UNION ALL (SELECT * FROM "b")`}, + expressionTestCase{val: ua, sql: ` UNION ALL (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: i, sql: ` INTERSECT (SELECT * FROM "b")`}, + expressionTestCase{val: i, sql: ` INTERSECT (SELECT * FROM "b")`, isPrepared: true}, + + expressionTestCase{val: ia, sql: ` INTERSECT ALL (SELECT * FROM "b")`}, + expressionTestCase{val: ia, sql: ` INTERSECT ALL (SELECT * FROM "b")`, isPrepared: true}, + ) + + opts := sqlgen.DefaultDialectOptions() + opts.WrapCompoundsInParens = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: u, sql: ` UNION SELECT * FROM "b"`}, + expressionTestCase{val: u, sql: ` UNION SELECT * FROM "b"`, isPrepared: true}, + + expressionTestCase{val: ua, sql: ` UNION ALL SELECT * FROM "b"`}, + expressionTestCase{val: ua, sql: ` UNION ALL SELECT * FROM "b"`, isPrepared: true}, + + expressionTestCase{val: i, sql: ` INTERSECT SELECT * FROM "b"`}, + expressionTestCase{val: i, sql: ` INTERSECT SELECT * FROM "b"`, isPrepared: true}, + + expressionTestCase{val: ia, sql: ` INTERSECT ALL SELECT * FROM "b"`}, + expressionTestCase{val: ia, sql: ` INTERSECT ALL SELECT * FROM "b"`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_IdentifierExpression() { + col := exp.NewIdentifierExpression("", "", "col") + colStar := exp.NewIdentifierExpression("", "", "*") + table := exp.NewIdentifierExpression("", "table", "") + schema := exp.NewIdentifierExpression("schema", "", "") + tableCol := exp.NewIdentifierExpression("", "table", "col") + schemaTableCol := exp.NewIdentifierExpression("schema", "table", "col") + + parsedCol := exp.ParseIdentifier("col") + parsedTableCol := exp.ParseIdentifier("table.col") + parsedSchemaTableCol := exp.ParseIdentifier("schema.table.col") + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.NewIdentifierExpression("", "", ""), + err: `db: a empty identifier was encountered, please specify a "schema", "table" or "column"`, + }, + expressionTestCase{ + val: exp.NewIdentifierExpression("", "", nil), + err: `db: a empty identifier was encountered, please specify a "schema", "table" or "column"`, + }, + expressionTestCase{ + val: exp.NewIdentifierExpression("", "", false), + err: `db: unexpected col type must be string or LiteralExpression received bool`, + }, + + expressionTestCase{val: col, sql: `"col"`}, + expressionTestCase{val: col, sql: `"col"`, isPrepared: true}, + + expressionTestCase{val: col.Table("table"), sql: `"table"."col"`}, + expressionTestCase{val: col.Table("table"), sql: `"table"."col"`, isPrepared: true}, + + expressionTestCase{val: col.Table("table").Schema("schema"), sql: `"schema"."table"."col"`}, + expressionTestCase{val: col.Table("table").Schema("schema"), sql: `"schema"."table"."col"`, isPrepared: true}, + + expressionTestCase{val: colStar, sql: `*`}, + expressionTestCase{val: colStar, sql: `*`, isPrepared: true}, + + expressionTestCase{val: colStar.Table("table"), sql: `"table".*`}, + expressionTestCase{val: colStar.Table("table"), sql: `"table".*`, isPrepared: true}, + + expressionTestCase{val: colStar.Table("table").Schema("schema"), sql: `"schema"."table".*`}, + expressionTestCase{val: colStar.Table("table").Schema("schema"), sql: `"schema"."table".*`, isPrepared: true}, + + expressionTestCase{val: table, sql: `"table"`}, + expressionTestCase{val: table, sql: `"table"`, isPrepared: true}, + + expressionTestCase{val: table.Col("col"), sql: `"table"."col"`}, + expressionTestCase{val: table.Col("col"), sql: `"table"."col"`, isPrepared: true}, + + expressionTestCase{val: table.Col(nil), sql: `"table"`}, + expressionTestCase{val: table.Col(nil), sql: `"table"`, isPrepared: true}, + + expressionTestCase{val: table.Col("*"), sql: `"table".*`}, + expressionTestCase{val: table.Col("*"), sql: `"table".*`, isPrepared: true}, + + expressionTestCase{val: table.Schema("schema").Col("col"), sql: `"schema"."table"."col"`}, + expressionTestCase{val: table.Schema("schema").Col("col"), sql: `"schema"."table"."col"`, isPrepared: true}, + + expressionTestCase{val: schema, sql: `"schema"`}, + expressionTestCase{val: schema, sql: `"schema"`, isPrepared: true}, + + expressionTestCase{val: schema.Table("table"), sql: `"schema"."table"`}, + expressionTestCase{val: schema.Table("table"), sql: `"schema"."table"`, isPrepared: true}, + + expressionTestCase{val: schema.Table("table").Col("col"), sql: `"schema"."table"."col"`}, + expressionTestCase{val: schema.Table("table").Col("col"), sql: `"schema"."table"."col"`, isPrepared: true}, + + expressionTestCase{val: schema.Table("table").Col(nil), sql: `"schema"."table"`}, + expressionTestCase{val: schema.Table("table").Col(nil), sql: `"schema"."table"`, isPrepared: true}, + + expressionTestCase{val: schema.Table("table").Col("*"), sql: `"schema"."table".*`}, + expressionTestCase{val: schema.Table("table").Col("*"), sql: `"schema"."table".*`, isPrepared: true}, + + expressionTestCase{val: tableCol, sql: `"table"."col"`}, + expressionTestCase{val: tableCol, sql: `"table"."col"`, isPrepared: true}, + + expressionTestCase{val: schemaTableCol, sql: `"schema"."table"."col"`}, + expressionTestCase{val: schemaTableCol, sql: `"schema"."table"."col"`, isPrepared: true}, + + expressionTestCase{val: parsedCol, sql: `"col"`}, + expressionTestCase{val: parsedCol, sql: `"col"`, isPrepared: true}, + + expressionTestCase{val: parsedTableCol, sql: `"table"."col"`}, + expressionTestCase{val: parsedTableCol, sql: `"table"."col"`, isPrepared: true}, + + expressionTestCase{val: parsedSchemaTableCol, sql: `"schema"."table"."col"`}, + expressionTestCase{val: parsedSchemaTableCol, sql: `"schema"."table"."col"`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_LateralExpression() { + lateralExp := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, emptyArgs, nil, nil)) + + do := sqlgen.DefaultDialectOptions() + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, sql: `LATERAL (SELECT * FROM "test")`}, + expressionTestCase{val: lateralExp, sql: `LATERAL (SELECT * FROM "test")`, isPrepared: true}, + ) + + do = sqlgen.DefaultDialectOptions() + do.LateralFragment = []byte("lateral ") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, sql: `lateral (SELECT * FROM "test")`}, + expressionTestCase{val: lateralExp, sql: `lateral (SELECT * FROM "test")`, isPrepared: true}, + ) + do = sqlgen.DefaultDialectOptions() + do.SupportsLateral = false + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, err: "db: dialect does not support lateral expressions [dialect=test]"}, + expressionTestCase{val: lateralExp, err: "db: dialect does not support lateral expressions [dialect=test]", isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_CaseExpression() { + ident := exp.NewIdentifierExpression("", "", "col") + valueCase := exp.NewCaseExpression(). + Value(ident). + When(true, "one"). + When(false, "two") + valueElseCase := exp.NewCaseExpression(). + Value(ident). + When(1, "one"). + When(2, "two"). + Else("three") + searchCase := exp.NewCaseExpression(). + When(ident.Gt(1), exp.NewLiteralExpression("? - 1", ident)). + When(ident.Lt(0), exp.NewLiteralExpression("? + 1", ident)) + searchElseCase := exp.NewCaseExpression(). + When(ident.Gt(1), exp.NewLiteralExpression("? - 1", ident)). + When(ident.Lt(0), exp.NewLiteralExpression("? + 1", ident)). + Else(ident) + + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: valueCase, sql: `CASE "col" WHEN TRUE THEN 'one' WHEN FALSE THEN 'two' END`}, + expressionTestCase{ + val: valueCase, + sql: `CASE "col" WHEN ? THEN ? WHEN ? THEN ? END`, + isPrepared: true, + args: []interface{}{true, "one", false, "two"}, + }, + + expressionTestCase{val: valueElseCase, sql: `CASE "col" WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'three' END`}, + expressionTestCase{ + val: valueElseCase, + sql: `CASE "col" WHEN ? THEN ? WHEN ? THEN ? ELSE ? END`, + isPrepared: true, + args: []interface{}{int64(1), "one", int64(2), "two", "three"}, + }, + + expressionTestCase{val: searchCase, sql: `CASE WHEN ("col" > 1) THEN "col" - 1 WHEN ("col" < 0) THEN "col" + 1 END`}, + expressionTestCase{ + val: searchCase, + sql: `CASE WHEN ("col" > ?) THEN "col" - 1 WHEN ("col" < ?) THEN "col" + 1 END`, + isPrepared: true, + args: []interface{}{int64(1), int64(0)}, + }, + + expressionTestCase{val: searchElseCase, sql: `CASE WHEN ("col" > 1) THEN "col" - 1 WHEN ("col" < 0) THEN "col" + 1 ELSE "col" END`}, + expressionTestCase{ + val: searchElseCase, + sql: `CASE WHEN ("col" > ?) THEN "col" - 1 WHEN ("col" < ?) THEN "col" + 1 ELSE "col" END`, + isPrepared: true, + args: []interface{}{int64(1), int64(0)}, + }, + expressionTestCase{ + val: exp.NewCaseExpression(), + err: "db: when conditions not found for case statement", + }, + ) + + opts := sqlgen.DefaultDialectOptions() + opts.CaseFragment = []byte("case ") + opts.WhenFragment = []byte(" when ") + opts.ThenFragment = []byte(" then ") + opts.ElseFragment = []byte(" else ") + opts.EndFragment = []byte(" end") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", opts), + expressionTestCase{val: valueCase, sql: `case "col" when TRUE then 'one' when FALSE then 'two' end`}, + expressionTestCase{ + val: valueCase, + sql: `case "col" when ? then ? when ? then ? end`, + isPrepared: true, + args: []interface{}{true, "one", false, "two"}, + }, + + expressionTestCase{val: valueElseCase, sql: `case "col" when 1 then 'one' when 2 then 'two' else 'three' end`}, + expressionTestCase{ + val: valueElseCase, + sql: `case "col" when ? then ? when ? then ? else ? end`, + isPrepared: true, + args: []interface{}{int64(1), "one", int64(2), "two", "three"}, + }, + + expressionTestCase{val: searchCase, sql: `case when ("col" > 1) then "col" - 1 when ("col" < 0) then "col" + 1 end`}, + expressionTestCase{ + val: searchCase, + sql: `case when ("col" > ?) then "col" - 1 when ("col" < ?) then "col" + 1 end`, + isPrepared: true, + args: []interface{}{int64(1), int64(0)}, + }, + + expressionTestCase{val: searchElseCase, sql: `case when ("col" > 1) then "col" - 1 when ("col" < 0) then "col" + 1 else "col" end`}, + expressionTestCase{ + val: searchElseCase, + sql: `case when ("col" > ?) then "col" - 1 when ("col" < ?) then "col" + 1 else "col" end`, + isPrepared: true, + args: []interface{}{int64(1), int64(0)}, + }, + expressionTestCase{ + val: exp.NewCaseExpression(), + err: "db: when conditions not found for case statement", + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMap() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{}}, + expressionTestCase{val: exp.Ex{}, isPrepared: true}, + + expressionTestCase{val: exp.Ex{"a": 1}, sql: `("a" = 1)`}, + expressionTestCase{val: exp.Ex{"a": 1}, sql: `("a" = ?)`, isPrepared: true, args: []interface{}{int64(1)}}, + + expressionTestCase{val: exp.Ex{"a": true}, sql: `("a" IS TRUE)`}, + expressionTestCase{val: exp.Ex{"a": true}, sql: `("a" IS TRUE)`, isPrepared: true}, + + expressionTestCase{val: exp.Ex{"a": false}, sql: `("a" IS FALSE)`}, + expressionTestCase{val: exp.Ex{"a": false}, sql: `("a" IS FALSE)`, isPrepared: true}, + + expressionTestCase{val: exp.Ex{"a": nil}, sql: `("a" IS NULL)`}, + expressionTestCase{val: exp.Ex{"a": nil}, sql: `("a" IS NULL)`, isPrepared: true}, + + expressionTestCase{val: exp.Ex{"a": []string{"a", "b", "c"}}, sql: `("a" IN ('a', 'b', 'c'))`}, + expressionTestCase{ + val: exp.Ex{"a": []string{"a", "b", "c"}}, + sql: `("a" IN (?, ?, ?))`, + isPrepared: true, + args: []interface{}{"a", "b", "c"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithABadOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"badOp": true}}, + err: "db: unsupported expression type badOp", + }, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"badOp": true}}, + isPrepared: true, + err: "db: unsupported expression type badOp", + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithNeqOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"neq": 1}}, sql: `("a" != 1)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"neq": 1}}, sql: `("a" != ?)`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithIsNotOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"isnot": true}}, sql: `("a" IS NOT TRUE)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"isnot": true}}, sql: `("a" IS NOT TRUE)`, isPrepared: true}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithGtOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"gt": 1}}, sql: `("a" > 1)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"gt": 1}}, sql: `("a" > ?)`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithGteOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"gte": 1}}, sql: `("a" >= 1)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"gte": 1}}, sql: `("a" >= ?)`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithLtOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"lt": 1}}, sql: `("a" < 1)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"lt": 1}}, sql: `("a" < ?)`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithLteOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"lte": 1}}, sql: `("a" <= 1)`}, + expressionTestCase{val: exp.Ex{"a": exp.Op{"lte": 1}}, sql: `("a" <= ?)`, isPrepared: true, args: []interface{}{ + int64(1), + }}, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithLikeOp() { + re := regexp.MustCompile("[ab]") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"like": "a%"}}, sql: `("a" LIKE 'a%')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"like": "a%"}}, + sql: `("a" LIKE ?)`, + isPrepared: true, + args: []interface{}{"a%"}, + }, + + expressionTestCase{val: exp.Ex{"a": exp.Op{"like": re}}, sql: `("a" ~ '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"like": re}}, + sql: `("a" ~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithNotLikeOp() { + re := regexp.MustCompile("[ab]") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"notLike": "a%"}}, sql: `("a" NOT LIKE 'a%')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notLike": "a%"}}, + sql: `("a" NOT LIKE ?)`, + isPrepared: true, + args: []interface{}{"a%"}, + }, + + expressionTestCase{val: exp.Ex{"a": exp.Op{"notLike": re}}, sql: `("a" !~ '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notLike": re}}, + sql: `("a" !~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithILikeOp() { + re := regexp.MustCompile("[ab]") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"iLike": "a%"}}, sql: `("a" ILIKE 'a%')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"iLike": "a%"}}, + sql: `("a" ILIKE ?)`, + isPrepared: true, + args: []interface{}{"a%"}, + }, + + expressionTestCase{val: exp.Ex{"a": exp.Op{"iLike": re}}, sql: `("a" ~* '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"iLike": re}}, + sql: `("a" ~* ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithNotILikeOp() { + re := regexp.MustCompile("[ab]") + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"notILike": "a%"}}, sql: `("a" NOT ILIKE 'a%')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notILike": "a%"}}, + sql: `("a" NOT ILIKE ?)`, + isPrepared: true, + args: []interface{}{"a%"}, + }, + + expressionTestCase{val: exp.Ex{"a": exp.Op{"notILike": re}}, sql: `("a" !~* '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notILike": re}}, + sql: `("a" !~* ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithRegExpLikeOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + + expressionTestCase{val: exp.Ex{"a": exp.Op{"regexpLike": "[ab]"}}, sql: `("a" ~ '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"regexpLike": "[ab]"}}, + sql: `("a" ~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithRegExpILikeOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"regexpILike": "[ab]"}}, sql: `("a" ~* '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"regexpILike": "[ab]"}}, + sql: `("a" ~* ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithRegExpNotLikeOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"regexpNotLike": "[ab]"}}, sql: `("a" !~ '[ab]')`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"regexpNotLike": "[ab]"}}, + sql: `("a" !~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithInOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.Ex{"a": exp.Op{"in": []string{"a", "b", "c"}}}, sql: `("a" IN ('a', 'b', 'c'))`}, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"in": []string{"a", "b", "c"}}}, + sql: `("a" IN (?, ?, ?))`, + isPrepared: true, + args: []interface{}{"a", "b", "c"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapWithNotInOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notIn": []string{"a", "b", "c"}}}, + sql: `("a" NOT IN ('a', 'b', 'c'))`, + }, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notIn": []string{"a", "b", "c"}}}, + sql: `("a" NOT IN (?, ?, ?))`, + isPrepared: true, + args: []interface{}{"a", "b", "c"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapBetweenOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"between": exp.NewRangeVal("aaa", "zzz")}}, + sql: `("a" BETWEEN 'aaa' AND 'zzz')`, + }, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"between": exp.NewRangeVal("aaa", "zzz")}}, + sql: `("a" BETWEEN ? AND ?)`, + isPrepared: true, + args: []interface{}{"aaa", "zzz"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapNotBetweenOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notBetween": exp.NewRangeVal("aaa", "zzz")}}, + sql: `("a" NOT BETWEEN 'aaa' AND 'zzz')`, + }, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"notBetween": exp.NewRangeVal("aaa", "zzz")}}, + sql: `("a" NOT BETWEEN ? AND ?)`, + isPrepared: true, + args: []interface{}{"aaa", "zzz"}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMapIsOp() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("test", sqlgen.DefaultDialectOptions()), + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"is": nil, "eq": 10}}, + sql: `(("a" = 10) OR ("a" IS NULL))`, + }, + expressionTestCase{ + val: exp.Ex{"a": exp.Op{"is": nil, "eq": 10}}, + sql: `(("a" = ?) OR ("a" IS NULL))`, + isPrepared: true, + args: []interface{}{int64(10)}, + }, + ) +} + +func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionOrMap() { + esgs.assertCases( + sqlgen.NewExpressionSQLGenerator("default", sqlgen.DefaultDialectOptions()), + expressionTestCase{val: exp.ExOr{}}, + expressionTestCase{val: exp.ExOr{}, isPrepared: true}, + + expressionTestCase{val: exp.ExOr{"a": exp.Op{"regexpLike": "[ab]"}}, sql: `("a" ~ '[ab]')`}, + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"regexpLike": "[ab]"}}, + sql: `("a" ~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + + expressionTestCase{val: exp.ExOr{"a": exp.Op{"regexpNotLike": "[ab]"}}, sql: `("a" !~ '[ab]')`}, + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"regexpNotLike": "[ab]"}}, + sql: `("a" !~ ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + + expressionTestCase{val: exp.ExOr{"a": exp.Op{"regexpILike": "[ab]"}}, sql: `("a" ~* '[ab]')`}, + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"regexpILike": "[ab]"}}, + sql: `("a" ~* ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + expressionTestCase{val: exp.ExOr{"a": exp.Op{"regexpNotILike": "[ab]"}}, sql: `("a" !~* '[ab]')`}, + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"regexpNotILike": "[ab]"}}, + sql: `("a" !~* ?)`, + isPrepared: true, + args: []interface{}{"[ab]"}, + }, + + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"badOp": true}}, + err: "db: unsupported expression type badOp", + }, + expressionTestCase{ + val: exp.ExOr{"a": exp.Op{"badOp": true}}, + isPrepared: true, + err: "db: unsupported expression type badOp", + }, + + expressionTestCase{val: exp.ExOr{"a": 1, "b": true}, sql: `(("a" = 1) OR ("b" IS TRUE))`}, + expressionTestCase{ + val: exp.ExOr{"a": 1, "b": true}, + sql: `(("a" = ?) OR ("b" IS TRUE))`, + isPrepared: true, + args: []interface{}{int64(1)}, + }, + + expressionTestCase{ + val: exp.ExOr{"a": 1, "b": []string{"a", "b", "c"}}, + sql: `(("a" = 1) OR ("b" IN ('a', 'b', 'c')))`, + }, + expressionTestCase{ + val: exp.ExOr{"a": 1, "b": []string{"a", "b", "c"}}, + sql: `(("a" = ?) OR ("b" IN (?, ?, ?)))`, + isPrepared: true, + args: []interface{}{int64(1), "a", "b", "c"}, + }, + ) +} + +func TestExpressionSQLGenerator(t *testing.T) { + suite.Run(t, new(expressionSQLGeneratorSuite)) +} diff --git a/sqlgen/insert_sql_generator.go b/sqlgen/insert_sql_generator.go new file mode 100644 index 0000000..9226147 --- /dev/null +++ b/sqlgen/insert_sql_generator.go @@ -0,0 +1,203 @@ +package sqlgen + +import ( + "strings" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + InsertSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, clauses exp.InsertClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + insertSQLGenerator struct { + CommonSQLGenerator + } +) + +var ( + ErrConflictUpdateValuesRequired = errors.New("values are required for on conflict update expression") + ErrNoSourceForInsert = errors.New("no source found when generating insert sql") +) + +func errMisMatchedRowLength(expectedL, actualL int) error { + return errors.New("rows with different value length expected %d got %d", expectedL, actualL) +} + +func errUpsertWithWhereNotSupported(dialect string) error { + return errors.New("dialect does not support upsert with where clause [dialect=%s]", dialect) +} + +func NewInsertSQLGenerator(dialect string, do *SQLDialectOptions) InsertSQLGenerator { + return &insertSQLGenerator{NewCommonSQLGenerator(dialect, do)} +} + +func (isg *insertSQLGenerator) Generate( + b sb.SQLBuilder, + clauses exp.InsertClauses, +) { + if !clauses.HasInto() { + b.SetError(ErrNoSourceForInsert) + return + } + for _, f := range isg.DialectOptions().InsertSQLOrder { + if b.Error() != nil { + return + } + switch f { + case CommonTableSQLFragment: + isg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables()) + case InsertBeingSQLFragment: + isg.InsertBeginSQL(b, clauses.OnConflict()) + case IntoSQLFragment: + b.WriteRunes(isg.DialectOptions().SpaceRune) + isg.ExpressionSQLGenerator().Generate(b, clauses.Into()) + case InsertSQLFragment: + isg.InsertSQL(b, clauses) + case ReturningSQLFragment: + isg.ReturningSQL(b, clauses.Returning()) + default: + b.SetError(ErrNotSupportedFragment("INSERT", f)) + } + } +} + +// Adds the correct fragment to being an INSERT statement +func (isg *insertSQLGenerator) InsertBeginSQL(b sb.SQLBuilder, o exp.ConflictExpression) { + if isg.DialectOptions().SupportsInsertIgnoreSyntax && o != nil { + b.Write(isg.DialectOptions().InsertIgnoreClause) + } else { + b.Write(isg.DialectOptions().InsertClause) + } +} + +// Adds the columns list to an insert statement +func (isg *insertSQLGenerator) InsertSQL(b sb.SQLBuilder, ic exp.InsertClauses) { + switch { + case ic.HasRows(): + ie, err := exp.NewInsertExpression(ic.Rows()...) + if err != nil { + b.SetError(err) + return + } + isg.InsertExpressionSQL(b, ie) + case ic.HasCols() && ic.HasVals(): + isg.insertColumnsSQL(b, ic.Cols()) + isg.insertValuesSQL(b, ic.Vals()) + case ic.HasCols() && ic.HasFrom(): + isg.insertColumnsSQL(b, ic.Cols()) + isg.insertFromSQL(b, ic.From()) + case ic.HasFrom(): + isg.insertFromSQL(b, ic.From()) + default: + isg.defaultValuesSQL(b) + } + if ic.HasAlias() { + b.Write(isg.DialectOptions().AsFragment) + isg.ExpressionSQLGenerator().Generate(b, ic.Alias()) + } + isg.onConflictSQL(b, ic.OnConflict()) +} + +func (isg *insertSQLGenerator) InsertExpressionSQL(b sb.SQLBuilder, ie exp.InsertExpression) { + switch { + case ie.IsInsertFrom(): + isg.insertFromSQL(b, ie.From()) + case ie.IsEmpty(): + isg.defaultValuesSQL(b) + default: + isg.insertColumnsSQL(b, ie.Cols()) + isg.insertValuesSQL(b, ie.Vals()) + } +} + +// Adds the DefaultValuesFragment to an SQL statement +func (isg *insertSQLGenerator) defaultValuesSQL(b sb.SQLBuilder) { + b.Write(isg.DialectOptions().DefaultValuesFragment) +} + +func (isg *insertSQLGenerator) insertFromSQL(b sb.SQLBuilder, ae exp.AppendableExpression) { + b.WriteRunes(isg.DialectOptions().SpaceRune) + ae.AppendSQL(b) +} + +// Adds the columns list to an insert statement +func (isg *insertSQLGenerator) insertColumnsSQL(b sb.SQLBuilder, cols exp.ColumnListExpression) { + b.WriteRunes(isg.DialectOptions().SpaceRune, isg.DialectOptions().LeftParenRune) + isg.ExpressionSQLGenerator().Generate(b, cols) + b.WriteRunes(isg.DialectOptions().RightParenRune) +} + +// Adds the values clause to an SQL statement +func (isg *insertSQLGenerator) insertValuesSQL(b sb.SQLBuilder, values [][]interface{}) { + b.Write(isg.DialectOptions().ValuesFragment) + rowLen := len(values[0]) + valueLen := len(values) + for i, row := range values { + if len(row) != rowLen { + b.SetError(errMisMatchedRowLength(rowLen, len(row))) + return + } + isg.ExpressionSQLGenerator().Generate(b, row) + if i < valueLen-1 { + b.WriteRunes(isg.DialectOptions().CommaRune, isg.DialectOptions().SpaceRune) + } + } +} + +// Adds the DefaultValuesFragment to an SQL statement +func (isg *insertSQLGenerator) onConflictSQL(b sb.SQLBuilder, o exp.ConflictExpression) { + if o == nil { + return + } + b.Write(isg.DialectOptions().ConflictFragment) + switch t := o.(type) { + case exp.ConflictUpdateExpression: + target := t.TargetColumn() + if isg.DialectOptions().SupportsConflictTarget && target != "" { + wrapParens := !strings.HasPrefix(strings.ToLower(target), "on constraint") + + b.WriteRunes(isg.DialectOptions().SpaceRune) + if wrapParens { + b.WriteRunes(isg.DialectOptions().LeftParenRune). + WriteStrings(target). + WriteRunes(isg.DialectOptions().RightParenRune) + } else { + b.Write([]byte(target)) + } + } + isg.onConflictDoUpdateSQL(b, t) + default: + b.Write(isg.DialectOptions().ConflictDoNothingFragment) + } +} + +func (isg *insertSQLGenerator) onConflictDoUpdateSQL(b sb.SQLBuilder, o exp.ConflictUpdateExpression) { + b.Write(isg.DialectOptions().ConflictDoUpdateFragment) + update := o.Update() + if update == nil { + b.SetError(ErrConflictUpdateValuesRequired) + return + } + ue, err := exp.NewUpdateExpressions(update) + if err != nil { + b.SetError(err) + return + } + isg.UpdateExpressionSQL(b, ue...) + if b.Error() == nil && o.WhereClause() != nil { + if !isg.DialectOptions().SupportsConflictUpdateWhere { + b.SetError(errUpsertWithWhereNotSupported(isg.Dialect())) + return + } + isg.WhereSQL(b, o.WhereClause()) + } +} diff --git a/sqlgen/insert_sql_generator_test.go b/sqlgen/insert_sql_generator_test.go new file mode 100644 index 0000000..f9430e7 --- /dev/null +++ b/sqlgen/insert_sql_generator_test.go @@ -0,0 +1,468 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + insertTestCase struct { + clause exp.InsertClauses + sql string + isPrepared bool + args []interface{} + err string + } + insertSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (igs *insertSQLGeneratorSuite) assertCases(isg sqlgen.InsertSQLGenerator, testCases ...insertTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + isg.Generate(b, tc.clause) + switch { + case len(tc.err) > 0: + igs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + igs.assertPreparedSQL(b, tc.sql, tc.args) + default: + igs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (igs *insertSQLGeneratorSuite) TestDialect() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewInsertSQLGenerator("test", opts) + igs.Equal("test", d.Dialect()) + + opts2 := sqlgen.DefaultDialectOptions() + d2 := sqlgen.NewInsertSQLGenerator("test2", opts2) + igs.Equal("test2", d2.Dialect()) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_UnsupportedFragment() { + opts := sqlgen.DefaultDialectOptions() + opts.InsertSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment} + d := sqlgen.NewInsertSQLGenerator("test", opts) + + b := sb.NewSQLBuilder(true) + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")) + d.Generate(b, ic) + igs.assertErrorSQL(b, `db: unsupported INSERT SQL fragment UpdateBeginSQLFragment`) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_empty() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`, isPrepared: true}, + ) + + opts2 := sqlgen.DefaultDialectOptions() + opts2.DefaultValuesFragment = []byte(" default values") + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts2), + insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_nilValues() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetCols(exp.NewColumnListExpression("a")). + SetVals([][]interface{}{ + {nil}, + }) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a") VALUES (NULL)`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a") VALUES (?)`, isPrepared: true, args: []interface{}{nil}}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_colsAndVals() { + opts := sqlgen.DefaultDialectOptions() + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.ValuesFragment = []byte(" values ") + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.CommaRune = ';' + opts.PlaceHolderFragment = []byte("#") + + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetCols(exp.NewColumnListExpression("a", "b")). + SetVals([][]interface{}{ + {"a1", "b1"}, + {"a2", "b2"}, + {"a3", "b3"}, + }) + + bic := ic.SetCols(exp.NewColumnListExpression("a", "b")). + SetVals([][]interface{}{ + {"a1"}, + {"a2", "b2"}, + {"a3", "b3"}, + }) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {'a1'; 'b1'}; {'a2'; 'b2'}; {'a3'; 'b3'}`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {#; #}; {#; #}; {#; #}`, isPrepared: true, args: []interface{}{ + "a1", "b1", "a2", "b2", "a3", "b3", + }}, + + insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`}, + insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withNoInto() { + opts := sqlgen.DefaultDialectOptions() + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.ValuesFragment = []byte(" values ") + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.CommaRune = ';' + opts.PlaceHolderFragment = []byte("#") + + ic := exp.NewInsertClauses(). + SetCols(exp.NewColumnListExpression("a", "b")). + SetVals([][]interface{}{ + {"a1", "b1"}, + {"a2", "b2"}, + {"a3", "b3"}, + }) + expectedErr := "db: no source found when generating insert sql" + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: ic, err: expectedErr}, + insertTestCase{clause: ic, err: expectedErr, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withRows() { + opts := sqlgen.DefaultDialectOptions() + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.ValuesFragment = []byte(" values ") + opts.LeftParenRune = '{' + opts.RightParenRune = '}' + opts.CommaRune = ';' + opts.PlaceHolderFragment = []byte("#") + + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetRows([]interface{}{ + exp.Record{"a": "a1", "b": "b1"}, + exp.Record{"a": "a2", "b": "b2"}, + exp.Record{"a": "a3", "b": "b3"}, + }) + + bic := ic.SetRows([]interface{}{ + exp.Record{"a": "a1"}, + exp.Record{"a": "a2", "b": "b2"}, + exp.Record{"a": "a3", "b": "b3"}, + }) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {'a1'; 'b1'}; {'a2'; 'b2'}; {'a3'; 'b3'}`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {#; #}; {#; #}; {#; #}`, isPrepared: true, args: []interface{}{ + "a1", "b1", "a2", "b2", "a3", "b3", + }}, + + insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`}, + insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withEmptyRows() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetRows([]interface{}{exp.Record{}}) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`, isPrepared: true}, + ) + + opts2 := sqlgen.DefaultDialectOptions() + opts2.DefaultValuesFragment = []byte(" default values") + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts2), + insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withRowsAppendableExpression() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetRows([]interface{}{newTestAppendableExpression(`select * from "other"`, emptyArgs, nil, nil)}) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" select * from "other"`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" select * from "other"`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withFrom() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetFrom(newTestAppendableExpression(`select c, d from test where a = 'b'`, nil, nil, nil)) + + icCols := ic.SetCols(exp.NewColumnListExpression("a", "b")) + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" select c, d from test where a = 'b'`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" select c, d from test where a = 'b'`, isPrepared: true}, + + insertTestCase{clause: icCols, sql: `INSERT INTO "test" ("a", "b") select c, d from test where a = 'b'`}, + insertTestCase{clause: icCols, sql: `INSERT INTO "test" ("a", "b") select c, d from test where a = 'b'`, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_onConflict() { + opts := sqlgen.DefaultDialectOptions() + // make sure the fragments are used + opts.ConflictFragment = []byte(" on conflict") + opts.ConflictDoNothingFragment = []byte(" do nothing") + opts.ConflictDoUpdateFragment = []byte(" do update set ") + + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetCols(exp.NewColumnListExpression("a")). + SetVals([][]interface{}{ + {"a1"}, + }) + icDn := ic.SetOnConflict(exp.NewDoNothingConflictExpression()) + icDu := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", exp.Record{"a": "b"})) + icAsDu := ic.SetAlias(exp.NewIdentifierExpression("", "new", "")).SetOnConflict( + exp.NewDoUpdateConflictExpression("test", exp.Record{"a": exp.NewIdentifierExpression("", "new", "a")}), + ) + icDoc := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("on constraint test", exp.Record{"a": "b"})) + icDuw := ic.SetOnConflict( + exp.NewDoUpdateConflictExpression("test", exp.Record{"a": "b"}).Where(exp.Ex{"foo": true}), + ) + + icDuNil := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", nil)) + icDuBad := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", true)) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: icDn, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict do nothing`}, + insertTestCase{ + clause: icDn, + sql: `INSERT INTO "test" ("a") VALUES (?) on conflict do nothing`, + isPrepared: true, + args: []interface{}{"a1"}, + }, + + insertTestCase{clause: icDu, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b'`}, + insertTestCase{ + clause: icDu, + sql: `INSERT INTO "test" ("a") VALUES (?) on conflict (test) do update set "a"=?`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{clause: icAsDu, sql: `INSERT INTO "test" ("a") VALUES ('a1') AS "new" on conflict (test) do update set "a"="new"."a"`}, + insertTestCase{ + clause: icAsDu, + sql: `INSERT INTO "test" ("a") VALUES (?) AS "new" on conflict (test) do update set "a"="new"."a"`, + isPrepared: true, + args: []interface{}{"a1"}, + }, + + insertTestCase{clause: icDoc, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict on constraint test do update set "a"='b'`}, + insertTestCase{ + clause: icDoc, + sql: `INSERT INTO "test" ("a") VALUES (?) on conflict on constraint test do update set "a"=?`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{ + clause: icDuw, + sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b' WHERE ("foo" IS TRUE)`, + }, + insertTestCase{ + clause: icDuw, + sql: `INSERT INTO "test" ("a") VALUES (?) on conflict (test) do update set "a"=? WHERE ("foo" IS TRUE)`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error()}, + insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error(), isPrepared: true}, + + insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool"}, + insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool", isPrepared: true}, + ) + opts.SupportsInsertIgnoreSyntax = true + opts.InsertIgnoreClause = []byte("insert ignore into") + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: icDn, sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict do nothing`}, + insertTestCase{ + clause: icDn, + sql: `insert ignore into "test" ("a") VALUES (?) on conflict do nothing`, + isPrepared: true, + args: []interface{}{"a1"}, + }, + + insertTestCase{ + clause: icDu, + sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b'`, + }, + insertTestCase{ + clause: icDu, + sql: `insert ignore into "test" ("a") VALUES (?) on conflict (test) do update set "a"=?`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{ + clause: icDoc, + sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict on constraint test do update set "a"='b'`, + }, + insertTestCase{ + clause: icDoc, + sql: `insert ignore into "test" ("a") VALUES (?) on conflict on constraint test do update set "a"=?`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{ + clause: icDuw, + sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b' WHERE ("foo" IS TRUE)`, + }, + insertTestCase{ + clause: icDuw, + sql: `insert ignore into "test" ("a") VALUES (?) on conflict (test) do update set "a"=? WHERE ("foo" IS TRUE)`, + isPrepared: true, + args: []interface{}{"a1", "b"}, + }, + + insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error()}, + insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error(), isPrepared: true}, + + insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool"}, + insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool", isPrepared: true}, + ) + + opts.SupportsConflictUpdateWhere = false + expectedErr := "db: dialect does not support upsert with where clause [dialect=test]" + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: icDuw, err: expectedErr}, + insertTestCase{clause: icDuw, err: expectedErr, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withCommonTables() { + opts := sqlgen.DefaultDialectOptions() + opts.WithFragment = []byte("with ") + opts.RecursiveFragment = []byte("recursive ") + + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + + ic := exp.NewInsertClauses().SetInto(exp.NewIdentifierExpression("", "test_cte", "")) + icCte1 := ic.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) + icCte2 := ic.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse)) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{ + clause: icCte1, + sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + }, + insertTestCase{ + clause: icCte1, + sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + isPrepared: true, + }, + + insertTestCase{ + clause: icCte2, + sql: `with recursive test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + }, + insertTestCase{ + clause: icCte2, + sql: `with recursive test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + isPrepared: true, + }, + ) + + opts.SupportsWithCTE = false + expectedErr := "db: dialect does not support CTE WITH clause [dialect=test]" + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{clause: icCte1, err: expectedErr}, + insertTestCase{clause: icCte1, err: expectedErr, isPrepared: true}, + + insertTestCase{clause: icCte2, err: expectedErr}, + insertTestCase{clause: icCte2, err: expectedErr, isPrepared: true}, + ) + + opts.SupportsWithCTE = true + opts.SupportsWithCTERecursive = false + expectedErr = "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]" + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", opts), + insertTestCase{ + clause: icCte1, + sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + }, + insertTestCase{ + clause: icCte1, + sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`, + isPrepared: true, + }, + + insertTestCase{clause: icCte2, err: expectedErr}, + insertTestCase{clause: icCte2, err: expectedErr, isPrepared: true}, + ) +} + +func (igs *insertSQLGeneratorSuite) TestGenerate_withReturning() { + ic := exp.NewInsertClauses(). + SetInto(exp.NewIdentifierExpression("", "test", "")). + SetCols(exp.NewColumnListExpression("a", "b")). + SetVals([][]interface{}{ + {"a1", "b1"}, + }). + SetReturning(exp.NewColumnListExpression("a", "b")) + + igs.assertCases( + sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()), + insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a", "b") VALUES ('a1', 'b1') RETURNING "a", "b"`}, + insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a", "b") VALUES (?, ?) RETURNING "a", "b"`, isPrepared: true, args: []interface{}{ + "a1", "b1", + }}, + ) +} + +func TestInsertSQLGenerator(t *testing.T) { + suite.Run(t, new(insertSQLGeneratorSuite)) +} diff --git a/sqlgen/mocks/DeleteSQLGenerator.go b/sqlgen/mocks/DeleteSQLGenerator.go new file mode 100644 index 0000000..56275a9 --- /dev/null +++ b/sqlgen/mocks/DeleteSQLGenerator.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// DeleteSQLGenerator is an autogenerated mock type for the DeleteSQLGenerator type +type DeleteSQLGenerator struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *DeleteSQLGenerator) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Generate provides a mock function with given fields: b, clauses +func (_m *DeleteSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.DeleteClauses) { + _m.Called(b, clauses) +} diff --git a/sqlgen/mocks/InsertSQLGenerator.go b/sqlgen/mocks/InsertSQLGenerator.go new file mode 100644 index 0000000..7fe4fc8 --- /dev/null +++ b/sqlgen/mocks/InsertSQLGenerator.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// InsertSQLGenerator is an autogenerated mock type for the InsertSQLGenerator type +type InsertSQLGenerator struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *InsertSQLGenerator) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Generate provides a mock function with given fields: b, clauses +func (_m *InsertSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.InsertClauses) { + _m.Called(b, clauses) +} diff --git a/sqlgen/mocks/SelectSQLGenerator.go b/sqlgen/mocks/SelectSQLGenerator.go new file mode 100644 index 0000000..c04f1e8 --- /dev/null +++ b/sqlgen/mocks/SelectSQLGenerator.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// SelectSQLGenerator is an autogenerated mock type for the SelectSQLGenerator type +type SelectSQLGenerator struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *SelectSQLGenerator) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Generate provides a mock function with given fields: b, clauses +func (_m *SelectSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.SelectClauses) { + _m.Called(b, clauses) +} diff --git a/sqlgen/mocks/TruncateSQLGenerator.go b/sqlgen/mocks/TruncateSQLGenerator.go new file mode 100644 index 0000000..9109876 --- /dev/null +++ b/sqlgen/mocks/TruncateSQLGenerator.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// TruncateSQLGenerator is an autogenerated mock type for the TruncateSQLGenerator type +type TruncateSQLGenerator struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *TruncateSQLGenerator) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Generate provides a mock function with given fields: b, clauses +func (_m *TruncateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.TruncateClauses) { + _m.Called(b, clauses) +} diff --git a/sqlgen/mocks/UpdateSQLGenerator.go b/sqlgen/mocks/UpdateSQLGenerator.go new file mode 100644 index 0000000..9d5de65 --- /dev/null +++ b/sqlgen/mocks/UpdateSQLGenerator.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import exp "git.fsdpf.net/go/db/v2/exp" +import mock "github.com/stretchr/testify/mock" +import sb "git.fsdpf.net/go/db/v2/internal/sb" + +// UpdateSQLGenerator is an autogenerated mock type for the UpdateSQLGenerator type +type UpdateSQLGenerator struct { + mock.Mock +} + +// Dialect provides a mock function with given fields: +func (_m *UpdateSQLGenerator) Dialect() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Generate provides a mock function with given fields: b, clauses +func (_m *UpdateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.UpdateClauses) { + _m.Called(b, clauses) +} diff --git a/sqlgen/select_sql_generator.go b/sqlgen/select_sql_generator.go new file mode 100644 index 0000000..dfae14c --- /dev/null +++ b/sqlgen/select_sql_generator.go @@ -0,0 +1,266 @@ +package sqlgen + +import ( + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + SelectSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, clauses exp.SelectClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + selectSQLGenerator struct { + CommonSQLGenerator + } +) + +func ErrNotSupportedJoinType(j exp.JoinExpression) error { + return errors.New("dialect does not support %v", j.JoinType()) +} + +func ErrJoinConditionRequired(j exp.JoinExpression) error { + return errors.New("join condition required for conditioned join %v", j.JoinType()) +} + +func ErrDistinctOnNotSupported(dialect string) error { + return errors.New("dialect does not support DISTINCT ON clause [dialect=%s]", dialect) +} + +func ErrWindowNotSupported(dialect string) error { + return errors.New("dialect does not support WINDOW clause [dialect=%s]", dialect) +} + +var ErrNoWindowName = errors.New("window expresion has no valid name") + +func NewSelectSQLGenerator(dialect string, do *SQLDialectOptions) SelectSQLGenerator { + return &selectSQLGenerator{NewCommonSQLGenerator(dialect, do)} +} + +func (ssg *selectSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.SelectClauses) { + for _, f := range ssg.DialectOptions().SelectSQLOrder { + if b.Error() != nil { + return + } + switch f { + case CommonTableSQLFragment: + ssg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables()) + case SelectSQLFragment: + ssg.SelectSQL(b, clauses) + case SelectWithLimitSQLFragment: + ssg.SelectWithLimitSQL(b, clauses) + case FromSQLFragment: + ssg.FromSQL(b, clauses.From()) + case JoinSQLFragment: + ssg.JoinSQL(b, clauses.Joins()) + case WhereSQLFragment: + ssg.WhereSQL(b, clauses.Where()) + case GroupBySQLFragment: + ssg.GroupBySQL(b, clauses.GroupBy()) + case HavingSQLFragment: + ssg.HavingSQL(b, clauses.Having()) + case WindowSQLFragment: + ssg.WindowSQL(b, clauses.Windows()) + case CompoundsSQLFragment: + ssg.CompoundsSQL(b, clauses.Compounds()) + case OrderSQLFragment: + ssg.OrderSQL(b, clauses.Order()) + case OrderWithOffsetFetchSQLFragment: + ssg.OrderWithOffsetFetchSQL(b, clauses.Order(), clauses.Offset(), clauses.Limit()) + case LimitSQLFragment: + ssg.LimitSQL(b, clauses.Limit()) + case OffsetSQLFragment: + ssg.OffsetSQL(b, clauses.Offset()) + case ForSQLFragment: + ssg.ForSQL(b, clauses.Lock()) + default: + b.SetError(ErrNotSupportedFragment("SELECT", f)) + } + } +} + +func (ssg *selectSQLGenerator) selectSQLCommon(b sb.SQLBuilder, clauses exp.SelectClauses) { + dc := clauses.Distinct() + if dc != nil { + b.Write(ssg.DialectOptions().DistinctFragment) + if !dc.IsEmpty() { + if ssg.DialectOptions().SupportsDistinctOn { + b.Write(ssg.DialectOptions().OnFragment).WriteRunes(ssg.DialectOptions().LeftParenRune) + ssg.ExpressionSQLGenerator().Generate(b, dc) + b.WriteRunes(ssg.DialectOptions().RightParenRune, ssg.DialectOptions().SpaceRune) + } else { + b.SetError(ErrDistinctOnNotSupported(ssg.Dialect())) + return + } + } else { + b.WriteRunes(ssg.DialectOptions().SpaceRune) + } + } + + if cols := clauses.Select(); clauses.IsDefaultSelect() || len(cols.Columns()) == 0 { + b.WriteRunes(ssg.DialectOptions().StarRune) + } else { + ssg.ExpressionSQLGenerator().Generate(b, cols) + } +} + +// Adds the SELECT clause and columns to a sql statement +func (ssg *selectSQLGenerator) SelectSQL(b sb.SQLBuilder, clauses exp.SelectClauses) { + b.Write(ssg.DialectOptions().SelectClause).WriteRunes(ssg.DialectOptions().SpaceRune) + ssg.selectSQLCommon(b, clauses) +} + +// Adds the SELECT clause along with LIMIT to a SQL statement (e.g. MSSQL dialect: SELECT TOP 10 ...) +func (ssg *selectSQLGenerator) SelectWithLimitSQL(b sb.SQLBuilder, clauses exp.SelectClauses) { + b.Write(ssg.DialectOptions().SelectClause).WriteRunes(ssg.DialectOptions().SpaceRune) + if clauses.Offset() == 0 && clauses.Limit() != nil { + ssg.LimitSQL(b, clauses.Limit()) + b.WriteRunes(ssg.DialectOptions().SpaceRune) + } + ssg.selectSQLCommon(b, clauses) +} + +// Generates the JOIN clauses for an SQL statement +func (ssg *selectSQLGenerator) JoinSQL(b sb.SQLBuilder, joins exp.JoinExpressions) { + if len(joins) > 0 { + for _, j := range joins { + joinType, ok := ssg.DialectOptions().JoinTypeLookup[j.JoinType()] + if !ok { + b.SetError(ErrNotSupportedJoinType(j)) + return + } + b.Write(joinType) + ssg.ExpressionSQLGenerator().Generate(b, j.Table()) + if t, ok := j.(exp.ConditionedJoinExpression); ok { + if t.IsConditionEmpty() { + b.SetError(ErrJoinConditionRequired(j)) + return + } + ssg.joinConditionSQL(b, t.Condition()) + } + } + } +} + +// Generates the GROUP BY clause for an SQL statement +func (ssg *selectSQLGenerator) GroupBySQL(b sb.SQLBuilder, groupBy exp.ColumnListExpression) { + if groupBy != nil && len(groupBy.Columns()) > 0 { + b.Write(ssg.DialectOptions().GroupByFragment) + ssg.ExpressionSQLGenerator().Generate(b, groupBy) + } +} + +// Generates the HAVING clause for an SQL statement +func (ssg *selectSQLGenerator) HavingSQL(b sb.SQLBuilder, having exp.ExpressionList) { + if having != nil && len(having.Expressions()) > 0 { + b.Write(ssg.DialectOptions().HavingFragment) + ssg.ExpressionSQLGenerator().Generate(b, having) + } +} + +// Generates the OFFSET clause for an SQL statement +func (ssg *selectSQLGenerator) OffsetSQL(b sb.SQLBuilder, offset uint) { + if offset > 0 { + b.Write(ssg.DialectOptions().OffsetFragment) + ssg.ExpressionSQLGenerator().Generate(b, offset) + } +} + +// Generates the compound sql clause for an SQL statement (e.g. UNION, INTERSECT) +func (ssg *selectSQLGenerator) CompoundsSQL(b sb.SQLBuilder, compounds []exp.CompoundExpression) { + for _, compound := range compounds { + ssg.ExpressionSQLGenerator().Generate(b, compound) + } +} + +// Generates the FOR (aka "locking") clause for an SQL statement +func (ssg *selectSQLGenerator) ForSQL(b sb.SQLBuilder, lockingClause exp.Lock) { + if lockingClause == nil { + return + } + switch lockingClause.Strength() { + case exp.ForNolock: + return + case exp.ForUpdate: + b.Write(ssg.DialectOptions().ForUpdateFragment) + case exp.ForNoKeyUpdate: + b.Write(ssg.DialectOptions().ForNoKeyUpdateFragment) + case exp.ForShare: + b.Write(ssg.DialectOptions().ForShareFragment) + case exp.ForKeyShare: + b.Write(ssg.DialectOptions().ForKeyShareFragment) + } + + of := lockingClause.Of() + if ofLen := len(of); ofLen > 0 { + if ofFragment := ssg.DialectOptions().OfFragment; len(ofFragment) > 0 { + b.Write(ofFragment) + for i, table := range of { + ssg.ExpressionSQLGenerator().Generate(b, table) + if i < ofLen-1 { + b.WriteRunes(ssg.DialectOptions().CommaRune, ssg.DialectOptions().SpaceRune) + } + } + b.WriteRunes(ssg.DialectOptions().SpaceRune) + } + } + + // the WAIT case is the default in Postgres, and is what you get if you don't specify NOWAIT or + // SKIP LOCKED. There's no special syntax for it in PG, so we don't do anything for it here + switch lockingClause.WaitOption() { + case exp.Wait: + return + case exp.NoWait: + b.Write(ssg.DialectOptions().NowaitFragment) + case exp.SkipLocked: + b.Write(ssg.DialectOptions().SkipLockedFragment) + } +} + +func (ssg *selectSQLGenerator) WindowSQL(b sb.SQLBuilder, windows []exp.WindowExpression) { + weLen := len(windows) + if weLen == 0 { + return + } + if !ssg.DialectOptions().SupportsWindowFunction { + b.SetError(ErrWindowNotSupported(ssg.Dialect())) + return + } + b.Write(ssg.DialectOptions().WindowFragment) + for i, we := range windows { + if !we.HasName() { + b.SetError(ErrNoWindowName) + } + ssg.ExpressionSQLGenerator().Generate(b, we) + if i < weLen-1 { + b.WriteRunes(ssg.DialectOptions().CommaRune, ssg.DialectOptions().SpaceRune) + } + } +} + +func (ssg *selectSQLGenerator) joinConditionSQL(b sb.SQLBuilder, jc exp.JoinCondition) { + switch t := jc.(type) { + case exp.JoinOnCondition: + ssg.joinOnConditionSQL(b, t) + case exp.JoinUsingCondition: + ssg.joinUsingConditionSQL(b, t) + } +} + +func (ssg *selectSQLGenerator) joinUsingConditionSQL(b sb.SQLBuilder, jc exp.JoinUsingCondition) { + b.Write(ssg.DialectOptions().UsingFragment). + WriteRunes(ssg.DialectOptions().LeftParenRune) + ssg.ExpressionSQLGenerator().Generate(b, jc.Using()) + b.WriteRunes(ssg.DialectOptions().RightParenRune) +} + +func (ssg *selectSQLGenerator) joinOnConditionSQL(b sb.SQLBuilder, jc exp.JoinOnCondition) { + b.Write(ssg.DialectOptions().OnFragment) + ssg.ExpressionSQLGenerator().Generate(b, jc.On()) +} diff --git a/sqlgen/select_sql_generator_test.go b/sqlgen/select_sql_generator_test.go new file mode 100644 index 0000000..70c1c3e --- /dev/null +++ b/sqlgen/select_sql_generator_test.go @@ -0,0 +1,615 @@ +package sqlgen_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + selectTestCase struct { + clause exp.SelectClauses + sql string + isPrepared bool + args []interface{} + err string + } + selectSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (ssgs *selectSQLGeneratorSuite) assertCases(ssg sqlgen.SelectSQLGenerator, testCases ...selectTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + ssg.Generate(b, tc.clause) + switch { + case len(tc.err) > 0: + ssgs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + ssgs.assertPreparedSQL(b, tc.sql, tc.args) + default: + ssgs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (ssgs *selectSQLGeneratorSuite) TestDialect() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewSelectSQLGenerator("test", opts) + ssgs.Equal("test", d.Dialect()) + + opts2 := sqlgen.DefaultDialectOptions() + d2 := sqlgen.NewSelectSQLGenerator("test2", opts2) + ssgs.Equal("test2", d2.Dialect()) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate() { + opts := sqlgen.DefaultDialectOptions() + opts.SelectClause = []byte("select") + opts.StarRune = '#' + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + scWithCols := sc.SetSelect(exp.NewColumnListExpression("a", "b")) + + ident := exp.NewIdentifierExpression("", "", "a") + scWithBooExpAliased := sc.SetSelect( + exp.NewColumnListExpression( + ident.Eq(1).As("x"), + ident.IsNull().As("y"), + ), + ) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, sql: `select # FROM "test"`}, + selectTestCase{clause: sc, sql: `select # FROM "test"`, isPrepared: true}, + + selectTestCase{clause: scWithCols, sql: `select "a", "b" FROM "test"`}, + selectTestCase{clause: scWithCols, sql: `select "a", "b" FROM "test"`, isPrepared: true}, + + selectTestCase{ + clause: scWithBooExpAliased, + sql: `select ("a" = 1) AS "x", ("a" IS NULL) AS "y" FROM "test"`, + }, + selectTestCase{ + clause: scWithBooExpAliased, + sql: `select ("a" = ?) AS "x", ("a" IS NULL) AS "y" FROM "test"`, + isPrepared: true, + args: []interface{}{int64(1)}, + }, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_UnsupportedFragment() { + opts := sqlgen.DefaultDialectOptions() + opts.SelectSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment} + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + expectedErr := "db: unsupported SELECT SQL fragment InsertBeingSQLFragment" + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, err: expectedErr}, + selectTestCase{clause: sc, err: expectedErr, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_WithErroredBuilder() { + opts := sqlgen.DefaultDialectOptions() + opts.SelectSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment} + d := sqlgen.NewSelectSQLGenerator("test", opts) + + b := sb.NewSQLBuilder(true).SetError(errors.New("test error")) + c := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + d.Generate(b, c) + ssgs.assertErrorSQL(b, `db: test error`) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withSelectedColumns() { + opts := sqlgen.DefaultDialectOptions() + // make sure the fragments are used + opts.SelectClause = []byte("select") + opts.StarRune = '#' + opts.SupportsDistinctOn = true + + sc := exp.NewSelectClauses() + scCols := sc.SetSelect(exp.NewColumnListExpression("a", "b")) + scFuncs := sc.SetSelect(exp.NewColumnListExpression( + exp.NewSQLFunctionExpression("COUNT", exp.Star()), + exp.NewSQLFunctionExpression("RANK"), + )) + + we := exp.NewWindowExpression( + nil, + nil, + exp.NewColumnListExpression("a", "b"), + exp.NewOrderedColumnList(exp.ParseIdentifier("c").Asc()), + ) + scFuncsPartition := sc.SetSelect(exp.NewColumnListExpression( + exp.NewSQLFunctionExpression("COUNT", exp.Star()).Over(we), + exp.NewSQLFunctionExpression("RANK").Over(we.Inherit("w")), + )) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, sql: `select #`}, + selectTestCase{clause: sc, sql: `select #`, isPrepared: true}, + + selectTestCase{clause: scCols, sql: `select "a", "b"`}, + selectTestCase{clause: scCols, sql: `select "a", "b"`, isPrepared: true}, + + selectTestCase{clause: scFuncs, sql: `select COUNT(*), RANK()`}, + selectTestCase{clause: scFuncs, sql: `select COUNT(*), RANK()`, isPrepared: true}, + + selectTestCase{ + clause: scFuncsPartition, + sql: `select COUNT(*) OVER (PARTITION BY "a", "b" ORDER BY "c" ASC), RANK() OVER ("w" PARTITION BY "a", "b" ORDER BY "c" ASC)`, + }, + selectTestCase{ + clause: scFuncsPartition, + sql: `select COUNT(*) OVER (PARTITION BY "a", "b" ORDER BY "c" ASC), RANK() OVER ("w" PARTITION BY "a", "b" ORDER BY "c" ASC)`, + isPrepared: true, + }, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withDistinct() { + opts := sqlgen.DefaultDialectOptions() + // make sure the fragments are used + opts.SelectClause = []byte("select") + opts.StarRune = '#' + opts.DistinctFragment = []byte("distinct") + opts.OnFragment = []byte(" on ") + opts.SupportsDistinctOn = true + + sc := exp.NewSelectClauses().SetDistinct(exp.NewColumnListExpression()) + scDistinctOn := sc.SetDistinct(exp.NewColumnListExpression("a", "b")) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, sql: `select distinct #`}, + selectTestCase{clause: sc, sql: `select distinct #`, isPrepared: true}, + + selectTestCase{clause: scDistinctOn, sql: `select distinct on ("a", "b") #`}, + selectTestCase{clause: scDistinctOn, sql: `select distinct on ("a", "b") #`, isPrepared: true}, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsDistinctOn = false + expectedErr := "db: dialect does not support DISTINCT ON clause [dialect=test]" + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, sql: `SELECT DISTINCT *`}, + selectTestCase{clause: sc, sql: `SELECT DISTINCT *`, isPrepared: true}, + + selectTestCase{clause: scDistinctOn, err: expectedErr}, + selectTestCase{clause: scDistinctOn, err: expectedErr, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withFromSQL() { + opts := sqlgen.DefaultDialectOptions() + opts.FromFragment = []byte(" from") + + sc := exp.NewSelectClauses() + scFrom := sc.SetFrom(exp.NewColumnListExpression("a", "b")) + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc, sql: `SELECT *`}, + selectTestCase{clause: sc, sql: `SELECT *`, isPrepared: true}, + + selectTestCase{clause: scFrom, sql: `SELECT * from "a", "b"`}, + selectTestCase{clause: scFrom, sql: `SELECT * from "a", "b"`, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withJoin() { + opts := sqlgen.DefaultDialectOptions() + // override fragements to make sure dialect is used + opts.UsingFragment = []byte(" using ") + opts.OnFragment = []byte(" on ") + opts.JoinTypeLookup = map[exp.JoinType][]byte{ + exp.LeftJoinType: []byte(" left join "), + exp.NaturalJoinType: []byte(" natural join "), + } + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + ti := exp.NewIdentifierExpression("", "test2", "") + uj := exp.NewUnConditionedJoinExpression(exp.NaturalJoinType, ti) + cjo := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinOnCondition(exp.Ex{"a": "foo"})) + cju := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinUsingCondition("a")) + rj := exp.NewConditionedJoinExpression(exp.RightJoinType, ti, exp.NewJoinUsingCondition(exp.NewIdentifierExpression("", "", "a"))) + badJoin := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinUsingCondition()) + + expectedRjError := "db: dialect does not support RightJoinType" + expectedJoinCondError := "db: join condition required for conditioned join LeftJoinType" + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: sc.JoinsAppend(uj), sql: `SELECT * FROM "test" natural join "test2"`}, + selectTestCase{clause: sc.JoinsAppend(uj), sql: `SELECT * FROM "test" natural join "test2"`, isPrepared: true}, + + selectTestCase{clause: sc.JoinsAppend(cjo), sql: `SELECT * FROM "test" left join "test2" on ("a" = 'foo')`}, + selectTestCase{ + clause: sc.JoinsAppend(cjo), + sql: `SELECT * FROM "test" left join "test2" on ("a" = ?)`, + isPrepared: true, + args: []interface{}{"foo"}, + }, + + selectTestCase{clause: sc.JoinsAppend(cju), sql: `SELECT * FROM "test" left join "test2" using ("a")`}, + selectTestCase{clause: sc.JoinsAppend(cju), sql: `SELECT * FROM "test" left join "test2" using ("a")`, isPrepared: true}, + + selectTestCase{ + clause: sc.JoinsAppend(uj).JoinsAppend(cjo).JoinsAppend(cju), + sql: `SELECT * FROM "test" natural join "test2" left join "test2" on ("a" = 'foo') left join "test2" using ("a")`, + }, + selectTestCase{ + clause: sc.JoinsAppend(uj).JoinsAppend(cjo).JoinsAppend(cju), + sql: `SELECT * FROM "test" natural join "test2" left join "test2" on ("a" = ?) left join "test2" using ("a")`, + isPrepared: true, + args: []interface{}{"foo"}, + }, + + selectTestCase{clause: sc.JoinsAppend(rj), err: expectedRjError}, + selectTestCase{clause: sc.JoinsAppend(rj), err: expectedRjError, isPrepared: true}, + + selectTestCase{clause: sc.JoinsAppend(badJoin), err: expectedJoinCondError}, + selectTestCase{clause: sc.JoinsAppend(badJoin), err: expectedJoinCondError, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withWhere() { + opts := sqlgen.DefaultDialectOptions() + opts.WhereFragment = []byte(" where ") + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + w := exp.Ex{"a": "b"} + w2 := exp.Ex{"b": "c"} + scWhere1 := sc.WhereAppend(w) + scWhere2 := sc.WhereAppend(w, w2) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: scWhere1, sql: `SELECT * FROM "test" where ("a" = 'b')`}, + selectTestCase{clause: scWhere1, sql: `SELECT * FROM "test" where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + selectTestCase{clause: scWhere2, sql: `SELECT * FROM "test" where (("a" = 'b') AND ("b" = 'c'))`}, + selectTestCase{ + clause: scWhere2, + sql: `SELECT * FROM "test" where (("a" = ?) AND ("b" = ?))`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withGroupBy() { + opts := sqlgen.DefaultDialectOptions() + opts.GroupByFragment = []byte(" group by ") + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + scGroup := sc.SetGroupBy(exp.NewColumnListExpression("a")) + scGroupMulti := sc.SetGroupBy(exp.NewColumnListExpression("a", "b")) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: scGroup, sql: `SELECT * FROM "test" group by "a"`}, + selectTestCase{clause: scGroup, sql: `SELECT * FROM "test" group by "a"`, isPrepared: true}, + + selectTestCase{clause: scGroupMulti, sql: `SELECT * FROM "test" group by "a", "b"`}, + selectTestCase{clause: scGroupMulti, sql: `SELECT * FROM "test" group by "a", "b"`, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withHaving() { + opts := sqlgen.DefaultDialectOptions() + opts.HavingFragment = []byte(" having ") + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + w := exp.Ex{"a": "b"} + w2 := exp.Ex{"b": "c"} + scHaving1 := sc.HavingAppend(w) + scHaving2 := sc.HavingAppend(w, w2) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: scHaving1, sql: `SELECT * FROM "test" having ("a" = 'b')`}, + selectTestCase{clause: scHaving1, sql: `SELECT * FROM "test" having ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}}, + + selectTestCase{clause: scHaving2, sql: `SELECT * FROM "test" having (("a" = 'b') AND ("b" = 'c'))`}, + selectTestCase{ + clause: scHaving2, + sql: `SELECT * FROM "test" having (("a" = ?) AND ("b" = ?))`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withWindow() { + opts := sqlgen.DefaultDialectOptions() + opts.WindowFragment = []byte(" window ") + opts.WindowPartitionByFragment = []byte("partition by ") + opts.WindowOrderByFragment = []byte("order by ") + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + we1 := exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w"), + nil, + nil, + nil, + ) + wePartitionBy := we1.PartitionBy("a", "b") + weOrderBy := we1.OrderBy("a", "b") + + weOrderAndPartitionBy := we1.PartitionBy("a", "b").OrderBy("a", "b") + + weInherits := exp.NewWindowExpression( + exp.NewIdentifierExpression("", "", "w2"), + exp.NewIdentifierExpression("", "", "w"), + nil, + nil, + ) + weInheritsPartitionBy := weInherits.PartitionBy("c", "d") + weInheritsOrderBy := weInherits.OrderBy("c", "d") + + weInheritsOrderAndPartitionBy := weInherits.PartitionBy("c", "d").OrderBy("c", "d") + + scNoName := sc.WindowsAppend(exp.NewWindowExpression(nil, nil, nil, nil)) + + scWindow1 := sc.WindowsAppend(we1) + scWindow2 := sc.WindowsAppend(wePartitionBy) + scWindow3 := sc.WindowsAppend(weOrderBy) + scWindow4 := sc.WindowsAppend(weOrderAndPartitionBy) + + scWindow5 := sc.WindowsAppend(we1, weInherits) + scWindow6 := sc.WindowsAppend(we1, weInheritsPartitionBy) + scWindow7 := sc.WindowsAppend(we1, weInheritsOrderBy) + scWindow8 := sc.WindowsAppend(we1, weInheritsOrderAndPartitionBy) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + + selectTestCase{clause: scNoName, err: sqlgen.ErrNoWindowName.Error()}, + selectTestCase{clause: scNoName, err: sqlgen.ErrNoWindowName.Error(), isPrepared: true}, + + selectTestCase{clause: scWindow1, sql: `SELECT * FROM "test" window "w" AS ()`}, + selectTestCase{clause: scWindow1, sql: `SELECT * FROM "test" window "w" AS ()`, isPrepared: true}, + + selectTestCase{clause: scWindow2, sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b")`}, + selectTestCase{ + clause: scWindow2, + sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b")`, + isPrepared: true, + }, + + selectTestCase{clause: scWindow3, sql: `SELECT * FROM "test" window "w" AS (order by "a", "b")`}, + selectTestCase{ + clause: scWindow3, + sql: `SELECT * FROM "test" window "w" AS (order by "a", "b")`, + isPrepared: true, + }, + + selectTestCase{ + clause: scWindow4, + sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b" order by "a", "b")`, + }, + selectTestCase{ + clause: scWindow4, + sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b" order by "a", "b")`, + isPrepared: true, + }, + + selectTestCase{ + clause: scWindow5, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w")`, + }, + selectTestCase{ + clause: scWindow5, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w")`, + isPrepared: true, + }, + + selectTestCase{ + clause: scWindow6, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d")`, + }, + selectTestCase{ + clause: scWindow6, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d")`, + isPrepared: true, + }, + + selectTestCase{ + clause: scWindow7, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" order by "c", "d")`, + }, + selectTestCase{ + clause: scWindow7, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" order by "c", "d")`, + isPrepared: true, + }, + + selectTestCase{ + clause: scWindow8, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d" order by "c", "d")`, + }, + selectTestCase{ + clause: scWindow8, + sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d" order by "c", "d")`, + isPrepared: true, + }, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsWindowFunction = false + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + + selectTestCase{clause: scWindow1, err: sqlgen.ErrWindowNotSupported("test").Error()}, + selectTestCase{clause: scWindow1, err: sqlgen.ErrWindowNotSupported("test").Error(), isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withOrder() { + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetOrder( + exp.NewIdentifierExpression("", "", "a").Asc(), + exp.NewIdentifierExpression("", "", "b").Desc(), + ) + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()), + selectTestCase{clause: sc, sql: `SELECT * FROM "test" ORDER BY "a" ASC, "b" DESC`}, + selectTestCase{clause: sc, sql: `SELECT * FROM "test" ORDER BY "a" ASC, "b" DESC`, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withLimit() { + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetLimit(10) + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()), + selectTestCase{clause: sc, sql: `SELECT * FROM "test" LIMIT 10`}, + selectTestCase{clause: sc, sql: `SELECT * FROM "test" LIMIT ?`, isPrepared: true, args: []interface{}{int64(10)}}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withOffset() { + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + SetOffset(10) + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()), + selectTestCase{clause: sc, sql: `SELECT * FROM "test" OFFSET 10`}, + selectTestCase{clause: sc, sql: `SELECT * FROM "test" OFFSET ?`, isPrepared: true, args: []interface{}{int64(10)}}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCommonTables() { + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test_cte")) + scCte1 := sc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) + scCte2 := sc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse)) + + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()), + selectTestCase{clause: scCte1, sql: `WITH test_cte AS (select * from foo) SELECT * FROM "test_cte"`}, + selectTestCase{clause: scCte1, sql: `WITH test_cte AS (select * from foo) SELECT * FROM "test_cte"`, isPrepared: true}, + + selectTestCase{clause: scCte2, sql: `WITH RECURSIVE test_cte AS (select * from foo) SELECT * FROM "test_cte"`}, + selectTestCase{clause: scCte2, sql: `WITH RECURSIVE test_cte AS (select * from foo) SELECT * FROM "test_cte"`, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCompounds() { + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). + CompoundsAppend(exp.NewCompoundExpression(exp.UnionCompoundType, tse)). + CompoundsAppend(exp.NewCompoundExpression(exp.IntersectCompoundType, tse)) + + expectedSQL := `SELECT * FROM "test" UNION (select * from foo) INTERSECT (select * from foo)` + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()), + selectTestCase{clause: sc, sql: expectedSQL}, + selectTestCase{clause: sc, sql: expectedSQL, isPrepared: true}, + ) +} + +func (ssgs *selectSQLGeneratorSuite) TestToSelectSQL_withFor() { + opts := sqlgen.DefaultDialectOptions() + opts.ForUpdateFragment = []byte(" for update ") + opts.ForNoKeyUpdateFragment = []byte(" for no key update ") + opts.ForShareFragment = []byte(" for share ") + opts.ForKeyShareFragment = []byte(" for key share ") + opts.OfFragment = []byte("of ") + opts.NowaitFragment = []byte("nowait") + opts.SkipLockedFragment = []byte("skip locked") + + sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")) + scFnW := sc.SetLock(exp.NewLock(exp.ForNolock, exp.Wait)) + scFnNw := sc.SetLock(exp.NewLock(exp.ForNolock, exp.NoWait)) + scFnSl := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked)) + scFnSlOf := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked, dbv2.T("my_table"))) + + scFsW := sc.SetLock(exp.NewLock(exp.ForShare, exp.Wait)) + scFsNw := sc.SetLock(exp.NewLock(exp.ForShare, exp.NoWait)) + scFsSl := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked)) + scFsSlOf := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, dbv2.T("my_table"))) + scFsSlOfMulti := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, dbv2.T("my_table"), dbv2.T("table2"))) + + scFksW := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.Wait)) + scFksNw := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.NoWait)) + scFksSl := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.SkipLocked)) + + scFuW := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.Wait)) + scFuNw := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.NoWait)) + scFuSl := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.SkipLocked)) + + scFkuW := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.Wait)) + scFkuNw := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.NoWait)) + scFkuSl := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.SkipLocked)) + ssgs.assertCases( + sqlgen.NewSelectSQLGenerator("test", opts), + selectTestCase{clause: scFnW, sql: `SELECT * FROM "test"`}, + selectTestCase{clause: scFnW, sql: `SELECT * FROM "test"`, isPrepared: true}, + + selectTestCase{clause: scFnNw, sql: `SELECT * FROM "test"`}, + selectTestCase{clause: scFnNw, sql: `SELECT * FROM "test"`, isPrepared: true}, + + selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`}, + selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`, isPrepared: true}, + selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`}, + selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`, isPrepared: true, args: []interface{}{}}, + + selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `}, + selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `, isPrepared: true}, + + selectTestCase{clause: scFsNw, sql: `SELECT * FROM "test" for share nowait`}, + selectTestCase{clause: scFsNw, sql: `SELECT * FROM "test" for share nowait`, isPrepared: true}, + + selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`}, + selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`, isPrepared: true}, + + selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`}, + selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`, isPrepared: true}, + + selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`}, + selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`, isPrepared: true}, + + selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `}, + selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `, isPrepared: true}, + + selectTestCase{clause: scFksNw, sql: `SELECT * FROM "test" for key share nowait`}, + selectTestCase{clause: scFksNw, sql: `SELECT * FROM "test" for key share nowait`, isPrepared: true}, + + selectTestCase{clause: scFksSl, sql: `SELECT * FROM "test" for key share skip locked`}, + selectTestCase{clause: scFksSl, sql: `SELECT * FROM "test" for key share skip locked`, isPrepared: true}, + + selectTestCase{clause: scFuW, sql: `SELECT * FROM "test" for update `}, + selectTestCase{clause: scFuW, sql: `SELECT * FROM "test" for update `, isPrepared: true}, + + selectTestCase{clause: scFuNw, sql: `SELECT * FROM "test" for update nowait`}, + selectTestCase{clause: scFuNw, sql: `SELECT * FROM "test" for update nowait`, isPrepared: true}, + + selectTestCase{clause: scFuSl, sql: `SELECT * FROM "test" for update skip locked`}, + selectTestCase{clause: scFuSl, sql: `SELECT * FROM "test" for update skip locked`, isPrepared: true}, + + selectTestCase{clause: scFkuW, sql: `SELECT * FROM "test" for no key update `}, + selectTestCase{clause: scFkuW, sql: `SELECT * FROM "test" for no key update `, isPrepared: true}, + + selectTestCase{clause: scFkuNw, sql: `SELECT * FROM "test" for no key update nowait`}, + selectTestCase{clause: scFkuNw, sql: `SELECT * FROM "test" for no key update nowait`, isPrepared: true}, + + selectTestCase{clause: scFkuSl, sql: `SELECT * FROM "test" for no key update skip locked`}, + selectTestCase{clause: scFkuSl, sql: `SELECT * FROM "test" for no key update skip locked`, isPrepared: true}, + ) +} + +func TestSelectSQLGenerator(t *testing.T) { + suite.Run(t, new(selectSQLGeneratorSuite)) +} diff --git a/sqlgen/sql_dialect_options.go b/sqlgen/sql_dialect_options.go new file mode 100644 index 0000000..6f1071a --- /dev/null +++ b/sqlgen/sql_dialect_options.go @@ -0,0 +1,607 @@ +package sqlgen + +import ( + "fmt" + "time" + + "git.fsdpf.net/go/db/v2/exp" +) + +type ( + SQLFragmentType int + SQLDialectOptions struct { + // Set to true if the dialect supports ORDER BY expressions in DELETE statements (DEFAULT=false) + SupportsOrderByOnDelete bool + // Set to true if the dialect supports table hint for DELETE statements (DELETE t FROM t ...), DEFAULT=false + SupportsDeleteTableHint bool + // Set to true if the dialect supports ORDER BY expressions in UPDATE statements (DEFAULT=false) + SupportsOrderByOnUpdate bool + // Set to true if the dialect supports LIMIT expressions in DELETE statements (DEFAULT=false) + SupportsLimitOnDelete bool + // Set to true if the dialect supports LIMIT expressions in UPDATE statements (DEFAULT=false) + SupportsLimitOnUpdate bool + // Set to true if the dialect supports RETURN expressions (DEFAULT=true) + SupportsReturn bool + // Set to true if the dialect supports Conflict Target (DEFAULT=true) + SupportsConflictTarget bool + // Set to true if the dialect supports Conflict Target (DEFAULT=true) + SupportsConflictUpdateWhere bool + // Set to true if the dialect supports Insert Ignore syntax (DEFAULT=false) + SupportsInsertIgnoreSyntax bool + // Set to true if the dialect supports Common Table Expressions (DEFAULT=true) + SupportsWithCTE bool + // Set to true if the dialect supports recursive Common Table Expressions (DEFAULT=true) + SupportsWithCTERecursive bool + // Set to true if multiple tables are supported in UPDATE statement. (DEFAULT=true) + SupportsMultipleUpdateTables bool + // Set to true if DISTINCT ON is supported (DEFAULT=true) + SupportsDistinctOn bool + // Set to true if LATERAL queries are supported (DEFAULT=true) + SupportsLateral bool + // Set to false if the dialect does not require expressions to be wrapped in parens (DEFAULT=true) + WrapCompoundsInParens bool + + // Set to true if window function are supported in SELECT statement. (DEFAULT=true) + SupportsWindowFunction bool + + // Set to true if the dialect requires join tables in UPDATE to be in a FROM clause (DEFAULT=true). + UseFromClauseForMultipleUpdateTables bool + + // Surround LIMIT parameter with parentheses, like in MSSQL: SELECT TOP (10) ... + SurroundLimitWithParentheses bool + + // The UPDATE fragment to use when generating sql. (DEFAULT=[]byte("UPDATE")) + UpdateClause []byte + // The INSERT fragment to use when generating sql. (DEFAULT=[]byte("INSERT INTO")) + InsertClause []byte + // The INSERT IGNORE INTO fragment to use when generating sql. (DEFAULT=[]byte("INSERT IGNORE INTO")) + InsertIgnoreClause []byte + // The SELECT fragment to use when generating sql. (DEFAULT=[]byte("SELECT")) + SelectClause []byte + // The DELETE fragment to use when generating sql. (DEFAULT=[]byte("DELETE")) + DeleteClause []byte + // The TRUNCATE fragment to use when generating sql. (DEFAULT=[]byte("TRUNCATE")) + TruncateClause []byte + // The WITH fragment to use when generating sql. (DEFAULT=[]byte("WITH ")) + WithFragment []byte + // The RECURSIVE fragment to use when generating sql (after WITH). (DEFAULT=[]byte("RECURSIVE ")) + RecursiveFragment []byte + // The CASCADE fragment to use when generating sql. (DEFAULT=[]byte(" CASCADE")) + CascadeFragment []byte + // The RESTRICT fragment to use when generating sql. (DEFAULT=[]byte(" RESTRICT")) + RestrictFragment []byte + // The SQL fragment to use when generating insert sql and using + // DEFAULT VALUES (e.g. postgres="DEFAULT VALUES", mysql="", sqlite3=""). (DEFAULT=[]byte(" DEFAULT VALUES")) + DefaultValuesFragment []byte + // The SQL fragment to use when generating insert sql and listing columns using a VALUES clause + // (DEFAULT=[]byte(" VALUES ")) + ValuesFragment []byte + // The SQL fragment to use when generating truncate sql and using the IDENTITY clause + // (DEFAULT=[]byte(" IDENTITY")) + IdentityFragment []byte + // The SQL fragment to use when generating update sql and using the SET clause (DEFAULT=[]byte(" SET ")) + SetFragment []byte + // The SQL DISTINCT keyword (DEFAULT=[]byte(" DISTINCT ")) + DistinctFragment []byte + // The SQL RETURNING clause (DEFAULT=[]byte(" RETURNING ")) + ReturningFragment []byte + // The SQL FROM clause fragment (DEFAULT=[]byte(" FROM")) + FromFragment []byte + // The SQL USING join clause fragment (DEFAULT=[]byte(" USING ")) + UsingFragment []byte + // The SQL ON join clause fragment (DEFAULT=[]byte(" ON ")) + OnFragment []byte + // The SQL WHERE clause fragment (DEFAULT=[]byte(" WHERE ")) + WhereFragment []byte + // The SQL GROUP BY clause fragment(DEFAULT=[]byte(" GROUP BY ")) + GroupByFragment []byte + // The SQL HAVING clause fragment(DEFAULT=[]byte(" HAVING ")) + HavingFragment []byte + // The SQL WINDOW clause fragment(DEFAULT=[]byte(" WINDOW ")) + WindowFragment []byte + // The SQL WINDOW clause PARTITION BY fragment(DEFAULT=[]byte("PARTITION BY ")) + WindowPartitionByFragment []byte + // The SQL WINDOW clause ORDER BY fragment(DEFAULT=[]byte("ORDER BY ")) + WindowOrderByFragment []byte + // The SQL WINDOW clause OVER fragment(DEFAULT=[]byte(" OVER ")) + WindowOverFragment []byte + // The SQL ORDER BY clause fragment(DEFAULT=[]byte(" ORDER BY ")) + OrderByFragment []byte + // The SQL FETCH fragment(DEFAULT=[]byte(" ")) + FetchFragment []byte + // The SQL LIMIT BY clause fragment(DEFAULT=[]byte(" LIMIT ")) + LimitFragment []byte + // The SQL OFFSET BY clause fragment(DEFAULT=[]byte(" OFFSET ")) + OffsetFragment []byte + // The SQL FOR UPDATE fragment(DEFAULT=[]byte(" FOR UPDATE ")) + ForUpdateFragment []byte + // The SQL FOR NO KEY UPDATE fragment(DEFAULT=[]byte(" FOR NO KEY UPDATE ")) + ForNoKeyUpdateFragment []byte + // The SQL FOR SHARE fragment(DEFAULT=[]byte(" FOR SHARE ")) + ForShareFragment []byte + // The SQL OF fragment(DEFAULT=[]byte("OF ")) + OfFragment []byte + // The SQL FOR KEY SHARE fragment(DEFAULT=[]byte(" FOR KEY SHARE ")) + ForKeyShareFragment []byte + // The SQL NOWAIT fragment(DEFAULT=[]byte("NOWAIT")) + NowaitFragment []byte + // The SQL SKIP LOCKED fragment(DEFAULT=[]byte("SKIP LOCKED")) + SkipLockedFragment []byte + // The SQL AS fragment when aliasing an Expression(DEFAULT=[]byte(" AS ")) + AsFragment []byte + // The SQL LATERAL fragment used for LATERAL joins + LateralFragment []byte + // The quote rune to use when quoting identifiers(DEFAULT='"') + QuoteRune rune + // The NULL literal to use when interpolating nulls values (DEFAULT=[]byte("NULL")) + Null []byte + // The TRUE literal to use when interpolating bool true values (DEFAULT=[]byte("TRUE")) + True []byte + // The FALSE literal to use when interpolating bool false values (DEFAULT=[]byte("FALSE")) + False []byte + // The ASC fragment when specifying column order (DEFAULT=[]byte(" ASC")) + AscFragment []byte + // The DESC fragment when specifying column order (DEFAULT=[]byte(" DESC")) + DescFragment []byte + // The NULLS FIRST fragment when specifying column order (DEFAULT=[]byte(" NULLS FIRST")) + NullsFirstFragment []byte + // The NULLS LAST fragment when specifying column order (DEFAULT=[]byte(" NULLS LAST")) + NullsLastFragment []byte + // The AND keyword used when joining ExpressionLists (DEFAULT=[]byte(" AND ")) + AndFragment []byte + // The OR keyword used when joining ExpressionLists (DEFAULT=[]byte(" OR ")) + OrFragment []byte + // The UNION keyword used when creating compound statements (DEFAULT=[]byte(" UNION ")) + UnionFragment []byte + // The UNION ALL keyword used when creating compound statements (DEFAULT=[]byte(" UNION ALL ")) + UnionAllFragment []byte + // The INTERSECT keyword used when creating compound statements (DEFAULT=[]byte(" INTERSECT ")) + IntersectFragment []byte + // The INTERSECT ALL keyword used when creating compound statements (DEFAULT=[]byte(" INTERSECT ALL ")) + IntersectAllFragment []byte + // The CAST keyword to use when casting a value (DEFAULT=[]byte("CAST")) + CastFragment []byte + // The CASE keyword to use when when creating a CASE statement (DEFAULT=[]byte("CASE ")) + CaseFragment []byte + // The WHEN keyword to use when when creating a CASE statement (DEFAULT=[]byte(" WHEN ")) + WhenFragment []byte + // The THEN keyword to use when when creating a CASE statement (DEFAULT=[]byte(" THEN ")) + ThenFragment []byte + // The ELSE keyword to use when when creating a CASE statement (DEFAULT=[]byte(" ELSE ")) + ElseFragment []byte + // The End keyword to use when when creating a CASE statement (DEFAULT=[]byte(" END")) + EndFragment []byte + // The quote rune to use when quoting string literals (DEFAULT='\'') + StringQuote rune + // The operator to use when setting values in an update statement (DEFAULT='=') + SetOperatorRune rune + // The placeholder fragment to use when generating a non interpolated statement (DEFAULT=[]byte"?") + PlaceHolderFragment []byte + // Empty string (DEFAULT="") + EmptyString string + // Comma rune (DEFAULT=',') + CommaRune rune + // Space rune (DEFAULT=' ') + SpaceRune rune + // Left paren rune (DEFAULT='(') + LeftParenRune rune + // Right paren rune (DEFAULT=')') + RightParenRune rune + // Star rune (DEFAULT='*') + StarRune rune + // Period rune (DEFAULT='.') + PeriodRune rune + // Set to true to include positional argument numbers when creating a prepared statement (Default=false) + IncludePlaceholderNum bool + // The time format to use when serializing time.Time (DEFAULT=time.RFC3339Nano) + TimeFormat string + // A map used to look up BooleanOperations and their SQL equivalents + // (Default= map[exp.BooleanOperation][]byte{ + // exp.EqOp: []byte("="), + // exp.NeqOp: []byte("!="), + // exp.GtOp: []byte(">"), + // exp.GteOp: []byte(">="), + // exp.LtOp: []byte("<"), + // exp.LteOp: []byte("<="), + // exp.InOp: []byte("IN"), + // exp.NotInOp: []byte("NOT IN"), + // exp.IsOp: []byte("IS"), + // exp.IsNotOp: []byte("IS NOT"), + // exp.LikeOp: []byte("LIKE"), + // exp.NotLikeOp: []byte("NOT LIKE"), + // exp.ILikeOp: []byte("ILIKE"), + // exp.NotILikeOp: []byte("NOT ILIKE"), + // exp.RegexpLikeOp: []byte("~"), + // exp.RegexpNotLikeOp: []byte("!~"), + // exp.RegexpILikeOp: []byte("~*"), + // exp.RegexpNotILikeOp: []byte("!~*"), + // }) + BooleanOperatorLookup map[exp.BooleanOperation][]byte + // A map used to look up BitwiseOperations and their SQL equivalents + // (Default=map[exp.BitwiseOperation][]byte{ + // exp.BitwiseInversionOp: []byte("~"), + // exp.BitwiseOrOp: []byte("|"), + // exp.BitwiseAndOp: []byte("&"), + // exp.BitwiseXorOp: []byte("#"), + // exp.BitwiseLeftShiftOp: []byte("<<"), + // exp.BitwiseRightShiftOp: []byte(">>"), + // }), + BitwiseOperatorLookup map[exp.BitwiseOperation][]byte + // A map used to look up RangeOperations and their SQL equivalents + // (Default=map[exp.RangeOperation][]byte{ + // exp.BetweenOp: []byte("BETWEEN"), + // exp.NotBetweenOp: []byte("NOT BETWEEN"), + // }) + RangeOperatorLookup map[exp.RangeOperation][]byte + // A map used to look up JoinTypes and their SQL equivalents + // (Default= map[exp.JoinType][]byte{ + // exp.InnerJoinType: []byte(" INNER JOIN "), + // exp.FullOuterJoinType: []byte(" FULL OUTER JOIN "), + // exp.RightOuterJoinType: []byte(" RIGHT OUTER JOIN "), + // exp.LeftOuterJoinType: []byte(" LEFT OUTER JOIN "), + // exp.FullJoinType: []byte(" FULL JOIN "), + // exp.RightJoinType: []byte(" RIGHT JOIN "), + // exp.LeftJoinType: []byte(" LEFT JOIN "), + // exp.NaturalJoinType: []byte(" NATURAL JOIN "), + // exp.NaturalLeftJoinType: []byte(" NATURAL LEFT JOIN "), + // exp.NaturalRightJoinType: []byte(" NATURAL RIGHT JOIN "), + // exp.NaturalFullJoinType: []byte(" NATURAL FULL JOIN "), + // exp.CrossJoinType: []byte(" CROSS JOIN "), + // }) + JoinTypeLookup map[exp.JoinType][]byte + // Whether or not boolean data type is supported + BooleanDataTypeSupported bool + // Whether or not to use literal TRUE or FALSE for IS statements (e.g. IS TRUE or IS 0) + UseLiteralIsBools bool + // EscapedRunes is a map of a rune and the corresponding escape sequence in bytes. Used when escaping text + // types. + // (Default= map[rune][]byte{ + // '\'': []byte("''"), + // }) + EscapedRunes map[rune][]byte + + // The SQL fragment to use for CONFLICT (Default=[]byte(" ON CONFLICT")) + ConflictFragment []byte + // The SQL fragment to use for CONFLICT DO NOTHING (Default=[]byte(" DO NOTHING")) + ConflictDoNothingFragment []byte + // The SQL fragment to use for CONFLICT DO UPDATE (Default=[]byte(" DO UPDATE SET")) + ConflictDoUpdateFragment []byte + + // The order of SQL fragments when creating a SELECT statement + // (Default=[]SQLFragmentType{ + // CommonTableSQLFragment, + // SelectSQLFragment, + // FromSQLFragment, + // JoinSQLFragment, + // WhereSQLFragment, + // GroupBySQLFragment, + // HavingSQLFragment, + // CompoundsSQLFragment, + // OrderSQLFragment, + // LimitSQLFragment, + // OffsetSQLFragment, + // ForSQLFragment, + // }) + SelectSQLOrder []SQLFragmentType + + // The order of SQL fragments when creating an UPDATE statement + // (Default=[]SQLFragmentType{ + // CommonTableSQLFragment, + // UpdateBeginSQLFragment, + // SourcesSQLFragment, + // UpdateSQLFragment, + // WhereSQLFragment, + // OrderSQLFragment, + // LimitSQLFragment, + // ReturningSQLFragment, + // }) + UpdateSQLOrder []SQLFragmentType + + // The order of SQL fragments when creating an INSERT statement + // (Default=[]SQLFragmentType{ + // CommonTableSQLFragment, + // InsertBeingSQLFragment, + // SourcesSQLFragment, + // InsertSQLFragment, + // ReturningSQLFragment, + // }) + InsertSQLOrder []SQLFragmentType + + // The order of SQL fragments when creating a DELETE statement + // (Default=[]SQLFragmentType{ + // CommonTableSQLFragment, + // DeleteBeginSQLFragment, + // FromSQLFragment, + // WhereSQLFragment, + // OrderSQLFragment, + // LimitSQLFragment, + // ReturningSQLFragment, + // }) + DeleteSQLOrder []SQLFragmentType + + // The order of SQL fragments when creating a TRUNCATE statement + // (Default=[]SQLFragmentType{ + // TruncateSQLFragment, + // }) + TruncateSQLOrder []SQLFragmentType + } +) + +const ( + CommonTableSQLFragment = iota + SelectSQLFragment + SelectWithLimitSQLFragment + FromSQLFragment + JoinSQLFragment + WhereSQLFragment + GroupBySQLFragment + HavingSQLFragment + CompoundsSQLFragment + OrderSQLFragment + OrderWithOffsetFetchSQLFragment + LimitSQLFragment + OffsetSQLFragment + ForSQLFragment + UpdateBeginSQLFragment + SourcesSQLFragment + IntoSQLFragment + UpdateSQLFragment + UpdateFromSQLFragment + ReturningSQLFragment + InsertBeingSQLFragment + InsertSQLFragment + DeleteBeginSQLFragment + TruncateSQLFragment + WindowSQLFragment +) + +//nolint:gocyclo // simple type to string conversion +func (sf SQLFragmentType) String() string { + switch sf { + case CommonTableSQLFragment: + return "CommonTableSQLFragment" + case SelectSQLFragment: + return "SelectSQLFragment" + case FromSQLFragment: + return "FromSQLFragment" + case JoinSQLFragment: + return "JoinSQLFragment" + case WhereSQLFragment: + return "WhereSQLFragment" + case GroupBySQLFragment: + return "GroupBySQLFragment" + case HavingSQLFragment: + return "HavingSQLFragment" + case CompoundsSQLFragment: + return "CompoundsSQLFragment" + case OrderSQLFragment: + return "OrderSQLFragment" + case LimitSQLFragment: + return "LimitSQLFragment" + case OffsetSQLFragment: + return "OffsetSQLFragment" + case ForSQLFragment: + return "ForSQLFragment" + case UpdateBeginSQLFragment: + return "UpdateBeginSQLFragment" + case SourcesSQLFragment: + return "SourcesSQLFragment" + case IntoSQLFragment: + return "IntoSQLFragment" + case UpdateSQLFragment: + return "UpdateSQLFragment" + case UpdateFromSQLFragment: + return "UpdateFromSQLFragment" + case ReturningSQLFragment: + return "ReturningSQLFragment" + case InsertBeingSQLFragment: + return "InsertBeingSQLFragment" + case DeleteBeginSQLFragment: + return "DeleteBeginSQLFragment" + case TruncateSQLFragment: + return "TruncateSQLFragment" + case WindowSQLFragment: + return "WindowSQLFragment" + } + return fmt.Sprintf("%d", sf) +} + +//nolint:funlen +func DefaultDialectOptions() *SQLDialectOptions { + return &SQLDialectOptions{ + SupportsOrderByOnDelete: false, + SupportsDeleteTableHint: false, + SupportsOrderByOnUpdate: false, + SupportsLimitOnDelete: false, + SupportsLimitOnUpdate: false, + SupportsReturn: true, + SupportsConflictUpdateWhere: true, + SupportsInsertIgnoreSyntax: false, + SupportsConflictTarget: true, + SupportsWithCTE: true, + SupportsWithCTERecursive: true, + SupportsDistinctOn: true, + WrapCompoundsInParens: true, + SupportsWindowFunction: true, + SupportsLateral: true, + + SupportsMultipleUpdateTables: true, + UseFromClauseForMultipleUpdateTables: true, + + UpdateClause: []byte("UPDATE"), + InsertClause: []byte("INSERT INTO"), + InsertIgnoreClause: []byte("INSERT IGNORE INTO"), + SelectClause: []byte("SELECT"), + DeleteClause: []byte("DELETE"), + TruncateClause: []byte("TRUNCATE"), + WithFragment: []byte("WITH "), + RecursiveFragment: []byte("RECURSIVE "), + CascadeFragment: []byte(" CASCADE"), + RestrictFragment: []byte(" RESTRICT"), + DefaultValuesFragment: []byte(" DEFAULT VALUES"), + ValuesFragment: []byte(" VALUES "), + IdentityFragment: []byte(" IDENTITY"), + SetFragment: []byte(" SET "), + DistinctFragment: []byte("DISTINCT"), + ReturningFragment: []byte(" RETURNING "), + FromFragment: []byte(" FROM"), + UsingFragment: []byte(" USING "), + OnFragment: []byte(" ON "), + WhereFragment: []byte(" WHERE "), + GroupByFragment: []byte(" GROUP BY "), + HavingFragment: []byte(" HAVING "), + WindowFragment: []byte(" WINDOW "), + WindowPartitionByFragment: []byte("PARTITION BY "), + WindowOrderByFragment: []byte("ORDER BY "), + WindowOverFragment: []byte(" OVER "), + OrderByFragment: []byte(" ORDER BY "), + FetchFragment: []byte(" "), + LimitFragment: []byte(" LIMIT "), + OffsetFragment: []byte(" OFFSET "), + ForUpdateFragment: []byte(" FOR UPDATE "), + ForNoKeyUpdateFragment: []byte(" FOR NO KEY UPDATE "), + ForShareFragment: []byte(" FOR SHARE "), + ForKeyShareFragment: []byte(" FOR KEY SHARE "), + OfFragment: []byte("OF "), + NowaitFragment: []byte("NOWAIT"), + SkipLockedFragment: []byte("SKIP LOCKED"), + LateralFragment: []byte("LATERAL "), + AsFragment: []byte(" AS "), + AscFragment: []byte(" ASC"), + DescFragment: []byte(" DESC"), + NullsFirstFragment: []byte(" NULLS FIRST"), + NullsLastFragment: []byte(" NULLS LAST"), + AndFragment: []byte(" AND "), + OrFragment: []byte(" OR "), + UnionFragment: []byte(" UNION "), + UnionAllFragment: []byte(" UNION ALL "), + IntersectFragment: []byte(" INTERSECT "), + IntersectAllFragment: []byte(" INTERSECT ALL "), + ConflictFragment: []byte(" ON CONFLICT"), + ConflictDoUpdateFragment: []byte(" DO UPDATE SET "), + ConflictDoNothingFragment: []byte(" DO NOTHING"), + CastFragment: []byte("CAST"), + CaseFragment: []byte("CASE "), + WhenFragment: []byte(" WHEN "), + ThenFragment: []byte(" THEN "), + ElseFragment: []byte(" ELSE "), + EndFragment: []byte(" END"), + Null: []byte("NULL"), + True: []byte("TRUE"), + False: []byte("FALSE"), + + PlaceHolderFragment: []byte("?"), + QuoteRune: '"', + StringQuote: '\'', + SetOperatorRune: '=', + CommaRune: ',', + SpaceRune: ' ', + LeftParenRune: '(', + RightParenRune: ')', + StarRune: '*', + PeriodRune: '.', + EmptyString: "", + + BooleanOperatorLookup: map[exp.BooleanOperation][]byte{ + exp.EqOp: []byte("="), + exp.NeqOp: []byte("!="), + exp.GtOp: []byte(">"), + exp.GteOp: []byte(">="), + exp.LtOp: []byte("<"), + exp.LteOp: []byte("<="), + exp.InOp: []byte("IN"), + exp.NotInOp: []byte("NOT IN"), + exp.IsOp: []byte("IS"), + exp.IsNotOp: []byte("IS NOT"), + exp.LikeOp: []byte("LIKE"), + exp.NotLikeOp: []byte("NOT LIKE"), + exp.ILikeOp: []byte("ILIKE"), + exp.NotILikeOp: []byte("NOT ILIKE"), + exp.RegexpLikeOp: []byte("~"), + exp.RegexpNotLikeOp: []byte("!~"), + exp.RegexpILikeOp: []byte("~*"), + exp.RegexpNotILikeOp: []byte("!~*"), + }, + BitwiseOperatorLookup: map[exp.BitwiseOperation][]byte{ + exp.BitwiseInversionOp: []byte("~"), + exp.BitwiseOrOp: []byte("|"), + exp.BitwiseAndOp: []byte("&"), + exp.BitwiseXorOp: []byte("#"), + exp.BitwiseLeftShiftOp: []byte("<<"), + exp.BitwiseRightShiftOp: []byte(">>"), + }, + RangeOperatorLookup: map[exp.RangeOperation][]byte{ + exp.BetweenOp: []byte("BETWEEN"), + exp.NotBetweenOp: []byte("NOT BETWEEN"), + }, + JoinTypeLookup: map[exp.JoinType][]byte{ + exp.InnerJoinType: []byte(" INNER JOIN "), + exp.FullOuterJoinType: []byte(" FULL OUTER JOIN "), + exp.RightOuterJoinType: []byte(" RIGHT OUTER JOIN "), + exp.LeftOuterJoinType: []byte(" LEFT OUTER JOIN "), + exp.FullJoinType: []byte(" FULL JOIN "), + exp.RightJoinType: []byte(" RIGHT JOIN "), + exp.LeftJoinType: []byte(" LEFT JOIN "), + exp.NaturalJoinType: []byte(" NATURAL JOIN "), + exp.NaturalLeftJoinType: []byte(" NATURAL LEFT JOIN "), + exp.NaturalRightJoinType: []byte(" NATURAL RIGHT JOIN "), + exp.NaturalFullJoinType: []byte(" NATURAL FULL JOIN "), + exp.CrossJoinType: []byte(" CROSS JOIN "), + }, + + TimeFormat: time.RFC3339Nano, + + BooleanDataTypeSupported: true, + UseLiteralIsBools: true, + + EscapedRunes: map[rune][]byte{ + '\'': []byte("''"), + }, + + SelectSQLOrder: []SQLFragmentType{ + CommonTableSQLFragment, + SelectSQLFragment, + FromSQLFragment, + JoinSQLFragment, + WhereSQLFragment, + GroupBySQLFragment, + HavingSQLFragment, + WindowSQLFragment, + CompoundsSQLFragment, + OrderSQLFragment, + LimitSQLFragment, + OffsetSQLFragment, + ForSQLFragment, + }, + UpdateSQLOrder: []SQLFragmentType{ + CommonTableSQLFragment, + UpdateBeginSQLFragment, + SourcesSQLFragment, + UpdateSQLFragment, + UpdateFromSQLFragment, + WhereSQLFragment, + OrderSQLFragment, + LimitSQLFragment, + ReturningSQLFragment, + }, + InsertSQLOrder: []SQLFragmentType{ + CommonTableSQLFragment, + InsertBeingSQLFragment, + IntoSQLFragment, + InsertSQLFragment, + ReturningSQLFragment, + }, + DeleteSQLOrder: []SQLFragmentType{ + CommonTableSQLFragment, + DeleteBeginSQLFragment, + FromSQLFragment, + WhereSQLFragment, + OrderSQLFragment, + LimitSQLFragment, + ReturningSQLFragment, + }, + TruncateSQLOrder: []SQLFragmentType{ + TruncateSQLFragment, + }, + } +} diff --git a/sqlgen/sql_dialect_options_test.go b/sqlgen/sql_dialect_options_test.go new file mode 100644 index 0000000..2c37f41 --- /dev/null +++ b/sqlgen/sql_dialect_options_test.go @@ -0,0 +1,49 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type sqlFragmentTypeSuite struct { + suite.Suite +} + +func (sfts *sqlFragmentTypeSuite) TestOptions_SQLFragmentType() { + for _, tt := range []struct { + typ sqlgen.SQLFragmentType + expectedStr string + }{ + {typ: sqlgen.CommonTableSQLFragment, expectedStr: "CommonTableSQLFragment"}, + {typ: sqlgen.SelectSQLFragment, expectedStr: "SelectSQLFragment"}, + {typ: sqlgen.FromSQLFragment, expectedStr: "FromSQLFragment"}, + {typ: sqlgen.JoinSQLFragment, expectedStr: "JoinSQLFragment"}, + {typ: sqlgen.WhereSQLFragment, expectedStr: "WhereSQLFragment"}, + {typ: sqlgen.GroupBySQLFragment, expectedStr: "GroupBySQLFragment"}, + {typ: sqlgen.HavingSQLFragment, expectedStr: "HavingSQLFragment"}, + {typ: sqlgen.CompoundsSQLFragment, expectedStr: "CompoundsSQLFragment"}, + {typ: sqlgen.OrderSQLFragment, expectedStr: "OrderSQLFragment"}, + {typ: sqlgen.LimitSQLFragment, expectedStr: "LimitSQLFragment"}, + {typ: sqlgen.OffsetSQLFragment, expectedStr: "OffsetSQLFragment"}, + {typ: sqlgen.ForSQLFragment, expectedStr: "ForSQLFragment"}, + {typ: sqlgen.UpdateBeginSQLFragment, expectedStr: "UpdateBeginSQLFragment"}, + {typ: sqlgen.SourcesSQLFragment, expectedStr: "SourcesSQLFragment"}, + {typ: sqlgen.IntoSQLFragment, expectedStr: "IntoSQLFragment"}, + {typ: sqlgen.UpdateSQLFragment, expectedStr: "UpdateSQLFragment"}, + {typ: sqlgen.UpdateFromSQLFragment, expectedStr: "UpdateFromSQLFragment"}, + {typ: sqlgen.ReturningSQLFragment, expectedStr: "ReturningSQLFragment"}, + {typ: sqlgen.InsertBeingSQLFragment, expectedStr: "InsertBeingSQLFragment"}, + {typ: sqlgen.DeleteBeginSQLFragment, expectedStr: "DeleteBeginSQLFragment"}, + {typ: sqlgen.TruncateSQLFragment, expectedStr: "TruncateSQLFragment"}, + {typ: sqlgen.WindowSQLFragment, expectedStr: "WindowSQLFragment"}, + {typ: sqlgen.SQLFragmentType(10000), expectedStr: "10000"}, + } { + sfts.Equal(tt.expectedStr, tt.typ.String()) + } +} + +func TestSQLFragmentType(t *testing.T) { + suite.Run(t, new(sqlFragmentTypeSuite)) +} diff --git a/sqlgen/sqlgen.go b/sqlgen/sqlgen.go new file mode 100644 index 0000000..0975711 --- /dev/null +++ b/sqlgen/sqlgen.go @@ -0,0 +1,15 @@ +package sqlgen + +import "time" + +var timeLocation = time.UTC + +// Set the location to use when interpolating time.Time instances. See https://golang.org/pkg/time/#LoadLocation +// NOTE: This has no effect when using prepared statements. +func SetTimeLocation(loc *time.Location) { + timeLocation = loc +} + +func GetTimeLocation() *time.Location { + return timeLocation +} diff --git a/sqlgen/truncate_sql_generator.go b/sqlgen/truncate_sql_generator.go new file mode 100644 index 0000000..9695fb2 --- /dev/null +++ b/sqlgen/truncate_sql_generator.go @@ -0,0 +1,64 @@ +package sqlgen + +import ( + "strings" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + TruncateSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, clauses exp.TruncateClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + truncateSQLGenerator struct { + CommonSQLGenerator + } +) + +var errNoSourceForTruncate = errors.New("no source found when generating truncate sql") + +func NewTruncateSQLGenerator(dialect string, do *SQLDialectOptions) TruncateSQLGenerator { + return &truncateSQLGenerator{NewCommonSQLGenerator(dialect, do)} +} + +func (tsg *truncateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.TruncateClauses) { + if !clauses.HasTable() { + b.SetError(errNoSourceForTruncate) + return + } + for _, f := range tsg.DialectOptions().TruncateSQLOrder { + if b.Error() != nil { + return + } + switch f { + case TruncateSQLFragment: + tsg.TruncateSQL(b, clauses.Table(), clauses.Options()) + default: + b.SetError(ErrNotSupportedFragment("TRUNCATE", f)) + } + } +} + +// Generates a TRUNCATE statement +func (tsg *truncateSQLGenerator) TruncateSQL(b sb.SQLBuilder, from exp.ColumnListExpression, opts exp.TruncateOptions) { + b.Write(tsg.DialectOptions().TruncateClause) + tsg.SourcesSQL(b, from) + if opts.Identity != tsg.DialectOptions().EmptyString { + b.WriteRunes(tsg.DialectOptions().SpaceRune). + WriteStrings(strings.ToUpper(opts.Identity)). + Write(tsg.DialectOptions().IdentityFragment) + } + if opts.Cascade { + b.Write(tsg.DialectOptions().CascadeFragment) + } else if opts.Restrict { + b.Write(tsg.DialectOptions().RestrictFragment) + } +} diff --git a/sqlgen/truncate_sql_generator_test.go b/sqlgen/truncate_sql_generator_test.go new file mode 100644 index 0000000..ae03ff3 --- /dev/null +++ b/sqlgen/truncate_sql_generator_test.go @@ -0,0 +1,121 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + truncateTestCase struct { + clause exp.TruncateClauses + sql string + isPrepared bool + args []interface{} + err string + } + truncateSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (tsgs *truncateSQLGeneratorSuite) assertCases(tsg sqlgen.TruncateSQLGenerator, testCases ...truncateTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + tsg.Generate(b, tc.clause) + switch { + case len(tc.err) > 0: + tsgs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + tsgs.assertPreparedSQL(b, tc.sql, tc.args) + default: + tsgs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (tsgs *truncateSQLGeneratorSuite) TestDialect() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewTruncateSQLGenerator("test", opts) + tsgs.Equal("test", d.Dialect()) + + opts2 := sqlgen.DefaultDialectOptions() + d2 := sqlgen.NewTruncateSQLGenerator("test2", opts2) + tsgs.Equal("test2", d2.Dialect()) +} + +func (tsgs *truncateSQLGeneratorSuite) TestGenerate() { + opts := sqlgen.DefaultDialectOptions() + opts.TruncateClause = []byte("truncate") + + tcNoTable := exp.NewTruncateClauses() + tcSingle := tcNoTable.SetTable(exp.NewColumnListExpression("a")) + tcMulti := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a", "b")) + + expectedNoSourceErr := "db: no source found when generating truncate sql" + tsgs.assertCases( + sqlgen.NewTruncateSQLGenerator("test", opts), + truncateTestCase{clause: tcSingle, sql: `truncate "a"`}, + truncateTestCase{clause: tcSingle, sql: `truncate "a"`, isPrepared: true}, + + truncateTestCase{clause: tcMulti, sql: `truncate "a", "b"`}, + truncateTestCase{clause: tcMulti, sql: `truncate "a", "b"`, isPrepared: true}, + + truncateTestCase{clause: tcNoTable, err: expectedNoSourceErr}, + truncateTestCase{clause: tcNoTable, err: expectedNoSourceErr, isPrepared: true}, + ) +} + +func (tsgs *truncateSQLGeneratorSuite) TestGenerate_UnsupportedFragment() { + opts := sqlgen.DefaultDialectOptions() + opts.TruncateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment} + tc := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a")) + expectedErr := "db: unsupported TRUNCATE SQL fragment UpdateBeginSQLFragment" + tsgs.assertCases( + sqlgen.NewTruncateSQLGenerator("test", opts), + truncateTestCase{clause: tc, err: expectedErr}, + truncateTestCase{clause: tc, err: expectedErr, isPrepared: true}, + ) +} + +func (tsgs *truncateSQLGeneratorSuite) TestGenerate_WithErroredBuilder() { + opts := sqlgen.DefaultDialectOptions() + opts.TruncateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment} + d := sqlgen.NewTruncateSQLGenerator("test", opts) + + b := sb.NewSQLBuilder(true).SetError(errors.New("expected error")) + d.Generate(b, exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a"))) + tsgs.assertErrorSQL(b, `db: expected error`) +} + +func (tsgs *truncateSQLGeneratorSuite) TestGenerate_WithCascade() { + opts := sqlgen.DefaultDialectOptions() + opts.CascadeFragment = []byte(" cascade") + opts.RestrictFragment = []byte(" restrict") + opts.IdentityFragment = []byte(" identity") + + tc := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a")) + tcCascade := tc.SetOptions(exp.TruncateOptions{Cascade: true}) + tcRestrict := tc.SetOptions(exp.TruncateOptions{Restrict: true}) + tcRestart := tc.SetOptions(exp.TruncateOptions{Identity: "restart"}) + + tsgs.assertCases( + sqlgen.NewTruncateSQLGenerator("test", opts), + truncateTestCase{clause: tcCascade, sql: `TRUNCATE "a" cascade`}, + truncateTestCase{clause: tcCascade, sql: `TRUNCATE "a" cascade`, isPrepared: true}, + + truncateTestCase{clause: tcRestrict, sql: `TRUNCATE "a" restrict`}, + truncateTestCase{clause: tcRestrict, sql: `TRUNCATE "a" restrict`, isPrepared: true}, + + truncateTestCase{clause: tcRestart, sql: `TRUNCATE "a" RESTART identity`}, + truncateTestCase{clause: tcRestart, sql: `TRUNCATE "a" RESTART identity`, isPrepared: true}, + ) +} + +func TestTruncateSQLGenerator(t *testing.T) { + suite.Run(t, new(truncateSQLGeneratorSuite)) +} diff --git a/sqlgen/update_sql_generator.go b/sqlgen/update_sql_generator.go new file mode 100644 index 0000000..4c68398 --- /dev/null +++ b/sqlgen/update_sql_generator.go @@ -0,0 +1,112 @@ +package sqlgen + +import ( + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type ( + // An adapter interface to be used by a Dataset to generate SQL for a specific dialect. + // See DefaultAdapter for a concrete implementation and examples. + UpdateSQLGenerator interface { + Dialect() string + Generate(b sb.SQLBuilder, clauses exp.UpdateClauses) + } + // The default adapter. This class should be used when building a new adapter. When creating a new adapter you can + // either override methods, or more typically update default values. + // See (github.com/doug-martin/goqu/dialect/postgres) + updateSQLGenerator struct { + CommonSQLGenerator + } +) + +var ( + ErrNoSourceForUpdate = errors.New("no source found when generating update sql") + ErrNoSetValuesForUpdate = errors.New("no set values found when generating UPDATE sql") +) + +func NewUpdateSQLGenerator(dialect string, do *SQLDialectOptions) UpdateSQLGenerator { + return &updateSQLGenerator{NewCommonSQLGenerator(dialect, do)} +} + +func (usg *updateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.UpdateClauses) { + if !clauses.HasTable() { + b.SetError(ErrNoSourceForUpdate) + return + } + if !clauses.HasSetValues() { + b.SetError(ErrNoSetValuesForUpdate) + return + } + if !usg.DialectOptions().SupportsMultipleUpdateTables && clauses.HasFrom() { + b.SetError(errors.New("%s dialect does not support multiple tables in UPDATE", usg.Dialect())) + } + updates, err := exp.NewUpdateExpressions(clauses.SetValues()) + if err != nil { + b.SetError(err) + return + } + for _, f := range usg.DialectOptions().UpdateSQLOrder { + if b.Error() != nil { + return + } + switch f { + case CommonTableSQLFragment: + usg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables()) + case UpdateBeginSQLFragment: + usg.UpdateBeginSQL(b) + case SourcesSQLFragment: + usg.updateTableSQL(b, clauses) + case UpdateSQLFragment: + usg.UpdateExpressionsSQL(b, updates...) + case UpdateFromSQLFragment: + usg.updateFromSQL(b, clauses.From()) + case WhereSQLFragment: + usg.WhereSQL(b, clauses.Where()) + case OrderSQLFragment: + if usg.DialectOptions().SupportsOrderByOnUpdate { + usg.OrderSQL(b, clauses.Order()) + } + case LimitSQLFragment: + if usg.DialectOptions().SupportsLimitOnUpdate { + usg.LimitSQL(b, clauses.Limit()) + } + case ReturningSQLFragment: + usg.ReturningSQL(b, clauses.Returning()) + default: + b.SetError(ErrNotSupportedFragment("UPDATE", f)) + } + } +} + +// Adds the correct fragment to being an UPDATE statement +func (usg *updateSQLGenerator) UpdateBeginSQL(b sb.SQLBuilder) { + b.Write(usg.DialectOptions().UpdateClause) +} + +// Adds column setters in an update SET clause +func (usg *updateSQLGenerator) UpdateExpressionsSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression) { + b.Write(usg.DialectOptions().SetFragment) + usg.UpdateExpressionSQL(b, updates...) +} + +func (usg *updateSQLGenerator) updateTableSQL(b sb.SQLBuilder, uc exp.UpdateClauses) { + b.WriteRunes(usg.DialectOptions().SpaceRune) + usg.ExpressionSQLGenerator().Generate(b, uc.Table()) + if uc.HasFrom() { + if !usg.DialectOptions().UseFromClauseForMultipleUpdateTables { + b.WriteRunes(usg.DialectOptions().CommaRune) + usg.ExpressionSQLGenerator().Generate(b, uc.From()) + } + } +} + +func (usg *updateSQLGenerator) updateFromSQL(b sb.SQLBuilder, ce exp.ColumnListExpression) { + if ce == nil || ce.IsEmpty() { + return + } + if usg.DialectOptions().UseFromClauseForMultipleUpdateTables { + usg.FromSQL(b, ce) + } +} diff --git a/sqlgen/update_sql_generator_test.go b/sqlgen/update_sql_generator_test.go new file mode 100644 index 0000000..a669671 --- /dev/null +++ b/sqlgen/update_sql_generator_test.go @@ -0,0 +1,259 @@ +package sqlgen_test + +import ( + "testing" + + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/sqlgen" + "github.com/stretchr/testify/suite" +) + +type ( + updateTestCase struct { + clause exp.UpdateClauses + sql string + isPrepared bool + args []interface{} + err string + } + updateSQLGeneratorSuite struct { + baseSQLGeneratorSuite + } +) + +func (usgs *updateSQLGeneratorSuite) assertCases(usg sqlgen.UpdateSQLGenerator, testCases ...updateTestCase) { + for _, tc := range testCases { + b := sb.NewSQLBuilder(tc.isPrepared) + usg.Generate(b, tc.clause) + switch { + case len(tc.err) > 0: + usgs.assertErrorSQL(b, tc.err) + case tc.isPrepared: + usgs.assertPreparedSQL(b, tc.sql, tc.args) + default: + usgs.assertNotPreparedSQL(b, tc.sql) + } + } +} + +func (usgs *updateSQLGeneratorSuite) TestDialect() { + opts := sqlgen.DefaultDialectOptions() + d := sqlgen.NewUpdateSQLGenerator("test", opts) + usgs.Equal("test", d.Dialect()) + + opts2 := sqlgen.DefaultDialectOptions() + d2 := sqlgen.NewUpdateSQLGenerator("test2", opts2) + usgs.Equal("test2", d2.Dialect()) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_unsupportedFragment() { + opts := sqlgen.DefaultDialectOptions() + opts.UpdateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment} + + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(exp.Record{"a": "b", "b": "c"}) + expectedErr := "db: unsupported UPDATE SQL fragment InsertBeingSQLFragment" + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, err: expectedErr}, + updateTestCase{clause: uc, err: expectedErr, isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_empty() { + uc := exp.NewUpdateClauses() + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()), + updateTestCase{clause: uc, err: sqlgen.ErrNoSourceForUpdate.Error()}, + updateTestCase{clause: uc, err: sqlgen.ErrNoSourceForUpdate.Error(), isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withBadUpdateValues() { + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(true) + + expectedErr := "db: unsupported update interface type bool" + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()), + updateTestCase{clause: uc, err: expectedErr}, + updateTestCase{clause: uc, err: expectedErr, isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_noSetValues() { + uc := exp.NewUpdateClauses().SetTable(exp.NewIdentifierExpression("", "test", "")) + + expectedErr := sqlgen.ErrNoSetValuesForUpdate.Error() + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()), + updateTestCase{clause: uc, err: expectedErr}, + updateTestCase{clause: uc, err: expectedErr, isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withFrom() { + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(exp.Record{"foo": "bar"}). + SetFrom(exp.NewColumnListExpression("other_test")) + + ucNullSet := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(exp.Record{"foo": nil}). + SetFrom(exp.NewColumnListExpression("other_test")) + + opts := sqlgen.DefaultDialectOptions() + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test" SET "foo"='bar' FROM "other_test"`}, + updateTestCase{clause: uc, sql: `UPDATE "test" SET "foo"=? FROM "other_test"`, isPrepared: true, args: []interface{}{"bar"}}, + + updateTestCase{clause: ucNullSet, sql: `UPDATE "test" SET "foo"=NULL FROM "other_test"`}, + updateTestCase{clause: ucNullSet, sql: `UPDATE "test" SET "foo"=? FROM "other_test"`, isPrepared: true, args: []interface{}{nil}}, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.UseFromClauseForMultipleUpdateTables = false + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test","other_test" SET "foo"='bar'`}, + updateTestCase{clause: uc, sql: `UPDATE "test","other_test" SET "foo"=?`, isPrepared: true, args: []interface{}{"bar"}}, + + updateTestCase{clause: ucNullSet, sql: `UPDATE "test","other_test" SET "foo"=NULL`}, + updateTestCase{clause: ucNullSet, sql: `UPDATE "test","other_test" SET "foo"=?`, isPrepared: true, args: []interface{}{nil}}, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsMultipleUpdateTables = false + expectedErr := "db: test dialect does not support multiple tables in UPDATE" + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, err: expectedErr}, + updateTestCase{clause: uc, err: expectedErr, isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withUpdateExpression() { + opts := sqlgen.DefaultDialectOptions() + // make sure the fragments are used + opts.SetFragment = []byte(" set ") + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")) + ucRecord := uc.SetSetValues(exp.Record{"a": "b", "b": "c"}) + ucRecordNullVal := uc.SetSetValues(exp.Record{"a": "b", "b": nil}) + ucRecordBoolVals := uc.SetSetValues(exp.Record{"a": true, "b": false}) + ucEmptyRecord := uc.SetSetValues(exp.Record{}) + + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: ucRecord, sql: `UPDATE "test" set "a"='b',"b"='c'`}, + updateTestCase{clause: ucRecord, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}}, + + updateTestCase{clause: ucRecordNullVal, sql: `UPDATE "test" set "a"='b',"b"=NULL`}, + updateTestCase{clause: ucRecordNullVal, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", nil}}, + + updateTestCase{clause: ucRecordBoolVals, sql: `UPDATE "test" set "a"=TRUE,"b"=FALSE`}, + updateTestCase{clause: ucRecordBoolVals, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{true, false}}, + + updateTestCase{clause: ucEmptyRecord, err: sqlgen.ErrNoUpdatedValuesProvided.Error()}, + updateTestCase{clause: ucEmptyRecord, err: sqlgen.ErrNoUpdatedValuesProvided.Error(), isPrepared: true}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withOrder() { + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(exp.Record{"a": "b", "b": "c"}). + SetOrder( + exp.NewIdentifierExpression("", "", "a").Asc(), + exp.NewIdentifierExpression("", "", "b").Desc(), + ) + + opts := sqlgen.DefaultDialectOptions() + opts.SupportsOrderByOnUpdate = true + + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c' ORDER BY "a" ASC, "b" DESC`}, + updateTestCase{ + clause: uc, + sql: `UPDATE "test" SET "a"=?,"b"=? ORDER BY "a" ASC, "b" DESC`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsOrderByOnUpdate = false + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c'`}, + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withLimit() { + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test", "")). + SetSetValues(exp.Record{"a": "b", "b": "c"}). + SetLimit(10) + + opts := sqlgen.DefaultDialectOptions() + opts.SupportsLimitOnUpdate = true + + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c' LIMIT 10`}, + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=? LIMIT ?`, isPrepared: true, args: []interface{}{"b", "c", int64(10)}}, + ) + + opts = sqlgen.DefaultDialectOptions() + opts.SupportsLimitOnUpdate = false + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", opts), + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c'`}, + updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}}, + ) +} + +func (usgs *updateSQLGeneratorSuite) TestGenerate_withCommonTables() { + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + uc := exp.NewUpdateClauses(). + SetTable(exp.NewIdentifierExpression("", "test_cte", "")). + SetSetValues(exp.Record{"a": "b", "b": "c"}) + ucCte1 := uc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) + ucCte2 := uc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse)) + + usgs.assertCases( + sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()), + updateTestCase{ + clause: ucCte1, + sql: `WITH test_cte AS (select * from foo) UPDATE "test_cte" SET "a"='b',"b"='c'`, + }, + updateTestCase{ + clause: ucCte1, + sql: `WITH test_cte AS (select * from foo) UPDATE "test_cte" SET "a"=?,"b"=?`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + + updateTestCase{ + clause: ucCte2, + sql: `WITH RECURSIVE test_cte AS (select * from foo) UPDATE "test_cte" SET "a"='b',"b"='c'`, + }, + updateTestCase{ + clause: ucCte2, + sql: `WITH RECURSIVE test_cte AS (select * from foo) UPDATE "test_cte" SET "a"=?,"b"=?`, + isPrepared: true, + args: []interface{}{"b", "c"}, + }, + ) +} + +func TestUpdateSQLGenerator(t *testing.T) { + suite.Run(t, new(updateSQLGeneratorSuite)) +} diff --git a/truncate_dataset.go b/truncate_dataset.go new file mode 100644 index 0000000..dbae2b0 --- /dev/null +++ b/truncate_dataset.go @@ -0,0 +1,171 @@ +package db + +import ( + "git.fsdpf.net/go/db/v2/exec" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type TruncateDataset struct { + dialect SQLDialect + clauses exp.TruncateClauses + isPrepared prepared + queryFactory exec.QueryFactory + err error +} + +// used internally by database to create a database with a specific adapter +func newTruncateDataset(d string, queryFactory exec.QueryFactory) *TruncateDataset { + return &TruncateDataset{ + clauses: exp.NewTruncateClauses(), + dialect: GetDialect(d), + queryFactory: queryFactory, + } +} + +func Truncate(table ...interface{}) *TruncateDataset { + return newTruncateDataset("default", nil).Table(table...) +} + +// Sets the adapter used to serialize values and create the SQL statement +func (td *TruncateDataset) WithDialect(dl string) *TruncateDataset { + ds := td.copy(td.GetClauses()) + ds.dialect = GetDialect(dl) + return ds +} + +// Set the parameter interpolation behavior. See examples +// +// prepared: If true the dataset WILL NOT interpolate the parameters. +func (td *TruncateDataset) Prepared(prepared bool) *TruncateDataset { + ret := td.copy(td.clauses) + ret.isPrepared = preparedFromBool(prepared) + return ret +} + +func (td *TruncateDataset) IsPrepared() bool { + return td.isPrepared.Bool() +} + +// Returns the current adapter on the dataset +func (td *TruncateDataset) Dialect() SQLDialect { + return td.dialect +} + +// Returns the current adapter on the dataset +func (td *TruncateDataset) SetDialect(dialect SQLDialect) *TruncateDataset { + cd := td.copy(td.GetClauses()) + cd.dialect = dialect + return cd +} + +func (td *TruncateDataset) Expression() exp.Expression { + return td +} + +// Clones the dataset +func (td *TruncateDataset) Clone() exp.Expression { + return td.copy(td.clauses) +} + +// Returns the current clauses on the dataset. +func (td *TruncateDataset) GetClauses() exp.TruncateClauses { + return td.clauses +} + +// used interally to copy the dataset +func (td *TruncateDataset) copy(clauses exp.TruncateClauses) *TruncateDataset { + return &TruncateDataset{ + dialect: td.dialect, + clauses: clauses, + isPrepared: td.isPrepared, + queryFactory: td.queryFactory, + err: td.err, + } +} + +// Adds a FROM clause. This return a new dataset with the original sources replaced. See examples. +// You can pass in the following. +// +// string: Will automatically be turned into an identifier +// IdentifierExpression +// LiteralExpression: (See Literal) Will use the literal SQL +func (td *TruncateDataset) Table(table ...interface{}) *TruncateDataset { + return td.copy(td.clauses.SetTable(exp.NewColumnListExpression(table...))) +} + +// Adds a CASCADE clause +func (td *TruncateDataset) Cascade() *TruncateDataset { + opts := td.clauses.Options() + opts.Cascade = true + return td.copy(td.clauses.SetOptions(opts)) +} + +// Clears the CASCADE clause +func (td *TruncateDataset) NoCascade() *TruncateDataset { + opts := td.clauses.Options() + opts.Cascade = false + return td.copy(td.clauses.SetOptions(opts)) +} + +// Adds a RESTRICT clause +func (td *TruncateDataset) Restrict() *TruncateDataset { + opts := td.clauses.Options() + opts.Restrict = true + return td.copy(td.clauses.SetOptions(opts)) +} + +// Clears the RESTRICT clause +func (td *TruncateDataset) NoRestrict() *TruncateDataset { + opts := td.clauses.Options() + opts.Restrict = false + return td.copy(td.clauses.SetOptions(opts)) +} + +// Add a IDENTITY clause (e.g. RESTART) +func (td *TruncateDataset) Identity(identity string) *TruncateDataset { + opts := td.clauses.Options() + opts.Identity = identity + return td.copy(td.clauses.SetOptions(opts)) +} + +// Get any error that has been set or nil if no error has been set. +func (td *TruncateDataset) Error() error { + return td.err +} + +// Set an error on the dataset if one has not already been set. This error will be returned by a future call to Error +// or as part of ToSQL. This can be used by end users to record errors while building up queries without having to +// track those separately. +func (td *TruncateDataset) SetError(err error) *TruncateDataset { + if td.err == nil { + td.err = err + } + + return td +} + +// Generates a TRUNCATE sql statement, if Prepared has been called with true then the parameters will not be interpolated. +// See examples. +// +// Errors: +// - There is an error generating the SQL +func (td *TruncateDataset) ToSQL() (sql string, params []interface{}, err error) { + return td.truncateSQLBuilder().ToSQL() +} + +// Generates the TRUNCATE sql, and returns an Exec struct with the sql set to the TRUNCATE statement +// +// db.From("test").Truncate().Executor().Exec() +func (td *TruncateDataset) Executor() exec.QueryExecutor { + return td.queryFactory.FromSQLBuilder(td.truncateSQLBuilder()) +} + +func (td *TruncateDataset) truncateSQLBuilder() sb.SQLBuilder { + buf := sb.NewSQLBuilder(td.isPrepared.Bool()) + if td.err != nil { + return buf.SetError(td.err) + } + td.dialect.ToTruncateSQL(buf, td.clauses) + return buf +} diff --git a/truncate_dataset_test.go b/truncate_dataset_test.go new file mode 100644 index 0000000..ee04d92 --- /dev/null +++ b/truncate_dataset_test.go @@ -0,0 +1,340 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/mocks" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ( + truncateTestCase struct { + ds *dbv2.TruncateDataset + clauses exp.TruncateClauses + } + truncateDatasetSuite struct { + suite.Suite + } +) + +func (tds *truncateDatasetSuite) assertCases(cases ...truncateTestCase) { + for _, s := range cases { + tds.Equal(s.clauses, s.ds.GetClauses()) + } +} + +func (tds *truncateDatasetSuite) TestClone() { + ds := dbv2.Truncate("test") + tds.Equal(ds, ds.Clone()) +} + +func (tds *truncateDatasetSuite) TestExpression() { + ds := dbv2.Truncate("test") + tds.Equal(ds, ds.Expression()) +} + +func (tds *truncateDatasetSuite) TestDialect() { + ds := dbv2.Truncate("test") + tds.NotNil(ds.Dialect()) +} + +func (tds *truncateDatasetSuite) TestWithDialect() { + ds := dbv2.Truncate("test") + md := new(mocks.SQLDialect) + ds = ds.SetDialect(md) + + dialect := dbv2.GetDialect("default") + dialectDs := ds.WithDialect("default") + tds.Equal(md, ds.Dialect()) + tds.Equal(dialect, dialectDs.Dialect()) +} + +func (tds *truncateDatasetSuite) TestPrepared() { + ds := dbv2.Truncate("test") + preparedDs := ds.Prepared(true) + tds.True(preparedDs.IsPrepared()) + tds.False(ds.IsPrepared()) + // should apply the prepared to any datasets created from the root + tds.True(preparedDs.Restrict().IsPrepared()) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + // should be prepared by default + ds = dbv2.Truncate("test") + tds.True(ds.IsPrepared()) +} + +func (tds *truncateDatasetSuite) TestGetClauses() { + ds := dbv2.Truncate("test") + ce := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression(dbv2.I("test"))) + tds.Equal(ce, ds.GetClauses()) +} + +func (tds *truncateDatasetSuite) TestTable() { + bd := dbv2.Truncate("test") + tds.assertCases( + truncateTestCase{ + ds: bd.Table("test2"), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test2")), + }, + truncateTestCase{ + ds: bd.Table("test1", "test2"), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test1", "test2")), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")), + }, + ) +} + +func (tds *truncateDatasetSuite) TestCascade() { + bd := dbv2.Truncate("test") + tds.assertCases( + truncateTestCase{ + ds: bd.Cascade(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true}), + }, + truncateTestCase{ + ds: bd.Restrict().Cascade(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true, Restrict: true}), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")), + }, + ) +} + +func (tds *truncateDatasetSuite) TestNoCascade() { + bd := dbv2.Truncate("test").Cascade() + tds.assertCases( + truncateTestCase{ + ds: bd.NoCascade(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{}), + }, + truncateTestCase{ + ds: bd.Restrict().NoCascade(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: false, Restrict: true}), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true}), + }, + ) +} + +func (tds *truncateDatasetSuite) TestRestrict() { + bd := dbv2.Truncate("test") + tds.assertCases( + truncateTestCase{ + ds: bd.Restrict(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Restrict: true}), + }, + truncateTestCase{ + ds: bd.Cascade().Restrict(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true, Restrict: true}), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")), + }, + ) +} + +func (tds *truncateDatasetSuite) TestNoRestrict() { + bd := dbv2.Truncate("test").Restrict() + tds.assertCases( + truncateTestCase{ + ds: bd.NoRestrict(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{}), + }, + truncateTestCase{ + ds: bd.Cascade().NoRestrict(), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true, Restrict: false}), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Restrict: true}), + }, + ) +} + +func (tds *truncateDatasetSuite) TestIdentity() { + bd := dbv2.Truncate("test") + tds.assertCases( + truncateTestCase{ + ds: bd.Identity("RESTART"), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Identity: "RESTART"}), + }, + truncateTestCase{ + ds: bd.Identity("CONTINUE"), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Identity: "CONTINUE"}), + }, + truncateTestCase{ + ds: bd.Cascade().Restrict().Identity("CONTINUE"), + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")). + SetOptions(exp.TruncateOptions{Cascade: true, Restrict: true, Identity: "CONTINUE"}), + }, + truncateTestCase{ + ds: bd, + clauses: exp.NewTruncateClauses(). + SetTable(exp.NewColumnListExpression("test")), + }, + ) +} + +func (tds *truncateDatasetSuite) TestToSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.Truncate("test").SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToTruncateSQL", sqlB, c).Return(nil).Once() + + sql, args, err := ds.ToSQL() + tds.NoError(err) + tds.Empty(sql) + tds.Empty(args) + md.AssertExpectations(tds.T()) +} + +func (tds *truncateDatasetSuite) TestToSQL__withPrepared() { + md := new(mocks.SQLDialect) + ds := dbv2.Truncate("test").Prepared(true).SetDialect(md) + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(true) + md.On("ToTruncateSQL", sqlB, c).Return(nil).Once() + + sql, args, err := ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Nil(err) + md.AssertExpectations(tds.T()) +} + +func (tds *truncateDatasetSuite) TestToSQL_withError() { + md := new(mocks.SQLDialect) + ds := dbv2.Truncate("test").SetDialect(md) + c := ds.GetClauses() + ee := errors.New("expected error") + sqlB := sb.NewSQLBuilder(false) + md.On("ToTruncateSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(ee) + }).Once() + + sql, args, err := ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Equal(ee, err) + md.AssertExpectations(tds.T()) +} + +func (tds *truncateDatasetSuite) TestExecutor() { + mDB, _, err := sqlmock.New() + tds.NoError(err) + + ds := dbv2.New("mock", mDB).Truncate("table1", "table2") + + tsql, args, err := ds.Executor().ToSQL() + tds.NoError(err) + tds.Empty(args) + tds.Equal(`TRUNCATE "table1", "table2"`, tsql) + + tsql, args, err = ds.Prepared(true).Executor().ToSQL() + tds.NoError(err) + tds.Empty(args) + tds.Equal(`TRUNCATE "table1", "table2"`, tsql) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + tsql, args, err = ds.Executor().ToSQL() + tds.NoError(err) + tds.Empty(args) + tds.Equal(`TRUNCATE "table1", "table2"`, tsql) +} + +func (tds *truncateDatasetSuite) TestSetError() { + err1 := errors.New("error #1") + err2 := errors.New("error #2") + err3 := errors.New("error #3") + + // Verify initial error set/get works properly + md := new(mocks.SQLDialect) + ds := dbv2.Truncate("test").SetDialect(md) + ds = ds.SetError(err1) + tds.Equal(err1, ds.Error()) + sql, args, err := ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Equal(err1, err) + + // Repeated SetError calls on Dataset should not overwrite the original error + ds = ds.SetError(err2) + tds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Equal(err1, err) + + // Builder functions should not lose the error + ds = ds.Cascade() + tds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Equal(err1, err) + + // Deeper errors inside SQL generation should still return original error + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToTruncateSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(err3) + }).Once() + + sql, args, err = ds.ToSQL() + tds.Empty(sql) + tds.Empty(args) + tds.Equal(err1, err) +} + +func TestTruncateDataset(t *testing.T) { + suite.Run(t, new(truncateDatasetSuite)) +} diff --git a/update_dataset.go b/update_dataset.go new file mode 100644 index 0000000..805788e --- /dev/null +++ b/update_dataset.go @@ -0,0 +1,246 @@ +package db + +import ( + "git.fsdpf.net/go/db/v2/exec" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" +) + +type UpdateDataset struct { + dialect SQLDialect + clauses exp.UpdateClauses + isPrepared prepared + queryFactory exec.QueryFactory + err error +} + +var ErrUnsupportedUpdateTableType = errors.New("unsupported table type, a string or identifier expression is required") + +// used internally by database to create a database with a specific adapter +func newUpdateDataset(d string, queryFactory exec.QueryFactory) *UpdateDataset { + return &UpdateDataset{ + clauses: exp.NewUpdateClauses(), + dialect: GetDialect(d), + queryFactory: queryFactory, + } +} + +func Update(table interface{}) *UpdateDataset { + return newUpdateDataset("default", nil).Table(table) +} + +// Set the parameter interpolation behavior. See examples +// +// prepared: If true the dataset WILL NOT interpolate the parameters. +func (ud *UpdateDataset) Prepared(prepared bool) *UpdateDataset { + ret := ud.copy(ud.clauses) + ret.isPrepared = preparedFromBool(prepared) + return ret +} + +func (ud *UpdateDataset) IsPrepared() bool { + return ud.isPrepared.Bool() +} + +// Sets the adapter used to serialize values and create the SQL statement +func (ud *UpdateDataset) WithDialect(dl string) *UpdateDataset { + ds := ud.copy(ud.GetClauses()) + ds.dialect = GetDialect(dl) + return ds +} + +// Returns the current adapter on the dataset +func (ud *UpdateDataset) Dialect() SQLDialect { + return ud.dialect +} + +// Returns the current adapter on the dataset +func (ud *UpdateDataset) SetDialect(dialect SQLDialect) *UpdateDataset { + cd := ud.copy(ud.GetClauses()) + cd.dialect = dialect + return cd +} + +func (ud *UpdateDataset) Expression() exp.Expression { + return ud +} + +// Clones the dataset +func (ud *UpdateDataset) Clone() exp.Expression { + return ud.copy(ud.clauses) +} + +// Returns the current clauses on the dataset. +func (ud *UpdateDataset) GetClauses() exp.UpdateClauses { + return ud.clauses +} + +// used internally to copy the dataset +func (ud *UpdateDataset) copy(clauses exp.UpdateClauses) *UpdateDataset { + return &UpdateDataset{ + dialect: ud.dialect, + clauses: clauses, + isPrepared: ud.isPrepared, + queryFactory: ud.queryFactory, + err: ud.err, + } +} + +// Creates a WITH clause for a common table expression (CTE). +// +// The name will be available to use in the UPDATE from in the associated query; and can optionally +// contain a list of column names "name(col1, col2, col3)". +// +// The name will refer to the results of the specified subquery. +func (ud *UpdateDataset) With(name string, subquery exp.Expression) *UpdateDataset { + return ud.copy(ud.clauses.CommonTablesAppend(exp.NewCommonTableExpression(false, name, subquery))) +} + +// Creates a WITH RECURSIVE clause for a common table expression (CTE) +// +// The name will be available to use in the UPDATE from in the associated query; and must +// contain a list of column names "name(col1, col2, col3)" for a recursive clause. +// +// The name will refer to the results of the specified subquery. The subquery for +// a recursive query will always end with a UNION or UNION ALL with a clause that +// refers to the CTE by name. +func (ud *UpdateDataset) WithRecursive(name string, subquery exp.Expression) *UpdateDataset { + return ud.copy(ud.clauses.CommonTablesAppend(exp.NewCommonTableExpression(true, name, subquery))) +} + +// Sets the table to update. +func (ud *UpdateDataset) Table(table interface{}) *UpdateDataset { + switch t := table.(type) { + case exp.Expression: + return ud.copy(ud.clauses.SetTable(t)) + case string: + return ud.copy(ud.clauses.SetTable(exp.ParseIdentifier(t))) + default: + panic(ErrUnsupportedUpdateTableType) + } +} + +// Sets the values to use in the SET clause. See examples. +func (ud *UpdateDataset) Set(values interface{}) *UpdateDataset { + return ud.copy(ud.clauses.SetSetValues(values)) +} + +// Allows specifying other tables to reference in your update (If your dialect supports it). See examples. +func (ud *UpdateDataset) From(tables ...interface{}) *UpdateDataset { + return ud.copy(ud.clauses.SetFrom(exp.NewColumnListExpression(tables...))) +} + +// Adds a WHERE clause. See examples. +func (ud *UpdateDataset) Where(expressions ...exp.Expression) *UpdateDataset { + return ud.copy(ud.clauses.WhereAppend(expressions...)) +} + +// Removes the WHERE clause. See examples. +func (ud *UpdateDataset) ClearWhere() *UpdateDataset { + return ud.copy(ud.clauses.ClearWhere()) +} + +// Adds a ORDER clause. If the ORDER is currently set it replaces it. See examples. +func (ud *UpdateDataset) Order(order ...exp.OrderedExpression) *UpdateDataset { + return ud.copy(ud.clauses.SetOrder(order...)) +} + +// Adds a more columns to the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (ud *UpdateDataset) OrderAppend(order ...exp.OrderedExpression) *UpdateDataset { + return ud.copy(ud.clauses.OrderAppend(order...)) +} + +// Adds a more columns to the beginning of the current ORDER BY clause. If no order has be previously specified it is the same as +// calling Order. See examples. +func (ud *UpdateDataset) OrderPrepend(order ...exp.OrderedExpression) *UpdateDataset { + return ud.copy(ud.clauses.OrderPrepend(order...)) +} + +// Removes the ORDER BY clause. See examples. +func (ud *UpdateDataset) ClearOrder() *UpdateDataset { + return ud.copy(ud.clauses.ClearOrder()) +} + +// Adds a LIMIT clause. If the LIMIT is currently set it replaces it. See examples. +func (ud *UpdateDataset) Limit(limit uint) *UpdateDataset { + if limit > 0 { + return ud.copy(ud.clauses.SetLimit(limit)) + } + return ud.copy(ud.clauses.ClearLimit()) +} + +// Adds a LIMIT ALL clause. If the LIMIT is currently set it replaces it. See examples. +func (ud *UpdateDataset) LimitAll() *UpdateDataset { + return ud.copy(ud.clauses.SetLimit(L("ALL"))) +} + +// Removes the LIMIT clause. +func (ud *UpdateDataset) ClearLimit() *UpdateDataset { + return ud.copy(ud.clauses.ClearLimit()) +} + +// Adds a RETURNING clause to the dataset if the adapter supports it. See examples. +func (ud *UpdateDataset) Returning(returning ...interface{}) *UpdateDataset { + return ud.copy(ud.clauses.SetReturning(exp.NewColumnListExpression(returning...))) +} + +// Get any error that has been set or nil if no error has been set. +func (ud *UpdateDataset) Error() error { + return ud.err +} + +// Set an error on the dataset if one has not already been set. This error will be returned by a future call to Error +// or as part of ToSQL. This can be used by end users to record errors while building up queries without having to +// track those separately. +func (ud *UpdateDataset) SetError(err error) *UpdateDataset { + if ud.err == nil { + ud.err = err + } + + return ud +} + +// Generates an UPDATE sql statement, if Prepared has been called with true then the parameters will not be interpolated. +// See examples. +// +// Errors: +// - There is an error generating the SQL +func (ud *UpdateDataset) ToSQL() (sql string, params []interface{}, err error) { + return ud.updateSQLBuilder().ToSQL() +} + +// Appends this Dataset's UPDATE statement to the SQLBuilder +// This is used internally when using updates in CTEs +func (ud *UpdateDataset) AppendSQL(b sb.SQLBuilder) { + if ud.err != nil { + b.SetError(ud.err) + return + } + ud.dialect.ToUpdateSQL(b, ud.GetClauses()) +} + +func (ud *UpdateDataset) GetAs() exp.IdentifierExpression { + return nil +} + +func (ud *UpdateDataset) ReturnsColumns() bool { + return ud.clauses.HasReturning() +} + +// Generates the UPDATE sql, and returns an exec.QueryExecutor with the sql set to the UPDATE statement +// +// db.Update("test").Set(Record{"name":"Bob", update: time.Now()}).Executor() +func (ud *UpdateDataset) Executor() exec.QueryExecutor { + return ud.queryFactory.FromSQLBuilder(ud.updateSQLBuilder()) +} + +func (ud *UpdateDataset) updateSQLBuilder() sb.SQLBuilder { + buf := sb.NewSQLBuilder(ud.isPrepared.Bool()) + if ud.err != nil { + return buf.SetError(ud.err) + } + ud.dialect.ToUpdateSQL(buf, ud.clauses) + return buf +} diff --git a/update_dataset_example_test.go b/update_dataset_example_test.go new file mode 100644 index 0000000..8225b76 --- /dev/null +++ b/update_dataset_example_test.go @@ -0,0 +1,722 @@ +//nolint:lll // sql statements are long +package db_test + +import ( + dbsql "database/sql" + "fmt" + + dbv2 "git.fsdpf.net/go/db/v2" + _ "git.fsdpf.net/go/db/v2/dialect/mysql" + "git.fsdpf.net/go/db/v2/exp" +) + +func ExampleUpdate_withStruct() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdate_withOmitNilTag() { + type item struct { + FirstName string `db:"first_name" ff:"omitnil"` + LastName string `db:"last_name" ff:"omitnil"` + Address1 *string `db:"address1" ff:"omitnil"` + Address2 *string `db:"address2" ff:"omitnil"` + Address3 *string `db:"address3" ff:"omitnil"` + } + address1 := "113 Test Addr" + var emptyString string + sql, args, _ := dbv2.Update("items").Set( + item{ + FirstName: "Test First Name", + LastName: "", + Address1: &address1, + Address2: &emptyString, + Address3: nil, // will omit nil pointer + }, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address1"='113 Test Addr',"address2"='',"first_name"='Test First Name',"last_name"='' [] +} + +func ExampleUpdate_withOmitEmptyTag() { + type item struct { + FirstName string `db:"first_name" ff:"omitempty"` + LastName string `db:"last_name" ff:"omitempty"` + Address1 *string `db:"address1" ff:"omitempty"` + Address2 *string `db:"address2" ff:"omitempty"` + Address3 *string `db:"address3" ff:"omitempty"` + } + address1 := "114 Test Addr" + var emptyString string + sql, args, _ := dbv2.Update("items").Set( + item{ + FirstName: "Test First Name", + LastName: "", // will omit zero field + Address1: &address1, + Address2: &emptyString, + Address3: nil, // will omit nil pointer + }, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address1"='114 Test Addr',"address2"='',"first_name"='Test First Name' [] +} + +func ExampleUpdate_withOmitEmptyTag_valuer() { + type item struct { + FirstName dbsql.NullString `db:"first_name" ff:"omitempty"` + MiddleName dbsql.NullString `db:"middle_name" ff:"omitempty"` + LastName dbsql.NullString `db:"last_name" ff:"omitempty"` + Address1 *dbsql.NullString `db:"address1" ff:"omitempty"` + Address2 *dbsql.NullString `db:"address2" ff:"omitempty"` + Address3 *dbsql.NullString `db:"address3" ff:"omitempty"` + Address4 *dbsql.NullString `db:"address4" ff:"omitempty"` + } + query, args, _ := dbv2.Update("items").Set( + item{ + FirstName: dbsql.NullString{Valid: true, String: "Test First Name"}, + MiddleName: dbsql.NullString{Valid: true, String: ""}, + LastName: dbsql.NullString{}, // will omit zero valuer struct + Address1: &dbsql.NullString{Valid: true, String: "Test Address 1"}, + Address2: &dbsql.NullString{Valid: true, String: ""}, + Address3: &dbsql.NullString{}, + Address4: nil, // will omit nil pointer + }, + ).ToSQL() + fmt.Println(query, args) + + // Output: + // UPDATE "items" SET "address1"='Test Address 1',"address2"='',"address3"=NULL,"first_name"='Test First Name',"middle_name"='' [] +} + +func ExampleUpdate_withDbv2Record() { + sql, args, _ := dbv2.Update("items").Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdate_withMap() { + sql, args, _ := dbv2.Update("items").Set( + map[string]interface{}{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdate_withExpressions() { + sql, args, _ := dbv2.Update("items").Set([]exp.UpdateExpression{ + dbv2.C("name").Set("Test"), + dbv2.C("address").Set("111 Test Addr"), + }).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "name"='Test',"address"='111 Test Addr' [] +} + +func ExampleUpdate_withSkipUpdateTag() { + type item struct { + Address string `db:"address"` + Name string `db:"name" ff:"skipupdate"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr' [] +} + +func ExampleUpdateDataset_Executor() { + db := getDB() + update := db.Update("test_user"). + Where(dbv2.C("first_name").Eq("Bob")). + Set(dbv2.Record{"first_name": "Bobby"}). + Executor() + + if r, err := update.Exec(); err != nil { + fmt.Println(err.Error()) + } else { + c, _ := r.RowsAffected() + fmt.Printf("Updated %d users", c) + } + + // Output: + // Updated 1 users +} + +func ExampleUpdateDataset_Executor_returning() { + db := getDB() + var ids []int64 + update := db.Update("test_user"). + Set(dbv2.Record{"last_name": "ucon"}). + Where(dbv2.Ex{"last_name": "Yukon"}). + Returning("id"). + Executor() + if err := update.ScanVals(&ids); err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("Updated users with ids %+v", ids) + } + + // Output: + // Updated users with ids [1 2 3] +} + +func ExampleUpdateDataset_Returning() { + sql, _, _ := dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Returning("id"). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Returning(dbv2.T("test").All()). + ToSQL() + fmt.Println(sql) + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Returning("a", "b"). + ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "test" SET "foo"='bar' RETURNING "id" + // UPDATE "test" SET "foo"='bar' RETURNING "test".* + // UPDATE "test" SET "foo"='bar' RETURNING "a", "b" +} + +func ExampleUpdateDataset_With() { + sql, _, _ := dbv2.Update("test"). + With("some_vals(val)", dbv2.From().Select(dbv2.L("123"))). + Where(dbv2.C("val").Eq(dbv2.From("some_vals").Select("val"))). + Set(dbv2.Record{"name": "Test"}).ToSQL() + fmt.Println(sql) + + // Output: + // WITH some_vals(val) AS (SELECT 123) UPDATE "test" SET "name"='Test' WHERE ("val" IN (SELECT "val" FROM "some_vals")) +} + +func ExampleUpdateDataset_WithRecursive() { + sql, _, _ := dbv2.Update("nums"). + WithRecursive("nums(x)", dbv2.From().Select(dbv2.L("1").As("num")). + UnionAll(dbv2.From("nums"). + Select(dbv2.L("x+1").As("num")).Where(dbv2.C("x").Lt(5)))). + Set(dbv2.Record{"foo": dbv2.T("nums").Col("num")}). + ToSQL() + fmt.Println(sql) + // Output: + // WITH RECURSIVE nums(x) AS (SELECT 1 AS "num" UNION ALL (SELECT x+1 AS "num" FROM "nums" WHERE ("x" < 5))) UPDATE "nums" SET "foo"="nums"."num" +} + +func ExampleUpdateDataset_Limit() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Limit(10) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' LIMIT 10 +} + +func ExampleUpdateDataset_LimitAll() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + LimitAll() + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' LIMIT ALL +} + +func ExampleUpdateDataset_ClearLimit() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Limit(10) + sql, _, _ := ds.ClearLimit().ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' +} + +func ExampleUpdateDataset_Order() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' ORDER BY `a` ASC +} + +func ExampleUpdateDataset_OrderAppend() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Order(dbv2.C("a").Asc()) + sql, _, _ := ds.OrderAppend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' ORDER BY `a` ASC, `b` DESC NULLS LAST +} + +func ExampleUpdateDataset_OrderPrepend() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Order(dbv2.C("a").Asc()) + + sql, _, _ := ds.OrderPrepend(dbv2.C("b").Desc().NullsLast()).ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' ORDER BY `b` DESC NULLS LAST, `a` ASC +} + +func ExampleUpdateDataset_ClearOrder() { + ds := dbv2.Dialect("mysql"). + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Order(dbv2.C("a").Asc()) + sql, _, _ := ds.ClearOrder().ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `test` SET `foo`='bar' +} + +func ExampleUpdateDataset_From() { + ds := dbv2.Update("table_one"). + Set(dbv2.Record{"foo": dbv2.I("table_two.bar")}). + From("table_two"). + Where(dbv2.Ex{"table_one.id": dbv2.I("table_two.id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "table_one" SET "foo"="table_two"."bar" FROM "table_two" WHERE ("table_one"."id" = "table_two"."id") +} + +func ExampleUpdateDataset_From_postgres() { + dialect := dbv2.Dialect("postgres") + + ds := dialect.Update("table_one"). + Set(dbv2.Record{"foo": dbv2.I("table_two.bar")}). + From("table_two"). + Where(dbv2.Ex{"table_one.id": dbv2.I("table_two.id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "table_one" SET "foo"="table_two"."bar" FROM "table_two" WHERE ("table_one"."id" = "table_two"."id") +} + +func ExampleUpdateDataset_From_mysql() { + dialect := dbv2.Dialect("mysql") + + ds := dialect.Update("table_one"). + Set(dbv2.Record{"foo": dbv2.I("table_two.bar")}). + From("table_two"). + Where(dbv2.Ex{"table_one.id": dbv2.I("table_two.id")}) + + sql, _, _ := ds.ToSQL() + fmt.Println(sql) + // Output: + // UPDATE `table_one`,`table_two` SET `foo`=`table_two`.`bar` WHERE (`table_one`.`id` = `table_two`.`id`) +} + +func ExampleUpdateDataset_Where() { + // By default everything is anded together + sql, _, _ := dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use ExOr to get ORed expressions together + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql) + // You can use Or with Ex to Or multiple Ex maps together + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql) + // By default everything is anded together + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql) + // You can use a combination of Ors and Ands + sql, _, _ = dbv2.Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "test" SET "foo"='bar' WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // UPDATE "test" SET "foo"='bar' WHERE (("a" > 10) OR ("b" < 10) OR ("c" IS NULL) OR ("d" IN ('a', 'b', 'c'))) + // UPDATE "test" SET "foo"='bar' WHERE ((("a" > 10) AND ("b" < 10)) OR (("c" IS NULL) AND ("d" IN ('a', 'b', 'c')))) + // UPDATE "test" SET "foo"='bar' WHERE (("a" > 10) AND ("b" < 10) AND ("c" IS NULL) AND ("d" IN ('a', 'b', 'c'))) + // UPDATE "test" SET "foo"='bar' WHERE (("a" > 10) OR (("b" < 10) AND ("c" IS NULL))) +} + +func ExampleUpdateDataset_Where_prepared() { + // By default everything is anded together + sql, args, _ := dbv2.Update("test"). + Prepared(true). + Set(dbv2.Record{"foo": "bar"}). + Where(dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use ExOr to get ORed expressions together + sql, args, _ = dbv2.Update("test").Prepared(true). + Set(dbv2.Record{"foo": "bar"}). + Where(dbv2.ExOr{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + "c": nil, + "d": []string{"a", "b", "c"}, + }).ToSQL() + fmt.Println(sql, args) + // You can use Or with Ex to Or multiple Ex maps together + sql, args, _ = dbv2.Update("test").Prepared(true). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.Or( + dbv2.Ex{ + "a": dbv2.Op{"gt": 10}, + "b": dbv2.Op{"lt": 10}, + }, + dbv2.Ex{ + "c": nil, + "d": []string{"a", "b", "c"}, + }, + ), + ).ToSQL() + fmt.Println(sql, args) + // By default everything is anded together + sql, args, _ = dbv2.Update("test").Prepared(true). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.C("a").Gt(10), + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + dbv2.C("d").In("a", "b", "c"), + ).ToSQL() + fmt.Println(sql, args) + // You can use a combination of Ors and Ands + sql, args, _ = dbv2.Update("test").Prepared(true). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ).ToSQL() + fmt.Println(sql, args) + // Output: + // UPDATE "test" SET "foo"=? WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [bar 10 10 a b c] + // UPDATE "test" SET "foo"=? WHERE (("a" > ?) OR ("b" < ?) OR ("c" IS NULL) OR ("d" IN (?, ?, ?))) [bar 10 10 a b c] + // UPDATE "test" SET "foo"=? WHERE ((("a" > ?) AND ("b" < ?)) OR (("c" IS NULL) AND ("d" IN (?, ?, ?)))) [bar 10 10 a b c] + // UPDATE "test" SET "foo"=? WHERE (("a" > ?) AND ("b" < ?) AND ("c" IS NULL) AND ("d" IN (?, ?, ?))) [bar 10 10 a b c] + // UPDATE "test" SET "foo"=? WHERE (("a" > ?) OR (("b" < ?) AND ("c" IS NULL))) [bar 10 10] +} + +func ExampleUpdateDataset_ClearWhere() { + ds := dbv2. + Update("test"). + Set(dbv2.Record{"foo": "bar"}). + Where( + dbv2.Or( + dbv2.C("a").Gt(10), + dbv2.And( + dbv2.C("b").Lt(10), + dbv2.C("c").IsNull(), + ), + ), + ) + sql, _, _ := ds.ClearWhere().ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "test" SET "foo"='bar' +} + +func ExampleUpdateDataset_Table() { + ds := dbv2.Update("test") + sql, _, _ := ds.Table("test2").Set(dbv2.Record{"foo": "bar"}).ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "test2" SET "foo"='bar' +} + +func ExampleUpdateDataset_Table_aliased() { + ds := dbv2.Update("test") + sql, _, _ := ds.Table(dbv2.T("test").As("t")).Set(dbv2.Record{"foo": "bar"}).ToSQL() + fmt.Println(sql) + // Output: + // UPDATE "test" AS "t" SET "foo"='bar' +} + +func ExampleUpdateDataset_Set() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.Update("items").Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.Update("items").Set( + map[string]interface{}{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdateDataset_Set_struct() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdateDataset_Set_dbv2Record() { + sql, args, _ := dbv2.Update("items").Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdateDataset_Set_map() { + sql, args, _ := dbv2.Update("items").Set( + map[string]interface{}{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdateDataset_Set_withSkipUpdateTag() { + type item struct { + Address string `db:"address"` + Name string `db:"name" ff:"skipupdate"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr' [] +} + +func ExampleUpdateDataset_Set_withDefaultIfEmptyTag() { + type item struct { + Address string `db:"address"` + Name string `db:"name" ff:"defaultifempty"` + } + sql, args, _ := dbv2.Update("items").Set( + item{Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.Update("items").Set( + item{Name: "Bob Yukon", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"=DEFAULT [] + // UPDATE "items" SET "address"='111 Test Addr',"name"='Bob Yukon' [] +} + +func ExampleUpdateDataset_Set_withNoTags() { + type item struct { + Address string + Name string + } + sql, args, _ := dbv2.Update("items").Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] +} + +func ExampleUpdateDataset_Set_withEmbeddedStruct() { + type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` + } + type User struct { + Address + FirstName string + LastName string + } + ds := dbv2.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "address_state"='NY',"address_street"='111 Street',"firstname"='Greg',"lastname"='Farley' [] +} + +func ExampleUpdateDataset_Set_withIgnoredEmbedded() { + type Address struct { + Street string + State string + } + type User struct { + Address `db:"-"` + FirstName string + LastName string + } + ds := dbv2.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +} + +func ExampleUpdateDataset_Set_withNilEmbeddedPointer() { + type Address struct { + Street string + State string + } + type User struct { + *Address + FirstName string + LastName string + } + ds := dbv2.Update("user").Set( + User{FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +} + +func ExampleUpdateDataset_ToSQL_prepared() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + + sql, args, _ := dbv2.From("items").Prepared(true).Update().Set( + item{Name: "Test", Address: "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Prepared(true).Update().Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + sql, args, _ = dbv2.From("items").Prepared(true).Update().Set( + map[string]interface{}{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + // Output: + // UPDATE "items" SET "address"=?,"name"=? [111 Test Addr Test] + // UPDATE "items" SET "address"=?,"name"=? [111 Test Addr Test] + // UPDATE "items" SET "address"=?,"name"=? [111 Test Addr Test] +} + +func ExampleUpdateDataset_Prepared() { + sql, args, _ := dbv2.Update("items").Prepared(true).Set( + dbv2.Record{"name": "Test", "address": "111 Test Addr"}, + ).ToSQL() + fmt.Println(sql, args) + + // Output: + // UPDATE "items" SET "address"=?,"name"=? [111 Test Addr Test] +} diff --git a/update_dataset_test.go b/update_dataset_test.go new file mode 100644 index 0000000..6063270 --- /dev/null +++ b/update_dataset_test.go @@ -0,0 +1,527 @@ +package db_test + +import ( + "testing" + + dbv2 "git.fsdpf.net/go/db/v2" + "git.fsdpf.net/go/db/v2/exp" + "git.fsdpf.net/go/db/v2/internal/errors" + "git.fsdpf.net/go/db/v2/internal/sb" + "git.fsdpf.net/go/db/v2/mocks" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type ( + updateTestCase struct { + ds *dbv2.UpdateDataset + clauses exp.UpdateClauses + } + updateDatasetSuite struct { + suite.Suite + } +) + +func (uds *updateDatasetSuite) assertCases(cases ...updateTestCase) { + for _, s := range cases { + uds.Equal(s.clauses, s.ds.GetClauses()) + } +} + +func (uds *updateDatasetSuite) TestUpdate() { + ds := dbv2.Update("test") + uds.IsType(&dbv2.UpdateDataset{}, ds) + uds.Implements((*exp.Expression)(nil), ds) + uds.Implements((*exp.AppendableExpression)(nil), ds) +} + +func (uds *updateDatasetSuite) TestClone() { + ds := dbv2.Update("test") + uds.Equal(ds, ds.Clone()) +} + +func (uds *updateDatasetSuite) TestExpression() { + ds := dbv2.Update("test") + uds.Equal(ds, ds.Expression()) +} + +func (uds *updateDatasetSuite) TestDialect() { + ds := dbv2.Update("test") + uds.NotNil(ds.Dialect()) +} + +func (uds *updateDatasetSuite) TestWithDialect() { + ds := dbv2.Update("test") + md := new(mocks.SQLDialect) + ds = ds.SetDialect(md) + + dialect := dbv2.GetDialect("default") + dialectDs := ds.WithDialect("default") + uds.Equal(md, ds.Dialect()) + uds.Equal(dialect, dialectDs.Dialect()) +} + +func (uds *updateDatasetSuite) TestPrepared() { + ds := dbv2.Update("test") + preparedDs := ds.Prepared(true) + uds.True(preparedDs.IsPrepared()) + uds.False(ds.IsPrepared()) + // should apply the prepared to any datasets created from the root + uds.True(preparedDs.Where(dbv2.Ex{"a": 1}).IsPrepared()) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + // should be prepared by default + ds = dbv2.Update("test") + uds.True(ds.IsPrepared()) +} + +func (uds *updateDatasetSuite) TestGetClauses() { + ds := dbv2.Update("test") + ce := exp.NewUpdateClauses().SetTable(dbv2.I("test")) + uds.Equal(ce, ds.GetClauses()) +} + +func (uds *updateDatasetSuite) TestWith() { + from := dbv2.Update("cte") + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.With("test-cte", from), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(false, "test-cte", from)), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestWithRecursive() { + from := dbv2.Update("cte") + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.WithRecursive("test-cte", from), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + CommonTablesAppend(exp.NewCommonTableExpression(true, "test-cte", from)), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestTable() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Table("items2"), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items2")), + }, + updateTestCase{ + ds: bd.Table(dbv2.L("literal_table")), + clauses: exp.NewUpdateClauses().SetTable(dbv2.L("literal_table")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) + uds.PanicsWithValue(dbv2.ErrUnsupportedUpdateTableType, func() { + bd.Table(true) + }) +} + +func (uds *updateDatasetSuite) TestSet() { + type item struct { + Address string `db:"address"` + Name string `db:"name"` + } + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Set(item{Name: "Test", Address: "111 Test Addr"}), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetSetValues(item{Name: "Test", Address: "111 Test Addr"}), + }, + updateTestCase{ + ds: bd.Set(dbv2.Record{"name": "Test", "address": "111 Test Addr"}), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetSetValues(dbv2.Record{"name": "Test", "address": "111 Test Addr"}), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd.Set([]exp.UpdateExpression{ + dbv2.C("name").Set("Test"), + dbv2.C("address").Set("111 Test Addr"), + }), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetSetValues([]exp.UpdateExpression{ + dbv2.C("name").Set("Test"), + dbv2.C("address").Set("111 Test Addr"), + }), + }, + ) +} + +func (uds *updateDatasetSuite) TestFrom() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.From("other"), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetFrom(exp.NewColumnListExpression("other")), + }, + updateTestCase{ + ds: bd.From("other").From("other2"), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetFrom(exp.NewColumnListExpression("other2")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestWhere() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Where(dbv2.Ex{"a": 1}), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}), + }, + updateTestCase{ + ds: bd.Where(dbv2.Ex{"a": 1}).Where(dbv2.C("b").Eq("c")), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}).WhereAppend(dbv2.C("b").Eq("c")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestClearWhere() { + bd := dbv2.Update("items").Where(dbv2.Ex{"a": 1}) + uds.assertCases( + updateTestCase{ + ds: bd.ClearWhere(), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + WhereAppend(dbv2.Ex{"a": 1}), + }, + ) +} + +func (uds *updateDatasetSuite) TestOrder() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Order(dbv2.C("a").Desc()), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")).OrderAppend(dbv2.C("a").Desc()), + }, + updateTestCase{ + ds: bd.Order(dbv2.C("a").Desc()).Order(dbv2.C("b").Asc()), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("b").Asc()), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestOrderAppend() { + bd := dbv2.Update("items").Order(dbv2.C("a").Desc()) + uds.assertCases( + updateTestCase{ + ds: bd.OrderAppend(dbv2.C("b").Asc()), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("a").Desc()). + OrderAppend(dbv2.C("b").Asc()), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("a").Desc()), + }, + ) +} + +func (uds *updateDatasetSuite) TestOrderPrepend() { + bd := dbv2.Update("items").Order(dbv2.C("a").Desc()) + uds.assertCases( + updateTestCase{ + ds: bd.OrderPrepend(dbv2.C("b").Asc()), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("b").Asc()). + OrderAppend(dbv2.C("a").Desc()), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("a").Desc()), + }, + ) +} + +func (uds *updateDatasetSuite) TestClearOrder() { + bd := dbv2.Update("items").Order(dbv2.C("a").Desc()) + uds.assertCases( + updateTestCase{ + ds: bd.ClearOrder(), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + OrderAppend(dbv2.C("a").Desc()), + }, + ) +} + +func (uds *updateDatasetSuite) TestLimit() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Limit(10), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")).SetLimit(uint(10)), + }, + updateTestCase{ + ds: bd.Limit(0), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestLimitAll() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.LimitAll(), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")).SetLimit(dbv2.L("ALL")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestClearLimit() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.LimitAll().ClearLimit(), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd.Limit(10).ClearLimit(), + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestReturning() { + bd := dbv2.Update("items") + uds.assertCases( + updateTestCase{ + ds: bd.Returning("a", "b"), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("a", "b")), + }, + updateTestCase{ + ds: bd.Returning(), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + updateTestCase{ + ds: bd.Returning(nil), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression()), + }, + updateTestCase{ + ds: bd.Returning("a", "b").Returning("c"), + clauses: exp.NewUpdateClauses(). + SetTable(dbv2.C("items")). + SetReturning(exp.NewColumnListExpression("c")), + }, + updateTestCase{ + ds: bd, + clauses: exp.NewUpdateClauses().SetTable(dbv2.C("items")), + }, + ) +} + +func (uds *updateDatasetSuite) TestReturnsColumns() { + ds := dbv2.Update("test") + uds.False(ds.ReturnsColumns()) + uds.True(ds.Returning("foo", "bar").ReturnsColumns()) +} + +func (uds *updateDatasetSuite) TestToSQL() { + md := new(mocks.SQLDialect) + ds := dbv2.Update("test").SetDialect(md) + r := dbv2.Record{"c": "a"} + c := ds.GetClauses().SetSetValues(r) + sqlB := sb.NewSQLBuilder(false) + md.On("ToUpdateSQL", sqlB, c).Return(nil).Once() + updateSQL, args, err := ds.Set(r).ToSQL() + uds.Empty(updateSQL) + uds.Empty(args) + uds.Nil(err) + md.AssertExpectations(uds.T()) +} + +func (uds *updateDatasetSuite) TestToSQL_Prepared() { + md := new(mocks.SQLDialect) + ds := dbv2.Update("test").Prepared(true).SetDialect(md) + r := dbv2.Record{"c": "a"} + c := ds.GetClauses().SetSetValues(r) + sqlB := sb.NewSQLBuilder(true) + md.On("ToUpdateSQL", sqlB, c).Return(nil).Once() + updateSQL, args, err := ds.Set(dbv2.Record{"c": "a"}).ToSQL() + uds.Empty(updateSQL) + uds.Empty(args) + uds.Nil(err) + md.AssertExpectations(uds.T()) +} + +func (uds *updateDatasetSuite) TestToSQL_WithError() { + md := new(mocks.SQLDialect) + ds := dbv2.Update("test").SetDialect(md) + r := dbv2.Record{"c": "a"} + c := ds.GetClauses().SetSetValues(r) + sqlB := sb.NewSQLBuilder(false) + ee := errors.New("expected error") + md.On("ToUpdateSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(ee) + }).Once() + + updateSQL, args, err := ds.Set(dbv2.Record{"c": "a"}).ToSQL() + uds.Empty(updateSQL) + uds.Empty(args) + uds.Equal(ee, err) + md.AssertExpectations(uds.T()) +} + +func (uds *updateDatasetSuite) TestExecutor() { + mDB, _, err := sqlmock.New() + uds.NoError(err) + ds := dbv2.New("mock", mDB). + Update("items"). + Set(dbv2.Record{"address": "111 Test Addr", "name": "Test1"}). + Where(dbv2.C("name").IsNull()) + + updateSQL, args, err := ds.Executor().ToSQL() + uds.NoError(err) + uds.Empty(args) + uds.Equal(`UPDATE "items" SET "address"='111 Test Addr',"name"='Test1' WHERE ("name" IS NULL)`, updateSQL) + + updateSQL, args, err = ds.Prepared(true).Executor().ToSQL() + uds.NoError(err) + uds.Equal([]interface{}{"111 Test Addr", "Test1"}, args) + uds.Equal(`UPDATE "items" SET "address"=?,"name"=? WHERE ("name" IS NULL)`, updateSQL) + + defer dbv2.SetDefaultPrepared(false) + dbv2.SetDefaultPrepared(true) + + updateSQL, args, err = ds.Executor().ToSQL() + uds.NoError(err) + uds.Equal([]interface{}{"111 Test Addr", "Test1"}, args) + uds.Equal(`UPDATE "items" SET "address"=?,"name"=? WHERE ("name" IS NULL)`, updateSQL) +} + +func (uds *updateDatasetSuite) TestSetError() { + err1 := errors.New("error #1") + err2 := errors.New("error #2") + err3 := errors.New("error #3") + + // Verify initial error set/get works properly + md := new(mocks.SQLDialect) + ds := dbv2.Update("test").SetDialect(md) + ds = ds.SetError(err1) + uds.Equal(err1, ds.Error()) + sql, args, err := ds.ToSQL() + uds.Empty(sql) + uds.Empty(args) + uds.Equal(err1, err) + + // Repeated SetError calls on Dataset should not overwrite the original error + ds = ds.SetError(err2) + uds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + uds.Empty(sql) + uds.Empty(args) + uds.Equal(err1, err) + + // Builder functions should not lose the error + ds = ds.ClearLimit() + uds.Equal(err1, ds.Error()) + sql, args, err = ds.ToSQL() + uds.Empty(sql) + uds.Empty(args) + uds.Equal(err1, err) + + // Deeper errors inside SQL generation should still return original error + c := ds.GetClauses() + sqlB := sb.NewSQLBuilder(false) + md.On("ToUpdateSQL", sqlB, c).Run(func(args mock.Arguments) { + args.Get(0).(sb.SQLBuilder).SetError(err3) + }).Once() + + sql, args, err = ds.ToSQL() + uds.Empty(sql) + uds.Empty(args) + uds.Equal(err1, err) +} + +func TestUpdateDataset(t *testing.T) { + suite.Run(t, new(updateDatasetSuite)) +}