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

@ -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

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