diff --git a/HISTORY.md b/HISTORY.md index 72179a03..2040f43c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +# v9.6.0 + +* [ADDED] Support for Lateral queries [#182](https://github.com/doug-martin/goqu/issues/182) + # v9.5.1 * [FIXED] WITH clause without a RETURNING clause will panic [#177](https://github.com/doug-martin/goqu/issues/177) diff --git a/dialect/sqlite3/sqlite3.go b/dialect/sqlite3/sqlite3.go index 6ad1149a..5f835c42 100644 --- a/dialect/sqlite3/sqlite3.go +++ b/dialect/sqlite3/sqlite3.go @@ -20,6 +20,7 @@ func DialectOptions() *goqu.SQLDialectOptions { opts.WrapCompoundsInParens = false opts.SupportsDistinctOn = false opts.SupportsWindowFunction = false + opts.SupportsLateral = false opts.PlaceHolderRune = '?' opts.IncludePlaceholderNum = false diff --git a/docs/expressions.md b/docs/expressions.md index 945db00a..c637e97f 100644 --- a/docs/expressions.md +++ b/docs/expressions.md @@ -12,7 +12,7 @@ * [`V`](#V) - An Value to be used in SQL. * [`And`](#and) - AND multiple expressions together. * [`Or`](#or) - OR multiple expressions together. -* [Complex Example] - Complex Example using most of the Expression DSL. +* [Complex Example](#complex) - Complex Example using most of the Expression DSL. The entry points for expressions are: diff --git a/docs/selecting.md b/docs/selecting.md index 30835603..c093f347 100644 --- a/docs/selecting.md +++ b/docs/selecting.md @@ -258,6 +258,39 @@ Output: SELECT * FROM (SELECT * FROM "test" WHERE ("age" > 10)) AS "test2" ``` +Lateral Query + +```go +maxEntry := goqu.From("entry"). + Select(goqu.MAX("int").As("max_int")). + Where(goqu.Ex{"time": goqu.Op{"lt": goqu.I("e.time")}}). + As("max_entry") + +maxId := goqu.From("entry"). + Select("id"). + Where(goqu.Ex{"int": goqu.I("max_entry.max_int")}). + As("max_id") + +ds := goqu. + Select("e.id", "max_entry.max_int", "max_id.id"). + From( + goqu.T("entry").As("e"), + goqu.Lateral(maxEntry), + goqu.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" [] +``` + **[`Join`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Join)** @@ -452,6 +485,38 @@ Output: SELECT * FROM "test" CROSS JOIN "test2" ``` +Join with a Lateral + +```go +maxEntry := goqu.From("entry"). + Select(goqu.MAX("int").As("max_int")). + Where(goqu.Ex{"time": goqu.Op{"lt": goqu.I("e.time")}}). + As("max_entry") + +maxId := goqu.From("entry"). + Select("id"). + Where(goqu.Ex{"int": goqu.I("max_entry.max_int")}). + As("max_id") + +ds := goqu. + Select("e.id", "max_entry.max_int", "max_id.id"). + From(goqu.T("entry").As("e")). + Join(goqu.Lateral(maxEntry), goqu.On(goqu.V(true))). + Join(goqu.Lateral(maxId), goqu.On(goqu.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] +``` + **[`Where`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Where)** @@ -1187,3 +1252,4 @@ fmt.Printf("\nIds := %+v", ids) + diff --git a/exp/exp.go b/exp/exp.go index 2b9e78c2..65d5fb50 100644 --- a/exp/exp.go +++ b/exp/exp.go @@ -316,6 +316,11 @@ type ( Condition() JoinCondition IsConditionEmpty() bool } + LateralExpression interface { + Expression + Aliaseable + Table() AppendableExpression + } // Expression for representing "literal" sql. // L("col = 1") -> col = 1) diff --git a/exp/lateral.go b/exp/lateral.go new file mode 100644 index 00000000..a4314089 --- /dev/null +++ b/exp/lateral.go @@ -0,0 +1,24 @@ +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 aliased(l, val) } diff --git a/exp/lateral_test.go b/exp/lateral_test.go new file mode 100644 index 00000000..314ae202 --- /dev/null +++ b/exp/lateral_test.go @@ -0,0 +1,35 @@ +package exp + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type lateralExpressionSuite struct { + suite.Suite +} + +func TestLateralExpressionSuite(t *testing.T) { + suite.Run(t, &lateralExpressionSuite{}) +} + +func (les *lateralExpressionSuite) TestClone() { + le := NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})), le.Clone()) +} + +func (les *lateralExpressionSuite) TestExpression() { + le := NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(le, le.Expression()) +} + +func (les *lateralExpressionSuite) TestLateral() { + le := NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{}), le.Table()) +} + +func (les *lateralExpressionSuite) TestAs() { + le := NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, []interface{}{})) + les.Equal(aliased(le, "foo"), le.As("foo")) +} diff --git a/expressions.go b/expressions.go index d229a9d5..1715fd15 100644 --- a/expressions.go +++ b/expressions.go @@ -283,3 +283,7 @@ func Star() exp.LiteralExpression { return exp.Star() } func Default() exp.LiteralExpression { return exp.Default() } + +func Lateral(table exp.AppendableExpression) exp.LateralExpression { + return exp.NewLateralExpression(table) +} diff --git a/expressions_example_test.go b/expressions_example_test.go index 7330ca08..8c0d92ef 100644 --- a/expressions_example_test.go +++ b/expressions_example_test.go @@ -1737,9 +1737,65 @@ func ExampleW() { Window(goqu.W("w").PartitionBy("a")) query, args, _ = ds.ToSQL() fmt.Println(query, args) - // Output + // 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 "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 := goqu.From("entry"). + Select(goqu.MAX("int").As("max_int")). + Where(goqu.Ex{"time": goqu.Op{"lt": goqu.I("e.time")}}). + As("max_entry") + + maxID := goqu.From("entry"). + Select("id"). + Where(goqu.Ex{"int": goqu.I("max_entry.max_int")}). + As("max_id") + + ds := goqu. + Select("e.id", "max_entry.max_int", "max_id.id"). + From( + goqu.T("entry").As("e"), + goqu.Lateral(maxEntry), + goqu.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 := goqu.From("entry"). + Select(goqu.MAX("int").As("max_int")). + Where(goqu.Ex{"time": goqu.Op{"lt": goqu.I("e.time")}}). + As("max_entry") + + maxID := goqu.From("entry"). + Select("id"). + Where(goqu.Ex{"int": goqu.I("max_entry.max_int")}). + As("max_id") + + ds := goqu. + Select("e.id", "max_entry.max_int", "max_id.id"). + From(goqu.T("entry").As("e")). + Join(goqu.Lateral(maxEntry), goqu.On(goqu.V(true))). + Join(goqu.Lateral(maxID), goqu.On(goqu.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] +} diff --git a/expressions_test.go b/expressions_test.go index b6cdf1e1..0057fdbe 100644 --- a/expressions_test.go +++ b/expressions_test.go @@ -169,6 +169,11 @@ func (ges *goquExpressionsSuite) TestDefault() { ges.Equal(exp.Default(), Default()) } +func (ges *goquExpressionsSuite) TestLateral() { + ds := From("test") + ges.Equal(exp.NewLateralExpression(ds), Lateral(ds)) +} + func TestGoquExpressions(t *testing.T) { suite.Run(t, new(goquExpressionsSuite)) } diff --git a/select_dataset_test.go b/select_dataset_test.go index 3d040752..f6d80803 100644 --- a/select_dataset_test.go +++ b/select_dataset_test.go @@ -33,18 +33,9 @@ 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) TestReturnsColumns() { + ds := Select(L("NOW()")) + sds.True(ds.ReturnsColumns()) } func (sds *selectDatasetSuite) TestClone() { diff --git a/sqlgen/expression_sql_generator.go b/sqlgen/expression_sql_generator.go index f3e255cf..ae7826d1 100644 --- a/sqlgen/expression_sql_generator.go +++ b/sqlgen/expression_sql_generator.go @@ -57,6 +57,10 @@ 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} } @@ -150,6 +154,8 @@ func (esg *expressionSQLGenerator) expressionSQL(b sb.SQLBuilder, expression exp 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: @@ -244,6 +250,15 @@ func (esg *expressionSQLGenerator) identifierExpressionSQL(b sb.SQLBuilder, iden } } +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() { diff --git a/sqlgen/expression_sql_generator_test.go b/sqlgen/expression_sql_generator_test.go index 33d797c2..a0a0e52b 100644 --- a/sqlgen/expression_sql_generator_test.go +++ b/sqlgen/expression_sql_generator_test.go @@ -1113,6 +1113,32 @@ func (esgs *expressionSQLGeneratorSuite) TestGenerate_IdentifierExpression() { ) } +func (esgs *expressionSQLGeneratorSuite) TestGenerate_LateralExpression() { + lateralExp := exp.NewLateralExpression(newTestAppendableExpression(`SELECT * FROM "test"`, emptyArgs, nil, nil)) + + do := DefaultDialectOptions() + esgs.assertCases( + NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, sql: `LATERAL (SELECT * FROM "test")`}, + expressionTestCase{val: lateralExp, sql: `LATERAL (SELECT * FROM "test")`, isPrepared: true}, + ) + + do = DefaultDialectOptions() + do.LateralFragment = []byte("lateral ") + esgs.assertCases( + NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, sql: `lateral (SELECT * FROM "test")`}, + expressionTestCase{val: lateralExp, sql: `lateral (SELECT * FROM "test")`, isPrepared: true}, + ) + do = DefaultDialectOptions() + do.SupportsLateral = false + esgs.assertCases( + NewExpressionSQLGenerator("test", do), + expressionTestCase{val: lateralExp, err: "goqu: dialect does not support lateral expressions [dialect=test]"}, + expressionTestCase{val: lateralExp, err: "goqu: dialect does not support lateral expressions [dialect=test]", isPrepared: true}, + ) +} + func (esgs *expressionSQLGeneratorSuite) TestGenerate_ExpressionMap() { re := regexp.MustCompile("(a|b)") esgs.assertCases( diff --git a/sqlgen/sql_dialect_options.go b/sqlgen/sql_dialect_options.go index f95a8b59..173e6c00 100644 --- a/sqlgen/sql_dialect_options.go +++ b/sqlgen/sql_dialect_options.go @@ -34,6 +34,8 @@ type ( 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 @@ -118,6 +120,8 @@ type ( 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")) @@ -384,6 +388,7 @@ func DefaultDialectOptions() *SQLDialectOptions { SupportsDistinctOn: true, WrapCompoundsInParens: true, SupportsWindowFunction: true, + SupportsLateral: true, SupportsMultipleUpdateTables: true, UseFromClauseForMultipleUpdateTables: true, @@ -423,6 +428,7 @@ func DefaultDialectOptions() *SQLDialectOptions { ForKeyShareFragment: []byte(" FOR KEY SHARE "), NowaitFragment: []byte("NOWAIT"), SkipLockedFragment: []byte("SKIP LOCKED"), + LateralFragment: []byte("LATERAL "), AsFragment: []byte(" AS "), AscFragment: []byte(" ASC"), DescFragment: []byte(" DESC"),