Merge pull request #525 from go-jet/pg_arrays

Add support for postgres array types
This commit is contained in:
go-jet 2025-10-17 14:34:43 +02:00 committed by GitHub
commit 937459b9cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2475 additions and 4033 deletions

390
README.md
View file

@ -198,10 +198,13 @@ stmt := SELECT(
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)). INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)), INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE( ).WHERE(
Language.Name.EQ(Char(20)("English")). AND(
AND(Category.Name.NOT_EQ(Text("Action"))). Language.Name.EQ(Char(20)("English")), // string columns Language.Name and Category.Name can be compared only with string expression
AND(Film.Length.GT(Int32(180))). Category.Name.NOT_EQ(Text("Action")),
AND(Film.Rating.NOT_EQ(enum.MpaaRating.R)), Film.Length.GT(Int32(180)), // Film.Length is integer column and can be compared only with integer expression
Film.Rating.NOT_EQ(enum.MpaaRating.R),
String("Trailers").EQ(ANY(Film.SpecialFeatures)), // type safety is also enforced on array element types
),
).ORDER_BY( ).ORDER_BY(
Actor.ActorID.ASC(), Actor.ActorID.ASC(),
Film.FilmID.ASC(), Film.FilmID.ASC(),
@ -212,7 +215,8 @@ stmt := SELECT(
Note that every column has a type. String columns, such as `Language.Name` and `Category.Name` can only be compared with Note that every column has a type. String columns, such as `Language.Name` and `Category.Name` can only be compared with
string columns and expressions. Similarity, `Actor.ActorID`, `FilmActor.ActorID`, `Film.Length` are integer columns string columns and expressions. Similarity, `Actor.ActorID`, `FilmActor.ActorID`, `Film.Length` are integer columns
and can only be compared with integer columns and expressions. and can only be compared with integer columns and expressions. The same type safety rules apply to arrays and their
element types.
__How to Get a Parametrized SQL Query from the Statement?__ __How to Get a Parametrized SQL Query from the Statement?__
```go ```go
@ -244,7 +248,6 @@ SELECT actor.actor_id AS "actor.actor_id",
film.fulltext AS "film.fulltext", film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id", language.language_id AS "language.language_id",
language.name AS "language.name", language.name AS "language.name",
language.last_update AS "language.last_update",
category.category_id AS "category.category_id", category.category_id AS "category.category_id",
category.name AS "category.name", category.name AS "category.name",
category.last_update AS "category.last_update" category.last_update AS "category.last_update"
@ -254,11 +257,18 @@ FROM dvds.actor
INNER JOIN dvds.language ON (language.language_id = film.language_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.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_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)) AND (film.rating != 'R') WHERE (
(language.name = $1::char(20))
AND (category.name != $2::text)
AND (film.length > $3::integer)
AND (film.rating != 'R')
AND ($4::text = ANY(film.special_features))
)
ORDER BY actor.actor_id ASC, film.film_id ASC; ORDER BY actor.actor_id ASC, film.film_id ASC;
``` ```
```sh ```sh
[English Action 180] [English Action 180 Trailers]
``` ```
@ -277,35 +287,40 @@ debugSql - this query string can be copy-pasted into sql editor and executed.
```sql ```sql
SELECT actor.actor_id AS "actor.actor_id", SELECT actor.actor_id AS "actor.actor_id",
actor.first_name AS "actor.first_name", actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name", actor.last_name AS "actor.last_name",
actor.last_update AS "actor.last_update", actor.last_update AS "actor.last_update",
film.film_id AS "film.film_id", film.film_id AS "film.film_id",
film.title AS "film.title", film.title AS "film.title",
film.description AS "film.description", film.description AS "film.description",
film.release_year AS "film.release_year", film.release_year AS "film.release_year",
film.language_id AS "film.language_id", film.language_id AS "film.language_id",
film.rental_duration AS "film.rental_duration", film.rental_duration AS "film.rental_duration",
film.rental_rate AS "film.rental_rate", film.rental_rate AS "film.rental_rate",
film.length AS "film.length", film.length AS "film.length",
film.replacement_cost AS "film.replacement_cost", film.replacement_cost AS "film.replacement_cost",
film.rating AS "film.rating", film.rating AS "film.rating",
film.last_update AS "film.last_update", film.last_update AS "film.last_update",
film.special_features AS "film.special_features", film.special_features AS "film.special_features",
film.fulltext AS "film.fulltext", film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id", language.language_id AS "language.language_id",
language.name AS "language.name", language.name AS "language.name",
language.last_update AS "language.last_update", category.category_id AS "category.category_id",
category.category_id AS "category.category_id", category.name AS "category.name",
category.name AS "category.name", category.last_update AS "category.last_update"
category.last_update AS "category.last_update"
FROM dvds.actor FROM dvds.actor
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id) 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.film ON (film.film_id = film_actor.film_id)
INNER JOIN dvds.language ON (language.language_id = film.language_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.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id) INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE (((language.name = 'English'::char(20)) AND (category.name != 'Action'::text)) AND (film.length > 180)) AND (film.rating != 'R') WHERE (
(language.name = 'English'::char(20))
AND (category.name != 'Action'::text)
AND (film.length > 180::integer)
AND (film.rating != 'R')
AND ('Trailers'::text = ANY(film.special_features))
)
ORDER BY actor.actor_id ASC, film.film_id ASC; ORDER BY actor.actor_id ASC, film.film_id ASC;
``` ```
</details> </details>
@ -355,8 +370,8 @@ __And that's it.__
The `dest` variable now contains a list of all actors (each with a list of The `dest` variable now contains a list of all actors (each with a list of
films they acted in). Each film includes information about its language and films they acted in). Each film includes information about its language and
a list of categories it belongs to. This list is filtered to include only a list of categories it belongs to. This list is filtered to include only
films longer than 180 minutes, where the film language is 'English', and films longer than 180 minutes, where the film language is 'English',
the film category is not 'Action'. the film category is not 'Action' and 'Trailers' are one of the film's special features.
> [!Tip] > [!Tip]
> It is recommended to enable **Strict Scan** on application startup, especially when destination contains > It is recommended to enable **Strict Scan** on application startup, especially when destination contains
@ -370,82 +385,88 @@ fmt.Println(string(jsonText))
```js ```js
[ [
{ {
"ActorID": 1, "ActorID": 1,
"FirstName": "Penelope", "FirstName": "Penelope",
"LastName": "Guiness", "LastName": "Guiness",
"LastUpdate": "2013-05-26T14:47:57.62Z", "LastUpdate": "2013-05-26T14:47:57.62Z",
"Films": [ "Films": [
{ {
"FilmID": 499, "FilmID": 499,
"Title": "King Evolution", "Title": "King Evolution",
"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon", "Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
"ReleaseYear": 2006, "ReleaseYear": 2006,
"LanguageID": 1, "LanguageID": 1,
"RentalDuration": 3, "RentalDuration": 3,
"RentalRate": 4.99, "RentalRate": 4.99,
"Length": 184, "Length": 184,
"ReplacementCost": 24.99, "ReplacementCost": 24.99,
"Rating": "NC-17", "Rating": "NC-17",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}", "SpecialFeatures": [
"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7", "Trailers",
"Language": { "Deleted Scenes",
"LanguageID": 1, "Behind the Scenes"
"Name": "English ", ],
"LastUpdate": "0001-01-01T00:00:00Z" "Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7",
}, "Language": {
"Categories": [ "LanguageID": 1,
{ "Name": "English ",
"CategoryID": 8, "LastUpdate": "0001-01-01T00:00:00Z"
"Name": "Family", },
"LastUpdate": "2006-02-15T09:46:27Z" "Categories": [
} {
] "CategoryID": 8,
} "Name": "Family",
] "LastUpdate": "2006-02-15T09:46:27Z"
}, }
{ ]
"ActorID": 3, }
"FirstName": "Ed", ]
"LastName": "Chase", },
"LastUpdate": "2013-05-26T14:47:57.62Z", {
"Films": [ "ActorID": 3,
{ "FirstName": "Ed",
"FilmID": 996, "LastName": "Chase",
"Title": "Young Language", "LastUpdate": "2013-05-26T14:47:57.62Z",
"Description": "A Unbelieveable Yarn of a Boat And a Database Administrator who must Meet a Boy in The First Manned Space Station", "Films": [
"ReleaseYear": 2006, {
"LanguageID": 1, "FilmID": 996,
"RentalDuration": 6, "Title": "Young Language",
"RentalRate": 0.99, "Description": "A Unbelieveable Yarn of a Boat And a Database Administrator who must Meet a Boy in The First Manned Space Station",
"Length": 183, "ReleaseYear": 2006,
"ReplacementCost": 9.99, "LanguageID": 1,
"Rating": "G", "RentalDuration": 6,
"LastUpdate": "2013-05-26T14:50:58.951Z", "RentalRate": 0.99,
"SpecialFeatures": "{Trailers,\"Behind the Scenes\"}", "Length": 183,
"Fulltext": "'administr':12 'boat':8 'boy':17 'databas':11 'first':20 'languag':2 'man':21 'meet':15 'must':14 'space':22 'station':23 'unbeliev':4 'yarn':5 'young':1", "ReplacementCost": 9.99,
"Language": { "Rating": "G",
"LanguageID": 1, "LastUpdate": "2013-05-26T14:50:58.951Z",
"Name": "English ", "SpecialFeatures": [
"LastUpdate": "0001-01-01T00:00:00Z" "Trailers",
}, "Behind the Scenes"
"Categories": [ ],
{ "Fulltext": "'administr':12 'boat':8 'boy':17 'databas':11 'first':20 'languag':2 'man':21 'meet':15 'must':14 'space':22 'station':23 'unbeliev':4 'yarn':5 'young':1",
"CategoryID": 6, "Language": {
"Name": "Documentary", "LanguageID": 1,
"LastUpdate": "2006-02-15T09:46:27Z" "Name": "English ",
} "LastUpdate": "0001-01-01T00:00:00Z"
] },
} "Categories": [
] {
}, "CategoryID": 6,
//...(125 more items) "Name": "Documentary",
"LastUpdate": "2006-02-15T09:46:27Z"
}
]
}
]
},
//...(125 more items)
] ]
``` ```
What if, we also want to have list of films per category and actors per category, where films are longer than 180 minutes, film language is 'English' What if, we also want to have a list of films per category and actors per category, with the same search conditions.
and film category is not 'Action'.
In that case we can reuse above statement `stmt`, and just change our destination: In that case we can reuse above statement `stmt`, and just change our destination:
```go ```go
@ -464,88 +485,71 @@ handleError(err)
```js ```js
[ [
{ {
"CategoryID": 8, "CategoryID": 8,
"Name": "Family", "Name": "Family",
"LastUpdate": "2006-02-15T09:46:27Z", "LastUpdate": "2006-02-15T09:46:27Z",
"Films": [ "Films": [
{ {
"FilmID": 499, "FilmID": 499,
"Title": "King Evolution", "Title": "King Evolution",
"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon", "Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
"ReleaseYear": 2006, "ReleaseYear": 2006,
"LanguageID": 1, "LanguageID": 1,
"RentalDuration": 3, "RentalDuration": 3,
"RentalRate": 4.99, "RentalRate": 4.99,
"Length": 184, "Length": 184,
"ReplacementCost": 24.99, "ReplacementCost": 24.99,
"Rating": "NC-17", "Rating": "NC-17",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}", "SpecialFeatures": [
"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7" "Trailers",
}, "Deleted Scenes",
{ "Behind the Scenes"
"FilmID": 50, ],
"Title": "Baked Cleopatra", "Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7"
"Description": "A Stunning Drama of a Forensic Psychologist And a Husband who must Overcome a Waitress in A Monastery", }
"ReleaseYear": 2006, ],
"LanguageID": 1, "Actors": [
"RentalDuration": 3, {
"RentalRate": 2.99, "ActorID": 1,
"Length": 182, "FirstName": "Penelope",
"ReplacementCost": 20.99, "LastName": "Guiness",
"Rating": "G", "LastUpdate": "2013-05-26T14:47:57.62Z"
"LastUpdate": "2013-05-26T14:50:58.951Z", },
"SpecialFeatures": "{Commentaries,\"Behind the Scenes\"}", {
"Fulltext": "'bake':1 'cleopatra':2 'drama':5 'forens':8 'husband':12 'monasteri':20 'must':14 'overcom':15 'psychologist':9 'stun':4 'waitress':17" "ActorID": 20,
} "FirstName": "Lucille",
], "LastName": "Tracy",
"Actors": [ "LastUpdate": "2013-05-26T14:47:57.62Z"
{ },
"ActorID": 1, {
"FirstName": "Penelope", "ActorID": 36,
"LastName": "Guiness", "FirstName": "Burt",
"LastUpdate": "2013-05-26T14:47:57.62Z" "LastName": "Dukakis",
}, "LastUpdate": "2013-05-26T14:47:57.62Z"
{ },
"ActorID": 20, {
"FirstName": "Lucille", "ActorID": 118,
"LastName": "Tracy", "FirstName": "Cuba",
"LastUpdate": "2013-05-26T14:47:57.62Z" "LastName": "Allen",
}, "LastUpdate": "2013-05-26T14:47:57.62Z"
{ },
"ActorID": 36, {
"FirstName": "Burt", "ActorID": 187,
"LastName": "Dukakis", "FirstName": "Renee",
"LastUpdate": "2013-05-26T14:47:57.62Z" "LastName": "Ball",
}, "LastUpdate": "2013-05-26T14:47:57.62Z"
{ },
"ActorID": 70, {
"FirstName": "Michelle", "ActorID": 198,
"LastName": "Mcconaughey", "FirstName": "Mary",
"LastUpdate": "2013-05-26T14:47:57.62Z" "LastName": "Keitel",
}, "LastUpdate": "2013-05-26T14:47:57.62Z"
{ }
"ActorID": 118, ]
"FirstName": "Cuba", },
"LastName": "Allen", // ...
"LastUpdate": "2013-05-26T14:47:57.62Z"
},
{
"ActorID": 187,
"FirstName": "Renee",
"LastName": "Ball",
"LastUpdate": "2013-05-26T14:47:57.62Z"
},
{
"ActorID": 198,
"FirstName": "Mary",
"LastName": "Keitel",
"LastUpdate": "2013-05-26T14:47:57.62Z"
}
]
},
//...
] ]
``` ```
</details> </details>

View file

@ -8,6 +8,7 @@
package model package model
import ( import (
"github.com/lib/pq"
"time" "time"
) )
@ -23,6 +24,6 @@ type Film struct {
ReplacementCost float64 ReplacementCost float64
Rating *MpaaRating Rating *MpaaRating
LastUpdate time.Time LastUpdate time.Time
SpecialFeatures *string SpecialFeatures *pq.StringArray
Fulltext string Fulltext string
} }

View file

