Skip to content

Commit

Permalink
Add PostgreSQL-specific character type constructors: Text, Char, and …
Browse files Browse the repository at this point in the history
…VarChar.
  • Loading branch information
go-jet committed Nov 1, 2024
1 parent 4f0832b commit 2183af4
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 144 deletions.
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ stmt := SELECT(
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
Language.Name.EQ(String("English")).
AND(Category.Name.NOT_EQ(String("Action"))).
AND(Film.Length.GT(Int(180))),
Language.Name.EQ(Char(20)("English")).
AND(Category.Name.NOT_EQ(Text("Action"))).
AND(Film.Length.GT(Int32(180))),
).ORDER_BY(
Actor.ActorID.ASC(),
Film.FilmID.ASC(),
Expand All @@ -200,35 +200,35 @@ args - query parameters

```sql
SELECT actor.actor_id AS "actor.actor_id",
actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name",
actor.last_update AS "actor.last_update",
film.film_id AS "film.film_id",
film.title AS "film.title",
film.description AS "film.description",
film.release_year AS "film.release_year",
film.language_id AS "film.language_id",
film.rental_duration AS "film.rental_duration",
film.rental_rate AS "film.rental_rate",
film.length AS "film.length",
film.replacement_cost AS "film.replacement_cost",
film.rating AS "film.rating",
film.last_update AS "film.last_update",
film.special_features AS "film.special_features",
film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id",
language.name AS "language.name",
language.last_update AS "language.last_update",
category.category_id AS "category.category_id",
category.name AS "category.name",
category.last_update AS "category.last_update"
actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name",
actor.last_update AS "actor.last_update",
film.film_id AS "film.film_id",
film.title AS "film.title",
film.description AS "film.description",
film.release_year AS "film.release_year",
film.language_id AS "film.language_id",
film.rental_duration AS "film.rental_duration",
film.rental_rate AS "film.rental_rate",
film.length AS "film.length",
film.replacement_cost AS "film.replacement_cost",
film.rating AS "film.rating",
film.last_update AS "film.last_update",
film.special_features AS "film.special_features",
film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id",
language.name AS "language.name",
language.last_update AS "language.last_update",
category.category_id AS "category.category_id",
category.name AS "category.name",
category.last_update AS "category.last_update"
FROM dvds.actor
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
INNER JOIN dvds.language ON (language.language_id = film.language_id)
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE ((language.name = $1) AND (category.name != $2)) AND (film.length > $3)
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
INNER JOIN dvds.language ON (language.language_id = film.language_id)
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE ((language.name = $1::char(20)) AND (category.name != $2::text)) AND (film.length > $3::integer)
ORDER BY actor.actor_id ASC, film.film_id ASC;
```
```sh
Expand Down Expand Up @@ -277,7 +277,7 @@ FROM dvds.actor
INNER JOIN dvds.language ON (language.language_id = film.language_id)
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE ((language.name = 'English') AND (category.name != 'Action')) AND (film.length > 180)
WHERE ((language.name = 'English'::char(20)) AND (category.name != 'Action'::text)) AND (film.length > 180::integer)
ORDER BY actor.actor_id ASC, film.film_id ASC;
```
</details>
Expand Down Expand Up @@ -545,18 +545,18 @@ The most expensive bugs are the one discovered on the production, and the least
With automatically generated type safe SQL, not only queries are written faster but bugs are found sooner.
Let's return to quick start example, and take closer look at a line:
```go
AND(Film.Length.GT(Int(180))),
AND(Film.Length.GT(Int32(180))),
```
Let's say someone changes column `length` to `duration` from `film` table. The next go build will fail at that line, and
the bug will be caught at compile time.

Let's say someone changes the type of `length` column to some non integer type. Build will also fail at the same line
Let's say someone changes the type of `length` column to some non-integer type. Build will also fail at the same line
because integer columns and expressions can be only compared to other integer columns and expressions.

Build will also fail if someone removes `length` column from `film` table. `Film` field will be omitted from SQL Builder and Model types,
next time `jet` generator is run.

Without Jet these bugs will have to be either caught by some test or by manual testing.
Without Jet these bugs will have to be either caught by tests or by manual testing.

## Dependencies
At the moment Jet dependence only of:
Expand Down
4 changes: 2 additions & 2 deletions examples/quick-start/quick-start.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func main() {
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
Language.Name.EQ(String("English")).
AND(Category.Name.NOT_EQ(String("Action"))).
Language.Name.EQ(Char(20)("English")).
AND(Category.Name.NOT_EQ(Text("Action"))).
AND(Film.Length.GT(Int(180))),
).ORDER_BY(
Actor.ActorID.ASC(),
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
module github.com/go-jet/jet/v2

go 1.20
go 1.21

// used by jet generator
require (
github.com/go-sql-driver/mysql v1.8.1
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgtype v1.14.3
github.com/jackc/pgtype v1.14.4
github.com/jackc/pgx/v4 v4.18.3
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
)

// used in tests
require (
github.com/google/go-cmp v0.6.0
github.com/pkg/profile v1.7.0
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus=
github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8=
github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
Expand Down
67 changes: 42 additions & 25 deletions postgres/literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,47 +34,64 @@ func Int64(value int64) IntegerExpression {
return CAST(jet.Int(value)).AS_BIGINT()
}

// Uint8 is constructor for 8 bit unsigned integer expressions literals.
func Uint8(value uint8) IntegerExpression {
return CAST(jet.Uint8(value)).AS_SMALLINT()
}

// Uint16 is constructor for 16 bit unsigned integer expressions literals.
func Uint16(value uint16) IntegerExpression {
return CAST(jet.Uint16(value)).AS_INTEGER()
}

// Uint32 is constructor for 32 bit unsigned integer expressions literals.
func Uint32(value uint32) IntegerExpression {
return CAST(jet.Uint32(value)).AS_BIGINT()
}

// Uint64 is constructor for 64 bit unsigned integer expressions literals.
func Uint64(value uint64) IntegerExpression {
return CAST(jet.Uint64(value)).AS_BIGINT()
}

// Float creates new float literal expression
var Float = jet.Float

// Float32 is constructor for 32 bit float literals
func Float32(value float32) FloatExpression {
// Real is placeholder constructor for 32-bit float literals
func Real(value float32) FloatExpression {
return CAST(jet.Literal(value)).AS_REAL()
}

// Float64 is constructor for 64 bit float literals
func Float64(value float64) FloatExpression {
// Double is placeholder constructor for 64-bit float literals
func Double(value float64) FloatExpression {
return CAST(jet.Literal(value)).AS_DOUBLE()
}

// Decimal creates new float literal expression
var Decimal = jet.Decimal

// String creates new string literal expression
// String is a parameter constructor for the PostgreSQL text type. Using the `Text` constructor is
// generally preferable.
//
// WARNING: String always applies a `text` type cast, which can be problematic if a parameter is compared
// to a `character` column, as this may prevent index usage. In such cases, consider using the Char
// constructor instead. See also other PostgreSQL-specific constructors: Text, Char, and VarChar.
func String(value string) StringExpression {
return CAST(jet.String(value)).AS_TEXT()
}

// Text is a parameter constructor for the PostgreSQL text type. This constructor also adds an
// explicit placeholder type cast to text in the generated query, such as `$3::text`.
// Example usage:
//
// Text("English")
func Text(value string) StringExpression {
return CAST(jet.Literal(value)).AS_TEXT()
}

// Char is a parameter constructor for the PostgreSQL character type. This constructor also adds an
// explicit placeholder type cast to text in the generated query, such as `$3::char(30)`.
// Example usage:
//
// Char(20)("English")
func Char(length ...int) func(value string) StringExpression {
return func(value string) StringExpression {
return CAST(StringExp(jet.Literal(value))).AS_CHAR(length...)
}
}

// VarChar is a parameter constructor for the PostgreSQL character varying type. This constructor
// also adds an explicit placeholder type cast to text in the generated query, such as `$3::varchar(30)`.
// Example usage:
//
// VarChar(20)("English")
// VarChar()("English")
func VarChar(length ...int) func(value string) StringExpression {
return func(value string) StringExpression {
return CAST(StringExp(jet.Literal(value))).AS_VARCHAR(length...)
}
}

// Json creates new json literal expression
func Json(value interface{}) StringExpression {
switch value.(type) {
Expand Down
29 changes: 9 additions & 20 deletions postgres/literal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,21 @@ func TestInt64(t *testing.T) {
assertSerialize(t, Int64(val), `$1::bigint`, val)
}

func TestUint8(t *testing.T) {
val := uint8(math.MaxUint8)
assertSerialize(t, Uint8(val), `$1::smallint`, val)
}

func TestUint16(t *testing.T) {
val := uint16(math.MaxUint16)
assertSerialize(t, Uint16(val), `$1::integer`, val)
}

func TestUint32(t *testing.T) {
val := uint32(math.MaxUint32)
assertSerialize(t, Uint32(val), `$1::bigint`, val)
}

func TestUint64(t *testing.T) {
val := uint64(math.MaxUint64)
assertSerialize(t, Uint64(val), `$1::bigint`, val)
}

func TestFloat(t *testing.T) {
assertSerialize(t, Float(12.34), `$1`, float64(12.34))

assertSerialize(t, Real(12.34), `$1::real`, float32(12.34))
assertSerialize(t, Double(12.34), `$1::double precision`, float64(12.34))
}

func TestString(t *testing.T) {
assertSerialize(t, String("Some text"), `$1::text`, "Some text")

assertSerialize(t, Text("Some text"), `$1::text`, "Some text")
assertSerialize(t, Char(20)("John Doe"), `$1::char(20)`, "John Doe")
assertSerialize(t, Char()("John Doe"), `$1::char`, "John Doe")
assertSerialize(t, VarChar(20)("John Doe"), `$1::varchar(20)`, "John Doe")
assertSerialize(t, VarChar()("John Doe"), `$1::varchar`, "John Doe")
}

func TestBytea(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions postgres/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type values struct {
// Example usage:
//
// VALUES(
// WRAP(Int32(204), Float32(1.21)),
// WRAP(Int32(207), Float32(1.02)),
// WRAP(Int32(204), Real(1.21)),
// WRAP(Int32(207), Real(1.02)),
// )
func VALUES(rows ...RowExpression) values {
return values{Values: jet.Values(rows)}
Expand Down
38 changes: 19 additions & 19 deletions tests/postgres/alltypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,15 +462,15 @@ func TestStringOperators(t *testing.T) {

query := AllTypes.SELECT(
AllTypes.Text.EQ(AllTypes.Char),
AllTypes.Text.EQ(String("Text")),
AllTypes.Text.EQ(Text("Text")),
AllTypes.Text.NOT_EQ(AllTypes.VarCharPtr),
AllTypes.Text.NOT_EQ(String("Text")),
AllTypes.Text.NOT_EQ(Text("Text")),
AllTypes.Text.GT(AllTypes.Text),
AllTypes.Text.GT(String("Text")),
AllTypes.Text.GT(Text("Text")),
AllTypes.Text.GT_EQ(AllTypes.TextPtr),
AllTypes.Text.GT_EQ(String("Text")),
AllTypes.Text.GT_EQ(Text("Text")),
AllTypes.Text.LT(AllTypes.Char),
AllTypes.Text.LT(String("Text")),
AllTypes.Text.LT(Text("Text")),
AllTypes.Text.LT_EQ(AllTypes.VarChar),
AllTypes.Text.LT_EQ(String("Text")),
AllTypes.Text.BETWEEN(String("min"), String("max")),
Expand All @@ -490,13 +490,13 @@ func TestStringOperators(t *testing.T) {
OCTET_LENGTH(AllTypes.Text),
OCTET_LENGTH(String("length")),
LOWER(AllTypes.VarCharPtr),
LOWER(String("length")),
LOWER(Char(4)("length")),
UPPER(AllTypes.Char),
UPPER(String("upper")),
UPPER(VarChar()("upper")),
BTRIM(AllTypes.VarChar),
BTRIM(String("btrim")),
BTRIM(Char()("btrim")),
BTRIM(AllTypes.VarChar, String("AA")),
BTRIM(String("btrim"), String("AA")),
BTRIM(VarChar(11)("btrim"), String("AA")),
LTRIM(AllTypes.VarChar),
LTRIM(String("ltrim")),
LTRIM(AllTypes.VarChar, String("A")),
Expand Down Expand Up @@ -533,7 +533,7 @@ func TestStringOperators(t *testing.T) {
TO_HEX(AllTypes.IntegerPtr),
)

dest := []struct{}{}
var dest []struct{}
err := query.Query(db, &dest)

require.NoError(t, err)
Expand Down Expand Up @@ -630,9 +630,9 @@ func TestFloatOperators(t *testing.T) {
AllTypes.Numeric.BETWEEN(Float(1.34), AllTypes.Decimal).AS("between"),
AllTypes.Numeric.NOT_BETWEEN(AllTypes.Decimal.MUL(Float(3)), Float(100.12)).AS("not_between"),

TRUNC(AllTypes.Decimal.ADD(AllTypes.Decimal), Uint8(2)).AS("add1"),
TRUNC(AllTypes.Decimal.ADD(AllTypes.Decimal), Int8(2)).AS("add1"),
TRUNC(AllTypes.Decimal.ADD(Float(11.22)), Int8(2)).AS("add2"),
TRUNC(AllTypes.Decimal.SUB(AllTypes.DecimalPtr), Uint16(2)).AS("sub1"),
TRUNC(AllTypes.Decimal.SUB(AllTypes.DecimalPtr), Int32(2)).AS("sub1"),
TRUNC(AllTypes.Decimal.SUB(Float(11.22)), Int16(2)).AS("sub2"),
TRUNC(AllTypes.Decimal.MUL(AllTypes.DecimalPtr), Int16(2)).AS("mul1"),
TRUNC(AllTypes.Decimal.MUL(Float(11.22)), Int32(2)).AS("mul2"),
Expand Down Expand Up @@ -736,13 +736,13 @@ func TestIntegerOperators(t *testing.T) {
AllTypes.Integer.BETWEEN(Int(11), Int(200)).AS("between"),
AllTypes.Integer.NOT_BETWEEN(Int(66), Int(77)).AS("not_between"),
AllTypes.BigInt.LT(AllTypes.BigIntPtr).AS("lt1"),
AllTypes.BigInt.LT(Uint8(65)).AS("lt2"),
AllTypes.BigInt.LT(Int16(65)).AS("lt2"),
AllTypes.BigInt.LT_EQ(AllTypes.BigIntPtr).AS("lte1"),
AllTypes.BigInt.LT_EQ(Uint16(65)).AS("lte2"),
AllTypes.BigInt.LT_EQ(Int32(65)).AS("lte2"),
AllTypes.BigInt.GT(AllTypes.BigIntPtr).AS("gt1"),
AllTypes.BigInt.GT(Uint32(65)).AS("gt2"),
AllTypes.BigInt.GT(Int64(65)).AS("gt2"),
AllTypes.BigInt.GT_EQ(AllTypes.BigIntPtr).AS("gte1"),
AllTypes.BigInt.GT_EQ(Uint64(65)).AS("gte2"),
AllTypes.BigInt.GT_EQ(Int64(65)).AS("gte2"),

AllTypes.BigInt.ADD(AllTypes.BigInt).AS("add1"),
AllTypes.BigInt.ADD(Int(11)).AS("add2"),
Expand Down Expand Up @@ -1111,8 +1111,8 @@ func TestRowExpression(t *testing.T) {
nowAddHour := time.Now().Add(time.Hour)

stmt := SELECT(
ROW(Int32(1), Float32(11.22), String("john")).AS("row"),
WRAP(Int64(1), Float64(11.22), String("john")).AS("wrap"),
ROW(Int32(1), Real(11.22), Text("john")).AS("row"),
WRAP(Int64(1), Double(11.22), VarChar(10)("john")).AS("wrap"),

ROW(Bool(false), DateT(now)).EQ(ROW(Bool(true), DateT(now))),
WRAP(Bool(false), DateT(now)).NOT_EQ(WRAP(Bool(true), DateT(now))),
Expand All @@ -1131,7 +1131,7 @@ func TestRowExpression(t *testing.T) {

testutils.AssertStatementSql(t, stmt, `
SELECT ROW($1::integer, $2::real, $3::text) AS "row",
($4::bigint, $5::double precision, $6::text) AS "wrap",
($4::bigint, $5::double precision, $6::varchar(10)) AS "wrap",
ROW($7::boolean, $8::date) = ROW($9::boolean, $10::date),
($11::boolean, $12::date) != ($13::boolean, $14::date),
ROW($15::time without time zone) IS DISTINCT FROM (row(NOW()::time)),
Expand Down
Loading

0 comments on commit 2183af4

Please sign in to comment.