Skip to content

Commit

Permalink
Merge pull request #192 from doug-martin/v9.6.0-rc
Browse files Browse the repository at this point in the history
V9.6.0 rc
  • Loading branch information
doug-martin committed Dec 16, 2019
2 parents 1dd9123 + d04abcc commit 751dbc6
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 15 deletions.
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
1 change: 1 addition & 0 deletions dialect/sqlite3/sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
66 changes: 66 additions & 0 deletions docs/selecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" []
```

<a name="joins"></a>
**[`Join`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Join)**

Expand Down Expand Up @@ -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]
```

<a name="where"></a>
**[`Where`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Where)**

Expand Down Expand Up @@ -1187,3 +1252,4 @@ fmt.Printf("\nIds := %+v", ids)




5 changes: 5 additions & 0 deletions exp/exp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions exp/lateral.go
Original file line number Diff line number Diff line change
@@ -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) }
35 changes: 35 additions & 0 deletions exp/lateral_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
4 changes: 4 additions & 0 deletions expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
60 changes: 58 additions & 2 deletions expressions_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
5 changes: 5 additions & 0 deletions expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
15 changes: 3 additions & 12 deletions select_dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
15 changes: 15 additions & 0 deletions sqlgen/expression_sql_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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() {
Expand Down
26 changes: 26 additions & 0 deletions sqlgen/expression_sql_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions sqlgen/sql_dialect_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -384,6 +388,7 @@ func DefaultDialectOptions() *SQLDialectOptions {
SupportsDistinctOn: true,
WrapCompoundsInParens: true,
SupportsWindowFunction: true,
SupportsLateral: true,

SupportsMultipleUpdateTables: true,
UseFromClauseForMultipleUpdateTables: true,
Expand Down Expand Up @@ -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"),
Expand Down

0 comments on commit 751dbc6

Please sign in to comment.