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.IncreaseIdent()
out.IncreaseIdent(6)
c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...)
out.DecreaseIdent()
out.DecreaseIdent(6)
}
// 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
type prefixExpression struct {
ExpressionInterfaceImpl
@ -209,8 +268,8 @@ type complexExpression struct {
expressions Expression
}
func complexExpr(expressions Expression) Expression {
complexExpression := &complexExpression{expressions: expressions}
func complexExpr(expression Expression) Expression {
complexExpression := &complexExpression{expressions: expression}
complexExpression.ExpressionInterfaceImpl.Parent = complexExpression
return complexExpression

View file

@ -1,5 +1,17 @@
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.
func ROW(expressions ...Expression) Expression {
return NewFunc("ROW", expressions, nil)

View file

@ -4,6 +4,28 @@ import (
"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) {
assertClauseSerialize(t, AVG(table1ColFloat), "AVG(table1.col_float)")
assertClauseSerialize(t, AVG(table1ColInt), "AVG(table1.col_int)")

View file

@ -26,6 +26,7 @@ type SQLBuilder struct {
Debug bool
}
const tabSize = 4
const defaultIdent = 5
// 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
}
// 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
func ColumnListToProjectionList(columns []ColumnExpression) []Projection {
var ret []Projection

View file

@ -2,6 +2,15 @@ package mysql
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.
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"
FROM db.table1
WHERE NOT (EXISTS (
SELECT table2.col_int AS "table2.col_int"
FROM db.table2
WHERE table1.col_int = table2.col_int
));
SELECT table2.col_int AS "table2.col_int"
FROM db.table2
WHERE table1.col_int = table2.col_int
));
`)
}

View file

@ -2,6 +2,15 @@ package postgres
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.
var ROW = jet.ROW

View file

@ -6,6 +6,15 @@ import (
"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.
func ROW(expressions ...Expression) Expression {
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"
FROM db.table1
WHERE NOT (EXISTS (
SELECT table2.col_int AS "table2.col_int"
FROM db.table2
WHERE table1.col_int = table2.col_int
));
SELECT table2.col_int AS "table2.col_int"
FROM db.table2
WHERE table1.col_int = table2.col_int
));
`)
}

View file

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

View file

@ -38,6 +38,152 @@ ORDER BY "Album"."AlbumId" ASC;
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) {
manager := Employee.AS("Manager")

View file

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

View file

@ -395,8 +395,15 @@ func TestExecution1(t *testing.T) {
Customer.CustomerID,
Customer.LastName,
).
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
WHERE(
OR(
City.City.EQ(String("London")),
City.City.EQ(String("York")),
),
).
ORDER_BY(
City.CityID, Address.AddressID, Customer.CustomerID,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT city.city_id AS "city.city_id",
@ -408,7 +415,10 @@ SELECT city.city_id AS "city.city_id",
FROM dvds.city
INNER JOIN dvds.address ON (address.city_id = city.city_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;
`, "London", "York")
@ -1073,9 +1083,9 @@ SELECT film.film_id AS "film.film_id",
film.fulltext AS "film.fulltext"
FROM dvds.film
WHERE film.rental_rate = (
SELECT MAX(film.rental_rate)
FROM dvds.film
)
SELECT MAX(film.rental_rate)
FROM dvds.film
)
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"
FROM regional_sales
WHERE regional_sales.total_sales > ((
SELECT SUM(regional_sales.total_sales)
FROM regional_sales
) / 50)
SELECT SUM(regional_sales.total_sales)
FROM regional_sales
) / 50)
)
SELECT orders.ship_region AS "orders.ship_region",
order_details.product_id AS "order_details.product_id",
@ -84,9 +84,9 @@ SELECT orders.ship_region AS "orders.ship_region",
FROM northwind.orders
INNER JOIN northwind.order_details ON (orders.order_id = order_details.order_id)
WHERE orders.ship_region IN (
SELECT top_region."orders.ship_region" AS "orders.ship_region"
FROM top_region
)
SELECT top_region."orders.ship_region" AS "orders.ship_region"
FROM top_region
)
GROUP BY orders.ship_region, order_details.product_id
ORDER BY SUM(order_details.quantity) DESC;
`)
@ -150,18 +150,18 @@ func TestWithStatementDeleteAndInsert(t *testing.T) {
WITH remove_discontinued_orders AS (
DELETE FROM northwind.order_details
WHERE order_details.product_id IN (
SELECT products.product_id AS "products.product_id"
FROM northwind.products
WHERE products.discontinued = $1
)
SELECT products.product_id AS "products.product_id"
FROM northwind.products
WHERE products.discontinued = $1
)
RETURNING order_details.product_id AS "order_details.product_id"
),update_discontinued_price AS (
UPDATE northwind.products
SET unit_price = $2
WHERE products.product_id IN (
SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id"
FROM remove_discontinued_orders
)
SELECT remove_discontinued_orders."order_details.product_id" AS "order_details.product_id"
FROM remove_discontinued_orders
)
RETURNING products.product_id AS "products.product_id",
products.product_name AS "products.product_name",
products.supplier_id AS "products.supplier_id",

View file

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