Global AND and OR functions for better indentation of a complex condition in the Go code and in the generated SQL.

This commit is contained in:
go-jet 2022-02-11 13:09:49 +01:00
parent 8ffbe38993
commit 9f91fd705a
17 changed files with 338 additions and 44 deletions

View file

@ -98,9 +98,9 @@ func (c *ClauseWhere) Serialize(statementType StatementType, out *SQLBuilder, op
} }
out.WriteString("WHERE") out.WriteString("WHERE")
out.IncreaseIdent() out.IncreaseIdent(6)
c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...) c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...)
out.DecreaseIdent() out.DecreaseIdent(6)
} }
// ClauseGroupBy struct // ClauseGroupBy struct

View file

@ -123,6 +123,65 @@ func (c *binaryOperatorExpression) serialize(statement StatementType, out *SQLBu
} }
} }
type expressionListOperator struct {
ExpressionInterfaceImpl
operator string
expressions []Expression
}
func newExpressionListOperator(operator string, expressions ...Expression) *expressionListOperator {
ret := &expressionListOperator{
operator: operator,
expressions: expressions,
}
ret.ExpressionInterfaceImpl.Parent = ret
return ret
}
func newBoolExpressionListOperator(operator string, expressions ...BoolExpression) BoolExpression {
return BoolExp(newExpressionListOperator(operator, BoolExpressionListToExpressionList(expressions)...))
}
func (elo *expressionListOperator) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
if len(elo.expressions) == 0 {
panic("jet: syntax error, expression list empty")
}
shouldWrap := len(elo.expressions) > 1
if shouldWrap {
out.WriteByte('(')
out.IncreaseIdent(tabSize)
out.NewLine()
}
for i, expression := range elo.expressions {
if i == 1 {
out.IncreaseIdent(tabSize)
}
if i > 0 {
out.NewLine()
out.WriteString(elo.operator)
}
out.IncreaseIdent(len(elo.operator) + 1)
expression.serialize(statement, out, FallTrough(options)...)
out.DecreaseIdent(len(elo.operator) + 1)
}
if len(elo.expressions) > 1 {
out.DecreaseIdent(tabSize)
}
if shouldWrap {
out.DecreaseIdent(tabSize)
out.NewLine()
out.WriteByte(')')
}
}
// A prefix operator Expression // A prefix operator Expression
type prefixExpression struct { type prefixExpression struct {
ExpressionInterfaceImpl ExpressionInterfaceImpl
@ -209,8 +268,8 @@ type complexExpression struct {
expressions Expression expressions Expression
} }
func complexExpr(expressions Expression) Expression { func complexExpr(expression Expression) Expression {
complexExpression := &complexExpression{expressions: expressions} complexExpression := &complexExpression{expressions: expression}
complexExpression.ExpressionInterfaceImpl.Parent = complexExpression complexExpression.ExpressionInterfaceImpl.Parent = complexExpression
return complexExpression return complexExpression

View file

@ -1,5 +1,17 @@
package jet package jet
// AND function adds AND operator between expressions. This function can be used, instead of method AND,
// to have a better inlining of a complex condition in the Go code and in the generated SQL.
func AND(expressions ...BoolExpression) BoolExpression {
return newBoolExpressionListOperator("AND", expressions...)
}
// OR function adds OR operator between expressions. This function can be used, instead of method OR,
// to have a better inlining of a complex condition in the Go code and in the generated SQL.
func OR(expressions ...BoolExpression) BoolExpression {
return newBoolExpressionListOperator("OR", expressions...)
}
// ROW is construct one table row from list of expressions. // ROW is construct one table row from list of expressions.
func ROW(expressions ...Expression) Expression { func ROW(expressions ...Expression) Expression {
return NewFunc("ROW", expressions, nil) return NewFunc("ROW", expressions, nil)

View file

@ -4,6 +4,28 @@ import (
"testing" "testing"
) )
func TestAND(t *testing.T) {
assertClauseSerializeErr(t, AND(), "jet: syntax error, expression list empty")
assertClauseSerialize(t, AND(table1ColInt.IS_NULL()), `table1.col_int IS NULL`) // IS NULL doesn't add parenthesis
assertClauseSerialize(t, AND(table1ColInt.LT(Int(11))), `(table1.col_int < $1)`, int64(11))
assertClauseSerialize(t, AND(table1ColInt.GT(Int(11)), table1ColFloat.EQ(Float(0))),
`(
(table1.col_int > $1)
AND (table1.col_float = $2)
)`, int64(11), 0.0)
}
func TestOR(t *testing.T) {
assertClauseSerializeErr(t, OR(), "jet: syntax error, expression list empty")
assertClauseSerialize(t, OR(table1ColInt.IS_NULL()), `table1.col_int IS NULL`) // IS NULL doesn't add parenthesis
assertClauseSerialize(t, OR(table1ColInt.LT(Int(11))), `(table1.col_int < $1)`, int64(11))
assertClauseSerialize(t, OR(table1ColInt.GT(Int(11)), table1ColFloat.EQ(Float(0))),
`(
(table1.col_int > $1)
OR (table1.col_float = $2)
)`, int64(11), 0.0)
}
func TestFuncAVG(t *testing.T) { func TestFuncAVG(t *testing.T) {
assertClauseSerialize(t, AVG(table1ColFloat), "AVG(table1.col_float)") assertClauseSerialize(t, AVG(table1ColFloat), "AVG(table1.col_float)")
assertClauseSerialize(t, AVG(table1ColInt), "AVG(table1.col_int)") assertClauseSerialize(t, AVG(table1ColInt), "AVG(table1.col_int)")

View file

@ -26,6 +26,7 @@ type SQLBuilder struct {
Debug bool Debug bool
} }
const tabSize = 4
const defaultIdent = 5 const defaultIdent = 5
// IncreaseIdent adds ident or defaultIdent number of spaces to each new line // IncreaseIdent adds ident or defaultIdent number of spaces to each new line

View file

@ -113,6 +113,17 @@ func ExpressionListToSerializerList(expressions []Expression) []Serializer {
return ret return ret
} }
// BoolExpressionListToExpressionList converts list of bool expressions to list of expressions
func BoolExpressionListToExpressionList(expressions []BoolExpression) []Expression {
var ret []Expression
for _, expression := range expressions {
ret = append(ret, expression)
}
return ret
}
// ColumnListToProjectionList func // ColumnListToProjectionList func
func ColumnListToProjectionList(columns []ColumnExpression) []Projection { func ColumnListToProjectionList(columns []ColumnExpression) []Projection {
var ret []Projection var ret []Projection

View file

@ -2,6 +2,15 @@ package mysql
import "github.com/go-jet/jet/v2/internal/jet" import "github.com/go-jet/jet/v2/internal/jet"
// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition
// in the Go code and in the generated SQL.
var (
// AND function adds AND operator between expressions.
AND = jet.AND
// OR function adds OR operator between expressions.
OR = jet.OR
)
// ROW is construct one table row from list of expressions. // ROW is construct one table row from list of expressions.
var ROW = jet.ROW var ROW = jet.ROW

View file

@ -148,9 +148,9 @@ func TestSelect_NOT_EXISTS(t *testing.T) {
SELECT table1.col_int AS "table1.col_int" SELECT table1.col_int AS "table1.col_int"
FROM db.table1 FROM db.table1
WHERE NOT (EXISTS ( WHERE NOT (EXISTS (
SELECT table2.col_int AS "table2.col_int" SELECT table2.col_int AS "table2.col_int"
FROM db.table2 FROM db.table2
WHERE table1.col_int = table2.col_int WHERE table1.col_int = table2.col_int
)); ));
`) `)
} }

View file

@ -2,6 +2,15 @@ package postgres
import "github.com/go-jet/jet/v2/internal/jet" import "github.com/go-jet/jet/v2/internal/jet"
// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition
// in the Go code and in the generated SQL.
var (
// AND function adds AND operator between expressions.
AND = jet.AND
// OR function adds OR operator between expressions.
OR = jet.OR
)
// ROW is construct one table row from list of expressions. // ROW is construct one table row from list of expressions.
var ROW = jet.ROW var ROW = jet.ROW

View file

@ -6,6 +6,15 @@ import (
"time" "time"
) )
// This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition
// in the Go code and in the generated SQL.
var (
// AND function adds AND operator between expressions.
AND = jet.AND
// OR function adds OR operator between expressions.
OR = jet.OR
)
// ROW is construct one table row from list of expressions. // ROW is construct one table row from list of expressions.
func ROW(expressions ...Expression) Expression { func ROW(expressions ...Expression) Expression {
return jet.NewFunc("", expressions, nil) return jet.NewFunc("", expressions, nil)

View file

@ -148,9 +148,9 @@ func TestSelect_NOT_EXISTS(t *testing.T) {
SELECT table1.col_int AS "table1.col_int" SELECT table1.col_int AS "table1.col_int"
FROM db.table1 FROM db.table1
WHERE NOT (EXISTS ( WHERE NOT (EXISTS (
SELECT table2.col_int AS "table2.col_int" SELECT table2.col_int AS "table2.col_int"
FROM db.table2 FROM db.table2
WHERE table1.col_int = table2.col_int WHERE table1.col_int = table2.col_int
)); ));
`) `)
} }

View file

@ -165,9 +165,9 @@ WITH payments_to_delete AS (
) )
DELETE FROM dvds.payment DELETE FROM dvds.payment
WHERE payment.payment_id IN ( WHERE payment.payment_id IN (
SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id"
FROM payments_to_delete FROM payments_to_delete
); );
`, "''", "`")) `, "''", "`"))
tx, err := db.Begin() tx, err := db.Begin()

View file

@ -38,6 +38,152 @@ ORDER BY "Album"."AlbumId" ASC;
requireQueryLogged(t, stmt, 347) requireQueryLogged(t, stmt, 347)
} }
func TestComplex_AND_OR(t *testing.T) {
stmt := SELECT(
Artist.AllColumns,
Album.AllColumns,
Track.AllColumns,
).FROM(
Artist.
LEFT_JOIN(Album, Artist.ArtistId.EQ(Album.ArtistId)).
LEFT_JOIN(Track, Track.AlbumId.EQ(Album.AlbumId)),
).WHERE(
AND(
Artist.ArtistId.BETWEEN(Int(5), Int(11)),
Album.AlbumId.GT_EQ(Int(7)),
Track.TrackId.GT(Int(74)),
OR(
Track.GenreId.EQ(Int(2)),
Track.UnitPrice.GT(Float(1.01)),
),
Track.TrackId.LT(Int(125)),
),
).ORDER_BY(
Artist.ArtistId,
Album.AlbumId,
Track.TrackId,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT "Artist"."ArtistId" AS "Artist.ArtistId",
"Artist"."Name" AS "Artist.Name",
"Album"."AlbumId" AS "Album.AlbumId",
"Album"."Title" AS "Album.Title",
"Album"."ArtistId" AS "Album.ArtistId",
"Track"."TrackId" AS "Track.TrackId",
"Track"."Name" AS "Track.Name",
"Track"."AlbumId" AS "Track.AlbumId",
"Track"."MediaTypeId" AS "Track.MediaTypeId",
"Track"."GenreId" AS "Track.GenreId",
"Track"."Composer" AS "Track.Composer",
"Track"."Milliseconds" AS "Track.Milliseconds",
"Track"."Bytes" AS "Track.Bytes",
"Track"."UnitPrice" AS "Track.UnitPrice"
FROM chinook."Artist"
LEFT JOIN chinook."Album" ON ("Artist"."ArtistId" = "Album"."ArtistId")
LEFT JOIN chinook."Track" ON ("Track"."AlbumId" = "Album"."AlbumId")
WHERE (
("Artist"."ArtistId" BETWEEN 5 AND 11)
AND ("Album"."AlbumId" >= 7)
AND ("Track"."TrackId" > 74)
AND (
("Track"."GenreId" = 2)
OR ("Track"."UnitPrice" > 1.01)
)
AND ("Track"."TrackId" < 125)
)
ORDER BY "Artist"."ArtistId", "Album"."AlbumId", "Track"."TrackId";
`)
var dest []struct {
model.Artist
Albums []struct {
model.Album
Tracks []model.Track
}
}
err := stmt.Query(db, &dest)
require.NoError(t, err)
testutils.AssertJSON(t, dest, `
[
{
"ArtistId": 6,
"Name": "Ant<6E>nio Carlos Jobim",
"Albums": [
{
"AlbumId": 8,
"Title": "Warner 25 Anos",
"ArtistId": 6,
"Tracks": [
{
"TrackId": 75,
"Name": "O Boto (B<>to)",
"AlbumId": 8,
"MediaTypeId": 1,
"GenreId": 2,
"Composer": null,
"Milliseconds": 366837,
"Bytes": 12089673,
"UnitPrice": 0.99
},
{
"TrackId": 76,
"Name": "Canta, Canta Mais",
"AlbumId": 8,
"MediaTypeId": 1,
"GenreId": 2,
"Composer": null,
"Milliseconds": 271856,
"Bytes": 8719426,
"UnitPrice": 0.99
}
]
}
]
},
{
"ArtistId": 10,
"Name": "Billy Cobham",
"Albums": [
{
"AlbumId": 13,
"Title": "The Best Of Billy Cobham",
"ArtistId": 10,
"Tracks": [
{
"TrackId": 123,
"Name": "Quadrant",
"AlbumId": 13,
"MediaTypeId": 1,
"GenreId": 2,
"Composer": "Billy Cobham",
"Milliseconds": 261851,
"Bytes": 8538199,
"UnitPrice": 0.99
},
{
"TrackId": 124,
"Name": "Snoopy's search-Red baron",
"AlbumId": 13,
"MediaTypeId": 1,
"GenreId": 2,
"Composer": "Billy Cobham",
"Milliseconds": 456071,
"Bytes": 15075616,
"UnitPrice": 0.99
}
]
}
]
}
]
`)
}
func TestJoinEverything(t *testing.T) { func TestJoinEverything(t *testing.T) {
manager := Employee.AS("Manager") manager := Employee.AS("Manager")

View file

@ -124,9 +124,11 @@ func TestDeleteFrom(t *testing.T) {
table.Actor, table.Actor,
). ).
WHERE( WHERE(
table.Staff.StaffID.EQ(table.Rental.StaffID). AND(
AND(table.Staff.StaffID.EQ(Int(2))). table.Staff.StaffID.EQ(table.Rental.StaffID),
AND(table.Rental.RentalID.LT(Int(10))), table.Store.StoreID.EQ(Int(2)),
table.Rental.RentalID.LT(Int(10)),
),
). ).
RETURNING( RETURNING(
table.Rental.AllColumns, table.Rental.AllColumns,
@ -138,7 +140,11 @@ DELETE FROM dvds.rental
USING dvds.staff USING dvds.staff
INNER JOIN dvds.store ON (store.store_id = staff.staff_id), INNER JOIN dvds.store ON (store.store_id = staff.staff_id),
dvds.actor dvds.actor
WHERE ((staff.staff_id = rental.staff_id) AND (staff.staff_id = $1)) AND (rental.rental_id < $2) WHERE (
(staff.staff_id = rental.staff_id)
AND (store.store_id = $1)
AND (rental.rental_id < $2)
)
RETURNING rental.rental_id AS "rental.rental_id", RETURNING rental.rental_id AS "rental.rental_id",
rental.rental_date AS "rental.rental_date", rental.rental_date AS "rental.rental_date",
rental.inventory_id AS "rental.inventory_id", rental.inventory_id AS "rental.inventory_id",

View file

@ -395,8 +395,15 @@ func TestExecution1(t *testing.T) {
Customer.CustomerID, Customer.CustomerID,
Customer.LastName, Customer.LastName,
). ).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))). WHERE(
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID) OR(
City.City.EQ(String("London")),
City.City.EQ(String("York")),
),
).
ORDER_BY(
City.CityID, Address.AddressID, Customer.CustomerID,
)
testutils.AssertDebugStatementSql(t, stmt, ` testutils.AssertDebugStatementSql(t, stmt, `
SELECT city.city_id AS "city.city_id", SELECT city.city_id AS "city.city_id",
@ -408,7 +415,10 @@ SELECT city.city_id AS "city.city_id",
FROM dvds.city FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_id) INNER JOIN dvds.address ON (address.city_id = city.city_id)
INNER JOIN dvds.customer ON (customer.address_id = address.address_id) INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
WHERE (city.city = 'London') OR (city.city = 'York') WHERE (
(city.city = 'London')
OR (city.city = 'York')
)
ORDER BY city.city_id, address.address_id, customer.customer_id; ORDER BY city.city_id, address.address_id, customer.customer_id;
`, "London", "York") `, "London", "York")
@ -1073,9 +1083,9 @@ SELECT film.film_id AS "film.film_id",
film.fulltext AS "film.fulltext" film.fulltext AS "film.fulltext"
FROM dvds.film FROM dvds.film
WHERE film.rental_rate = ( WHERE film.rental_rate = (
SELECT MAX(film.rental_rate) SELECT MAX(film.rental_rate)
FROM dvds.film FROM dvds.film
) )
ORDER BY film.film_id ASC; ORDER BY film.film_id ASC;
` `

