diff --git a/dialect/sqlite3/sqlite3.go b/dialect/sqlite3/sqlite3.go index 48d434f2..1afc842f 100644 --- a/dialect/sqlite3/sqlite3.go +++ b/dialect/sqlite3/sqlite3.go @@ -23,6 +23,7 @@ func DialectOptions() *goqu.SQLDialectOptions { opts.SupportsDistinctOn = false opts.SupportsWindowFunction = false opts.SupportsLateral = false + opts.SupportsQualify = false opts.PlaceHolderFragment = []byte("?") opts.IncludePlaceholderNum = false diff --git a/docs/selecting.md b/docs/selecting.md index f35f2aa1..9139aa75 100644 --- a/docs/selecting.md +++ b/docs/selecting.md @@ -680,6 +680,22 @@ Output: SELECT * FROM "test" GROUP BY "age" HAVING (SUM("income") > 1000) ``` + + +**[`Qualify`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.Qualify)** + +```go +sql, _, _ = goqu.From("test").GroupBy("age").Qualify(goqu.SUM("income").Gt(1000)).ToSQL() +fmt.Println(sql) +``` + +Output: + +``` +SELECT * FROM "test" GROUP BY "age" Qualify (SUM("income") > 1000) +``` + + **[`With`](https://godoc.org/github.com/doug-martin/goqu/#SelectDataset.With)** diff --git a/exp/select_clauses.go b/exp/select_clauses.go index f802a88b..3e1aeba1 100644 --- a/exp/select_clauses.go +++ b/exp/select_clauses.go @@ -64,6 +64,10 @@ type ( SetWindows(ws []WindowExpression) SelectClauses WindowsAppend(ws ...WindowExpression) SelectClauses ClearWindows() SelectClauses + + Qualify() ExpressionList + ClearQualify() SelectClauses + QualifyAppend(expressions ...Expression) SelectClauses } selectClauses struct { commonTables []CommonTableExpression @@ -81,6 +85,7 @@ type ( compounds []CompoundExpression lock Lock windows []WindowExpression + qualify ExpressionList } ) @@ -124,6 +129,7 @@ func (c *selectClauses) clone() *selectClauses { compounds: c.compounds, lock: c.lock, windows: c.windows, + qualify: c.qualify, } } @@ -243,6 +249,29 @@ func (c *selectClauses) HavingAppend(expressions ...Expression) SelectClauses { return ret } +func (c *selectClauses) Qualify() ExpressionList { + return c.qualify +} + +func (c *selectClauses) ClearQualify() SelectClauses { + ret := c.clone() + ret.qualify = nil + return ret +} + +func (c *selectClauses) QualifyAppend(expressions ...Expression) SelectClauses { + if len(expressions) == 0 { + return c + } + ret := c.clone() + if ret.qualify == nil { + ret.qualify = NewExpressionList(AndType, expressions...) + } else { + ret.qualify = ret.qualify.Append(expressions...) + } + return ret +} + func (c *selectClauses) Lock() Lock { return c.lock } diff --git a/exp/select_clauses_test.go b/exp/select_clauses_test.go index 8bfe252f..e8e6fcf1 100644 --- a/exp/select_clauses_test.go +++ b/exp/select_clauses_test.go @@ -271,6 +271,46 @@ func (scs *selectClausesSuite) TestHavingAppend() { scs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Having()) } +func (scs *selectClausesSuite) TestQualify() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses() + c2 := c.QualifyAppend(w) + + scs.Nil(c.Qualify()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Qualify()) +} + +func (scs *selectClausesSuite) TestClearQualify() { + w := exp.Ex{"a": 1} + + c := exp.NewSelectClauses().QualifyAppend(w) + c2 := c.ClearQualify() + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c.Qualify()) + + scs.Nil(c2.Qualify()) +} + +func (scs *selectClausesSuite) TestQualifyAppend() { + w := exp.Ex{"a": 1} + w2 := exp.Ex{"b": 2} + + c := exp.NewSelectClauses() + c2 := c.QualifyAppend(w) + + c3 := c.QualifyAppend(w).QualifyAppend(w2) + + c4 := c.QualifyAppend(w, w2) + + scs.Nil(c.Qualify()) + + scs.Equal(exp.NewExpressionList(exp.AndType, w), c2.Qualify()) + scs.Equal(exp.NewExpressionList(exp.AndType, w).Append(w2), c3.Qualify()) + scs.Equal(exp.NewExpressionList(exp.AndType, w, w2), c4.Qualify()) +} + func (scs *selectClausesSuite) TestWindows() { w := exp.NewWindowExpression(exp.NewIdentifierExpression("", "", "w"), nil, nil, nil) diff --git a/select_dataset.go b/select_dataset.go index aa6f1924..5fb0cc91 100644 --- a/select_dataset.go +++ b/select_dataset.go @@ -402,6 +402,11 @@ func (sd *SelectDataset) Having(expressions ...exp.Expression) *SelectDataset { return sd.copy(sd.clauses.HavingAppend(expressions...)) } +// Adds a QUALIFY clause. See examples. +func (sd *SelectDataset) Qualify(expressions ...exp.Expression) *SelectDataset { + return sd.copy(sd.clauses.QualifyAppend(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...)) diff --git a/select_dataset_example_test.go b/select_dataset_example_test.go index cb5c2a2e..5fa2d522 100644 --- a/select_dataset_example_test.go +++ b/select_dataset_example_test.go @@ -449,6 +449,20 @@ func ExampleSelectDataset_Having() { // SELECT * FROM "test" GROUP BY "age" HAVING (SUM("income") > 1000) } +func ExampleSelectDataset_Qualify() { + opts := goqu.DefaultDialectOptions() + opts.SupportsQualify = true + goqu.RegisterDialect("qualify", opts) + var dialect = goqu.Dialect("qualify") + sql, _, _ := dialect.From("test").Qualify(goqu.SUM("income").Gt(1000)).ToSQL() + fmt.Println(sql) + sql, _, _ = dialect.From("test").GroupBy("age").Qualify(goqu.SUM("income").Gt(1000)).ToSQL() + fmt.Println(sql) + // Output: + // SELECT * FROM "test" QUALIFY (SUM("income") > 1000) + // SELECT * FROM "test" GROUP BY "age" QUALIFY (SUM("income") > 1000) +} + func ExampleSelectDataset_Window() { ds := goqu.From("test"). Select(goqu.ROW_NUMBER().Over(goqu.W().PartitionBy("a").OrderBy(goqu.I("b").Asc()))) diff --git a/sqlgen/select_sql_generator.go b/sqlgen/select_sql_generator.go index de322910..8d280ed6 100644 --- a/sqlgen/select_sql_generator.go +++ b/sqlgen/select_sql_generator.go @@ -37,6 +37,10 @@ func ErrWindowNotSupported(dialect string) error { return errors.New("dialect does not support WINDOW clause [dialect=%s]", dialect) } +func ErrQualifyNotSupported(dialect string) error { + return errors.New("dialect does not support QUALIFY clause [dialect=%s]", dialect) +} + var ErrNoWindowName = errors.New("window expresion has no valid name") func NewSelectSQLGenerator(dialect string, do *SQLDialectOptions) SelectSQLGenerator { @@ -65,6 +69,8 @@ func (ssg *selectSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.SelectClaus ssg.GroupBySQL(b, clauses.GroupBy()) case HavingSQLFragment: ssg.HavingSQL(b, clauses.Having()) + case QualifySQLFragment: + ssg.QualifySQL(b, clauses.Qualify()) case WindowSQLFragment: ssg.WindowSQL(b, clauses.Windows()) case CompoundsSQLFragment: @@ -164,6 +170,18 @@ func (ssg *selectSQLGenerator) HavingSQL(b sb.SQLBuilder, having exp.ExpressionL } } +// Generates the QUALIFY clause for an SQL statement +func (ssg *selectSQLGenerator) QualifySQL(b sb.SQLBuilder, qualify exp.ExpressionList) { + if qualify != nil && len(qualify.Expressions()) > 0 { + if ssg.DialectOptions().SupportsQualify { + b.Write(ssg.DialectOptions().QualifyFragment) + ssg.ExpressionSQLGenerator().Generate(b, qualify) + } else { + b.SetError(ErrQualifyNotSupported(ssg.Dialect())) + } + } +} + // Generates the OFFSET clause for an SQL statement func (ssg *selectSQLGenerator) OffsetSQL(b sb.SQLBuilder, offset uint) { if offset > 0 { diff --git a/sqlgen/sql_dialect_options.go b/sqlgen/sql_dialect_options.go index 3d9a981b..44e0c327 100644 --- a/sqlgen/sql_dialect_options.go +++ b/sqlgen/sql_dialect_options.go @@ -38,6 +38,8 @@ type ( SupportsDistinctOn bool // Set to true if LATERAL queries are supported (DEFAULT=true) SupportsLateral bool + // Set to true if the dialect supports QUALIFY expressions (DEFAULT=false) + SupportsQualify bool // Set to false if the dialect does not require expressions to be wrapped in parens (DEFAULT=true) WrapCompoundsInParens bool @@ -97,6 +99,8 @@ type ( GroupByFragment []byte // The SQL HAVING clause fragment(DEFAULT=[]byte(" HAVING ")) HavingFragment []byte + // The SQL QUALIFY clause fragment(DEFAULT=[]byte(" QUALIFY ")) + QualifyFragment []byte // The SQL WINDOW clause fragment(DEFAULT=[]byte(" WINDOW ")) WindowFragment []byte // The SQL WINDOW clause PARTITION BY fragment(DEFAULT=[]byte("PARTITION BY ")) @@ -276,6 +280,7 @@ type ( // WhereSQLFragment, // GroupBySQLFragment, // HavingSQLFragment, + // QualifySQLFragment, // CompoundsSQLFragment, // OrderSQLFragment, // LimitSQLFragment, @@ -336,6 +341,7 @@ const ( WhereSQLFragment GroupBySQLFragment HavingSQLFragment + QualifySQLFragment CompoundsSQLFragment OrderSQLFragment OrderWithOffsetFetchSQLFragment @@ -372,6 +378,8 @@ func (sf SQLFragmentType) String() string { return "GroupBySQLFragment" case HavingSQLFragment: return "HavingSQLFragment" + case QualifySQLFragment: + return "QualifySQLFragment" case CompoundsSQLFragment: return "CompoundsSQLFragment" case OrderSQLFragment: @@ -424,6 +432,7 @@ func DefaultDialectOptions() *SQLDialectOptions { WrapCompoundsInParens: true, SupportsWindowFunction: true, SupportsLateral: true, + SupportsQualify: false, SupportsMultipleUpdateTables: true, UseFromClauseForMultipleUpdateTables: true, @@ -451,6 +460,7 @@ func DefaultDialectOptions() *SQLDialectOptions { GroupByFragment: []byte(" GROUP BY "), HavingFragment: []byte(" HAVING "), WindowFragment: []byte(" WINDOW "), + QualifyFragment: []byte(" QUALIFY "), WindowPartitionByFragment: []byte("PARTITION BY "), WindowOrderByFragment: []byte("ORDER BY "), WindowOverFragment: []byte(" OVER "), @@ -566,6 +576,7 @@ func DefaultDialectOptions() *SQLDialectOptions { WhereSQLFragment, GroupBySQLFragment, HavingSQLFragment, + QualifySQLFragment, WindowSQLFragment, CompoundsSQLFragment, OrderSQLFragment, diff --git a/sqlgen/sql_dialect_options_test.go b/sqlgen/sql_dialect_options_test.go index 018b80ce..61238b41 100644 --- a/sqlgen/sql_dialect_options_test.go +++ b/sqlgen/sql_dialect_options_test.go @@ -23,6 +23,7 @@ func (sfts *sqlFragmentTypeSuite) TestOptions_SQLFragmentType() { {typ: sqlgen.WhereSQLFragment, expectedStr: "WhereSQLFragment"}, {typ: sqlgen.GroupBySQLFragment, expectedStr: "GroupBySQLFragment"}, {typ: sqlgen.HavingSQLFragment, expectedStr: "HavingSQLFragment"}, + {typ: sqlgen.QualifySQLFragment, expectedStr: "QualifySQLFragment"}, {typ: sqlgen.CompoundsSQLFragment, expectedStr: "CompoundsSQLFragment"}, {typ: sqlgen.OrderSQLFragment, expectedStr: "OrderSQLFragment"}, {typ: sqlgen.LimitSQLFragment, expectedStr: "LimitSQLFragment"},