From 7e97acaf9c0f06372ddb1bdcb4e14f0aec09d2b1 Mon Sep 17 00:00:00 2001 From: doug-martin Date: Wed, 25 Sep 2019 18:01:35 +0530 Subject: [PATCH] [ADDED] Using Update, Insert, or Delete datasets in sub selects and CTEs #164 --- HISTORY.md | 4 + delete_dataset.go | 23 +++++ delete_dataset_test.go | 17 ++++ docs/selecting.md | 123 +++++++++++++++++++++++- exp/exp.go | 7 +- insert_dataset.go | 14 +++ insert_dataset_test.go | 7 ++ issues_test.go | 76 +++++++++++++++ select_dataset.go | 9 ++ select_dataset_example_test.go | 57 +++++++++++ select_dataset_test.go | 14 +++ sqlgen/delete_sql_generator_test.go | 2 +- sqlgen/expression_sql_generator.go | 21 ++-- sqlgen/expression_sql_generator_test.go | 61 +++++++----- sqlgen/insert_sql_generator_test.go | 6 +- sqlgen/select_sql_generator_test.go | 4 +- sqlgen/update_sql_generator_test.go | 2 +- update_dataset.go | 14 +++ update_dataset_test.go | 7 ++ 19 files changed, 428 insertions(+), 40 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 397fb8f7..f369d059 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +# v9.3.0 + +* [ADDED] Using Update, Insert, or Delete datasets in sub selects and CTEs [#164](https://github.com/doug-martin/goqu/issues/164) + # v9.2.0 * [ADDED] exec.Scanner: New exposed scanner supports iterative scanning [#157](https://github.com/doug-martin/goqu/pull/157) - [@akarl](https://github.com/akarl) diff --git a/delete_dataset.go b/delete_dataset.go index 957fe1d2..f57b1e5d 100644 --- a/delete_dataset.go +++ b/delete_dataset.go @@ -30,6 +30,15 @@ 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. @@ -197,6 +206,20 @@ 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) { + dd.dialect.ToDeleteSQL(b, dd.GetClauses()) +} + +func (dd *DeleteDataset) GetAs() exp.IdentifierExpression { + return nil +} + +func (dd *DeleteDataset) ReturnsColumns() bool { + return !dd.clauses.Returning().IsEmpty() +} + // Creates an QueryExecutor to execute the query. // db.Delete("test").Exec() // diff --git a/delete_dataset_test.go b/delete_dataset_test.go index c249ce40..4b53ed5d 100644 --- a/delete_dataset_test.go +++ b/delete_dataset_test.go @@ -49,6 +49,23 @@ func (dds *deleteDatasetSuite) TearDownSuite() { DeregisterDialect("order-on-delete") } +func (dds *deleteDatasetSuite) TestDelete() { + ds := Delete("test") + dds.IsType(&DeleteDataset{}, ds) + dds.Implements((*exp.Expression)(nil), ds) + dds.Implements((*exp.AppendableExpression)(nil), ds) +} + +func (dds *deleteDatasetSuite) TestClone() { + ds := Delete("test") + dds.Equal(ds.Clone(), ds) +} + +func (dds *deleteDatasetSuite) TestExpression() { + ds := Delete("test") + dds.Equal(ds.Expression(), ds) +} + func (dds *deleteDatasetSuite) TestDialect() { ds := Delete("test") dds.NotNil(ds.Dialect()) diff --git a/docs/selecting.md b/docs/selecting.md index c54c703a..d2b35d1a 100644 --- a/docs/selecting.md +++ b/docs/selecting.md @@ -12,6 +12,7 @@ * [`GroupBy`](#group_by) * [`Having`](#having) * [`Window`](#window) + * [`With`](#with) * [`SetError`](#seterror) * Executing Queries * [`ScanStructs`](#scan-structs) - Scans rows into a slice of structs @@ -613,13 +614,132 @@ Output: SELECT * FROM "test" GROUP BY "age" HAVING (SUM("income") > 1000) ``` + +**[`With`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.With)** + +To use CTEs in `SELECT` statements you can use the `With` method. + +Simple Example + +```go +sql, _, _ := goqu.From("one"). + With("one", goqu.From().Select(goqu.L("1"))). + Select(goqu.Star()). + ToSQL() +fmt.Println(sql) +``` + +Output: + +``` +WITH one AS (SELECT 1) SELECT * FROM "one" +``` + +Dependent `WITH` clauses: + +```go +sql, _, _ = goqu.From("derived"). + With("intermed", goqu.From("test").Select(goqu.Star()).Where(goqu.C("x").Gte(5))). + With("derived", goqu.From("intermed").Select(goqu.Star()).Where(goqu.C("x").Lt(10))). + Select(goqu.Star()). + ToSQL() +fmt.Println(sql) +``` + +Output: +``` +WITH intermed AS (SELECT * FROM "test" WHERE ("x" >= 5)), derived AS (SELECT * FROM "intermed" WHERE ("x" < 10)) SELECT * FROM "derived" +``` + +`WITH` clause with arguments + +```go +sql, _, _ = goqu.From("multi"). + With("multi(x,y)", goqu.From().Select(goqu.L("1"), goqu.L("2"))). + Select(goqu.C("x"), goqu.C("y")). + ToSQL() +fmt.Println(sql) +``` + +Output: +``` +WITH multi(x,y) AS (SELECT 1, 2) SELECT "x", "y" FROM "multi" +``` + +Using a `InsertDataset`. + +```go +insertDs := goqu.Insert("foo").Rows(goqu.Record{"user_id": 10}).Returning("id") + +ds := goqu.From("bar"). + With("ins", insertDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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] +``` + +Using an `UpdateDataset` + +```go +updateDs := goqu.Update("foo").Set(goqu.Record{"bar": "baz"}).Returning("id") + +ds := goqu.From("bar"). + With("upd", updateDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.I("ins.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] +``` + +Using a `DeleteDataset` + +```go +deleteDs := goqu.Delete("foo").Where(goqu.Ex{"bar": "baz"}).Returning("id") + +ds := goqu.From("bar"). + With("del", deleteDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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] +``` **[`Window Function`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Window)** **NOTE** currently only the `postgres`, `mysql8` (NOT `mysql`) and the default dialect support `Window Function` -To use windowing in select you can use the `Over` method on an `SQLFunction` +To use windowing in `SELECT` statements you can use the `Over` method on an `SQLFunction` ```go sql, _, _ := goqu.From("test").Select( @@ -916,3 +1036,4 @@ fmt.Printf("\nIds := %+v", ids) ``` + diff --git a/exp/exp.go b/exp/exp.go index fd447944..8f0c6978 100644 --- a/exp/exp.go +++ b/exp/exp.go @@ -143,7 +143,12 @@ type ( AppendableExpression interface { Expression AppendSQL(b sb.SQLBuilder) - GetClauses() SelectClauses + // 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" diff --git a/insert_dataset.go b/insert_dataset.go index 353af3f4..3f3ddc90 100644 --- a/insert_dataset.go +++ b/insert_dataset.go @@ -218,6 +218,20 @@ 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) { + id.dialect.ToInsertSQL(b, id.GetClauses()) +} + +func (id *InsertDataset) GetAs() exp.IdentifierExpression { + return nil +} + +func (id *InsertDataset) ReturnsColumns() bool { + return !id.clauses.Returning().IsEmpty() +} + // 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() // diff --git a/insert_dataset_test.go b/insert_dataset_test.go index 08a2499c..f9d27e00 100644 --- a/insert_dataset_test.go +++ b/insert_dataset_test.go @@ -30,6 +30,13 @@ func (ids *insertDatasetSuite) assertCases(cases ...insertTestCase) { } } +func (ids *insertDatasetSuite) TestInsert() { + ds := Insert("test") + ids.IsType(&InsertDataset{}, ds) + ids.Implements((*exp.Expression)(nil), ds) + ids.Implements((*exp.AppendableExpression)(nil), ds) +} + func (ids *insertDatasetSuite) TestClone() { ds := Insert("test") ids.Equal(ds.Clone(), ds) diff --git a/issues_test.go b/issues_test.go index 16501a36..25e05abe 100644 --- a/issues_test.go +++ b/issues_test.go @@ -248,6 +248,82 @@ func (gis *githubIssuesSuite) TestIssue140() { } +// Test for https://github.com/doug-martin/goqu/issues/164 +func (gis *githubIssuesSuite) TestIssue164() { + insertDs := goqu.Insert("foo").Rows(goqu.Record{"user_id": 10}).Returning("id") + + ds := goqu.From("bar"). + With("ins", insertDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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 := goqu.Update("foo").Set(goqu.Record{"bar": "baz"}).Returning("id") + + ds = goqu.From("bar"). + With("upd", updateDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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 := goqu.Delete("foo").Where(goqu.Ex{"bar": "baz"}).Returning("id") + + ds = goqu.From("bar"). + With("del", deleteDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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, + ) +} + func TestGithubIssuesSuite(t *testing.T) { suite.Run(t, new(githubIssuesSuite)) } diff --git a/select_dataset.go b/select_dataset.go index 65375d96..ede6b452 100644 --- a/select_dataset.go +++ b/select_dataset.go @@ -498,6 +498,11 @@ 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)) @@ -552,6 +557,10 @@ func (sd *SelectDataset) AppendSQL(b sb.SQLBuilder) { 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 diff --git a/select_dataset_example_test.go b/select_dataset_example_test.go index cce3d102..33d59ce3 100644 --- a/select_dataset_example_test.go +++ b/select_dataset_example_test.go @@ -1,3 +1,4 @@ +// nolint:lll package goqu_test import ( @@ -178,6 +179,62 @@ func ExampleSelectDataset_With() { // WITH multi(x,y) AS (SELECT 1, 2) SELECT "x", "y" FROM "multi" } +func ExampleSelectDataset_With_insertDataset() { + insertDs := goqu.Insert("foo").Rows(goqu.Record{"user_id": 10}).Returning("id") + + ds := goqu.From("bar"). + With("ins", insertDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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 := goqu.Update("foo").Set(goqu.Record{"bar": "baz"}).Returning("id") + + ds := goqu.From("bar"). + With("upd", updateDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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 := goqu.Delete("foo").Where(goqu.Ex{"bar": "baz"}).Returning("id") + + ds := goqu.From("bar"). + With("del", deleteDs). + Select("bar_name"). + Where(goqu.Ex{"bar.user_id": goqu.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, _, _ := goqu.From("nums"). WithRecursive("nums(x)", diff --git a/select_dataset_test.go b/select_dataset_test.go index 161976d7..3d040752 100644 --- a/select_dataset_test.go +++ b/select_dataset_test.go @@ -33,6 +33,20 @@ func (sds *selectDatasetSuite) assertCases(cases ...selectTestCase) { } } +func (ids *insertDatasetSuite) TestFrom() { + ds := From("test") + ids.IsType(&SelectDataset{}, ds) + ids.Implements((*exp.Expression)(nil), ds) + ids.Implements((*exp.AppendableExpression)(nil), ds) +} + +func (ids *insertDatasetSuite) TestSelect() { + ds := Select(L("NoW()")) + ids.IsType(&SelectDataset{}, ds) + ids.Implements((*exp.Expression)(nil), ds) + ids.Implements((*exp.AppendableExpression)(nil), ds) +} + func (sds *selectDatasetSuite) TestClone() { ds := From("test") sds.Equal(ds, ds.Clone()) diff --git a/sqlgen/delete_sql_generator_test.go b/sqlgen/delete_sql_generator_test.go index 6582ffca..fc27344c 100644 --- a/sqlgen/delete_sql_generator_test.go +++ b/sqlgen/delete_sql_generator_test.go @@ -109,7 +109,7 @@ func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withCommonTables() { opts.WithFragment = []byte("with ") opts.RecursiveFragment = []byte("recursive ") - tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil, true) dc := exp.NewDeleteClauses().SetFrom(exp.NewIdentifierExpression("", "test_cte", "")) dcCte1 := dc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) diff --git a/sqlgen/expression_sql_generator.go b/sqlgen/expression_sql_generator.go index ee4ac7ae..4fb6de62 100644 --- a/sqlgen/expression_sql_generator.go +++ b/sqlgen/expression_sql_generator.go @@ -34,8 +34,11 @@ var ( 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`) + errEmptyIdentifier = errors.New( + `a empty identifier was encountered, please specify a "schema", "table" or "column"`, + ) + errUnexpectedNamedWindow = errors.New(`unexpected named window function`) + errNoReturnColumnsForAppendableExpression = errors.New(`no return columns found for appendable expression`) ) func errUnsupportedExpressionType(e exp.Expression) error { @@ -191,16 +194,16 @@ func (esg *expressionSQLGenerator) placeHolderSQL(b sb.SQLBuilder, i interface{} // Generates creates the sql for a sub select on a Dataset func (esg *expressionSQLGenerator) appendableExpressionSQL(b sb.SQLBuilder, a exp.AppendableExpression) { + if !a.ReturnsColumns() { + b.SetError(errNoReturnColumnsForAppendableExpression) + return + } b.WriteRunes(esg.dialectOptions.LeftParenRune) a.AppendSQL(b) b.WriteRunes(esg.dialectOptions.RightParenRune) - c := a.GetClauses() - if c != nil { - alias := c.Alias() - if alias != nil { - b.Write(esg.dialectOptions.AsFragment) - esg.Generate(b, alias) - } + if a.GetAs() != nil { + b.Write(esg.dialectOptions.AsFragment) + esg.Generate(b, a.GetAs()) } } diff --git a/sqlgen/expression_sql_generator_test.go b/sqlgen/expression_sql_generator_test.go index 9446ceb9..f6cc9bea 100644 --- a/sqlgen/expression_sql_generator_test.go +++ b/sqlgen/expression_sql_generator_test.go @@ -16,32 +16,38 @@ import ( var emptyArgs = make([]interface{}, 0) type testAppendableExpression struct { - exp.AppendableExpression - sql string - args []interface{} - err error - clauses exp.SelectClauses + sql string + args []interface{} + err error + alias exp.IdentifierExpression + returnsColumns bool } -func newTestAppendableExpression(sql string, args []interface{}, err error, clauses exp.SelectClauses) exp.AppendableExpression { - if clauses == nil { - clauses = exp.NewSelectClauses() - } - return &testAppendableExpression{sql: sql, args: args, err: err, clauses: clauses} +func newTestAppendableExpression( + sql string, + args []interface{}, + err error, + alias exp.IdentifierExpression, + returnsColumns bool) exp.AppendableExpression { + return &testAppendableExpression{sql: sql, args: args, err: err, alias: alias, returnsColumns: returnsColumns} } 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 } +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) @@ -250,6 +256,9 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_TimeTypes() { NewExpressionSQLGenerator("test", 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}}, ) SetTimeLocation(time.UTC) // utc time @@ -257,6 +266,9 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_TimeTypes() { NewExpressionSQLGenerator("test", 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( NewExpressionSQLGenerator("test", DefaultDialectOptions()), @@ -338,10 +350,12 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerateUnsupportedExpression() { func (esgs *expressionSQLGeneratorSuite) TestGenerate_AppendableExpression() { ti := exp.NewIdentifierExpression("", "b", "") - a := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, nil) - aliasedA := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, exp.NewSelectClauses().SetAlias(ti)) - argsA := newTestAppendableExpression(`select * from "a" where x=?`, []interface{}{true}, nil, exp.NewSelectClauses().SetAlias(ti)) - ae := newTestAppendableExpression(`select * from "a"`, emptyArgs, errors.New("expected error"), nil) + a := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, nil, true) + aliasedA := newTestAppendableExpression(`select * from "a"`, []interface{}{}, nil, ti, true) + argsA := newTestAppendableExpression(`select * from "a" where x=?`, []interface{}{true}, nil, ti, true) + ae := newTestAppendableExpression(`select * from "a"`, emptyArgs, errors.New("expected error"), nil, true) + + aenr := newTestAppendableExpression(`update "foo" set "a"='b'`, emptyArgs, nil, nil, false) esgs.assertCases( NewExpressionSQLGenerator("test", DefaultDialectOptions()), @@ -354,6 +368,9 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_AppendableExpression() { expressionTestCase{val: ae, err: "goqu: expected error"}, expressionTestCase{val: ae, err: "goqu: expected error", isPrepared: true}, + expressionTestCase{val: aenr, err: errNoReturnColumnsForAppendableExpression.Error()}, + expressionTestCase{val: aenr, err: errNoReturnColumnsForAppendableExpression.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}}, ) @@ -458,7 +475,7 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_AliasedExpression() { } func (esgs *expressionSQLGeneratorSuite) TestGenerate_BooleanExpression() { - ae := newTestAppendableExpression(`SELECT "id" FROM "test2"`, emptyArgs, nil, nil) + ae := newTestAppendableExpression(`SELECT "id" FROM "test2"`, emptyArgs, nil, nil, true) re := regexp.MustCompile("(a|b)") ident := exp.NewIdentifierExpression("", "", "a") @@ -845,7 +862,7 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_CastExpression() { // 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) + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil, true) cteNoArgs := []exp.CommonTableExpression{ exp.NewCommonTableExpression(false, "a", ae), @@ -944,7 +961,7 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_CommonTableExpressionSlice } func (esgs *expressionSQLGeneratorSuite) TestGenerate_CommonTableExpression() { - ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil) + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil, true) cteNoArgs := exp.NewCommonTableExpression(false, "a", ae) cteArgs := exp.NewCommonTableExpression(false, "a(x,y)", ae) @@ -969,7 +986,7 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_CommonTableExpression() { } func (esgs *expressionSQLGeneratorSuite) TestGenerate_CompoundExpression() { - ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil) + ae := newTestAppendableExpression(`SELECT * FROM "b"`, emptyArgs, nil, nil, true) u := exp.NewCompoundExpression(exp.UnionCompoundType, ae) ua := exp.NewCompoundExpression(exp.UnionAllCompoundType, ae) diff --git a/sqlgen/insert_sql_generator_test.go b/sqlgen/insert_sql_generator_test.go index 8f3e4286..ef31871a 100644 --- a/sqlgen/insert_sql_generator_test.go +++ b/sqlgen/insert_sql_generator_test.go @@ -215,7 +215,7 @@ func (igs *insertSQLGeneratorSuite) TestGenerate_withEmptyRows() { func (igs *insertSQLGeneratorSuite) TestGenerate_withRowsAppendableExpression() { ic := exp.NewInsertClauses(). SetInto(exp.NewIdentifierExpression("", "test", "")). - SetRows([]interface{}{newTestAppendableExpression(`select * from "other"`, emptyArgs, nil, nil)}) + SetRows([]interface{}{newTestAppendableExpression(`select * from "other"`, emptyArgs, nil, nil, true)}) igs.assertCases( NewInsertSQLGenerator("test", DefaultDialectOptions()), @@ -227,7 +227,7 @@ func (igs *insertSQLGeneratorSuite) TestGenerate_withRowsAppendableExpression() 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)) + SetFrom(newTestAppendableExpression(`select c, d from test where a = 'b'`, nil, nil, nil, true)) icCols := ic.SetCols(exp.NewColumnListExpression("a", "b")) igs.assertCases( @@ -372,7 +372,7 @@ func (igs *insertSQLGeneratorSuite) TestGenerate_withCommonTables() { opts.WithFragment = []byte("with ") opts.RecursiveFragment = []byte("recursive ") - tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil, true) ic := exp.NewInsertClauses().SetInto(exp.NewIdentifierExpression("", "test_cte", "")) icCte1 := ic.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) diff --git a/sqlgen/select_sql_generator_test.go b/sqlgen/select_sql_generator_test.go index 6e0c73ca..b46645f5 100644 --- a/sqlgen/select_sql_generator_test.go +++ b/sqlgen/select_sql_generator_test.go @@ -471,7 +471,7 @@ func (ssgs *selectSQLGeneratorSuite) TestGenerate_withOffset() { func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCommonTables() { - tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil, true) sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test_cte")) scCte1 := sc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse)) @@ -489,7 +489,7 @@ func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCommonTables() { } func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCompounds() { - tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil, true) sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")). CompoundsAppend(exp.NewCompoundExpression(exp.UnionCompoundType, tse)). CompoundsAppend(exp.NewCompoundExpression(exp.IntersectCompoundType, tse)) diff --git a/sqlgen/update_sql_generator_test.go b/sqlgen/update_sql_generator_test.go index 5b6527dd..bb438ddc 100644 --- a/sqlgen/update_sql_generator_test.go +++ b/sqlgen/update_sql_generator_test.go @@ -222,7 +222,7 @@ func (usgs *updateSQLGeneratorSuite) TestGenerate_withLimit() { } func (usgs *updateSQLGeneratorSuite) TestGenerate_withCommonTables() { - tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil) + tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil, true) uc := exp.NewUpdateClauses(). SetTable(exp.NewIdentifierExpression("", "test_cte", "")). SetSetValues(exp.Record{"a": "b", "b": "c"}) diff --git a/update_dataset.go b/update_dataset.go index 0e46f830..2d24d732 100644 --- a/update_dataset.go +++ b/update_dataset.go @@ -211,6 +211,20 @@ 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) { + ud.dialect.ToUpdateSQL(b, ud.GetClauses()) +} + +func (ud *UpdateDataset) GetAs() exp.IdentifierExpression { + return nil +} + +func (ud *UpdateDataset) ReturnsColumns() bool { + return !ud.clauses.Returning().IsEmpty() +} + // 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 { diff --git a/update_dataset_test.go b/update_dataset_test.go index 7c5468ef..2bbc4046 100644 --- a/update_dataset_test.go +++ b/update_dataset_test.go @@ -29,6 +29,13 @@ func (uds *updateDatasetSuite) assertCases(cases ...updateTestCase) { } } +func (uds *updateDatasetSuite) TestUpdate() { + ds := Update("test") + uds.IsType(&UpdateDataset{}, ds) + uds.Implements((*exp.Expression)(nil), ds) + uds.Implements((*exp.AppendableExpression)(nil), ds) +} + func (uds *updateDatasetSuite) TestClone() { ds := Update("test") uds.Equal(ds, ds.Clone())