View file

@ -73,9 +73,9 @@ WITH regional_sales AS (
SELECT regional_sales."orders.ship_region" AS "orders.ship_region" SELECT regional_sales."orders.ship_region" AS "orders.ship_region"
FROM regional_sales FROM regional_sales
WHERE regional_sales.total_sales > (( WHERE regional_sales.total_sales > ((
SELECT SUM(regional_sales.total_sales) SELECT SUM(regional_sales.total_sales)
FROM regional_sales FROM regional_sales
) / 50) ) / 50)
) )
SELECT orders.ship_region AS "orders.ship_region", SELECT orders.ship_region AS "orders.ship_region",
order_details.product_id AS "order_details.product_id", order_details.product_id AS "order_details.product_id",
@ -84,9 +84,9 @@ SELECT orders.ship_region AS "orders.ship_region",
FROM northwind.orders FROM northwind.orders
INNER JOIN northwind.order_details ON (orders.order_id = order_details.order_id) INNER JOIN northwind.order_details ON (orders.order_id = order_details.order_id)
WHERE orders.ship_region IN ( WHERE orders.ship_region IN (
SELECT top_region."orders.ship_region" AS "orders.ship_region" SELECT top_region."orders.ship_region" AS "orders.ship_region"
FROM top_region FROM top_region
) )
GROUP BY orders.ship_region, order_details.product_id GROUP BY orders.ship_region, order_details.product_id
ORDER BY SUM(order_details.quantity) DESC; ORDER BY SUM(order_details.quantity) DESC;
`) `)
@ -150,18 +150,18 @@ func TestWithStatementDeleteAndInsert(t *testing.T) {
WITH remove_discontinued_orders AS ( WITH remove_discontinued_orders AS (
DELETE FROM northwind.order_details DELETE FROM northwind.order_details
WHERE order_details.product_id IN ( WHERE order_details.product_id IN (
SELECT products.product_id AS "products.product_id" SELECT products.product_id AS "products.product_id"
FROM northwind.products FROM northwind.products
WHERE products.discontinued = $1 WHERE products.discontinued = $1
) )
RETURNING order_details.product_id AS "order_details.product_id" RETURNING order_details.product_id AS "order_details.product_id"
),update_discontinued_price AS ( ),update_discontinued_price AS (
UPDATE northwind.products UPDATE northwind.products
SET unit_price = $2 SET unit_price = $2
WHERE products.product_id IN ( WHERE products.product_id IN (
SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id" SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id"
FROM remove_discontinued_orders FROM remove_discontinued_orders
) )
RETURNING products.product_id AS "products.product_id", RETURNING products.product_id AS "products.product_id",
products.product_name AS "products.product_name", products.product_name AS "products.product_name",
products.supplier_id AS "products.supplier_id", products.supplier_id AS "products.supplier_id",

View file

@ -154,9 +154,9 @@ WITH payments_to_update AS (
UPDATE payment UPDATE payment
SET amount = 0 SET amount = 0
WHERE payment.payment_id IN ( WHERE payment.payment_id IN (
SELECT payments_to_update.''payment.payment_id'' AS "payment.payment_id" SELECT payments_to_update.''payment.payment_id'' AS "payment.payment_id"
FROM payments_to_update FROM payments_to_update
); );
`, "''", "`", -1)) `, "''", "`", -1))
tx := beginDBTx(t) tx := beginDBTx(t)
@ -206,9 +206,9 @@ WITH payments_to_delete AS (
) )
DELETE FROM payment DELETE FROM payment
WHERE payment.payment_id IN ( WHERE payment.payment_id IN (
SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id" SELECT payments_to_delete.''payment.payment_id'' AS "payment.payment_id"
FROM payments_to_delete FROM payments_to_delete
); );
`, "''", "`", -1)) `, "''", "`", -1))
tx := beginDBTx(t) tx := beginDBTx(t)