@ -19,6 +19,14 @@ const (
MpaaRating_Nc17 MpaaRating = "NC-17" MpaaRating_Nc17 MpaaRating = "NC-17"
) )
var MpaaRatingAllValues = []MpaaRating{
MpaaRating_G,
MpaaRating_Pg,
MpaaRating_Pg13,
MpaaRating_R,
MpaaRating_Nc17,
}
func (e *MpaaRating) Scan(value interface{}) error { func (e *MpaaRating) Scan(value interface{}) error {
var enumValue string var enumValue string
switch val := value.(type) { switch val := value.(type) {

View file

@ -16,7 +16,7 @@ var Actor = newActorTable("dvds", "actor", "")
type actorTable struct { type actorTable struct {
postgres.Table postgres.Table
//Columns // Columns
ActorID postgres.ColumnInteger ActorID postgres.ColumnInteger
FirstName postgres.ColumnString FirstName postgres.ColumnString
LastName postgres.ColumnString LastName postgres.ColumnString
@ -24,6 +24,7 @@ type actorTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ActorTable struct { type ActorTable struct {
@ -67,6 +68,7 @@ func newActorTableImpl(schemaName, tableName, alias string) actorTable {
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
allColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, LastUpdateColumn} allColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, LastUpdateColumn}
mutableColumns = postgres.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn} mutableColumns = postgres.ColumnList{FirstNameColumn, LastNameColumn, LastUpdateColumn}
defaultColumns = postgres.ColumnList{ActorIDColumn}
) )
return actorTable{ return actorTable{
@ -80,5 +82,6 @@ func newActorTableImpl(schemaName, tableName, alias string) actorTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,13 +16,14 @@ var Category = newCategoryTable("dvds", "category", "")
type categoryTable struct { type categoryTable struct {
postgres.Table postgres.Table
//Columns // Columns
CategoryID postgres.ColumnInteger CategoryID postgres.ColumnInteger
Name postgres.ColumnString Name postgres.ColumnString
LastUpdate postgres.ColumnTimestamp LastUpdate postgres.ColumnTimestamp
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type CategoryTable struct { type CategoryTable struct {
@ -65,6 +66,7 @@ func newCategoryTableImpl(schemaName, tableName, alias string) categoryTable {
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
allColumns = postgres.ColumnList{CategoryIDColumn, NameColumn, LastUpdateColumn} allColumns = postgres.ColumnList{CategoryIDColumn, NameColumn, LastUpdateColumn}
mutableColumns = postgres.ColumnList{NameColumn, LastUpdateColumn} mutableColumns = postgres.ColumnList{NameColumn, LastUpdateColumn}
defaultColumns = postgres.ColumnList{CategoryIDColumn, LastUpdateColumn}
) )
return categoryTable{ return categoryTable{
@ -77,5 +79,6 @@ func newCategoryTableImpl(schemaName, tableName, alias string) categoryTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,7 +16,7 @@ var Film = newFilmTable("dvds", "film", "")
type filmTable struct { type filmTable struct {
postgres.Table postgres.Table
//Columns // Columns
FilmID postgres.ColumnInteger FilmID postgres.ColumnInteger
Title postgres.ColumnString Title postgres.ColumnString
Description postgres.ColumnString Description postgres.ColumnString
@ -28,11 +28,12 @@ type filmTable struct {
ReplacementCost postgres.ColumnFloat ReplacementCost postgres.ColumnFloat
Rating postgres.ColumnString Rating postgres.ColumnString
LastUpdate postgres.ColumnTimestamp LastUpdate postgres.ColumnTimestamp
SpecialFeatures postgres.ColumnString SpecialFeatures postgres.ColumnStringArray
Fulltext postgres.ColumnString Fulltext postgres.ColumnString
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type FilmTable struct { type FilmTable struct {
@ -81,10 +82,11 @@ func newFilmTableImpl(schemaName, tableName, alias string) filmTable {
ReplacementCostColumn = postgres.FloatColumn("replacement_cost") ReplacementCostColumn = postgres.FloatColumn("replacement_cost")
RatingColumn = postgres.StringColumn("rating") RatingColumn = postgres.StringColumn("rating")
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
SpecialFeaturesColumn = postgres.StringColumn("special_features") SpecialFeaturesColumn = postgres.StringArrayColumn("special_features")
FulltextColumn = postgres.StringColumn("fulltext") FulltextColumn = postgres.StringColumn("fulltext")
allColumns = postgres.ColumnList{FilmIDColumn, TitleColumn, DescriptionColumn, ReleaseYearColumn, LanguageIDColumn, RentalDurationColumn, RentalRateColumn, LengthColumn, ReplacementCostColumn, RatingColumn, LastUpdateColumn, SpecialFeaturesColumn, FulltextColumn} allColumns = postgres.ColumnList{FilmIDColumn, TitleColumn, DescriptionColumn, ReleaseYearColumn, LanguageIDColumn, RentalDurationColumn, RentalRateColumn, LengthColumn, ReplacementCostColumn, RatingColumn, LastUpdateColumn, SpecialFeaturesColumn, FulltextColumn}
mutableColumns = postgres.ColumnList{TitleColumn, DescriptionColumn, ReleaseYearColumn, LanguageIDColumn, RentalDurationColumn, RentalRateColumn, LengthColumn, ReplacementCostColumn, RatingColumn, LastUpdateColumn, SpecialFeaturesColumn, FulltextColumn} mutableColumns = postgres.ColumnList{TitleColumn, DescriptionColumn, ReleaseYearColumn, LanguageIDColumn, RentalDurationColumn, RentalRateColumn, LengthColumn, ReplacementCostColumn, RatingColumn, LastUpdateColumn, SpecialFeaturesColumn, FulltextColumn}
defaultColumns = postgres.ColumnList{FilmIDColumn, RentalDurationColumn, RentalRateColumn, ReplacementCostColumn, RatingColumn, LastUpdateColumn}
) )
return filmTable{ return filmTable{
@ -107,5 +109,6 @@ func newFilmTableImpl(schemaName, tableName, alias string) filmTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,13 +16,14 @@ var FilmActor = newFilmActorTable("dvds", "film_actor", "")
type filmActorTable struct { type filmActorTable struct {
postgres.Table postgres.Table
//Columns // Columns
ActorID postgres.ColumnInteger ActorID postgres.ColumnInteger
FilmID postgres.ColumnInteger FilmID postgres.ColumnInteger
LastUpdate postgres.ColumnTimestamp LastUpdate postgres.ColumnTimestamp
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type FilmActorTable struct { type FilmActorTable struct {
@ -65,6 +66,7 @@ func newFilmActorTableImpl(schemaName, tableName, alias string) filmActorTable {
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
allColumns = postgres.ColumnList{ActorIDColumn, FilmIDColumn, LastUpdateColumn} allColumns = postgres.ColumnList{ActorIDColumn, FilmIDColumn, LastUpdateColumn}
mutableColumns = postgres.ColumnList{LastUpdateColumn} mutableColumns = postgres.ColumnList{LastUpdateColumn}
defaultColumns = postgres.ColumnList{LastUpdateColumn}
) )
return filmActorTable{ return filmActorTable{
@ -77,5 +79,6 @@ func newFilmActorTableImpl(schemaName, tableName, alias string) filmActorTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,13 +16,14 @@ var FilmCategory = newFilmCategoryTable("dvds", "film_category", "")
type filmCategoryTable struct { type filmCategoryTable struct {
postgres.Table postgres.Table
//Columns // Columns
FilmID postgres.ColumnInteger FilmID postgres.ColumnInteger
CategoryID postgres.ColumnInteger CategoryID postgres.ColumnInteger
LastUpdate postgres.ColumnTimestamp LastUpdate postgres.ColumnTimestamp
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type FilmCategoryTable struct { type FilmCategoryTable struct {
@ -65,6 +66,7 @@ func newFilmCategoryTableImpl(schemaName, tableName, alias string) filmCategoryT
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
allColumns = postgres.ColumnList{FilmIDColumn, CategoryIDColumn, LastUpdateColumn} allColumns = postgres.ColumnList{FilmIDColumn, CategoryIDColumn, LastUpdateColumn}
mutableColumns = postgres.ColumnList{LastUpdateColumn} mutableColumns = postgres.ColumnList{LastUpdateColumn}
defaultColumns = postgres.ColumnList{LastUpdateColumn}
) )
return filmCategoryTable{ return filmCategoryTable{
@ -77,5 +79,6 @@ func newFilmCategoryTableImpl(schemaName, tableName, alias string) filmCategoryT
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,13 +16,14 @@ var Language = newLanguageTable("dvds", "language", "")
type languageTable struct { type languageTable struct {
postgres.Table postgres.Table
//Columns // Columns
LanguageID postgres.ColumnInteger LanguageID postgres.ColumnInteger
Name postgres.ColumnString Name postgres.ColumnString
LastUpdate postgres.ColumnTimestamp LastUpdate postgres.ColumnTimestamp
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type LanguageTable struct { type LanguageTable struct {
@ -65,6 +66,7 @@ func newLanguageTableImpl(schemaName, tableName, alias string) languageTable {
LastUpdateColumn = postgres.TimestampColumn("last_update") LastUpdateColumn = postgres.TimestampColumn("last_update")
allColumns = postgres.ColumnList{LanguageIDColumn, NameColumn, LastUpdateColumn} allColumns = postgres.ColumnList{LanguageIDColumn, NameColumn, LastUpdateColumn}
mutableColumns = postgres.ColumnList{NameColumn, LastUpdateColumn} mutableColumns = postgres.ColumnList{NameColumn, LastUpdateColumn}
defaultColumns = postgres.ColumnList{LanguageIDColumn, LastUpdateColumn}
) )
return languageTable{ return languageTable{
@ -77,5 +79,6 @@ func newLanguageTableImpl(schemaName, tableName, alias string) languageTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,7 +16,7 @@ var ActorInfo = newActorInfoTable("dvds", "actor_info", "")
type actorInfoTable struct { type actorInfoTable struct {
postgres.Table postgres.Table
//Columns // Columns
ActorID postgres.ColumnInteger ActorID postgres.ColumnInteger
FirstName postgres.ColumnString FirstName postgres.ColumnString
LastName postgres.ColumnString LastName postgres.ColumnString
@ -24,6 +24,7 @@ type actorInfoTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ActorInfoTable struct { type ActorInfoTable struct {
@ -67,6 +68,7 @@ func newActorInfoTableImpl(schemaName, tableName, alias string) actorInfoTable {
FilmInfoColumn = postgres.StringColumn("film_info") FilmInfoColumn = postgres.StringColumn("film_info")
allColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn} allColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn}
mutableColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn} mutableColumns = postgres.ColumnList{ActorIDColumn, FirstNameColumn, LastNameColumn, FilmInfoColumn}
defaultColumns = postgres.ColumnList{}
) )
return actorInfoTable{ return actorInfoTable{
@ -80,5 +82,6 @@ func newActorInfoTableImpl(schemaName, tableName, alias string) actorInfoTable {
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View file

@ -16,7 +16,7 @@ var CustomerList = newCustomerListTable("dvds", "customer_list", "")
type customerListTable struct { type customerListTable struct {
postgres.Table postgres.Table
//Columns // Columns
ID postgres.ColumnInteger ID postgres.ColumnInteger
Name postgres.ColumnString Name postgres.ColumnString
Address postgres.ColumnString Address postgres.ColumnString
@ -29,6 +29,7 @@ type customerListTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type CustomerListTable struct { type CustomerListTable struct {
@ -77,6 +78,7 @@ func newCustomerListTableImpl(schemaName, tableName, alias string) customerListT
SidColumn = postgres.IntegerColumn("sid") SidColumn = postgres.IntegerColumn("sid")
allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, ZipCodeColumn, PhoneColumn, CityColumn, CountryColumn, NotesColumn, SidColumn} allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, ZipCodeColumn, PhoneColumn, CityColumn, CountryColumn, NotesColumn, SidColumn}
mutableColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, ZipCodeColumn, PhoneColumn, CityColumn, CountryColumn, NotesColumn, SidColumn} mutableColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, ZipCodeColumn, PhoneColumn, CityColumn, CountryColumn, NotesColumn, SidColumn}
defaultColumns = postgres.ColumnList{}
) )
return customerListTable{ return customerListTable{
@ -95,5 +97,6 @@ func newCustomerListTableImpl(schemaName, tableName, alias string) customerListT
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -35,9 +35,9 @@ func main() {
// Write query // Write query
stmt := SELECT( stmt := SELECT(
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // or just Actor.AllColumns
Film.AllColumns, Film.AllColumns,
Language.AllColumns.Except(Language.LastUpdate), Language.AllColumns.Except(Language.LastUpdate), // all language columns except last_update
Category.AllColumns, Category.AllColumns,
).FROM( ).FROM(
Actor. Actor.
@ -47,10 +47,13 @@ func main() {
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)). INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)), INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE( ).WHERE(
Language.Name.EQ(Char(20)("English")). AND(
AND(Category.Name.NOT_EQ(Text("Action"))). Language.Name.EQ(Char(20)("English")), // string columns Language.Name and Category.Name can be compared only with string expression
AND(Film.Length.GT(Int(180))). Category.Name.NOT_EQ(Text("Action")),
AND(Film.Rating.NOT_EQ(enum.MpaaRating.R)), Film.Length.GT(Int32(180)), // Film.Length is integer column and can be compared only with integer expression
Film.Rating.NOT_EQ(enum.MpaaRating.R),
String("Trailers").EQ(ANY(Film.SpecialFeatures)), // type safety is also enforced on array element types
),
).ORDER_BY( ).ORDER_BY(
Actor.ActorID.ASC(), Actor.ActorID.ASC(),
Film.FilmID.ASC(), Film.FilmID.ASC(),

View file

@ -19,7 +19,6 @@ const (
BaseType DataTypeKind = "base" BaseType DataTypeKind = "base"
EnumType DataTypeKind = "enum" EnumType DataTypeKind = "enum"
UserDefinedType DataTypeKind = "user-defined" UserDefinedType DataTypeKind = "user-defined"
ArrayType DataTypeKind = "array"
RangeType DataTypeKind = "range" RangeType DataTypeKind = "range"
) )
@ -28,4 +27,9 @@ type DataType struct {
Name string Name string
Kind DataTypeKind Kind DataTypeKind
IsUnsigned bool IsUnsigned bool
Dimensions int // The number of array dimensions
}
func (d DataType) IsArray() bool {
return d.Dimensions > 0
} }

View file

@ -66,22 +66,25 @@ select
not attr.attnotnull as "column.isNullable", not attr.attnotnull as "column.isNullable",
attr.attgenerated = 's' as "column.isGenerated", attr.attgenerated = 's' as "column.isGenerated",
attr.atthasdef as "column.hasDefault", attr.atthasdef as "column.hasDefault",
(case (case when tp.typcategory = 'A' then greatest(1, attr.attndims) --cockroach num dims fix
when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base' else 0
when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array' end) as "dataType.dimensions",
when tp.typtype = 'd' then 'base' (case coalesce(elem.typtype, tp.typtype)
when tp.typtype = 'e' then 'enum' when 'b' then 'base'
when tp.typtype = 'r' then 'range' when 'd' then 'base'
end) as "dataType.Kind", when 'e' then 'enum'
when 'r' then 'range'
end) as "dataType.Kind",
(case when tp.typtype = 'd' then (select pg_type.typname from pg_catalog.pg_type where pg_type.oid = tp.typbasetype) (case when tp.typtype = 'd' then (select pg_type.typname from pg_catalog.pg_type where pg_type.oid = tp.typbasetype)
when tp.typcategory = 'A' then pg_catalog.format_type(attr.atttypid, attr.atttypmod) when tp.typcategory = 'A' then elem.typname
else tp.typname else tp.typname
end) as "dataType.Name", end) as "dataType.Name",
false as "dataType.isUnsigned" false as "dataType.isUnsigned"
from pg_catalog.pg_attribute as attr from pg_catalog.pg_attribute as attr
join pg_catalog.pg_class as cls on cls.oid = attr.attrelid join pg_catalog.pg_class as cls on cls.oid = attr.attrelid
join pg_catalog.pg_namespace as ns on ns.oid = cls.relnamespace join pg_catalog.pg_namespace as ns on ns.oid = cls.relnamespace
join pg_catalog.pg_type as tp on tp.oid = attr.atttypid join pg_catalog.pg_type as tp on tp.oid = attr.atttypid
left join pg_catalog.pg_type elem ON tp.typelem = elem.oid -- only for arrays
where where
ns.nspname = $1 and ns.nspname = $1 and
cls.relname = $2 and cls.relname = $2 and

View file

@ -2,6 +2,7 @@ package template
import ( import (
"fmt" "fmt"
"github.com/lib/pq"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@ -247,10 +248,21 @@ func getType(columnMetadata metadata.Column) Type {
userDefinedType := getUserDefinedType(columnMetadata) userDefinedType := getUserDefinedType(columnMetadata)
if userDefinedType != "" { if userDefinedType != "" {
if columnMetadata.IsNullable { var importPath string
return Type{Name: "*" + userDefinedType}
if columnMetadata.DataType.IsArray() {
userDefinedType = "pq.StringArray"
importPath = "github.com/lib/pq"
}
if columnMetadata.IsNullable {
userDefinedType = "*" + userDefinedType
}
return Type{
Name: userDefinedType,
ImportPath: importPath,
} }
return Type{Name: userDefinedType}
} }
return NewType(getGoType(columnMetadata)) return NewType(getGoType(columnMetadata))
@ -260,7 +272,7 @@ func getUserDefinedType(column metadata.Column) string {
switch column.DataType.Kind { switch column.DataType.Kind {
case metadata.EnumType: case metadata.EnumType:
return dbidentifier.ToGoIdentifier(column.DataType.Name) return dbidentifier.ToGoIdentifier(column.DataType.Name)
case metadata.UserDefinedType, metadata.ArrayType: case metadata.UserDefinedType:
return "string" return "string"
} }
@ -268,17 +280,45 @@ func getUserDefinedType(column metadata.Column) string {
} }
func getGoType(column metadata.Column) interface{} { func getGoType(column metadata.Column) interface{} {
defaultGoType := toGoType(column) goType := toGoType(column)
if column.IsNullable { if column.DataType.IsArray() {
return reflect.New(reflect.TypeOf(defaultGoType)).Interface() goType = toGoArrayType(goType, column)
} }
return defaultGoType if column.IsNullable {
return reflect.New(reflect.TypeOf(goType)).Interface()
}
return goType
}
func toGoArrayType(elemType any, column metadata.Column) any {
if column.DataType.Dimensions > 1 {
return "" // unsupported multidimensional arrays
}
switch elemType.(type) {
case bool:
return pq.BoolArray{}
case int32:
return pq.Int32Array{}
case int64:
return pq.Int64Array{}
case float32:
return pq.Float32Array{}
case float64:
return pq.Float64Array{}
case []byte:
return pq.ByteaArray{}
default:
return pq.StringArray{}
}
} }
// toGoType returns model type for column info. // toGoType returns model type for column info.
func toGoType(column metadata.Column) interface{} { func toGoType(column metadata.Column) interface{} {
switch strings.ToLower(column.DataType.Name) { switch strings.ToLower(column.DataType.Name) {
case "user-defined", "enum": case "user-defined", "enum":
return "" return ""

View file

@ -162,11 +162,32 @@ func DefaultTableSQLBuilderColumn(columnMetaData metadata.Column) TableSQLBuilde
// getSqlBuilderColumnType returns type of jet sql builder column // getSqlBuilderColumnType returns type of jet sql builder column
func getSqlBuilderColumnType(columnMetaData metadata.Column) string { func getSqlBuilderColumnType(columnMetaData metadata.Column) string {
if columnMetaData.DataType.Kind != metadata.BaseType && switch columnMetaData.DataType.Kind {
columnMetaData.DataType.Kind != metadata.RangeType { case metadata.EnumType, metadata.UserDefinedType:
if columnMetaData.DataType.IsArray() {
return "StringArray"
}
return "String" return "String"
} }
columnType := sqlToColumnType(columnMetaData)
if columnMetaData.DataType.IsArray() {
if columnMetaData.DataType.Dimensions > 1 {
fmt.Println("- [SQL Builder] Unsupported sql array with multiple dimensions column '" +
columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
return "String"
}
columnType = columnType + "Array"
}
return columnType
}
// sqlToColumnType maps the type of a SQL column type to a go jet sql builder column. The second return value returns
// whether the given type is supported.
func sqlToColumnType(columnMetaData metadata.Column) string {
switch strings.ToLower(columnMetaData.DataType.Name) { switch strings.ToLower(columnMetaData.DataType.Name) {
case "boolean", "bool": case "boolean", "bool":
return "Bool" return "Bool"

View file

@ -0,0 +1,136 @@
package jet
// Array interface
type Array[E Expression] interface {
Expression
EQ(rhs Array[E]) BoolExpression
NOT_EQ(rhs Array[E]) BoolExpression
LT(rhs Array[E]) BoolExpression
GT(rhs Array[E]) BoolExpression
LT_EQ(rhs Array[E]) BoolExpression
GT_EQ(rhs Array[E]) BoolExpression
CONTAINS(rhs Array[E]) BoolExpression
IS_CONTAINED_BY(rhs Array[E]) BoolExpression
OVERLAP(rhs Array[E]) BoolExpression
CONCAT(rhs Array[E]) Array[E]
CONCAT_ELEMENT(E) Array[E]
AT(expression IntegerExpression) E
}
type arrayInterfaceImpl[E Expression] struct {
parent Array[E]
}
type BinaryBoolOp func(Expression, Expression) BoolExpression
func (a arrayInterfaceImpl[E]) EQ(rhs Array[E]) BoolExpression {
return Eq(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) NOT_EQ(rhs Array[E]) BoolExpression {
return NotEq(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) LT(rhs Array[E]) BoolExpression {
return Lt(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) GT(rhs Array[E]) BoolExpression {
return Gt(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) LT_EQ(rhs Array[E]) BoolExpression {
return LtEq(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) GT_EQ(rhs Array[E]) BoolExpression {
return GtEq(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) CONTAINS(rhs Array[E]) BoolExpression {
return Contains(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs Array[E]) BoolExpression {
return IsContainedBy(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) OVERLAP(rhs Array[E]) BoolExpression {
return Overlap(a.parent, rhs)
}
func (a arrayInterfaceImpl[E]) CONCAT(rhs Array[E]) Array[E] {
return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||"))
}
func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) Array[E] {
return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||"))
}
func (a arrayInterfaceImpl[E]) AT(at IntegerExpression) E {
return CastToArrayElemType[E](a.parent, CustomExpression(a.parent, Token("["), at, Token("]")))
}
type arrayExpressionWrapper[E Expression] struct {
arrayInterfaceImpl[E]
Expression
}
func newArrayExpressionWrap[E Expression](expression Expression) Array[E] {
arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression}
arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper
return &arrayExpressionWrapper
}
// ArrayExp is array expression wrapper around arbitrary expression.
// Allows go compiler to see any expression as array expression.
// Does not add sql cast to generated sql builder output.
func ArrayExp[E Expression](expression Expression) Array[E] {
return newArrayExpressionWrap[E](expression)
}
// CastToArrayElemType casts exp to array element type
func CastToArrayElemType[E Expression](array Array[E], exp Expression) E {
var i Expression
switch array.(type) {
case Array[BoolExpression]:
i = BoolExp(exp)
case Array[StringExpression]:
i = StringExp(exp)
case Array[IntegerExpression]:
i = IntExp(exp)
case Array[FloatExpression]:
i = FloatExp(exp)
case Array[BlobExpression]:
i = BlobExp(exp)
case Array[DateExpression]:
i = DateExp(exp)
case Array[TimestampExpression]:
i = TimestampExp(exp)
case Array[TimestampzExpression]:
i = TimestampzExp(exp)
case Array[TimeExpression]:
i = TimeExp(exp)
case Array[TimezExpression]:
i = TimezExp(exp)
case Array[IntervalExpression]:
i = IntervalExp(exp)
}
return i.(E)
}
// ARRAY constructor builds an array value using list of expressions.
func ARRAY[E Expression](elems ...E) Array[E] {
var args = make([]Serializer, len(elems))
for i, each := range elems {
args[i] = each
}
return ArrayExp[E](CustomExpression(Token("ARRAY["), ListSerializer{
Serializers: args,
Separator: ",",
}, Token("]")))
}

View file

@ -0,0 +1,68 @@
package jet
import (
"testing"
)
func TestArrayExpressionEQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)")
}
func TestArrayExpressionNOT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)")
}
func TestArrayExpressionLT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)")
}
func TestArrayExpressionGT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)")
}
func TestArrayExpressionLT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)")
}
func TestArrayExpressionGT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)")
}
func TestArrayExpressionCONTAINS(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)")
}
func TestArrayExpressionCONTAINED_BY(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)")
}
func TestArrayExpressionOVERLAP(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)")
}
func TestArrayExpressionCONCAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)")
}
func TestArrayExpressionCONCAT_ELEMENT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(StringExp(table2ColArray.AT(Int(1)))), "(table1.col_array_string || table2.col_array_string[$1])", int64(1))
assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x")
}
func TestArrayExpressionAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "table1.col_array_string[$1]", int64(1))
}
func TestCastToArrayElemType(t *testing.T) {
var _ BoolExpression = CastToArrayElemType[BoolExpression](ARRAY[BoolExpression](), table1Col1)
var _ IntegerExpression = CastToArrayElemType[IntegerExpression](ARRAY[IntegerExpression](), table1Col1)
var _ FloatExpression = CastToArrayElemType[FloatExpression](ARRAY[FloatExpression](), table1Col1)
var _ StringExpression = CastToArrayElemType[StringExpression](ARRAY[StringExpression](), table1Col1)
var _ BlobExpression = CastToArrayElemType[BlobExpression](ARRAY[BlobExpression](), table1Col1)
var _ DateExpression = CastToArrayElemType[DateExpression](ARRAY[DateExpression](), table1Col1)
var _ TimestampExpression = CastToArrayElemType[TimestampExpression](ARRAY[TimestampExpression](), table1Col1)
var _ TimestampzExpression = CastToArrayElemType[TimestampzExpression](ARRAY[TimestampzExpression](), table1Col1)
var _ TimeExpression = CastToArrayElemType[TimeExpression](ARRAY[TimeExpression](), table1Col1)
var _ TimezExpression = CastToArrayElemType[TimezExpression](ARRAY[TimezExpression](), table1Col1)
var _ IntervalExpression = CastToArrayElemType[IntervalExpression](ARRAY[IntervalExpression](), table1Col1)
}

View file

@ -133,6 +133,50 @@ func IntegerColumn(name string) ColumnInteger {
//------------------------------------------------------// //------------------------------------------------------//
type ColumnArray[E Expression] interface {
Array[E]
Column
From(subQuery SelectTable) ColumnArray[E]
SET(stringExp Array[E]) ColumnAssigment
}
type arrayColumnImpl[E Expression] struct {
arrayInterfaceImpl[E]
*ColumnExpressionImpl
}
func (a arrayColumnImpl[E]) fromImpl(subQuery SelectTable) Projection {
return a.From(subQuery)
}
func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] {
newArrayColumn := ArrayColumn[E](a.name)
newArrayColumn.setTableName(a.tableName)
newArrayColumn.setSubQuery(subQuery)
return newArrayColumn
}
func (a *arrayColumnImpl[E]) SET(stringExp Array[E]) ColumnAssigment {
return columnAssigmentImpl{
column: a,
toAssign: stringExp,
}
}
// StringColumn creates named string column.
func ArrayColumn[E Expression](name string) ColumnArray[E] {
arrayColumn := &arrayColumnImpl[E]{}
arrayColumn.arrayInterfaceImpl.parent = arrayColumn
arrayColumn.ColumnExpressionImpl = NewColumnImpl(name, "", arrayColumn)
return arrayColumn
}
//------------------------------------------------------//
// ColumnString is interface for SQL text, character, character varying // ColumnString is interface for SQL text, character, character varying
// uuid columns and enums types. // uuid columns and enums types.
type ColumnString interface { type ColumnString interface {

View file

@ -8,6 +8,36 @@ var subQuery = &selectTableImpl{
alias: "sub_query", alias: "sub_query",
} }
func TestNewArrayColumnString(t *testing.T) {
stringArrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery)
assertClauseSerialize(t, stringArrayColumn, `sub_query."colArray"`)
assertProjectionSerialize(t, stringArrayColumn, `sub_query."colArray" AS "colArray"`)
arrayColumn2 := table1ColStringArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_string"`)
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_string" AS "table1.col_array_string"`)
}
func TestNewArrayColumnBool(t *testing.T) {
boolArrayColumn := ArrayColumn[BoolExpression]("colArrayBool").From(subQuery)
assertClauseSerialize(t, boolArrayColumn, `sub_query."colArrayBool"`)
assertProjectionSerialize(t, boolArrayColumn, `sub_query."colArrayBool" AS "colArrayBool"`)
arrayColumn2 := table1ColBoolArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool"`)
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool" AS "table1.col_array_bool"`)
}
func TestNewArrayColumnInteger(t *testing.T) {
intArrayColumn := ArrayColumn[IntegerExpression]("colArrayInt").From(subQuery)
assertClauseSerialize(t, intArrayColumn, `sub_query."colArrayInt"`)
assertProjectionSerialize(t, intArrayColumn, `sub_query."colArrayInt" AS "colArrayInt"`)
arrayColumn2 := table1ColIntArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_int"`)
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_int" AS "table1.col_array_int"`)
}
func TestNewBoolColumn(t *testing.T) { func TestNewBoolColumn(t *testing.T) {
boolColumn := BoolColumn("colBool").From(subQuery) boolColumn := BoolColumn("colBool").From(subQuery)
assertClauseSerialize(t, boolColumn, `sub_query."colBool"`) assertClauseSerialize(t, boolColumn, `sub_query."colBool"`)

View file

@ -74,6 +74,11 @@ func Contains(lhs Expression, rhs Expression) BoolExpression {
return newBinaryBoolOperatorExpression(lhs, rhs, "@>") return newBinaryBoolOperatorExpression(lhs, rhs, "@>")
} }
// IsContainedBy returns a representation of "a <@ b"
func IsContainedBy(lhs Expression, rhs Expression) BoolExpression {
return newBinaryBoolOperatorExpression(lhs, rhs, "<@")
}
// Overlap returns a representation of "a && b" // Overlap returns a representation of "a && b"
func Overlap(lhs, rhs Expression) BoolExpression { func Overlap(lhs, rhs Expression) BoolExpression {
return newBinaryBoolOperatorExpression(lhs, rhs, "&&") return newBinaryBoolOperatorExpression(lhs, rhs, "&&")

View file

@ -4,16 +4,15 @@ import (
"bytes" "bytes"
"database/sql/driver" "database/sql/driver"
"fmt" "fmt"
"github.com/go-jet/jet/v2/internal/3rdparty/pq"
"github.com/go-jet/jet/v2/internal/utils/is"
"github.com/google/uuid"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/go-jet/jet/v2/internal/3rdparty/pq"
"github.com/go-jet/jet/v2/internal/utils/is"
"github.com/google/uuid"
) )
// SQLBuilder generates output SQL // SQLBuilder generates output SQL
@ -93,11 +92,11 @@ func (s *SQLBuilder) write(data []byte) {
} }
func isPreSeparator(b byte) bool { func isPreSeparator(b byte) bool {
return b == ' ' || b == '.' || b == ',' || b == '(' || b == '\n' || b == ':' return b == ' ' || b == '.' || b == ',' || b == '(' || b == '\n' || b == ':' || b == '['
} }
func isPostSeparator(b byte) bool { func isPostSeparator(b byte) bool {
return b == ' ' || b == '.' || b == ',' || b == ')' || b == '\n' || b == ':' return b == ' ' || b == '.' || b == ',' || b == ')' || b == '\n' || b == ':' || b == '[' || b == ']'
} }
// WriteAlias is used to add alias to output SQL // WriteAlias is used to add alias to output SQL

View file

@ -18,19 +18,22 @@ var defaultDialect = NewDialect(DialectParams{ // just for tests
}) })
var ( var (
table1Col1 = IntegerColumn("col1") table1Col1 = IntegerColumn("col1")
table1ColInt = IntegerColumn("col_int") table1ColInt = IntegerColumn("col_int")
table1ColFloat = FloatColumn("col_float") table1ColFloat = FloatColumn("col_float")
table1Col3 = IntegerColumn("col3") table1Col3 = IntegerColumn("col3")
table1ColTime = TimeColumn("col_time") table1ColTime = TimeColumn("col_time")
table1ColTimez = TimezColumn("col_timez") table1ColTimez = TimezColumn("col_timez")
table1ColTimestamp = TimestampColumn("col_timestamp") table1ColTimestamp = TimestampColumn("col_timestamp")
table1ColTimestampz = TimestampzColumn("col_timestampz") table1ColTimestampz = TimestampzColumn("col_timestampz")
table1ColBool = BoolColumn("col_bool") table1ColBool = BoolColumn("col_bool")
table1ColDate = DateColumn("col_date") table1ColDate = DateColumn("col_date")
table1ColRange = RangeColumn[Int8Expression]("col_range") table1ColRange = RangeColumn[Int8Expression]("col_range")
table1ColStringArray = ArrayColumn[StringExpression]("col_array_string")
table1ColBoolArray = ArrayColumn[BoolExpression]("col_array_bool")
table1ColIntArray = ArrayColumn[IntegerExpression]("col_array_int")
) )
var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz) var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz, table1ColStringArray, table1ColBoolArray, table1ColIntArray)
var ( var (
table2Col3 = IntegerColumn("col3") table2Col3 = IntegerColumn("col3")
@ -45,8 +48,9 @@ var (
table2ColTimestampz = TimestampzColumn("col_timestampz") table2ColTimestampz = TimestampzColumn("col_timestampz")
table2ColDate = DateColumn("col_date") table2ColDate = DateColumn("col_date")
table2ColRange = RangeColumn[Int8Expression]("col_range") table2ColRange = RangeColumn[Int8Expression]("col_range")
table2ColArray = ArrayColumn[StringExpression]("col_array_string")
) )
var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz) var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz, table2ColArray)
var ( var (
table3Col1 = IntegerColumn("col1") table3Col1 = IntegerColumn("col1")

View file

@ -299,6 +299,16 @@ func AssertFileNamesEqual(t *testing.T, dirPath string, fileNames ...string) {
} }
} }
// DeepCopy create deep copy of src
func DeepCopy[T any](t require.TestingT, src T) T {
var dst T
data, err := json.Marshal(src)
require.NoError(t, err)
err = json.Unmarshal(data, &dst)
require.NoError(t, err)
return dst
}
// AssertDeepEqual checks if actual and expected objects are deeply equal. // AssertDeepEqual checks if actual and expected objects are deeply equal.
func AssertDeepEqual(t require.TestingT, actual, expected interface{}, option ...cmp.Option) { func AssertDeepEqual(t require.TestingT, actual, expected interface{}, option ...cmp.Option) {
if !assert.True(t, cmp.Equal(actual, expected, option...)) { if !assert.True(t, cmp.Equal(actual, expected, option...)) {

33
postgres/array_columns.go Executable file
View file

@ -0,0 +1,33 @@
package postgres
import "github.com/go-jet/jet/v2/internal/jet"
// Interfaces for different postgres array column types
type (
ColumnBoolArray jet.ColumnArray[BoolExpression]
ColumnStringArray jet.ColumnArray[StringExpression]
ColumnIntegerArray jet.ColumnArray[IntegerExpression]
ColumnFloatArray jet.ColumnArray[FloatExpression]
ColumnByteaArray jet.ColumnArray[ByteaExpression]
ColumnDateArray jet.ColumnArray[DateExpression]
ColumnTimestampArray jet.ColumnArray[TimestampExpression]
ColumnTimestampzArray jet.ColumnArray[TimestampzExpression]
ColumnTimeArray jet.ColumnArray[TimeExpression]
ColumnTimezArray jet.ColumnArray[TimezExpression]
ColumnIntervalArray jet.ColumnArray[IntervalExpression]
)
// Column constructors for different postgres array column types
var (
BoolArrayColumn = jet.ArrayColumn[BoolExpression]
StringArrayColumn = jet.ArrayColumn[StringExpression]
IntegerArrayColumn = jet.ArrayColumn[IntegerExpression]
FloatArrayColumn = jet.ArrayColumn[FloatExpression]
ByteaArrayColumn = jet.ArrayColumn[ByteaExpression]
DateArrayColumn = jet.ArrayColumn[DateExpression]
TimestampArrayColumn = jet.ArrayColumn[TimestampExpression]
TimestampzArrayColumn = jet.ArrayColumn[TimestampzExpression]
TimeArrayColumn = jet.ArrayColumn[TimeExpression]
TimezArrayColumn = jet.ArrayColumn[TimezExpression]
IntervalArrayColumn = jet.ArrayColumn[IntervalExpression]
)

View file

@ -96,7 +96,7 @@ func (b *cast) AS_DATE() DateExpression {
return DateExp(b.AS("date")) return DateExp(b.AS("date"))
} }
// AS_DECIMAL casts expression AS date type // AS_DECIMAL casts expression AS decimal type
func (b *cast) AS_DECIMAL() FloatExpression { func (b *cast) AS_DECIMAL() FloatExpression {
return FloatExp(b.AS("decimal")) return FloatExp(b.AS("decimal"))
} }
@ -130,3 +130,68 @@ func (b *cast) AS_TIMESTAMPZ() TimestampzExpression {
func (b *cast) AS_INTERVAL() IntervalExpression { func (b *cast) AS_INTERVAL() IntervalExpression {
return IntervalExp(b.AS("interval")) return IntervalExp(b.AS("interval"))
} }
// AS_UUID casts expression AS uuid type
func (b *cast) AS_UUID() StringExpression {
return StringExp(b.AS("uuid"))
}
// AS_BOOL_ARRAY casts expression as boolean array type
func (b *cast) AS_BOOL_ARRAY() Array[BoolExpression] {
return ArrayExp[BoolExpression](b.AS("boolean[]"))
}
// AS_INTEGER_ARRAY casts expression as integer array type
func (b *cast) AS_INTEGER_ARRAY() Array[IntegerExpression] {
return ArrayExp[IntegerExpression](b.AS("integer[]"))
}
// AS_BIGINT_ARRAY casts expression as bigint array type
func (b *cast) AS_BIGINT_ARRAY() Array[IntegerExpression] {
return ArrayExp[IntegerExpression](b.AS("bigint[]"))
}
// AS_REAL_ARRAY casts expression as real array
func (b *cast) AS_REAL_ARRAY() Array[FloatExpression] {
return ArrayExp[FloatExpression](b.AS("real[]"))
}
// AS_DOUBLE_ARRAY casts expression as double precision array
func (b *cast) AS_DOUBLE_ARRAY() Array[FloatExpression] {
return ArrayExp[FloatExpression](b.AS("double precision[]"))
}
// AS_TEXT_ARRAY casts expression as text array
func (b *cast) AS_TEXT_ARRAY() Array[StringExpression] {
return ArrayExp[StringExpression](b.AS("text[]"))
}
// AS_BYTEA_ARRAY casts expression as bytea array
func (b *cast) AS_BYTEA_ARRAY() Array[ByteaExpression] {
return ArrayExp[ByteaExpression](b.AS("bytea[]"))
}
// AS_DATE_ARRAY casts expression as date array
func (b *cast) AS_DATE_ARRAY() Array[DateExpression] {
return ArrayExp[DateExpression](b.AS("date[]"))
}
// AS_TIMESTAMP_ARRAY casts expression as timestamp array
func (b *cast) AS_TIMESTAMP_ARRAY() Array[TimestampExpression] {
return ArrayExp[TimestampExpression](b.AS("timestamp without time zone[]"))
}
// AS_TIMESTAMPZ_ARRAY casts expression as timestamp with time zone array
func (b *cast) AS_TIMESTAMPZ_ARRAY() Array[TimestampzExpression] {
return ArrayExp[TimestampzExpression](b.AS("timestamp with time zone[]"))
}
// AS_TIME_ARRAY casts expression as time array
func (b *cast) AS_TIME_ARRAY() Array[TimeExpression] {
return ArrayExp[TimeExpression](b.AS("time without time zone[]"))
}
// AS_TIMEZ_ARRAY casts expression as time with timezone array
func (b *cast) AS_TIMEZ_ARRAY() Array[TimezExpression] {
return ArrayExp[TimezExpression](b.AS("time with time zone[]"))
}

View file

@ -2,7 +2,7 @@ package postgres
import "github.com/go-jet/jet/v2/internal/jet" import "github.com/go-jet/jet/v2/internal/jet"
// Expression is common interface for all expressions. // Expression is a common interface for all expressions.
// Can be Bool, Int, Float, String, Date, Time, Timez, Timestamp or Timestampz expressions. // Can be Bool, Int, Float, String, Date, Time, Timez, Timestamp or Timestampz expressions.
type Expression = jet.Expression type Expression = jet.Expression
@ -176,3 +176,13 @@ var NewEnumValue = jet.NewEnumValue
// BinaryOperator can be used to use custom or unsupported operators that take two operands. // BinaryOperator can be used to use custom or unsupported operators that take two operands.
var BinaryOperator = jet.BinaryOperator var BinaryOperator = jet.BinaryOperator
// Array is a common template interface for all array expressions.
type Array[T Expression] jet.Array[T]
// ArrayExp serves as a wrapper for an arbitrary expression, treating it as an array expression of type T.
// This enables the Go compiler to interpret any expression as an array expression of type T.
// Note: This does not modify the generated SQL builder output by adding an SQL CAST operation.
func ArrayExp[T Expression](exp Expression) Array[T] {
return jet.ArrayExp[T](exp)
}

View file

@ -335,7 +335,7 @@ var TO_ASCII = jet.TO_ASCII
// TO_HEX converts number to its equivalent hexadecimal representation // TO_HEX converts number to its equivalent hexadecimal representation
var TO_HEX = jet.TO_HEX var TO_HEX = jet.TO_HEX
//----------Data Type Formatting Functions ----------------------// //---------- Range Functions ----------------------//
// LOWER_BOUND returns range expressions lower bound // LOWER_BOUND returns range expressions lower bound
func LOWER_BOUND[T Expression](expression jet.Range[T]) T { func LOWER_BOUND[T Expression](expression jet.Range[T]) T {
@ -347,7 +347,144 @@ func UPPER_BOUND[T Expression](expression jet.Range[T]) T {
return jet.UPPER_BOUND[T](expression) return jet.UPPER_BOUND[T](expression)
} }
//----------Data Type Formatting Functions ----------------------// // ---------- Array Functions ----------------------//
// ANY should be used in combination with a boolean operator. The result of ANY is "true" if any true result is obtained
func ANY[E Expression](arr Array[E]) E {
return jet.CastToArrayElemType(arr, Func("ANY", arr))
}
// ALL should be used in combination with a boolean operator. The result of ALL is “true” if all comparisons yield true
func ALL[E Expression](arr Array[E]) E {
return jet.CastToArrayElemType(arr, Func("ALL", arr))
}
// ARRAY_APPEND appends an element to the end of an array
func ARRAY_APPEND[E Expression](arr Array[E], elem E) Array[E] {
return ArrayExp[E](Func("ARRAY_APPEND", arr, elem))
}
// ARRAY_CAT concatenates two arrays
func ARRAY_CAT[E Expression](arr1, arr2 Array[E]) Array[E] {
return ArrayExp[E](Func("ARRAY_CAT", arr1, arr2))
}
// ARRAY_DIMS returns a text representation of the array's dimensions.
func ARRAY_DIMS[E Expression](arr Array[E]) StringExpression {
return StringExp(Func("ARRAY_DIMS", arr))
}
// ARRAY_LENGTH returns the length of the requested array dimension.
// Produces NULL instead of 0 for empty or missing array dimensions.
func ARRAY_LENGTH[E Expression](arr Array[E], elem IntegerExpression) IntegerExpression {
return IntExp(Func("ARRAY_LENGTH", arr, elem))
}
// ARRAY_LOWER returns the lower bound of the requested array dimension.
func ARRAY_LOWER[E Expression](arr Array[E]) IntegerExpression {
return IntExp(Func("ARRAY_LOWER", arr))
}
// ARRAY_NDIMS returns the number of dimensions of the array.
func ARRAY_NDIMS[E Expression](arr Array[E]) IntegerExpression {
return IntExp(Func("ARRAY_NDIMS", arr))
}
// ARRAY_POSITION returns the subscript of the first occurrence of the second argument in the array, or NULL if it's not present.
// If the third argument is given, the search begins at that subscript.
// The array must be one-dimensional.
// Comparisons are done using IS NOT DISTINCT FROM semantics, so it is possible to search for NULL.
func ARRAY_POSITION[E Expression](arr Array[E], elem E, start ...IntegerExpression) IntegerExpression {
return IntExp(Func("ARRAY_POSITION", optionalAppend([]Expression{arr, elem}, start)...))
}
// ARRAY_POSITIONS returns an array of the subscripts of all occurrences of the second argument in the array given as first argument.
// The array must be one-dimensional.
// Comparisons are done using IS NOT DISTINCT FROM semantics, so it is possible to search for NULL.
// NULL is returned only if the array is NULL; if the value is not found in the array, an empty array is returned.
func ARRAY_POSITIONS[E Expression](arr Array[E], elem E) Array[IntegerExpression] {
return ArrayExp[IntegerExpression](Func("ARRAY_POSITIONS", arr, elem))
}
// ARRAY_PREPEND prepends an element to the beginning of an array
func ARRAY_PREPEND[E Expression](el E, arr Array[E]) Array[E] {
return ArrayExp[E](Func("ARRAY_PREPEND", el, arr))
}
// ARRAY_REMOVE removes all elements equal to the given value from the array. The array must be one-dimensional.
// Comparisons are done using IS NOT DISTINCT FROM semantics, so it is possible to remove NULLs.
func ARRAY_REMOVE[E Expression](arr Array[E], elem Expression) IntegerExpression {
return IntExp(Func("ARRAY_REMOVE", arr, elem))
}
// ARRAY_REPLACE replaces each array element equal to the second argument with the third argument.
func ARRAY_REPLACE[E Expression](arr Array[E], existing E, new E) Array[E] {
return ArrayExp[E](Func("ARRAY_REPLACE", arr, existing, new))
}
// ARRAY_REVERSE reverses the first dimension of the array.
func ARRAY_REVERSE[E Expression](arr Array[E]) Array[E] {
return ArrayExp[E](Func("ARRAY_REVERSE", arr))
}
// ARRAY_SAMPLE returns an array of n items randomly selected from array.
// n may not exceed the length of array's first dimension.
// If array is multi-dimensional, an “item” is a slice having a given first subscript.
func ARRAY_SAMPLE[E Expression](arr Array[E], n IntegerExpression) Array[E] {
return ArrayExp[E](Func("ARRAY_SAMPLE", arr, n))
}
// ARRAY_SHUFFLE randomly shuffles the first dimension of the array.
func ARRAY_SHUFFLE[E Expression](arr Array[E]) Array[E] {
return ArrayExp[E](Func("ARRAY_SHUFFLE", arr))
}
// ARRAY_SORT sorts the first dimension of the array.
// The sort order is determined by the default sort ordering of the array's element type; however, if the element type is collatable, the collation to use can be specified by adding a COLLATE clause to the array argument.
//
// If descending is true then sort in descending order, otherwise ascending order.
// If omitted, the default is ascending order.
// If nulls_first is true then nulls appear before non-null values, otherwise nulls appear after non-null values.
// If omitted, nulls_first is taken to have the same value as descending.
func ARRAY_SORT[E Expression](arr Array[E], desc BoolExpression, nullFirst ...BoolExpression) Array[E] {
return ArrayExp[E](Func("ARRAY_SORT", optionalAppend([]Expression{arr, desc}, nullFirst)...))
}
// ARRAY_TO_STRING Converts each array element to its text representation, and concatenates those separated by the delimiter string.
// If null_string is given and is not NULL, then NULL array entries are represented by that string; otherwise, they are omitted.
func ARRAY_TO_STRING[T Expression](arr Array[T], delim StringExpression) StringExpression {
return StringExp(Func("ARRAY_TO_STRING", arr, delim))
}
// ARRAY_UPPER returns the upper bound of the requested array dimension.
func ARRAY_UPPER[E Expression](arr Array[E], dim IntegerExpression) IntegerExpression {
return IntExp(Func("ARRAY_UPPER", arr, dim))
}
// CARDINALITY returns the total number of elements in the array, or 0 if the array is empty.
func CARDINALITY[E Expression](arr Array[E]) IntegerExpression {
return IntExp(Func("CARDINALITY", arr))
}
// TRIM_ARRAY trims an array by removing the last n elements. If the array is multidimensional, only the first dimension is trimmed.
func TRIM_ARRAY[E Expression](arr Array[E], n IntegerExpression) Array[E] {
return ArrayExp[E](Func("TRIM_ARRAY", arr, n))
}
// ARRAY constructor is an expression that builds an array value using values for its member elements.
func ARRAY[T Expression](elems ...T) Array[T] {
return jet.ARRAY[T](elems...)
}
func optionalAppend[O Expression](elem []Expression, optional []O) []Expression {
if len(optional) == 0 {
return elem
}
return append(elem, optional[0])
}
//---------- Data Type Formatting Functions ----------------------//
// TO_CHAR converts expression to string with format // TO_CHAR converts expression to string with format
var TO_CHAR = jet.TO_CHAR var TO_CHAR = jet.TO_CHAR

View file

@ -1,6 +1,7 @@
package postgres package postgres
import ( import (
"github.com/lib/pq"
"testing" "testing"
) )
@ -34,3 +35,41 @@ func TestGENERATE_SERIES(t *testing.T) {
"GENERATE_SERIES(NOW(), NOW() + INTERVAL '10 DAY', INTERVAL '2 DAY')", "GENERATE_SERIES(NOW(), NOW() + INTERVAL '10 DAY', INTERVAL '2 DAY')",
) )
} }
func TestArrayFunctions(t *testing.T) {
intArray := Int32Array(1, 2, 3)
stringArrayColumn := StringArrayColumn("str_arr_col")
assertSerialize(t, ARRAY_LOWER(intArray), "ARRAY_LOWER($1::integer[])", pq.Int32Array{1, 2, 3})
assertSerialize(t, ARRAY_DIMS(stringArrayColumn).EQ(String("[1,1]")),
"(ARRAY_DIMS(str_arr_col) = $1::text)",
"[1,1]",
)
assertSerialize(t, ARRAY_NDIMS(stringArrayColumn).EQ(Int(1)), "(ARRAY_NDIMS(str_arr_col) = $1)", int64(1))
assertSerialize(t, ARRAY_REVERSE(stringArrayColumn), "ARRAY_REVERSE(str_arr_col)")
assertSerialize(t, ARRAY_SAMPLE(stringArrayColumn, Int(2)).AT(Int(1)).EQ(String("john")),
"(ARRAY_SAMPLE(str_arr_col, $1)[$2] = $3::text)",
int64(2), int64(1), "john",
)
assertSerialize(t, ARRAY_SHUFFLE(intArray).AT(Int(2)).EQ(Int(33)),
"(ARRAY_SHUFFLE($1::integer[])[$2] = $3)",
pq.Int32Array{1, 2, 3}, int64(2), int64(33),
)
assertSerialize(t, ARRAY_SORT(stringArrayColumn, Bool(true)),
"ARRAY_SORT(str_arr_col, $1::boolean)",
true,
)
assertSerialize(t, ARRAY_SORT(stringArrayColumn, Bool(true), Bool(false)),
"ARRAY_SORT(str_arr_col, $1::boolean, $2::boolean)",
true, false,
)
assertSerialize(t, TRIM_ARRAY(stringArrayColumn, Int(6)),
"TRIM_ARRAY(str_arr_col, $1)",
int64(6),
)
}

View file

@ -1,6 +1,8 @@
package postgres package postgres
import ( import (
"fmt"
"github.com/lib/pq"
"time" "time"
"github.com/go-jet/jet/v2/internal/jet" "github.com/go-jet/jet/v2/internal/jet"
@ -68,8 +70,11 @@ func Double(value float64) FloatExpression {
} }
// Decimal creates new float literal expression // Decimal creates new float literal expression
var Decimal = jet.Decimal func Decimal(value string) FloatExpression {
return CAST(jet.Literal(value)).AS_DECIMAL()
}
// String creates new string literal expression
// String is a parameter constructor for the PostgreSQL text type. Using the `Text` constructor is // String is a parameter constructor for the PostgreSQL text type. Using the `Text` constructor is
// generally preferable. // generally preferable.
// //
@ -124,7 +129,9 @@ func Json(value interface{}) StringExpression {
// UUID is a helper function to create string literal expression from uuid object // UUID is a helper function to create string literal expression from uuid object
// value can be any uuid type with a String method // value can be any uuid type with a String method
var UUID = jet.UUID func UUID(value fmt.Stringer) StringExpression {
return CAST(jet.Literal(value.String())).AS_UUID()
}
// Bytea creates new bytea literal expression // Bytea creates new bytea literal expression
func Bytea(value interface{}) ByteaExpression { func Bytea(value interface{}) ByteaExpression {
@ -185,3 +192,63 @@ func Timestampz(year int, month time.Month, day, hour, minute, second int, milli
func TimestampzT(t time.Time) TimestampzExpression { func TimestampzT(t time.Time) TimestampzExpression {
return CAST(jet.TimestampzT(t)).AS_TIMESTAMPZ() return CAST(jet.TimestampzT(t)).AS_TIMESTAMPZ()
} }
// BoolArray creates new bool array literal expression from list of values
func BoolArray(values ...bool) Array[BoolExpression] {
return CAST(jet.Literal(pq.BoolArray(values))).AS_BOOL_ARRAY()
}
// Int32Array creates new integer array literal expression from list of values
func Int32Array(values ...int32) Array[IntegerExpression] {
return CAST(jet.Literal(pq.Int32Array(values))).AS_INTEGER_ARRAY()
}
// Int64Array creates new bigint array literal expression from list of values
func Int64Array(values ...int64) Array[IntegerExpression] {
return CAST(jet.Literal(pq.Int64Array(values))).AS_BIGINT_ARRAY()
}
// Float32Array creates new real array literal expression from list of values
func Float32Array(values ...float32) Array[FloatExpression] {
return CAST(jet.Literal(pq.Float32Array(values))).AS_REAL_ARRAY()
}
// Float64Array creates new double precision array literal expression from list of values
func Float64Array(values ...float64) Array[FloatExpression] {
return CAST(jet.Literal(pq.Float64Array(values))).AS_DOUBLE_ARRAY()
}
// StringArray creates new string array literal expression from list of values
func StringArray(values ...string) Array[StringExpression] {
return CAST(jet.Literal(pq.StringArray(values))).AS_TEXT_ARRAY()
}
// ByteaArray creates new bytea array literal expression from list of values
func ByteaArray(values ...[]byte) Array[ByteaExpression] {
return CAST(jet.Literal(pq.ByteaArray(values))).AS_BYTEA_ARRAY()
}
// DateArray creates new date array literal expression from list of values
func DateArray(values ...time.Time) Array[DateExpression] {
return CAST(jet.Literal(pq.Array(values))).AS_DATE_ARRAY()
}
// TimestampArray creates new timestamp array literal expression from list of values
func TimestampArray(values ...time.Time) Array[TimestampExpression] {
return CAST(jet.Literal(pq.Array(values))).AS_TIMESTAMP_ARRAY()
}
// TimestampzArray creates new timestampt with timezone array literal expression from list of values
func TimestampzArray(values ...time.Time) Array[TimestampzExpression] {
return CAST(jet.Literal(pq.Array(values))).AS_TIMESTAMPZ_ARRAY()
}
// TimeArray creates new time array literal expression from list of values
func TimeArray(values ...time.Time) Array[TimeExpression] {
return CAST(jet.Literal(pq.Array(values))).AS_TIME_ARRAY()
}
// TimezArray creates new time with timezone array literal expression from list of values
func TimezArray(values ...time.Time) Array[TimezExpression] {
return CAST(jet.Literal(pq.Array(values))).AS_TIMEZ_ARRAY()
}

View file

@ -18,6 +18,8 @@ var table1ColBool = BoolColumn("col_bool")
var table1ColDate = DateColumn("col_date") var table1ColDate = DateColumn("col_date")
var table1ColInterval = IntervalColumn("col_interval") var table1ColInterval = IntervalColumn("col_interval")
var table1ColRange = Int8RangeColumn("col_range") var table1ColRange = Int8RangeColumn("col_range")
var table1ColStringArray = StringArrayColumn("col_string_array")
var table1ColIntArray = IntegerArrayColumn("col_int_array")
var table1 = NewTable( var table1 = NewTable(
"db", "db",
@ -34,6 +36,8 @@ var table1 = NewTable(
table1ColTimestampz, table1ColTimestampz,
table1ColInterval, table1ColInterval,
table1ColRange, table1ColRange,
table1ColStringArray,
table1ColIntArray,
) )
var table2Col3 = IntegerColumn("col3") var table2Col3 = IntegerColumn("col3")
@ -49,8 +53,10 @@ var table2ColTimestampz = TimestampzColumn("col_timestampz")
var table2ColDate = DateColumn("col_date") var table2ColDate = DateColumn("col_date")
var table2ColInterval = IntervalColumn("col_interval") var table2ColInterval = IntervalColumn("col_interval")
var table2ColRange = Int8RangeColumn("col_range") var table2ColRange = Int8RangeColumn("col_range")
var table2ColStringArray = StringArrayColumn("col_string_array")
var table2ColIntArray = IntegerArrayColumn("col_int_array")
var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange) var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange, table2ColStringArray, table2ColIntArray)
var table3Col1 = IntegerColumn("col1") var table3Col1 = IntegerColumn("col1")
var table3ColInt = IntegerColumn("col_int") var table3ColInt = IntegerColumn("col_int")

View file

@ -1,4 +1,3 @@
version: '3'
services: services:
postgres: postgres:
image: postgres:14.1 image: postgres:14.1
@ -13,7 +12,7 @@ services:
- ./testdata/init/postgres:/docker-entrypoint-initdb.d - ./testdata/init/postgres:/docker-entrypoint-initdb.d
mysql: mysql:
image: mysql:8.0.27 image: mysql/mysql-server:8.0.27
command: ['--default-authentication-plugin=mysql_native_password', '--log_bin_trust_function_creators=1'] command: ['--default-authentication-plugin=mysql_native_password', '--log_bin_trust_function_creators=1']
restart: always restart: always
environment: environment:

View file

@ -2,12 +2,12 @@ package postgres
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"github.com/go-jet/jet/v2/internal/utils/ptr" "github.com/go-jet/jet/v2/internal/utils/ptr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"math" "math"
"github.com/go-jet/jet/v2/qrm" "github.com/go-jet/jet/v2/qrm"
"github.com/lib/pq"
"testing" "testing"
"time" "time"
@ -44,8 +44,7 @@ func TestAllTypesSelectJson(t *testing.T) {
AllTypesAllColumns.Except( AllTypesAllColumns.Except(
AllTypes.JSON, AllTypes.JSONPtr, AllTypes.JSON, AllTypes.JSONPtr,
AllTypes.Jsonb, AllTypes.JsonbPtr, AllTypes.Jsonb, AllTypes.JsonbPtr,
AllTypes.TextArray, AllTypes.TextArrayPtr, AllTypes.JsonbArray,
AllTypes.JsonbArray, AllTypes.IntegerArray, AllTypes.IntegerArrayPtr,
AllTypes.TextMultiDimArray, AllTypes.TextMultiDimArrayPtr, AllTypes.TextMultiDimArray, AllTypes.TextMultiDimArrayPtr,
), ),
// unsupported at the moment, casting to text allows these columns to be assigned to string fields // unsupported at the moment, casting to text allows these columns to be assigned to string fields
@ -53,11 +52,7 @@ func TestAllTypesSelectJson(t *testing.T) {
CAST(AllTypes.JSON).AS_TEXT().AS("JSON"), CAST(AllTypes.JSON).AS_TEXT().AS("JSON"),
CAST(AllTypes.JsonbPtr).AS_TEXT().AS("jsonbPtr"), CAST(AllTypes.JsonbPtr).AS_TEXT().AS("jsonbPtr"),
CAST(AllTypes.Jsonb).AS_TEXT().AS("Jsonb"), CAST(AllTypes.Jsonb).AS_TEXT().AS("Jsonb"),
CAST(AllTypes.TextArrayPtr).AS_TEXT().AS("TextArrayPtr"), CAST(AllTypes.JsonbArray).AS_TEXT_ARRAY().AS("JsonbArray"),
CAST(AllTypes.TextArray).AS_TEXT().AS("TextArray"),
CAST(AllTypes.JsonbArray).AS_TEXT().AS("JsonbArray"),
CAST(AllTypes.IntegerArray).AS_TEXT().AS("IntegerArray"),
CAST(AllTypes.IntegerArrayPtr).AS_TEXT().AS("IntegerArrayPtr"),
CAST(AllTypes.TextMultiDimArray).AS_TEXT().AS("TextMultiDimArray"), CAST(AllTypes.TextMultiDimArray).AS_TEXT().AS("TextMultiDimArray"),
CAST(AllTypes.TextMultiDimArrayPtr).AS_TEXT().AS("TextMultiDimArrayPtr"), CAST(AllTypes.TextMultiDimArrayPtr).AS_TEXT().AS("TextMultiDimArrayPtr"),
).FROM(AllTypes) ).FROM(AllTypes)
@ -115,17 +110,17 @@ FROM (
all_types.uuid AS "uuid", all_types.uuid AS "uuid",
all_types.xml_ptr AS "xmlPtr", all_types.xml_ptr AS "xmlPtr",
all_types.xml AS "xml", all_types.xml AS "xml",
all_types.integer_array_ptr AS "integerArrayPtr",
all_types.integer_array AS "integerArray",
all_types.text_array_ptr AS "textArrayPtr",
all_types.text_array AS "textArray",
all_types.mood_ptr AS "moodPtr", all_types.mood_ptr AS "moodPtr",
all_types.mood AS "mood", all_types.mood AS "mood",
all_types.json_ptr::text AS "jsonPtr", all_types.json_ptr::text AS "jsonPtr",
all_types.json::text AS "JSON", all_types.json::text AS "JSON",
all_types.jsonb_ptr::text AS "jsonbPtr", all_types.jsonb_ptr::text AS "jsonbPtr",
all_types.jsonb::text AS "Jsonb", all_types.jsonb::text AS "Jsonb",
all_types.text_array_ptr::text AS "TextArrayPtr", all_types.jsonb_array::text[] AS "JsonbArray",
all_types.text_array::text AS "TextArray",
all_types.jsonb_array::text AS "JsonbArray",
all_types.integer_array::text AS "IntegerArray",
all_types.integer_array_ptr::text AS "IntegerArrayPtr",
all_types.text_multi_dim_array::text AS "TextMultiDimArray", all_types.text_multi_dim_array::text AS "TextMultiDimArray",
all_types.text_multi_dim_array_ptr::text AS "TextMultiDimArrayPtr" all_types.text_multi_dim_array_ptr::text AS "TextMultiDimArrayPtr"
FROM test_sample.all_types FROM test_sample.all_types
@ -257,7 +252,7 @@ func TestUUIDType(t *testing.T) {
SELECT all_types.uuid AS "all_types.uuid", SELECT all_types.uuid AS "all_types.uuid",
all_types.uuid_ptr AS "all_types.uuid_ptr" all_types.uuid_ptr AS "all_types.uuid_ptr"
FROM test_sample.all_types FROM test_sample.all_types
WHERE all_types.uuid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'; WHERE all_types.uuid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid;
`, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") `, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
result := model.AllTypes{} result := model.AllTypes{}
@ -873,10 +868,7 @@ FROM (
err := stmtJson.QueryContext(ctx, db, &destSelectJson) err := stmtJson.QueryContext(ctx, db, &destSelectJson)
require.NoError(t, err) require.NoError(t, err)
testutils.PrintJson(destSelectJson)
require.Equal(t, dest, destSelectJson) require.Equal(t, dest, destSelectJson)
}) })
} }
@ -1581,8 +1573,6 @@ func TestInterval(t *testing.T) {
AllTypes.IntervalPtr.DIV(Float(22.222)).EQ(AllTypes.IntervalPtr), AllTypes.IntervalPtr.DIV(Float(22.222)).EQ(AllTypes.IntervalPtr),
).FROM(AllTypes) ).FROM(AllTypes)
fmt.Println(stmt.Sql())
testutils.AssertDebugStatementSql(t, stmt, ` testutils.AssertDebugStatementSql(t, stmt, `
SELECT INTERVAL '1 YEAR', SELECT INTERVAL '1 YEAR',
INTERVAL '1 MONTH', INTERVAL '1 MONTH',
@ -2243,11 +2233,11 @@ var allTypesRow0 = model.AllTypes{
JSON: `{"a": 1, "b": 3}`, JSON: `{"a": 1, "b": 3}`,
JsonbPtr: ptr.Of(`{"a": 1, "b": 3}`), JsonbPtr: ptr.Of(`{"a": 1, "b": 3}`),
Jsonb: `{"a": 1, "b": 3}`, Jsonb: `{"a": 1, "b": 3}`,
IntegerArrayPtr: ptr.Of("{1,2,3}"), IntegerArrayPtr: &pq.Int32Array{1, 2, 3},
IntegerArray: "{1,2,3}", IntegerArray: pq.Int32Array{1, 2, 3},
TextArrayPtr: ptr.Of("{breakfast,consulting}"), TextArrayPtr: &pq.StringArray{"breakfast", "consulting"},
TextArray: "{breakfast,consulting}", TextArray: pq.StringArray{"breakfast", "consulting"},
JsonbArray: `{"{\"a\": 1, \"b\": 2}","{\"a\": 3, \"b\": 4}"}`, JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`},
TextMultiDimArrayPtr: ptr.Of("{{meeting,lunch},{training,presentation}}"), TextMultiDimArrayPtr: ptr.Of("{{meeting,lunch},{training,presentation}}"),
TextMultiDimArray: "{{meeting,lunch},{training,presentation}}", TextMultiDimArray: "{{meeting,lunch},{training,presentation}}",
MoodPtr: &moodSad, MoodPtr: &moodSad,
@ -2312,10 +2302,10 @@ var allTypesRow1 = model.AllTypes{
JsonbPtr: nil, JsonbPtr: nil,
Jsonb: `{"a": 1, "b": 3}`, Jsonb: `{"a": 1, "b": 3}`,
IntegerArrayPtr: nil, IntegerArrayPtr: nil,
IntegerArray: "{1,2,3}", IntegerArray: pq.Int32Array{1, 2, 3},
TextArrayPtr: nil, TextArrayPtr: nil,
TextArray: "{breakfast,consulting}", TextArray: pq.StringArray{"breakfast", "consulting"},
JsonbArray: `{"{\"a\": 1, \"b\": 2}","{\"a\": 3, \"b\": 4}"}`, JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`},
TextMultiDimArrayPtr: nil, TextMultiDimArrayPtr: nil,
TextMultiDimArray: "{{meeting,lunch},{training,presentation}}", TextMultiDimArray: "{{meeting,lunch},{training,presentation}}",
MoodPtr: nil, MoodPtr: nil,

View file

@ -0,0 +1,726 @@
package postgres
import (
"github.com/go-jet/jet/v2/internal/testutils"
"github.com/go-jet/jet/v2/internal/utils/ptr"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/enum"
"github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/model"
. "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestArraySelect(t *testing.T) {
stmt := SELECT(
SampleArrays.AllColumns,
).FROM(
SampleArrays,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT sample_arrays.id AS "sample_arrays.id",
sample_arrays.bool_array AS "sample_arrays.bool_array",
sample_arrays.int2_array_ptr AS "sample_arrays.int2_array_ptr",
sample_arrays.int4_array AS "sample_arrays.int4_array",
sample_arrays.int8_array AS "sample_arrays.int8_array",
sample_arrays.numeric_array AS "sample_arrays.numeric_array",
sample_arrays.decimal_array AS "sample_arrays.decimal_array",
sample_arrays.real_array AS "sample_arrays.real_array",
sample_arrays.double_array AS "sample_arrays.double_array",
sample_arrays.text_array AS "sample_arrays.text_array",
sample_arrays.varchar_array AS "sample_arrays.varchar_array",
sample_arrays.char_array AS "sample_arrays.char_array",
sample_arrays.bytea_array AS "sample_arrays.bytea_array",
sample_arrays.date_array AS "sample_arrays.date_array",
sample_arrays.timestamp_array AS "sample_arrays.timestamp_array",
sample_arrays.timestamptz_array AS "sample_arrays.timestamptz_array",
sample_arrays.time_array AS "sample_arrays.time_array",
sample_arrays.timetz_array AS "sample_arrays.timetz_array",
sample_arrays.interval_array AS "sample_arrays.interval_array",
sample_arrays.uuid_array AS "sample_arrays.uuid_array",
sample_arrays.mood_enum_array AS "sample_arrays.mood_enum_array"
FROM test_sample.sample_arrays;
`)
var dest []model.SampleArrays
err := stmt.Query(db, &dest)
require.NoError(t, err)
require.Len(t, dest, 1)
require.Equal(t, dest[0], sampleArrayRow0)
}
func TestArrayOperations(t *testing.T) {
boolArray := BoolArray(true, false, false, true)
int32Array := Int32Array(-1, 2, -3, 4, 5)
int64Array := Int64Array(10, -11, 12, -13, 14, 1515551)
float32Array := Float32Array(1.01, -2.02, 3.03, 4.04)
float64Array := Float64Array(10.001, 20.002, -3.003, 400050.04)
stringArray := StringArray("temp", "text", "array")
byteaArray := ByteaArray([]byte("temporal"), []byte("byte array"), []byte("array"))
dateArray := DateArray(*testutils.Date("2022-04-06"), *testutils.Date("2018-05-06"))
dateArray2 := ARRAY(Date(2022, 2, 3), Date(2023, 2, 2))
timestampArray := TimestampArray(*testutils.TimestampWithoutTimeZone("1999-01-08 04:05:06", 0))
timestampzArray := TimestampzArray(time.Date(2022, 1, 1, 8, 30, 50, 0, time.UTC))
timestampzArray2 := ARRAY(Timestampz(2023, 2, 3, 10, 29, 34, 1000, "UTC"))
timeArray := TimeArray(*testutils.TimeWithoutTimeZone("14:20:45"), *testutils.TimeWithoutTimeZone("8:10:15"))
timezArray := TimezArray(*testutils.TimeWithTimeZone("04:05:06 -0800"), *testutils.TimeWithTimeZone("01:02:03 -0600"))
timeArray2 := ARRAY(Time(10, 20, 30), Time(5, 45, 45))
timezArray2 := ARRAY(Timez(10, 20, 30, 0, "UTC"), Timez(5, 45, 45, 0, "UTC"))
timestampz := Timestampz(1950, 2, 3, 10, 30, 40, 0, "UTC")
query := SELECT(
// array constructors
boolArray.AS("bool_array"),
int32Array.AS("int32_array"),
int64Array.AS("int64_array"),
float32Array.AS("float32_array"),
float64Array.AS("float64_array"),
stringArray.AS("string_array"),
byteaArray.AS("bytea_array"),
dateArray.AS("date_array"),
timestampArray.AS("timestamp_array"),
timestampzArray.AS("timestampz_array"),
timeArray.AS("time_array"),
timezArray.AS("timez_array"),
// operators
SampleArrays.BoolArray.EQ(boolArray).AS("bool_eq"),
SampleArrays.TextArray.EQ(SampleArrays.TextArray).AS("text_eq"),
SampleArrays.TextArray.NOT_EQ(stringArray).AS("text_neq"),
SampleArrays.Int4Array.LT(int32Array).IS_TRUE().AS("int4_lt"),
SampleArrays.Int8Array.LT_EQ(int64Array).IS_FALSE().AS("int8_lteq"),
SampleArrays.RealArray.GT(float32Array).AS("decimal_gt"),
SampleArrays.DoubleArray.GT_EQ(float64Array).AS("numeric_gt_eq"),
SampleArrays.ByteaArray.CONTAINS(byteaArray).AS("bytea_contains"),
byteaArray.IS_CONTAINED_BY(SampleArrays.ByteaArray).AS("bytea_contained_by"),
SampleArrays.DateArray.OVERLAP(dateArray2).AS("date_overlaps"),
SampleArrays.TimestampArray.CONCAT(timestampArray).AS("timestamp_concat"),
timestampzArray2.CONCAT_ELEMENT(timestampz).AS("timestampz_concat_elem"),
SampleArrays.TimeArray.AT(Int32(1)).AS("time_at"),
Int32(22).EQ(ANY(SampleArrays.Int4Array)).AS("int32_eq_any"),
Double(7.89).NOT_EQ(ANY(SampleArrays.DoubleArray)).AS("double_neq_any"),
String("temp").EQ(ALL(stringArray)).AS("string_eq_all"),
// functions
ARRAY_APPEND(SampleArrays.TextArray, String("after")).AS("append"),
ARRAY_CAT(SampleArrays.TimeArray, timeArray2).AS("cat"),
ARRAY_LENGTH(timezArray2, Int32(1)).AS("length"),
// ARRAY_LOWER(SampleArrays.UUIDArray).AS("lower"),
ARRAY_POSITION(SampleArrays.Int4Array, Int32(30)).AS("position"),
//ARRAY_POSITION(SampleArrays.DoubleArray, Float(33.33), Int(1)).AS("position_from"),
ARRAY_POSITIONS(stringArray, String("text")).AS("positions"),
ARRAY_PREPEND(String("before"), SampleArrays.TextArray).AS("prepend"),
ARRAY_REMOVE(boolArray, Bool(true)).AS("remove"),
ARRAY_REPLACE(SampleArrays.VarcharArray, String("hello"), String("hi")).AS("replace"),
//ARRAY_REVERSE(SampleArrays.TextArray).AS("reverse"),
//ARRAY_SAMPLE(SampleArrays.TimestampArray, Int(2)).AS("sample"),
//ARRAY_SHUFFLE(SampleArrays.BoolArray).AS("shuffle"),
//ARRAY_SORT(SampleArrays.Int8Array, Bool(true)),
ARRAY_TO_STRING(SampleArrays.MoodEnumArray, String(", ")).AS("to_string"),
ARRAY_UPPER(SampleArrays.Int8Array, Int(1)).AS("upper"),
CARDINALITY(SampleArrays.DoubleArray).AS("cardinality"),
// unsupported by cockroachdb
//ARRAY_DIMS(SampleArrays.IntervalArray).AS("dims"),
//ARRAY_NDIMS(SampleArrays.TextArray).AS("ndims"),
//TRIM_ARRAY(SampleArrays.DateArray, Int(1)).AS("trim"),
).FROM(
SampleArrays,
).WHERE(
SampleArrays.BoolArray.CONTAINS(BoolArray(true)),
)
testutils.AssertStatementSql(t, query, `
SELECT $1::boolean[] AS "bool_array",
$2::integer[] AS "int32_array",
$3::bigint[] AS "int64_array",
$4::real[] AS "float32_array",
$5::double precision[] AS "float64_array",
$6::text[] AS "string_array",
$7::bytea[] AS "bytea_array",
$8::date[] AS "date_array",
$9::timestamp without time zone[] AS "timestamp_array",
$10::timestamp with time zone[] AS "timestampz_array",
$11::time without time zone[] AS "time_array",
$12::time with time zone[] AS "timez_array",
(sample_arrays.bool_array = $13::boolean[]) AS "bool_eq",
(sample_arrays.text_array = sample_arrays.text_array) AS "text_eq",
(sample_arrays.text_array != $14::text[]) AS "text_neq",
(sample_arrays.int4_array < $15::integer[]) IS TRUE AS "int4_lt",
(sample_arrays.int8_array <= $16::bigint[]) IS FALSE AS "int8_lteq",
(sample_arrays.real_array > $17::real[]) AS "decimal_gt",
(sample_arrays.double_array >= $18::double precision[]) AS "numeric_gt_eq",
(sample_arrays.bytea_array @> $19::bytea[]) AS "bytea_contains",
($20::bytea[] <@ sample_arrays.bytea_array) AS "bytea_contained_by",
(sample_arrays.date_array && ARRAY[$21::date,$22::date]) AS "date_overlaps",
(sample_arrays.timestamp_array || $23::timestamp without time zone[]) AS "timestamp_concat",
(ARRAY[$24::timestamp with time zone] || $25::timestamp with time zone) AS "timestampz_concat_elem",
sample_arrays.time_array[$26::integer] AS "time_at",
($27::integer = ANY(sample_arrays.int4_array)) AS "int32_eq_any",
($28::double precision != ANY(sample_arrays.double_array)) AS "double_neq_any",
($29::text = ALL($30::text[])) AS "string_eq_all",
ARRAY_APPEND(sample_arrays.text_array, $31::text) AS "append",
ARRAY_CAT(sample_arrays.time_array, ARRAY[$32::time without time zone,$33::time without time zone]) AS "cat",
ARRAY_LENGTH(ARRAY[$34::time with time zone,$35::time with time zone], $36::integer) AS "length",
ARRAY_POSITION(sample_arrays.int4_array, $37::integer) AS "position",
ARRAY_POSITIONS($38::text[], $39::text) AS "positions",
ARRAY_PREPEND($40::text, sample_arrays.text_array) AS "prepend",
ARRAY_REMOVE($41::boolean[], $42::boolean) AS "remove",
ARRAY_REPLACE(sample_arrays.varchar_array, $43::text, $44::text) AS "replace",
ARRAY_TO_STRING(sample_arrays.mood_enum_array, $45::text) AS "to_string",
ARRAY_UPPER(sample_arrays.int8_array, $46) AS "upper",
CARDINALITY(sample_arrays.double_array) AS "cardinality"
FROM test_sample.sample_arrays
WHERE sample_arrays.bool_array @> $47::boolean[];
`)
var dest struct {
// array constructors
BoolArray pq.BoolArray
Int32Array pq.Int32Array
Int64Array pq.Int64Array
Float32Array pq.Float32Array
Float64Array pq.Float64Array
StringArray pq.StringArray
ByteaArray pq.ByteaArray
DateArray pq.StringArray
TimestampArray pq.StringArray
TimestampzArray pq.StringArray
TimeArray pq.StringArray
TimezArray pq.StringArray
// array operators
TextEq bool
BoolEq bool
TextNeq bool
Int4Lt bool
Int8Lteq bool
DecimalGt bool
NumericGtEq bool
ByteaContains bool
ByteaContainedBy bool
DateOverlaps bool
TimestampConcat pq.StringArray
TimestampzConcatElem pq.StringArray
TimeAt time.Time
Int32EqAny bool
DoubleNeqAny bool
StringEqAll bool
// functions
Append pq.StringArray
Cat pq.StringArray
Dims string
Length int32
Lower int32
NDims int32
Position *int32
PositionFrom *int32
Positions pq.Int32Array
Prepend pq.StringArray
Remove pq.BoolArray
Replace pq.StringArray
ToString string
Upper int32
Cardinality int32
Trim pq.StringArray
}
err := query.Query(db, &dest)
require.NoError(t, err)
testutils.AssertJSON(t, dest, `
{
"BoolArray": [
true,
false,
false,
true
],
"Int32Array": [
-1,
2,
-3,
4,
5
],
"Int64Array": [
10,
-11,
12,
-13,
14,
1515551
],
"Float32Array": [
1.01,
-2.02,
3.03,
4.04
],
"Float64Array": [
10.001,
20.002,
-3.003,
400050.04
],
"StringArray": [
"temp",
"text",
"array"
],
"ByteaArray": [
"dGVtcG9yYWw=",
"Ynl0ZSBhcnJheQ==",
"YXJyYXk="
],
"DateArray": [
"2022-04-06",
"2018-05-06"
],
"TimestampArray": [
"1999-01-08 04:05:06"
],
"TimestampzArray": [
"2022-01-01 08:30:50+00"
],
"TimeArray": [
"14:20:45",
"08:10:15"
],
"TimezArray": [
"04:05:06-08",
"01:02:03-06"
],
"TextEq": true,
"BoolEq": false,
"TextNeq": true,
"Int4Lt": false,
"Int8Lteq": true,
"DecimalGt": true,
"NumericGtEq": true,
"ByteaContains": false,
"ByteaContainedBy": false,
"DateOverlaps": false,
"TimestampConcat": [
"2025-01-01 10:00:00",
"2025-02-01 10:00:00",
"1999-01-08 04:05:06"
],
"TimestampzConcatElem": [
"2023-02-03 10:29:34.000001+00",
"1950-02-03 10:30:40+00"
],
"TimeAt": "0000-01-01T12:00:00Z",
"Int32EqAny": false,
"DoubleNeqAny": true,
"StringEqAll": false,
"Append": [
"alpha",
"beta",
"gama",
"after"
],
"Cat": [
"12:00:00",
"13:00:00",
"10:20:30",
"05:45:45"
],
"Dims": "",
"Length": 2,
"Lower": 0,
"NDims": 0,
"Position": 3,
"PositionFrom": null,
"Positions": [
2
],
"Prepend": [
"before",
"alpha",
"beta",
"gama"
],
"Remove": [
false,
false
],
"Replace": [
"hi",
"world"
],
"ToString": "happy, ok",
"Upper": 4,
"Cardinality": 3,
"Trim": null
}
`)
requireLogged(t, query)
}
func TestArraySelectColumnsFromSubQuery(t *testing.T) {
subQuery := SELECT(
SampleArrays.AllColumns,
Int64Array(10, -11, 12, -13, 14, 1515551).AS("int64_array"),
).FROM(
SampleArrays,
).AsTable("sub_query")
int64Array := IntegerArrayColumn("int64_array").From(subQuery)
stmt := SELECT(
subQuery.AllColumns().Except(int64Array),
int64Array,
).FROM(
subQuery,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT sub_query."sample_arrays.id" AS "sample_arrays.id",
sub_query."sample_arrays.bool_array" AS "sample_arrays.bool_array",
sub_query."sample_arrays.int2_array_ptr" AS "sample_arrays.int2_array_ptr",
sub_query."sample_arrays.int4_array" AS "sample_arrays.int4_array",
sub_query."sample_arrays.int8_array" AS "sample_arrays.int8_array",
sub_query."sample_arrays.numeric_array" AS "sample_arrays.numeric_array",
sub_query."sample_arrays.decimal_array" AS "sample_arrays.decimal_array",
sub_query."sample_arrays.real_array" AS "sample_arrays.real_array",
sub_query."sample_arrays.double_array" AS "sample_arrays.double_array",
sub_query."sample_arrays.text_array" AS "sample_arrays.text_array",
sub_query."sample_arrays.varchar_array" AS "sample_arrays.varchar_array",
sub_query."sample_arrays.char_array" AS "sample_arrays.char_array",
sub_query."sample_arrays.bytea_array" AS "sample_arrays.bytea_array",
sub_query."sample_arrays.date_array" AS "sample_arrays.date_array",
sub_query."sample_arrays.timestamp_array" AS "sample_arrays.timestamp_array",
sub_query."sample_arrays.timestamptz_array" AS "sample_arrays.timestamptz_array",
sub_query."sample_arrays.time_array" AS "sample_arrays.time_array",
sub_query."sample_arrays.timetz_array" AS "sample_arrays.timetz_array",
sub_query."sample_arrays.interval_array" AS "sample_arrays.interval_array",
sub_query."sample_arrays.uuid_array" AS "sample_arrays.uuid_array",
sub_query."sample_arrays.mood_enum_array" AS "sample_arrays.mood_enum_array",
sub_query.int64_array AS "int64_array"
FROM (
SELECT sample_arrays.id AS "sample_arrays.id",
sample_arrays.bool_array AS "sample_arrays.bool_array",
sample_arrays.int2_array_ptr AS "sample_arrays.int2_array_ptr",
sample_arrays.int4_array AS "sample_arrays.int4_array",
sample_arrays.int8_array AS "sample_arrays.int8_array",
sample_arrays.numeric_array AS "sample_arrays.numeric_array",
sample_arrays.decimal_array AS "sample_arrays.decimal_array",
sample_arrays.real_array AS "sample_arrays.real_array",
sample_arrays.double_array AS "sample_arrays.double_array",
sample_arrays.text_array AS "sample_arrays.text_array",
sample_arrays.varchar_array AS "sample_arrays.varchar_array",
sample_arrays.char_array AS "sample_arrays.char_array",
sample_arrays.bytea_array AS "sample_arrays.bytea_array",
sample_arrays.date_array AS "sample_arrays.date_array",
sample_arrays.timestamp_array AS "sample_arrays.timestamp_array",
sample_arrays.timestamptz_array AS "sample_arrays.timestamptz_array",
sample_arrays.time_array AS "sample_arrays.time_array",
sample_arrays.timetz_array AS "sample_arrays.timetz_array",
sample_arrays.interval_array AS "sample_arrays.interval_array",
sample_arrays.uuid_array AS "sample_arrays.uuid_array",
sample_arrays.mood_enum_array AS "sample_arrays.mood_enum_array",
'{10,-11,12,-13,14,1515551}'::bigint[] AS "int64_array"
FROM test_sample.sample_arrays
) AS sub_query;
`)
var dest struct {
model.SampleArrays
Int64Array pq.Int64Array
}
err := stmt.Query(db, &dest)
require.NoError(t, err)
testutils.AssertDeepEqual(t, dest.SampleArrays, sampleArrayRow0)
testutils.AssertDeepEqual(t, dest.Int64Array, pq.Int64Array{10, -11, 12, -13, 14, 1515551})
}
func TestArrayTableInsert(t *testing.T) {
sampleArrayRow3 := testutils.DeepCopy(t, sampleArrayRow0)
sampleArrayRow3.ID = 3
insertQuery := SampleArrays.INSERT(SampleArrays.AllColumns).
VALUES(
Int64(2),
ARRAY(Bool(false), Bool(false), Bool(true)),
ARRAY(Int16(4), Int16(5), Int(6)),
Int32Array(40, 50, 60),
Int64Array(400, 500, 600),
ARRAY(Decimal("1.11"), Decimal("2.22")),
ARRAY(Decimal("1.11"), Decimal("2.22")),
Float32Array(4.4, 5.5, 6.6, 7.7),
Float64Array(40.04, 50.05, 60.06, 70.07),
StringArray("john", "doe"),
ARRAY(VarChar(10)("Andy"), VarChar(10)("Bob")),
ARRAY(Char(1)("q"), Char(1)("w"), Char(1)("e")),
ByteaArray([]byte("title"), []byte("name")),
ARRAY(Date(2010, 2, 3), Date(2025, 4, 5)),
ARRAY(Timestamp(2025, 2, 3, 0, 10, 20, 0)),
ARRAY(Timestampz(2025, 2, 3, 0, 10, 20, 0, "UTC")),
ARRAY(Time(12, 15, 45), Time(2, 30, 40)),
ARRAY(Timez(12, 15, 45, 0, "UTC"), Timez(2, 30, 40, 0, "UTC")),
ARRAY(INTERVAL(1, DAY, 3, MINUTE), INTERVAL(2, YEAR, 30, DAY)),
ARRAY(UUID(uuid.MustParse("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"))),
CAST(ARRAY(enum.Mood.Happy, enum.Mood.Sad)).AS("test_sample.mood[]"),
).
MODEL(
sampleArrayRow3,
).
RETURNING(SampleArrays.AllColumns)
testutils.AssertStatementSql(t, insertQuery, `
INSERT INTO test_sample.sample_arrays (id, bool_array, int2_array_ptr, int4_array, int8_array, numeric_array, decimal_array, real_array, double_array, text_array, varchar_array, char_array, bytea_array, date_array, timestamp_array, timestamptz_array, time_array, timetz_array, interval_array, uuid_array, mood_enum_array)
VALUES ($1::bigint, ARRAY[$2::boolean,$3::boolean,$4::boolean], ARRAY[$5::smallint,$6::smallint,$7], $8::integer[], $9::bigint[], ARRAY[$10::decimal,$11::decimal], ARRAY[$12::decimal,$13::decimal], $14::real[], $15::double precision[], $16::text[], ARRAY[$17::varchar(10),$18::varchar(10)], ARRAY[$19::char(1),$20::char(1),$21::char(1)], $22::bytea[], ARRAY[$23::date,$24::date], ARRAY[$25::timestamp without time zone], ARRAY[$26::timestamp with time zone], ARRAY[$27::time without time zone,$28::time without time zone], ARRAY[$29::time with time zone,$30::time with time zone], ARRAY[INTERVAL '1 DAY 3 MINUTE',INTERVAL '2 YEAR 30 DAY'], ARRAY[$31::uuid], ARRAY['happy','sad']::test_sample.mood[]),
($32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52)
RETURNING sample_arrays.id AS "sample_arrays.id",
sample_arrays.bool_array AS "sample_arrays.bool_array",
sample_arrays.int2_array_ptr AS "sample_arrays.int2_array_ptr",
sample_arrays.int4_array AS "sample_arrays.int4_array",
sample_arrays.int8_array AS "sample_arrays.int8_array",
sample_arrays.numeric_array AS "sample_arrays.numeric_array",
sample_arrays.decimal_array AS "sample_arrays.decimal_array",
sample_arrays.real_array AS "sample_arrays.real_array",
sample_arrays.double_array AS "sample_arrays.double_array",
sample_arrays.text_array AS "sample_arrays.text_array",
sample_arrays.varchar_array AS "sample_arrays.varchar_array",
sample_arrays.char_array AS "sample_arrays.char_array",
sample_arrays.bytea_array AS "sample_arrays.bytea_array",
sample_arrays.date_array AS "sample_arrays.date_array",
sample_arrays.timestamp_array AS "sample_arrays.timestamp_array",
sample_arrays.timestamptz_array AS "sample_arrays.timestamptz_array",
sample_arrays.time_array AS "sample_arrays.time_array",
sample_arrays.timetz_array AS "sample_arrays.timetz_array",
sample_arrays.interval_array AS "sample_arrays.interval_array",
sample_arrays.uuid_array AS "sample_arrays.uuid_array",
sample_arrays.mood_enum_array AS "sample_arrays.mood_enum_array";
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) {
var dest []model.SampleArrays
err := insertQuery.Query(tx, &dest)
require.NoError(t, err)
require.Len(t, dest, 2)
testutils.AssertDeepEqual(t, sampleArrayRow3, dest[1])
testutils.AssertJSON(t, dest[0], `
{
"ID": 2,
"BoolArray": [
false,
false,
true
],
"Int2ArrayPtr": [
"4",
"5",
"6"
],
"Int4Array": [
40,
50,
60
],
"Int8Array": [
400,
500,
600
],
"NumericArray": [
1.11,
2.22
],
"DecimalArray": [
1.11,
2.22
],
"RealArray": [
4.4,
5.5,
6.6,
7.7
],
"DoubleArray": [
40.04,
50.05,
60.06,
70.07
],
"TextArray": [
"john",
"doe"
],
"VarcharArray": [
"Andy",
"Bob"
],
"CharArray": [
"q",
"w",
"e"
],
"ByteaArray": [
"dGl0bGU=",
"bmFtZQ=="
],
"DateArray": [
"2010-02-03",
"2025-04-05"
],
"TimestampArray": [
"2025-02-03 00:10:20"
],
"TimestamptzArray": [
"2025-02-03 00:10:20+00"
],
"TimeArray": [
"12:15:45",
"02:30:40"
],
"TimetzArray": [
"12:15:45+00",
"02:30:40+00"
],
"IntervalArray": [
"1 day 00:03:00",
"2 years 30 days"
],
"UUIDArray": [
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
],
"MoodEnumArray": [
"happy",
"sad"
]
}
`)
})
}
func TestArrayTableUpdate(t *testing.T) {
t.Run("using model", func(t *testing.T) {
sampleArrayRow1Clone := testutils.DeepCopy(t, sampleArrayRow0)
sampleArrayRow1Clone.RealArray = pq.Float32Array{100.11, 200.22, 300.33}
stmt := SampleArrays.UPDATE(SampleArrays.MutableColumns).
MODEL(sampleArrayRow1Clone).
WHERE(String("alpha").EQ(ANY(SampleArrays.TextArray))).
RETURNING(SampleArrays.AllColumns)
testutils.AssertDebugStatementSql(t, stmt, `
UPDATE test_sample.sample_arrays
SET (bool_array, int2_array_ptr, int4_array, int8_array, numeric_array, decimal_array, real_array, double_array, text_array, varchar_array, char_array, bytea_array, date_array, timestamp_array, timestamptz_array, time_array, timetz_array, interval_array, uuid_array, mood_enum_array) = ('{t,f,t}', '{"1","2","3","4"}', '{10,20,30,40}', '{100,200,300,400}', '{1.8881,2.8882,3.8883,4.8884}', '{1.0001,2.0002,3.0003,4.0004}', '{100.11,200.22,300.33}', '{11.11,22.22,33.33}', '{"alpha","beta","gama"}', '{"hello","world"}', '{"a","b","c"}', '{"\\x01020304","\\x11223344"}', '{"2024-11-01","2025-02-28"}', '{"2025-01-01 10:00:00","2025-02-01 10:00:00"}', '{"2025-01-01 09:00:00+00","2025-02-01 09:00:00+00"}', '{"12:00:00","13:00:00"}', '{"12:00:00+01","13:00:00+02"}', '{"1 day","02:00:00"}', '{"550e8400-e29b-41d4-a716-446655440000"}', '{"happy","ok"}')
WHERE 'alpha'::text = ANY(sample_arrays.text_array)
RETURNING sample_arrays.id AS "sample_arrays.id",
sample_arrays.bool_array AS "sample_arrays.bool_array",
sample_arrays.int2_array_ptr AS "sample_arrays.int2_array_ptr",
sample_arrays.int4_array AS "sample_arrays.int4_array",
sample_arrays.int8_array AS "sample_arrays.int8_array",
sample_arrays.numeric_array AS "sample_arrays.numeric_array",
sample_arrays.decimal_array AS "sample_arrays.decimal_array",
sample_arrays.real_array AS "sample_arrays.real_array",
sample_arrays.double_array AS "sample_arrays.double_array",
sample_arrays.text_array AS "sample_arrays.text_array",
sample_arrays.varchar_array AS "sample_arrays.varchar_array",
sample_arrays.char_array AS "sample_arrays.char_array",
sample_arrays.bytea_array AS "sample_arrays.bytea_array",
sample_arrays.date_array AS "sample_arrays.date_array",
sample_arrays.timestamp_array AS "sample_arrays.timestamp_array",
sample_arrays.timestamptz_array AS "sample_arrays.timestamptz_array",
sample_arrays.time_array AS "sample_arrays.time_array",
sample_arrays.timetz_array AS "sample_arrays.timetz_array",
sample_arrays.interval_array AS "sample_arrays.interval_array",
sample_arrays.uuid_array AS "sample_arrays.uuid_array",
sample_arrays.mood_enum_array AS "sample_arrays.mood_enum_array";
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) {
var dest []model.SampleArrays
err := stmt.Query(tx, &dest)
require.NoError(t, err)
require.Len(t, dest, 1)
testutils.AssertDeepEqual(t, sampleArrayRow1Clone, dest[0])
})
})
t.Run("update using SET", func(t *testing.T) {
stmt := SampleArrays.UPDATE().
SET(
SampleArrays.Int4Array.SET(ARRAY(Int32(-10), Int32(11))),
SampleArrays.Int8Array.SET(ARRAY(Int64(-1200), Int64(7800))),
).
WHERE(String("alpha").EQ(ANY(SampleArrays.TextArray))).
RETURNING(
SampleArrays.Int4Array,
SampleArrays.Int8Array,
)
testutils.AssertStatementSql(t, stmt, `
UPDATE test_sample.sample_arrays
SET int4_array = ARRAY[$1::integer,$2::integer],
int8_array = ARRAY[$3::bigint,$4::bigint]
WHERE $5::text = ANY(sample_arrays.text_array)
RETURNING sample_arrays.int4_array AS "sample_arrays.int4_array",
sample_arrays.int8_array AS "sample_arrays.int8_array";
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) {
var dest []model.SampleArrays
err := stmt.Query(tx, &dest)
require.NoError(t, err)
require.Len(t, dest, 1)
testutils.AssertDeepEqual(t, dest[0].Int4Array, pq.Int32Array{-10, 11})
testutils.AssertDeepEqual(t, dest[0].Int8Array, pq.Int64Array{-1200, 7800})
})
})
}
var sampleArrayRow0 = model.SampleArrays{
ID: 1,
BoolArray: ptr.Of(pq.BoolArray{true, false, true}),
Int2ArrayPtr: ptr.Of(pq.StringArray{"1", "2", "3", "4"}),
Int4Array: pq.Int32Array{10, 20, 30, 40},
Int8Array: pq.Int64Array{100, 200, 300, 400},
NumericArray: pq.Float64Array{1.8881, 2.8882, 3.8883, 4.8884},
DecimalArray: pq.Float64Array{1.0001, 2.0002, 3.0003, 4.0004},
RealArray: pq.Float32Array{1.0099999904632568, 2.0199999809265137, 3.0299999713897705, 4.039999961853027},
DoubleArray: pq.Float64Array{11.11, 22.22, 33.33},
TextArray: pq.StringArray{"alpha", "beta", "gama"},
CharArray: pq.StringArray{"a", "b", "c"},
VarcharArray: pq.StringArray{"hello", "world"},
ByteaArray: pq.ByteaArray{{0x01, 0x02, 0x03, 0x04}, {0x11, 0x22, 0x33, 0x44}},
DateArray: pq.StringArray{"2024-11-01", "2025-02-28"},
TimestampArray: &pq.StringArray{"2025-01-01 10:00:00", "2025-02-01 10:00:00"},
TimestamptzArray: pq.StringArray{"2025-01-01 09:00:00+00", "2025-02-01 09:00:00+00"},
TimeArray: pq.StringArray{"12:00:00", "13:00:00"},
TimetzArray: pq.StringArray{"12:00:00+01", "13:00:00+02"},
IntervalArray: pq.StringArray{"1 day", "02:00:00"},
UUIDArray: pq.StringArray{"550e8400-e29b-41d4-a716-446655440000"},
MoodEnumArray: pq.StringArray{"happy", "ok"},
}

View file

@ -450,7 +450,7 @@ func TestGeneratorTemplate_Model_ChangeFieldTypes(t *testing.T) {
require.Contains(t, data, `"github.com/google/uuid"`) require.Contains(t, data, `"github.com/google/uuid"`)
require.Contains(t, data, "Description sql.NullString") require.Contains(t, data, "Description sql.NullString")
require.Contains(t, data, "ReleaseYear sql.NullInt32") require.Contains(t, data, "ReleaseYear sql.NullInt32")
require.Contains(t, data, "SpecialFeatures sql.NullString") require.Contains(t, data, "SpecialFeatures *pq.StringArray")
require.Contains(t, data, "LastUpdate sql.Null[uuid.UUID]") require.Contains(t, data, "LastUpdate sql.Null[uuid.UUID]")
} }

View file

@ -319,7 +319,7 @@ func TestGenerator_TableMetadata(t *testing.T) {
}, },
} }
require.Equal(t, want, got) require.Equal(t, want, got)
require.Equal(t, metadata.ArrayType, specialFeatures.DataType.Kind) require.Equal(t, 1, specialFeatures.DataType.Dimensions)
} }
func TestGeneratorSpecialCharacters(t *testing.T) { func TestGeneratorSpecialCharacters(t *testing.T) {
@ -765,13 +765,13 @@ func TestGeneratedAllTypesSQLBuilderFiles(t *testing.T) {
testutils.AssertFileNamesEqual(t, modelDir, "all_types.go", "all_types_view.go", "employee.go", "link.go", testutils.AssertFileNamesEqual(t, modelDir, "all_types.go", "all_types_view.go", "employee.go", "link.go",
"mood.go", "person.go", "person_phone.go", "weird_names_table.go", "level.go", "user.go", "floats.go", "people.go", "mood.go", "person.go", "person_phone.go", "weird_names_table.go", "level.go", "user.go", "floats.go", "people.go",
"components.go", "vulnerabilities.go", "all_types_materialized_view.go", "sample_ranges.go") "components.go", "vulnerabilities.go", "all_types_materialized_view.go", "sample_ranges.go", "sample_arrays.go")
testutils.AssertFileContent(t, modelDir+"/all_types.go", allTypesModelContent) testutils.AssertFileContent(t, modelDir+"/all_types.go", allTypesModelContent)
testutils.AssertFileContent(t, modelDir+"/link.go", linkModelContent) testutils.AssertFileContent(t, modelDir+"/link.go", linkModelContent)
testutils.AssertFileNamesEqual(t, tableDir, "all_types.go", "employee.go", "link.go", testutils.AssertFileNamesEqual(t, tableDir, "all_types.go", "employee.go", "link.go",
"person.go", "person_phone.go", "weird_names_table.go", "user.go", "floats.go", "people.go", "table_use_schema.go", "person.go", "person_phone.go", "weird_names_table.go", "user.go", "floats.go", "people.go", "table_use_schema.go",
"components.go", "vulnerabilities.go", "sample_ranges.go") "components.go", "vulnerabilities.go", "sample_ranges.go", "sample_arrays.go")
testutils.AssertFileContent(t, tableDir+"/all_types.go", allTypesTableContent) testutils.AssertFileContent(t, tableDir+"/all_types.go", allTypesTableContent)
testutils.AssertFileContent(t, tableDir+"/sample_ranges.go", sampleRangeTableContent) testutils.AssertFileContent(t, tableDir+"/sample_ranges.go", sampleRangeTableContent)
@ -844,6 +844,7 @@ package model
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lib/pq"
"time" "time"
) )
@ -902,11 +903,11 @@ type AllTypes struct {
JSON string JSON string
JsonbPtr *string JsonbPtr *string
Jsonb string Jsonb string
IntegerArrayPtr *string IntegerArrayPtr *pq.Int32Array
IntegerArray string IntegerArray pq.Int32Array
TextArrayPtr *string TextArrayPtr *pq.StringArray
TextArray string TextArray pq.StringArray
JsonbArray string JsonbArray pq.StringArray
TextMultiDimArrayPtr *string TextMultiDimArrayPtr *string
TextMultiDimArray string TextMultiDimArray string
MoodPtr *Mood MoodPtr *Mood
@ -1007,11 +1008,11 @@ type allTypesTable struct {
JSON postgres.ColumnString JSON postgres.ColumnString
JsonbPtr postgres.ColumnString JsonbPtr postgres.ColumnString
Jsonb postgres.ColumnString Jsonb postgres.ColumnString
IntegerArrayPtr postgres.ColumnString IntegerArrayPtr postgres.ColumnIntegerArray
IntegerArray postgres.ColumnString IntegerArray postgres.ColumnIntegerArray
TextArrayPtr postgres.ColumnString TextArrayPtr postgres.ColumnStringArray
TextArray postgres.ColumnString TextArray postgres.ColumnStringArray
JsonbArray postgres.ColumnString JsonbArray postgres.ColumnStringArray
TextMultiDimArrayPtr postgres.ColumnString TextMultiDimArrayPtr postgres.ColumnString
TextMultiDimArray postgres.ColumnString TextMultiDimArray postgres.ColumnString
MoodPtr postgres.ColumnString MoodPtr postgres.ColumnString
@ -1111,11 +1112,11 @@ func newAllTypesTableImpl(schemaName, tableName, alias string) allTypesTable {
JSONColumn = postgres.StringColumn("json") JSONColumn = postgres.StringColumn("json")
JsonbPtrColumn = postgres.StringColumn("jsonb_ptr") JsonbPtrColumn = postgres.StringColumn("jsonb_ptr")
JsonbColumn = postgres.StringColumn("jsonb") JsonbColumn = postgres.StringColumn("jsonb")
IntegerArrayPtrColumn = postgres.StringColumn("integer_array_ptr") IntegerArrayPtrColumn = postgres.IntegerArrayColumn("integer_array_ptr")
IntegerArrayColumn = postgres.StringColumn("integer_array") IntegerArrayColumn = postgres.IntegerArrayColumn("integer_array")
TextArrayPtrColumn = postgres.StringColumn("text_array_ptr") TextArrayPtrColumn = postgres.StringArrayColumn("text_array_ptr")
TextArrayColumn = postgres.StringColumn("text_array") TextArrayColumn = postgres.StringArrayColumn("text_array")
JsonbArrayColumn = postgres.StringColumn("jsonb_array") JsonbArrayColumn = postgres.StringArrayColumn("jsonb_array")
TextMultiDimArrayPtrColumn = postgres.StringColumn("text_multi_dim_array_ptr") TextMultiDimArrayPtrColumn = postgres.StringColumn("text_multi_dim_array_ptr")
TextMultiDimArrayColumn = postgres.StringColumn("text_multi_dim_array") TextMultiDimArrayColumn = postgres.StringColumn("text_multi_dim_array")
MoodPtrColumn = postgres.StringColumn("mood_ptr") MoodPtrColumn = postgres.StringColumn("mood_ptr")
@ -1687,3 +1688,192 @@ func TestCamelCaseModelJsonTag(t *testing.T) {
}) })
} }
} }
func TestGeneratorTestSample(t *testing.T) {
err := postgres.GenerateDSN(defaultDSN(), "test_sample", genTestDir2)
require.NoError(t, err)
modelDir := filepath.Join(testRoot, "/postgres/.gentestdata2/jetdb/test_sample/model/")
tableDir := filepath.Join(testRoot, "/postgres/.gentestdata2/jetdb/test_sample/table/")
testutils.AssertFileContent(t, modelDir+"/sample_arrays.go", `
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/lib/pq"
)
type SampleArrays struct {
ID int32 `+"`sql:\"primary_key\"`"+`
BoolArray *pq.BoolArray
Int2ArrayPtr *pq.StringArray
Int4Array pq.Int32Array
Int8Array pq.Int64Array
NumericArray pq.Float64Array
DecimalArray pq.Float64Array
RealArray pq.Float32Array
DoubleArray pq.Float64Array
TextArray pq.StringArray
VarcharArray pq.StringArray
CharArray pq.StringArray
ByteaArray pq.ByteaArray
DateArray pq.StringArray
TimestampArray *pq.StringArray
TimestamptzArray pq.StringArray
TimeArray pq.StringArray
TimetzArray pq.StringArray
IntervalArray pq.StringArray
UUIDArray pq.StringArray
MoodEnumArray pq.StringArray
}
`)
testutils.AssertFileContent(t, tableDir+"/sample_arrays.go", `
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var SampleArrays = newSampleArraysTable("test_sample", "sample_arrays", "")
type sampleArraysTable struct {
postgres.Table
// Columns
ID postgres.ColumnInteger
BoolArray postgres.ColumnBoolArray
Int2ArrayPtr postgres.ColumnIntegerArray
Int4Array postgres.ColumnIntegerArray
Int8Array postgres.ColumnIntegerArray
NumericArray postgres.ColumnFloatArray
DecimalArray postgres.ColumnFloatArray
RealArray postgres.ColumnFloatArray
DoubleArray postgres.ColumnFloatArray
TextArray postgres.ColumnStringArray
VarcharArray postgres.ColumnStringArray
CharArray postgres.ColumnStringArray
ByteaArray postgres.ColumnByteaArray
DateArray postgres.ColumnDateArray
TimestampArray postgres.ColumnTimestampArray
TimestamptzArray postgres.ColumnTimestampzArray
TimeArray postgres.ColumnTimeArray
TimetzArray postgres.ColumnTimezArray
IntervalArray postgres.ColumnIntervalArray
UUIDArray postgres.ColumnStringArray
MoodEnumArray postgres.ColumnStringArray
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type SampleArraysTable struct {
sampleArraysTable
EXCLUDED sampleArraysTable
}
// AS creates new SampleArraysTable with assigned alias
func (a SampleArraysTable) AS(alias string) *SampleArraysTable {
return newSampleArraysTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new SampleArraysTable with assigned schema name
func (a SampleArraysTable) FromSchema(schemaName string) *SampleArraysTable {
return newSampleArraysTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new SampleArraysTable with assigned table prefix
func (a SampleArraysTable) WithPrefix(prefix string) *SampleArraysTable {
return newSampleArraysTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new SampleArraysTable with assigned table suffix
func (a SampleArraysTable) WithSuffix(suffix string) *SampleArraysTable {
return newSampleArraysTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newSampleArraysTable(schemaName, tableName, alias string) *SampleArraysTable {
return &SampleArraysTable{
sampleArraysTable: newSampleArraysTableImpl(schemaName, tableName, alias),
EXCLUDED: newSampleArraysTableImpl("", "excluded", ""),
}
}
func newSampleArraysTableImpl(schemaName, tableName, alias string) sampleArraysTable {
var (
IDColumn = postgres.IntegerColumn("id")
BoolArrayColumn = postgres.BoolArrayColumn("bool_array")
Int2ArrayPtrColumn = postgres.IntegerArrayColumn("int2_array_ptr")
Int4ArrayColumn = postgres.IntegerArrayColumn("int4_array")
Int8ArrayColumn = postgres.IntegerArrayColumn("int8_array")
NumericArrayColumn = postgres.FloatArrayColumn("numeric_array")
DecimalArrayColumn = postgres.FloatArrayColumn("decimal_array")
RealArrayColumn = postgres.FloatArrayColumn("real_array")
DoubleArrayColumn = postgres.FloatArrayColumn("double_array")
TextArrayColumn = postgres.StringArrayColumn("text_array")
VarcharArrayColumn = postgres.StringArrayColumn("varchar_array")
CharArrayColumn = postgres.StringArrayColumn("char_array")
ByteaArrayColumn = postgres.ByteaArrayColumn("bytea_array")
DateArrayColumn = postgres.DateArrayColumn("date_array")
TimestampArrayColumn = postgres.TimestampArrayColumn("timestamp_array")
TimestamptzArrayColumn = postgres.TimestampzArrayColumn("timestamptz_array")
TimeArrayColumn = postgres.TimeArrayColumn("time_array")
TimetzArrayColumn = postgres.TimezArrayColumn("timetz_array")
IntervalArrayColumn = postgres.IntervalArrayColumn("interval_array")
UUIDArrayColumn = postgres.StringArrayColumn("uuid_array")
MoodEnumArrayColumn = postgres.StringArrayColumn("mood_enum_array")
allColumns = postgres.ColumnList{IDColumn, BoolArrayColumn, Int2ArrayPtrColumn, Int4ArrayColumn, Int8ArrayColumn, NumericArrayColumn, DecimalArrayColumn, RealArrayColumn, DoubleArrayColumn, TextArrayColumn, VarcharArrayColumn, CharArrayColumn, ByteaArrayColumn, DateArrayColumn, TimestampArrayColumn, TimestamptzArrayColumn, TimeArrayColumn, TimetzArrayColumn, IntervalArrayColumn, UUIDArrayColumn, MoodEnumArrayColumn}
mutableColumns = postgres.ColumnList{BoolArrayColumn, Int2ArrayPtrColumn, Int4ArrayColumn, Int8ArrayColumn, NumericArrayColumn, DecimalArrayColumn, RealArrayColumn, DoubleArrayColumn, TextArrayColumn, VarcharArrayColumn, CharArrayColumn, ByteaArrayColumn, DateArrayColumn, TimestampArrayColumn, TimestamptzArrayColumn, TimeArrayColumn, TimetzArrayColumn, IntervalArrayColumn, UUIDArrayColumn, MoodEnumArrayColumn}
defaultColumns = postgres.ColumnList{IDColumn, Int4ArrayColumn, Int8ArrayColumn, TextArrayColumn}
)
return sampleArraysTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
BoolArray: BoolArrayColumn,
Int2ArrayPtr: Int2ArrayPtrColumn,
Int4Array: Int4ArrayColumn,
Int8Array: Int8ArrayColumn,
NumericArray: NumericArrayColumn,
DecimalArray: DecimalArrayColumn,
RealArray: RealArrayColumn,
DoubleArray: DoubleArrayColumn,
TextArray: TextArrayColumn,
VarcharArray: VarcharArrayColumn,
CharArray: CharArrayColumn,
ByteaArray: ByteaArrayColumn,
DateArray: DateArrayColumn,
TimestampArray: TimestampArrayColumn,
TimestamptzArray: TimestamptzArrayColumn,
TimeArray: TimeArrayColumn,
TimetzArray: TimetzArrayColumn,
IntervalArray: IntervalArrayColumn,
UUIDArray: UUIDArrayColumn,
MoodEnumArray: MoodEnumArrayColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
`)
}

View file

@ -53,8 +53,8 @@ func TestRangeTableSelect(t *testing.T) {
table.SampleRanges.TimestampzRange.DIFFERENCE(tstzRange).AS("sample.tstz_diff"), table.SampleRanges.TimestampzRange.DIFFERENCE(tstzRange).AS("sample.tstz_diff"),
table.SampleRanges.Int4Range.UPPER_BOUND().ADD(Int(5)).AS("sample.int4_upper"), table.SampleRanges.Int4Range.UPPER_BOUND().ADD(Int(5)).AS("sample.int4_upper"),
table.SampleRanges.Int8Range.LOWER_BOUND().SUB(Int(12)).AS("sample.int8_lower"), table.SampleRanges.Int8Range.LOWER_BOUND().SUB(Int(12)).AS("sample.int8_lower"),
LOWER_BOUND[DateExpression](table.SampleRanges.DateRange), LOWER_BOUND(table.SampleRanges.DateRange),
UPPER_BOUND[NumericExpression](table.SampleRanges.NumRange).AS("sample.num_upper"), UPPER_BOUND(table.SampleRanges.NumRange).AS("sample.num_upper"),
table.SampleRanges.TimestampRange.UPPER_BOUND(), table.SampleRanges.TimestampRange.UPPER_BOUND(),
table.SampleRanges.DateRange.IS_EMPTY().AS("sample.date_empty"), table.SampleRanges.DateRange.IS_EMPTY().AS("sample.date_empty"),
table.SampleRanges.TimestampRange.LOWER_INC().AS("sample.ts_low_inc"), table.SampleRanges.TimestampRange.LOWER_INC().AS("sample.ts_low_inc"),

View file

@ -3,6 +3,7 @@ package postgres
import ( import (
"context" "context"
"github.com/go-jet/jet/v2/internal/utils/ptr" "github.com/go-jet/jet/v2/internal/utils/ptr"
"github.com/lib/pq"
"github.com/volatiletech/null/v8" "github.com/volatiletech/null/v8"
"testing" "testing"
"time" "time"
@ -1020,7 +1021,7 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
ReplacementCost MyFloat64 ReplacementCost MyFloat64
Rating *model.MpaaRating Rating *model.MpaaRating
LastUpdate MyTime LastUpdate MyTime
SpecialFeatures *MyString SpecialFeatures pq.StringArray
Fulltext MyString Fulltext MyString
} }
@ -1032,14 +1033,12 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
Film.FilmID.ASC(), Film.FilmID.ASC(),
).LIMIT(3) ).LIMIT(3)
var films []model.Film var myFilms []film
err := stmt.Query(db, &myFilms)
err := stmt.Query(db, &films)
require.NoError(t, err) require.NoError(t, err)
var myFilms []film var films []model.Film
err = stmt.Query(db, &films)
err = stmt.Query(db, &myFilms)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms)) require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms))
@ -1254,7 +1253,7 @@ var film1 = model.Film{
ReplacementCost: 20.99, ReplacementCost: 20.99,
Rating: &pgRating, Rating: &pgRating,
LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3),
SpecialFeatures: ptr.Of("{\"Deleted Scenes\",\"Behind the Scenes\"}"), SpecialFeatures: &pq.StringArray{"Deleted Scenes", "Behind the Scenes"},
Fulltext: "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", Fulltext: "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17",
} }
@ -1270,7 +1269,7 @@ var film2 = model.Film{
ReplacementCost: 12.99, ReplacementCost: 12.99,
Rating: &gRating, Rating: &gRating,
LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3),
SpecialFeatures: ptr.Of(`{Trailers,"Deleted Scenes"}`), SpecialFeatures: &pq.StringArray{"Trailers", "Deleted Scenes"},
Fulltext: `'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14`, Fulltext: `'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14`,
} }

View file

@ -358,8 +358,7 @@ func TestSelectQuickStartJSON(t *testing.T) {
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,
SELECT_JSON_ARR( SELECT_JSON_ARR(
Film.AllColumns.Except(Film.SpecialFeatures), Film.AllColumns,
CAST(Film.SpecialFeatures).AS_TEXT().AS("SpecialFeatures"),
SELECT_JSON_OBJ( SELECT_JSON_OBJ(
Language.AllColumns, Language.AllColumns,
@ -385,7 +384,11 @@ func TestSelectQuickStartJSON(t *testing.T) {
Film. Film.
INNER_JOIN(FilmActor, FilmActor.FilmID.EQ(Film.FilmID)), INNER_JOIN(FilmActor, FilmActor.FilmID.EQ(Film.FilmID)),
).WHERE( ).WHERE(
FilmActor.ActorID.EQ(Actor.ActorID).AND(Film.Length.GT(Int32(180))), AND(
FilmActor.ActorID.EQ(Actor.ActorID),
Film.Length.GT(Int32(180)),
String("Trailers").EQ(ANY(Film.SpecialFeatures)),
),
).ORDER_BY( ).ORDER_BY(
Film.FilmID.ASC(), Film.FilmID.ASC(),
).AS("Films"), ).AS("Films"),
@ -416,8 +419,8 @@ FROM (
film.replacement_cost AS "replacementCost", film.replacement_cost AS "replacementCost",
film.rating AS "rating", film.rating AS "rating",
to_char(film.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", to_char(film.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate",
film.special_features AS "specialFeatures",
film.fulltext AS "fulltext", film.fulltext AS "fulltext",
film.special_features::text AS "SpecialFeatures",
( (
SELECT row_to_json(language_records) AS "language_json" SELECT row_to_json(language_records) AS "language_json"
FROM ( FROM (
@ -441,7 +444,11 @@ FROM (
) AS "Categories" ) AS "Categories"
FROM dvds.film FROM dvds.film
INNER JOIN dvds.film_actor ON (film_actor.film_id = film.film_id) INNER JOIN dvds.film_actor ON (film_actor.film_id = film.film_id)
WHERE (film_actor.actor_id = actor.actor_id) AND (film.length > 180::integer) WHERE (
(film_actor.actor_id = actor.actor_id)
AND (film.length > 180::integer)
AND ('Trailers'::text = ANY(film.special_features))
)
ORDER BY film.film_id ASC ORDER BY film.film_id ASC
) AS films_records ) AS films_records
) AS "Films" ) AS "Films"
@ -469,8 +476,8 @@ FROM (
return // char[n] columns whitespaces are trimmed when returned as json in cockroachdb return // char[n] columns whitespaces are trimmed when returned as json in cockroachdb
} }
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-json-dest2.json") //testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-json-dest.json")
//testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-json-dest.json") testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-json-dest.json")
} }
func TestSelectJsonInReturning(t *testing.T) { func TestSelectJsonInReturning(t *testing.T) {

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"github.com/go-jet/jet/v2/internal/utils/ptr" "github.com/go-jet/jet/v2/internal/utils/ptr"
"github.com/lib/pq"
"testing" "testing"
"time" "time"
@ -1852,7 +1853,7 @@ ORDER BY film.film_id ASC;
Rating: &gRating, Rating: &gRating,
RentalDuration: 3, RentalDuration: 3,
LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3),
SpecialFeatures: ptr.Of("{Trailers,\"Deleted Scenes\"}"), SpecialFeatures: &pq.StringArray{"Trailers", "Deleted Scenes"},
Fulltext: "'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14", Fulltext: "'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14",
}) })
} }
@ -2709,7 +2710,32 @@ FOR UPDATE OF "myFilm";
func TestSelectQuickStart(t *testing.T) { func TestSelectQuickStart(t *testing.T) {
var expectedSQL = ` stmt := SELECT(
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // list of all actor columns (equivalent to Actor.AllColumns)
Film.AllColumns, // list of all film columns (equivalent to Film.FilmID, Film.Title, ...)
Language.AllColumns.Except(Language.LastUpdate),
Category.AllColumns,
).FROM(
Actor.
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)). // INNER JOIN Actor with FilmActor on condition Actor.ActorID = FilmActor.ActorID
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)). // then with Film, Language, FilmCategory and Category.
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
AND(
Language.Name.EQ(Char(20)("English")), // String column Language.Name and Category.Name can be compared only with string expression
Category.Name.NOT_EQ(Text("Action")),
Film.Length.GT(Int32(180)), // Film.Length is integer column and can be compared only with integer expression
Film.Rating.NOT_EQ(enum.MpaaRating.R),
String("Trailers").EQ(ANY(Film.SpecialFeatures)), // arrays element type safety is also enforced
),
).ORDER_BY(
Actor.ActorID.ASC(),
Film.FilmID.ASC(),
)
testutils.AssertStatementSql(t, stmt, `
SELECT actor.actor_id AS "actor.actor_id", SELECT actor.actor_id AS "actor.actor_id",
actor.first_name AS "actor.first_name", actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name", actor.last_name AS "actor.last_name",
@ -2729,7 +2755,6 @@ SELECT actor.actor_id AS "actor.actor_id",
film.fulltext AS "film.fulltext", film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id", language.language_id AS "language.language_id",
language.name AS "language.name", language.name AS "language.name",
language.last_update AS "language.last_update",
category.category_id AS "category.category_id", category.category_id AS "category.category_id",
category.name AS "category.name", category.name AS "category.name",
category.last_update AS "category.last_update" category.last_update AS "category.last_update"
@ -2739,33 +2764,54 @@ FROM dvds.actor
INNER JOIN dvds.language ON (language.language_id = film.language_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.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id) INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE (((language.name = 'English'::char(20)) AND (category.name != 'Action'::text)) AND (film.length > 180::integer)) AND (film.rating != 'R') WHERE (
(language.name = $1::char(20))
AND (category.name != $2::text)
AND (film.length > $3::integer)
AND (film.rating != 'R')
AND ($4::text = ANY(film.special_features))
)
ORDER BY actor.actor_id ASC, film.film_id ASC; ORDER BY actor.actor_id ASC, film.film_id ASC;
` `, "English", "Action", int32(180), "Trailers")
stmt := SELECT( testutils.AssertDebugStatementSql(t, stmt, `
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // list of all actor columns (equivalent to Actor.AllColumns) SELECT actor.actor_id AS "actor.actor_id",
Film.AllColumns, // list of all film columns (equivalent to Film.FilmID, Film.Title, ...) actor.first_name AS "actor.first_name",
Language.AllColumns, actor.last_name AS "actor.last_name",
Category.AllColumns, actor.last_update AS "actor.last_update",
).FROM( film.film_id AS "film.film_id",
Actor. film.title AS "film.title",
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)). // INNER JOIN Actor with FilmActor on condition Actor.ActorID = FilmActor.ActorID film.description AS "film.description",
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)). // then with Film, Language, FilmCategory and Category. film.release_year AS "film.release_year",
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)). film.language_id AS "film.language_id",
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)). film.rental_duration AS "film.rental_duration",
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)), film.rental_rate AS "film.rental_rate",
).WHERE( film.length AS "film.length",
Language.Name.EQ(Char(20)("English")). // note that every column has type. film.replacement_cost AS "film.replacement_cost",
AND(Category.Name.NOT_EQ(Text("Action"))). // String column Language.Name and Category.Name can be compared only with string expression film.rating AS "film.rating",
AND(Film.Length.GT(Int32(180))). // Film.Length is integer column and can be compared only with integer expression film.last_update AS "film.last_update",
AND(Film.Rating.NOT_EQ(enum.MpaaRating.R)), film.special_features AS "film.special_features",
).ORDER_BY( film.fulltext AS "film.fulltext",
Actor.ActorID.ASC(), language.language_id AS "language.language_id",
Film.FilmID.ASC(), language.name AS "language.name",
) category.category_id AS "category.category_id",
category.name AS "category.name",
testutils.AssertDebugStatementSql(t, stmt, expectedSQL, "English", "Action", int32(180)) 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 = 'English'::char(20))
AND (category.name != 'Action'::text)
AND (film.length > 180::integer)
AND (film.rating != 'R')
AND ('Trailers'::text = ANY(film.special_features))
)
ORDER BY actor.actor_id ASC, film.film_id ASC;
`)
var dest []struct { var dest []struct {
model.Actor model.Actor
@ -2783,7 +2829,6 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;
require.NoError(t, err) require.NoError(t, err)
//testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-dest.json") //testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-dest.json")
testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-dest.json") testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-dest.json")
var dest2 []struct { var dest2 []struct {
@ -2806,7 +2851,11 @@ func TestSelectQuickStartWithSubQueries(t *testing.T) {
filmLogerThan180 := Film. filmLogerThan180 := Film.
SELECT(Film.AllColumns). SELECT(Film.AllColumns).
WHERE(Film.Length.GT(Int(180)).AND(Film.Rating.NOT_EQ(enum.MpaaRating.R))). WHERE(
Film.Length.GT(Int(180)).
AND(Film.Rating.NOT_EQ(enum.MpaaRating.R)).
AND(String("Trailers").EQ(ANY(Film.SpecialFeatures))),
).
AsTable("films") AsTable("films")
filmID := Film.FilmID.From(filmLogerThan180) filmID := Film.FilmID.From(filmLogerThan180)
@ -2822,7 +2871,7 @@ func TestSelectQuickStartWithSubQueries(t *testing.T) {
stmt := SELECT( stmt := SELECT(
Actor.AllColumns, Actor.AllColumns,
filmLogerThan180.AllColumns(), filmLogerThan180.AllColumns(),
Language.AllColumns, Language.AllColumns.Except(Language.LastUpdate),
categoriesNotAction.AllColumns(), categoriesNotAction.AllColumns(),
).FROM( ).FROM(
Actor. Actor.
@ -3389,7 +3438,10 @@ func TestSelectRecursionScanNxM(t *testing.T) {
"ReplacementCost": 20.99, "ReplacementCost": 20.99,
"Rating": "PG", "Rating": "PG",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{\"Deleted Scenes\",\"Behind the Scenes\"}", "SpecialFeatures": [
"Deleted Scenes",
"Behind the Scenes"
],
"Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", "Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17",
"Actors": [ "Actors": [
{ {
@ -3413,7 +3465,10 @@ func TestSelectRecursionScanNxM(t *testing.T) {
"ReplacementCost": 9.99, "ReplacementCost": 9.99,
"Rating": "R", "Rating": "R",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers,\"Deleted Scenes\"}", "SpecialFeatures": [
"Trailers",
"Deleted Scenes"
],
"Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13", "Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13",
"Actors": [ "Actors": [
{ {
@ -3461,7 +3516,10 @@ func TestSelectRecursionScanNxM(t *testing.T) {
"ReplacementCost": 20.99, "ReplacementCost": 20.99,
"Rating": "PG", "Rating": "PG",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{\"Deleted Scenes\",\"Behind the Scenes\"}", "SpecialFeatures": [
"Deleted Scenes",
"Behind the Scenes"
],
"Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", "Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17",
"Actors": null "Actors": null
}, },
@ -3477,7 +3535,10 @@ func TestSelectRecursionScanNxM(t *testing.T) {
"ReplacementCost": 9.99, "ReplacementCost": 9.99,
"Rating": "R", "Rating": "R",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers,\"Deleted Scenes\"}", "SpecialFeatures": [
"Trailers",
"Deleted Scenes"
],
"Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13", "Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13",
"Actors": null "Actors": null
} }

View file

@ -172,7 +172,9 @@ ORDER BY film_values.title;
"ReplacementCost": 15.99, "ReplacementCost": 15.99,
"Rating": "R", "Rating": "R",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers}", "SpecialFeatures": [
"Trailers"
],
"Fulltext": "'airport':1 'ancient':18 'confront':14 'epic':4 'girl':11 'india':19 'monkey':16 'moos':8 'must':13 'pollock':2 'tale':5" "Fulltext": "'airport':1 'ancient':18 'confront':14 'epic':4 'girl':11 'india':19 'monkey':16 'moos':8 'must':13 'pollock':2 'tale':5"
}, },
"Title": "Airport Pollock", "Title": "Airport Pollock",
@ -194,7 +196,9 @@ ORDER BY film_values.title;
"ReplacementCost": 12.99, "ReplacementCost": 12.99,
"Rating": "PG-13", "Rating": "PG-13",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers}", "SpecialFeatures": [
"Trailers"
],
"Fulltext": "'boat':20 'bright':1 'conquer':14 'encount':2 'fate':4 'feminist':11 'jet':19 'lumberjack':8 'must':13 'student':16 'yarn':5" "Fulltext": "'boat':20 'bright':1 'conquer':14 'encount':2 'fate':4 'feminist':11 'jet':19 'lumberjack':8 'must':13 'student':16 'yarn':5"
}, },
"Title": "Bright Encounters", "Title": "Bright Encounters",

@ -1 +1 @@
Subproject commit 94f3504ef8245b8f76ca391f068705ba1375c03b Subproject commit 0387785d9e9ceacba2247d477181436f27bf2068