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"),