Add support for additional array types.

This commit is contained in:
go-jet 2025-10-16 15:09:07 +02:00
parent 45d4ced9b0
commit 4ee047a675
47 changed files with 1994 additions and 4277 deletions

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]
``` ```
@ -295,7 +305,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"
@ -305,7 +314,13 @@ 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)) 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
@ -388,7 +403,11 @@ fmt.Println(string(jsonText))
"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": [
"Trailers",
"Deleted Scenes",
"Behind the Scenes"
],
"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", "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": { "Language": {
"LanguageID": 1, "LanguageID": 1,
@ -423,7 +442,10 @@ fmt.Println(string(jsonText))
"ReplacementCost": 9.99, "ReplacementCost": 9.99,
"Rating": "G", "Rating": "G",
"LastUpdate": "2013-05-26T14:50:58.951Z", "LastUpdate": "2013-05-26T14:50:58.951Z",
"SpecialFeatures": "{Trailers,\"Behind the Scenes\"}", "SpecialFeatures": [
"Trailers",
"Behind the Scenes"
],
"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", "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",
"Language": { "Language": {
"LanguageID": 1, "LanguageID": 1,
@ -444,8 +466,7 @@ fmt.Println(string(jsonText))
] ]
``` ```
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
@ -481,23 +502,12 @@ handleError(err)
"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": [
"Trailers",
"Deleted Scenes",
"Behind the Scenes"
],
"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" "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"
},
{
"FilmID": 50,
"Title": "Baked Cleopatra",
"Description": "A Stunning Drama of a Forensic Psychologist And a Husband who must Overcome a Waitress in A Monastery",
"ReleaseYear": 2006,
"LanguageID": 1,
"RentalDuration": 3,
"RentalRate": 2.99,
"Length": 182,
"ReplacementCost": 20.99,
"Rating": "G",
"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"
} }
], ],
"Actors": [ "Actors": [
@ -519,12 +529,6 @@ handleError(err)
"LastName": "Dukakis", "LastName": "Dukakis",
"LastUpdate": "2013-05-26T14:47:57.62Z" "LastUpdate": "2013-05-26T14:47:57.62Z"
}, },
{
"ActorID": 70,
"FirstName": "Michelle",
"LastName": "Mcconaughey",
"LastUpdate": "2013-05-26T14:47:57.62Z"
},
{ {
"ActorID": 118, "ActorID": 118,
"FirstName": "Cuba", "FirstName": "Cuba",
@ -545,7 +549,7 @@ handleError(err)
} }
] ]
}, },
//... // ...
] ]
``` ```
</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"
) )
@ -30,3 +29,7 @@ type DataType struct {
IsUnsigned bool IsUnsigned bool
Dimensions int // The number of array dimensions Dimensions int // The number of array dimensions
} }
func (d DataType) IsArray() bool {
return d.Dimensions > 0
}

View file

@ -66,16 +66,17 @@ 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",
attr.attndims as "dataType.dimensions", (case when tp.typcategory = 'A' then greatest(1, attr.attndims) --cockroach num dims fix
(case else 0
when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base' end) as "dataType.dimensions",
when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array' (case coalesce(elem.typtype, tp.typtype)
when tp.typtype = 'd' then 'base' when 'b' then 'base'
when tp.typtype = 'e' then 'enum' when 'd' then 'base'
when tp.typtype = 'r' then 'range' when 'e' then 'enum'
when 'r' then 'range'
end) as "dataType.Kind", 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"
@ -83,6 +84,7 @@ 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,15 +2,17 @@ package template
import ( import (
"fmt" "fmt"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
"github.com/google/uuid"
"github.com/jackc/pgtype"
"github.com/lib/pq" "github.com/lib/pq"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"github.com/jackc/pgtype"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
) )
// Model is template for model files generation // Model is template for model files generation
@ -246,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))
@ -267,21 +280,44 @@ 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{} {
// We don't support multi-dimensional arrays
if column.DataType.Dimensions > 1 {
return ""
}
switch strings.ToLower(column.DataType.Name) { switch strings.ToLower(column.DataType.Name) {
case "user-defined", "enum": case "user-defined", "enum":
@ -348,14 +384,6 @@ func toGoType(column metadata.Column) interface{} {
return pgtype.Int8range{} return pgtype.Int8range{}
case "numrange": case "numrange":
return pgtype.Numrange{} return pgtype.Numrange{}
case "bool[]":
return pq.BoolArray{}
case "integer[]", "int4[]":
return pq.Int32Array{}
case "bigint[]":
return pq.Int64Array{}
case "text[]", "jsonb[]", "json[]":
return pq.StringArray{}
default: default:
fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.") fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.")
return "" return ""

View file

@ -162,43 +162,33 @@ 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:
columnMetaData.DataType.Kind != metadata.ArrayType { if columnMetaData.DataType.IsArray() {
return "StringArray"
}
return "String" return "String"
} }
typeName := columnMetaData.DataType.Name columnType := sqlToColumnType(columnMetaData)
columnName := columnMetaData.Name
if columnMetaData.DataType.Kind == metadata.ArrayType { if columnMetaData.DataType.IsArray() {
if columnMetaData.DataType.Dimensions > 1 { if columnMetaData.DataType.Dimensions > 1 {
fmt.Println("- [SQL Builder] Unsupported sql array with multiple dimensions column '" + columnName + " " + typeName + "', using StringColumn instead.") fmt.Println("- [SQL Builder] Unsupported sql array with multiple dimensions column '" +
columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
return "String" return "String"
} }
c := sqlToColumnType(strings.TrimSuffix(typeName, "[]")) columnType = columnType + "Array"
// These are the supported array types
if slices.Index([]string{"Bool", "String", "Integer"}, c) == -1 {
fmt.Println("- [SQL Builder] Unsupported sql array column '" + columnName + " " + typeName + "', using StringColumn instead.")
return "String"
}
return c + "Array"
}
columnType := sqlToColumnType(typeName)
if columnType == "" {
fmt.Println("- [SQL Builder] Unsupported sql column '" + columnName + " " + typeName + "', using StringColumn instead.")
return "String"
} }
return columnType return columnType
} }
func sqlToColumnType(typeName string) string { // sqlToColumnType maps the type of a SQL column type to a go jet sql builder column. The second return value returns
switch strings.ToLower(typeName) { // whether the given type is supported.
func sqlToColumnType(columnMetaData metadata.Column) string {
switch strings.ToLower(columnMetaData.DataType.Name) {
case "boolean", "bool": case "boolean", "bool":
return "Bool" return "Bool"
case "smallint", "integer", "bigint", "int2", "int4", "int8", case "smallint", "integer", "bigint", "int2", "int4", "int8",
@ -243,7 +233,8 @@ func sqlToColumnType(typeName string) string {
case "numrange": case "numrange":
return "NumericRange" return "NumericRange"
default: default:
return "" fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
return "String"
} }
} }

View file

@ -1,77 +1,77 @@
package jet package jet
// ArrayExpression interface // Array interface
type ArrayExpression[E Expression] interface { type Array[E Expression] interface {
Expression Expression
EQ(rhs ArrayExpression[E]) BoolExpression EQ(rhs Array[E]) BoolExpression
NOT_EQ(rhs ArrayExpression[E]) BoolExpression NOT_EQ(rhs Array[E]) BoolExpression
LT(rhs ArrayExpression[E]) BoolExpression LT(rhs Array[E]) BoolExpression
GT(rhs ArrayExpression[E]) BoolExpression GT(rhs Array[E]) BoolExpression
LT_EQ(rhs ArrayExpression[E]) BoolExpression LT_EQ(rhs Array[E]) BoolExpression
GT_EQ(rhs ArrayExpression[E]) BoolExpression GT_EQ(rhs Array[E]) BoolExpression
CONTAINS(rhs ArrayExpression[E]) BoolExpression CONTAINS(rhs Array[E]) BoolExpression
IS_CONTAINED_BY(rhs ArrayExpression[E]) BoolExpression IS_CONTAINED_BY(rhs Array[E]) BoolExpression
OVERLAP(rhs ArrayExpression[E]) BoolExpression OVERLAP(rhs Array[E]) BoolExpression
CONCAT(rhs ArrayExpression[E]) ArrayExpression[E] CONCAT(rhs Array[E]) Array[E]
CONCAT_ELEMENT(E) ArrayExpression[E] CONCAT_ELEMENT(E) Array[E]
AT(expression IntegerExpression) Expression AT(expression IntegerExpression) E
} }
type arrayInterfaceImpl[E Expression] struct { type arrayInterfaceImpl[E Expression] struct {
parent ArrayExpression[E] parent Array[E]
} }
type BinaryBoolOp func(Expression, Expression) BoolExpression type BinaryBoolOp func(Expression, Expression) BoolExpression
func (a arrayInterfaceImpl[E]) EQ(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) EQ(rhs Array[E]) BoolExpression {
return Eq(a.parent, rhs) return Eq(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) NOT_EQ(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) NOT_EQ(rhs Array[E]) BoolExpression {
return NotEq(a.parent, rhs) return NotEq(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) LT(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) LT(rhs Array[E]) BoolExpression {
return Lt(a.parent, rhs) return Lt(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) GT(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) GT(rhs Array[E]) BoolExpression {
return Gt(a.parent, rhs) return Gt(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) LT_EQ(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) LT_EQ(rhs Array[E]) BoolExpression {
return LtEq(a.parent, rhs) return LtEq(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) GT_EQ(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) GT_EQ(rhs Array[E]) BoolExpression {
return GtEq(a.parent, rhs) return GtEq(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) CONTAINS(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) CONTAINS(rhs Array[E]) BoolExpression {
return Contains(a.parent, rhs) return Contains(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs Array[E]) BoolExpression {
return IsContainedBy(a.parent, rhs) return IsContainedBy(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) OVERLAP(rhs ArrayExpression[E]) BoolExpression { func (a arrayInterfaceImpl[E]) OVERLAP(rhs Array[E]) BoolExpression {
return Overlap(a.parent, rhs) return Overlap(a.parent, rhs)
} }
func (a arrayInterfaceImpl[E]) CONCAT(rhs ArrayExpression[E]) ArrayExpression[E] { func (a arrayInterfaceImpl[E]) CONCAT(rhs Array[E]) Array[E] {
return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||"))
} }
func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) ArrayExpression[E] { func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) Array[E] {
return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||"))
} }
func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) Expression { func (a arrayInterfaceImpl[E]) AT(at IntegerExpression) E {
return arraySubscriptExpr(a.parent, expression) return CastToArrayElemType[E](a.parent, CustomExpression(a.parent, Token("["), at, Token("]")))
} }
type arrayExpressionWrapper[E Expression] struct { type arrayExpressionWrapper[E Expression] struct {
@ -79,7 +79,7 @@ type arrayExpressionWrapper[E Expression] struct {
Expression Expression
} }
func newArrayExpressionWrap[E Expression](expression Expression) ArrayExpression[E] { func newArrayExpressionWrap[E Expression](expression Expression) Array[E] {
arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression} arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression}
arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper
return &arrayExpressionWrapper return &arrayExpressionWrapper
@ -88,6 +88,49 @@ func newArrayExpressionWrap[E Expression](expression Expression) ArrayExpression
// ArrayExp is array expression wrapper around arbitrary expression. // ArrayExp is array expression wrapper around arbitrary expression.
// Allows go compiler to see any expression as array expression. // Allows go compiler to see any expression as array expression.
// Does not add sql cast to generated sql builder output. // Does not add sql cast to generated sql builder output.
func ArrayExp[E Expression](expression Expression) ArrayExpression[E] { func ArrayExp[E Expression](expression Expression) Array[E] {
return newArrayExpressionWrap[E](expression) 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

@ -1,7 +1,6 @@
package jet package jet
import ( import (
"github.com/lib/pq"
"testing" "testing"
) )
@ -11,7 +10,6 @@ func TestArrayExpressionEQ(t *testing.T) {
func TestArrayExpressionNOT_EQ(t *testing.T) { func TestArrayExpressionNOT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)") assertClauseSerialize(t, table1ColStringArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.NOT_EQ(StringArray([]string{"x"})), "(table1.col_array_string != $1)", pq.StringArray{"x"})
} }
func TestArrayExpressionLT(t *testing.T) { func TestArrayExpressionLT(t *testing.T) {
@ -32,12 +30,10 @@ func TestArrayExpressionGT_EQ(t *testing.T) {
func TestArrayExpressionCONTAINS(t *testing.T) { func TestArrayExpressionCONTAINS(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)") assertClauseSerialize(t, table1ColStringArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.CONTAINS(StringArray([]string{"x"})), "(table1.col_array_string @> $1)", pq.StringArray{"x"})
} }
func TestArrayExpressionCONTAINED_BY(t *testing.T) { func TestArrayExpressionCONTAINED_BY(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)") assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(StringArray([]string{"x"})), "(table1.col_array_string <@ $1)", pq.StringArray{"x"})
} }
func TestArrayExpressionOVERLAP(t *testing.T) { func TestArrayExpressionOVERLAP(t *testing.T) {
@ -46,14 +42,27 @@ func TestArrayExpressionOVERLAP(t *testing.T) {
func TestArrayExpressionCONCAT(t *testing.T) { func TestArrayExpressionCONCAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)") assertClauseSerialize(t, table1ColStringArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.CONCAT(StringArray([]string{"x"})), "(table1.col_array_string || $1)", pq.StringArray{"x"})
} }
func TestArrayExpressionCONCAT_ELEMENT(t *testing.T) { 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(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") assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x")
} }
func TestArrayExpressionAT(t *testing.T) { func TestArrayExpressionAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "(table1.col_array_string[$1])", int64(1)) 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

@ -134,17 +134,21 @@ func IntegerColumn(name string) ColumnInteger {
//------------------------------------------------------// //------------------------------------------------------//
type ColumnArray[E Expression] interface { type ColumnArray[E Expression] interface {
ArrayExpression[E] Array[E]
Column Column
From(subQuery SelectTable) ColumnArray[E] From(subQuery SelectTable) ColumnArray[E]
SET(stringExp ArrayExpression[E]) ColumnAssigment SET(stringExp Array[E]) ColumnAssigment
} }
type arrayColumnImpl[E Expression] struct { type arrayColumnImpl[E Expression] struct {
arrayInterfaceImpl[E] arrayInterfaceImpl[E]
ColumnExpressionImpl *ColumnExpressionImpl
}
func (a arrayColumnImpl[E]) fromImpl(subQuery SelectTable) Projection {
return a.From(subQuery)
} }
func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] { func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] {
@ -155,10 +159,10 @@ func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] {
return newArrayColumn return newArrayColumn
} }
func (a *arrayColumnImpl[E]) SET(stringExp ArrayExpression[E]) ColumnAssigment { func (a *arrayColumnImpl[E]) SET(stringExp Array[E]) ColumnAssigment {
return columnAssigmentImpl{ return columnAssigmentImpl{
column: a, column: a,
expression: stringExp, toAssign: stringExp,
} }
} }

View file

@ -1,7 +1,6 @@
package jet package jet
import ( import (
"github.com/lib/pq"
"testing" "testing"
) )
@ -12,36 +11,30 @@ var subQuery = &selectTableImpl{
func TestNewArrayColumnString(t *testing.T) { func TestNewArrayColumnString(t *testing.T) {
stringArrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery) stringArrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery)
assertClauseSerialize(t, stringArrayColumn, `sub_query."colArray"`) assertClauseSerialize(t, stringArrayColumn, `sub_query."colArray"`)
assertClauseSerialize(t, stringArrayColumn.EQ(StringArray([]string{"X"})), `(sub_query."colArray" = $1)`, pq.StringArray{"X"})
assertProjectionSerialize(t, stringArrayColumn, `sub_query."colArray" AS "colArray"`) assertProjectionSerialize(t, stringArrayColumn, `sub_query."colArray" AS "colArray"`)
arrayColumn2 := table1ColStringArray.From(subQuery) arrayColumn2 := table1ColStringArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_string"`) assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_string"`)
assertClauseSerialize(t, arrayColumn2.EQ(StringArray([]string{"X"})), `(sub_query."table1.col_array_string" = $1)`, pq.StringArray{"X"})
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_string" AS "table1.col_array_string"`) assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_string" AS "table1.col_array_string"`)
} }
func TestNewArrayColumnBool(t *testing.T) { func TestNewArrayColumnBool(t *testing.T) {
boolArrayColumn := ArrayColumn[BoolExpression]("colArrayBool").From(subQuery) boolArrayColumn := ArrayColumn[BoolExpression]("colArrayBool").From(subQuery)
assertClauseSerialize(t, boolArrayColumn, `sub_query."colArrayBool"`) assertClauseSerialize(t, boolArrayColumn, `sub_query."colArrayBool"`)
assertClauseSerialize(t, boolArrayColumn.EQ(BoolArray([]bool{true})), `(sub_query."colArrayBool" = $1)`, pq.BoolArray{true})
assertProjectionSerialize(t, boolArrayColumn, `sub_query."colArrayBool" AS "colArrayBool"`) assertProjectionSerialize(t, boolArrayColumn, `sub_query."colArrayBool" AS "colArrayBool"`)
arrayColumn2 := table1ColBoolArray.From(subQuery) arrayColumn2 := table1ColBoolArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool"`) assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool"`)
assertClauseSerialize(t, arrayColumn2.EQ(BoolArray([]bool{true})), `(sub_query."table1.col_array_bool" = $1)`, pq.BoolArray{true})
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool" AS "table1.col_array_bool"`) assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool" AS "table1.col_array_bool"`)
} }
func TestNewArrayColumnInteger(t *testing.T) { func TestNewArrayColumnInteger(t *testing.T) {
intArrayColumn := ArrayColumn[IntegerExpression]("colArrayInt").From(subQuery) intArrayColumn := ArrayColumn[IntegerExpression]("colArrayInt").From(subQuery)
assertClauseSerialize(t, intArrayColumn, `sub_query."colArrayInt"`) assertClauseSerialize(t, intArrayColumn, `sub_query."colArrayInt"`)
assertClauseSerialize(t, intArrayColumn.EQ(Int32Array([]int32{42})), `(sub_query."colArrayInt" = $1)`, pq.Int32Array{42})
assertProjectionSerialize(t, intArrayColumn, `sub_query."colArrayInt" AS "colArrayInt"`) assertProjectionSerialize(t, intArrayColumn, `sub_query."colArrayInt" AS "colArrayInt"`)
arrayColumn2 := table1ColIntArray.From(subQuery) arrayColumn2 := table1ColIntArray.From(subQuery)
assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_int"`) assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_int"`)
assertClauseSerialize(t, arrayColumn2.EQ(Int32Array([]int32{42})), `(sub_query."table1.col_array_int" = $1)`, pq.Int32Array{42})
assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_int" AS "table1.col_array_int"`) assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_int" AS "table1.col_array_int"`)
} }

View file

@ -341,42 +341,3 @@ func (s *complexExpression) serialize(statement StatementType, out *SQLBuilder,
func wrap(expressions ...Expression) Expression { func wrap(expressions ...Expression) Expression {
return NewFunc("", expressions, nil) return NewFunc("", expressions, nil)
} }
type arraySubscriptExpression struct {
ExpressionInterfaceImpl
array Expression
subscript IntegerExpression
}
func (a arraySubscriptExpression) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
if !contains(options, NoWrap) {
out.WriteString("(")
}
a.array.serialize(statement, out, FallTrough(options)...) // FallTrough here because complexExpression is just a wrapper
out.WriteString("[")
a.subscript.serialize(statement, out, FallTrough(options)...) // FallTrough here because complexExpression is just a wrapper
out.WriteString("]")
if !contains(options, NoWrap) {
out.WriteString(")")
}
}
func arraySubscriptExpr(array Expression, subscript IntegerExpression) Expression {
arraySubscriptExpression := &arraySubscriptExpression{array: array, subscript: subscript}
arraySubscriptExpression.ExpressionInterfaceImpl.Parent = arraySubscriptExpression
return arraySubscriptExpression
}
type skipParenthesisWrap struct {
Expression
}
func skipWrap(expression Expression) Expression {
return &skipParenthesisWrap{expression}
}
// since the expression is a function parameter, there is no need to wrap it in parentheses
func (s *skipParenthesisWrap) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
s.Expression.serialize(statement, out, append(options, NoWrap)...)
}

View file

@ -2,7 +2,6 @@ package jet
import ( import (
"fmt" "fmt"
"github.com/lib/pq"
"time" "time"
) )
@ -161,66 +160,6 @@ func Decimal(value string) FloatExpression {
return &floatLiteral return &floatLiteral
} }
// ---------------------------------------------------//
type boolArrayLiteral struct {
arrayInterfaceImpl[BoolExpression]
literalExpressionImpl
}
func BoolArray(values []bool) ArrayExpression[BoolExpression] {
l := boolArrayLiteral{}
l.literalExpressionImpl = *literal(pq.BoolArray(values))
l.arrayInterfaceImpl.parent = &l
return &l
}
type integerArrayLiteral struct {
arrayInterfaceImpl[IntegerExpression]
literalExpressionImpl
}
func Int64Array(values []int64) ArrayExpression[IntegerExpression] {
l := integerArrayLiteral{}
l.literalExpressionImpl = *literal(pq.Int64Array(values))
l.arrayInterfaceImpl.parent = &l
return &l
}
func Int32Array(values []int32) ArrayExpression[IntegerExpression] {
l := integerArrayLiteral{}
l.literalExpressionImpl = *literal(pq.Int32Array(values))
l.arrayInterfaceImpl.parent = &l
return &l
}
type stringArrayLiteral struct {
arrayInterfaceImpl[StringExpression]
literalExpressionImpl
}
func StringArray(values []string) ArrayExpression[StringExpression] {
l := stringArrayLiteral{}
l.literalExpressionImpl = *literal(pq.StringArray(values))
l.arrayInterfaceImpl.parent = &l
return &l
}
type unsafeArrayLiteral[E Expression] struct {
arrayInterfaceImpl[E]
literalExpressionImpl
}
func UnsafeArray[E LiteralExpression](values []interface{}) ArrayExpression[E] {
l := unsafeArrayLiteral[E]{}
l.literalExpressionImpl = *literal(pq.Array(values))
l.arrayInterfaceImpl.parent = &l
return &l
}
// ---------------------------------------------------// // ---------------------------------------------------//
type stringLiteral struct { type stringLiteral struct {
stringInterfaceImpl stringInterfaceImpl

View file

@ -22,15 +22,6 @@ func BIT_NOT(expr IntegerExpression) IntegerExpression {
return newPrefixIntegerOperatorExpression(expr, "~") return newPrefixIntegerOperatorExpression(expr, "~")
} }
// ----------- Array operators -------------- //
func Any(lhs Expression, op BinaryBoolOp, rhs Expression) BoolExpression {
return op(lhs, Func("ANY", rhs))
}
func All(lhs Expression, op BinaryBoolOp, rhs Expression) BoolExpression {
return op(lhs, Func("ALL", rhs))
}
//----------- Comparison operators ---------------// //----------- Comparison operators ---------------//
// EXISTS checks for existence of the rows in subQuery // EXISTS checks for existence of the rows in subQuery

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
@ -249,8 +248,6 @@ func (s *SQLBuilder) argToString(value interface{}) string {
case string: case string:
return stringQuote(bindVal) return stringQuote(bindVal)
case []string:
return stringArrayQuote(bindVal)
case []byte: case []byte:
return stringQuote(string(bindVal)) return stringQuote(string(bindVal))
case uuid.UUID: case uuid.UUID:
@ -278,19 +275,6 @@ func (s *SQLBuilder) argToString(value interface{}) string {
} }
} }
func stringArrayQuote(val []string) string {
var sb strings.Builder
sb.WriteString(`'{`)
for i := 0; i < len(val); i++ {
if i > 0 {
sb.WriteString(`, `)
}
sb.WriteString(stringDoubleQuote(val[i]))
}
sb.WriteString(`}'`)
return sb.String()
}
func integerTypesToString(value interface{}) string { func integerTypesToString(value interface{}) string {
switch bindVal := value.(type) { switch bindVal := value.(type) {
case int: case int:
@ -339,7 +323,3 @@ func shouldQuoteIdentifier(identifier string) bool {
func stringQuote(value string) string { func stringQuote(value string) string {
return `'` + strings.Replace(value, "'", "''", -1) + `'` return `'` + strings.Replace(value, "'", "''", -1) + `'`
} }
func stringDoubleQuote(value string) string {
return `"` + strings.Replace(value, `"`, `""`, -1) + `"`
}

View file

@ -17,9 +17,6 @@ type StringExpression interface {
BETWEEN(min, max StringExpression) BoolExpression BETWEEN(min, max StringExpression) BoolExpression
NOT_BETWEEN(min, max StringExpression) BoolExpression NOT_BETWEEN(min, max StringExpression) BoolExpression
ANY_EQ(rhs ArrayExpression[StringExpression]) BoolExpression
ALL_EQ(rhs ArrayExpression[StringExpression]) BoolExpression
CONCAT(rhs Expression) StringExpression CONCAT(rhs Expression) StringExpression
LIKE(pattern StringExpression) BoolExpression LIKE(pattern StringExpression) BoolExpression
@ -75,14 +72,6 @@ func (s *stringInterfaceImpl) NOT_BETWEEN(min, max StringExpression) BoolExpress
return NewBetweenOperatorExpression(s.root, min, max, true) return NewBetweenOperatorExpression(s.root, min, max, true)
} }
func (i *stringInterfaceImpl) ANY_EQ(rhs ArrayExpression[StringExpression]) BoolExpression {
return Any(i.parent, Eq, rhs)
}
func (i *stringInterfaceImpl) ALL_EQ(rhs ArrayExpression[StringExpression]) BoolExpression {
return All(i.parent, Eq, rhs)
}
func (s *stringInterfaceImpl) CONCAT(rhs Expression) StringExpression { func (s *stringInterfaceImpl) CONCAT(rhs Expression) StringExpression {
return newBinaryStringOperatorExpression(s.root, rhs, StringConcatOperator) return newBinaryStringOperatorExpression(s.root, rhs, StringConcatOperator)
} }

View file

@ -76,14 +76,6 @@ func TestStringNOT_REGEXP_LIKE(t *testing.T) {
assertClauseSerialize(t, table3StrCol.NOT_REGEXP_LIKE(String("JOHN"), true), "(table3.col2 NOT REGEXP $1)", "JOHN") assertClauseSerialize(t, table3StrCol.NOT_REGEXP_LIKE(String("JOHN"), true), "(table3.col2 NOT REGEXP $1)", "JOHN")
} }
func TestStringANY_EQ(t *testing.T) {
assertClauseSerialize(t, table2ColStr.ANY_EQ(table1ColStringArray), "(table2.col_str = ANY(table1.col_array_string))")
}
func TestStringALL_EQ(t *testing.T) {
assertClauseSerialize(t, table2ColStr.ALL_EQ(table1ColStringArray), "(table2.col_str = ALL(table1.col_array_string))")
}
func TestStringExp(t *testing.T) { func TestStringExp(t *testing.T) {
assertClauseSerialize(t, StringExp(table2ColFloat), "table2.col_float") assertClauseSerialize(t, StringExp(table2ColFloat), "table2.col_float")
assertClauseSerialize(t, StringExp(table2ColFloat).NOT_LIKE(String("abc")), "(table2.col_float NOT LIKE $1)", "abc") assertClauseSerialize(t, StringExp(table2ColFloat).NOT_LIKE(String("abc")), "(table2.col_float NOT LIKE $1)", "abc")

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

@ -112,21 +112,3 @@ type ColumnInt8Range jet.ColumnRange[jet.Int8Expression]
// Int8RangeColumn creates named range with range column // Int8RangeColumn creates named range with range column
var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression] var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression]
// ColumnStringArray is interface of column
type ColumnStringArray jet.ColumnArray[jet.StringExpression]
// StringArrayColumn creates named string array column
var StringArrayColumn = jet.ArrayColumn[jet.StringExpression]
// ColumnIntegerArray is interface of column
type ColumnIntegerArray jet.ColumnArray[jet.IntegerExpression]
// IntegerArrayColumn creates named integer array column
var IntegerArrayColumn = jet.ArrayColumn[jet.IntegerExpression]
// ColumnBoolArray is interface of column
type ColumnBoolArray jet.ColumnArray[jet.BoolExpression]
// BoolArrayColumn creates named bool array column
var BoolArrayColumn = jet.ArrayColumn[jet.BoolExpression]

View file

@ -2,33 +2,24 @@ 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
// BoolExpression interface // BoolExpression interface
type BoolExpression = jet.BoolExpression type BoolExpression = jet.BoolExpression
// BoolArrayExpression interface
type BoolArrayExpression = jet.ArrayExpression[BoolExpression]
// StringExpression interface // StringExpression interface
type StringExpression = jet.StringExpression type StringExpression = jet.StringExpression
type ByteaExpression = jet.BlobExpression type ByteaExpression = jet.BlobExpression
// StringArrayExpression interface
type StringArrayExpression = jet.ArrayExpression[StringExpression]
// NumericExpression interface // NumericExpression interface
type NumericExpression = jet.NumericExpression type NumericExpression = jet.NumericExpression
// IntegerExpression interface // IntegerExpression interface
type IntegerExpression = jet.IntegerExpression type IntegerExpression = jet.IntegerExpression
// IntegerArrayExpression interface
type IntegerArrayExpression = jet.ArrayExpression[IntegerExpression]
// FloatExpression is interface // FloatExpression is interface
type FloatExpression = jet.FloatExpression type FloatExpression = jet.FloatExpression
@ -185,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, beginAt ...IntegerExpression) IntegerExpression {
return IntExp(Func("ARRAY_POSITION", optionalAppend([]Expression{arr, elem}, beginAt)...))
}
// 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

@ -175,30 +175,27 @@ RETURNING table1.col1 AS "table1.col1",
} }
func TestInsert_ON_CONFLICT_ON_CONSTRAINT(t *testing.T) { func TestInsert_ON_CONFLICT_ON_CONSTRAINT(t *testing.T) {
stmt := table1.INSERT(table1Col1, table1ColBool, table1ColStringArray). stmt := table1.INSERT(table1Col1, table1ColBool).
VALUES("one", "two", "three"). VALUES("one", "two").
VALUES("1", "2", "3"). VALUES("1", "2").
ON_CONFLICT().ON_CONSTRAINT("idk_primary_key").DO_UPDATE( ON_CONFLICT().ON_CONSTRAINT("idk_primary_key").
DO_UPDATE(
SET(table1ColBool.SET(Bool(false)), SET(table1ColBool.SET(Bool(false)),
table2ColInt.SET(Int(1)), table2ColInt.SET(Int(1)),
table1ColStringArray.SET(StringArray([]string{"one"})), ColumnList{table1Col1, table1ColBool}.SET(ROW(Int(2), String("two"))),
ColumnList{table1Col1, table1ColBool, table1ColStringArray}.SET(jet.ROW(Int(2), String("two"), StringArray([]string{"two"}))), ).WHERE(table1Col1.GT(Int(2)))).
).WHERE(table1Col1.GT(Int(2))), RETURNING(table1Col1, table1ColBool)
).
RETURNING(table1Col1, table1ColBool, table1ColStringArray)
assertDebugStatementSql(t, stmt, ` assertDebugStatementSql(t, stmt, `
INSERT INTO db.table1 (col1, col_bool, col_string_array) INSERT INTO db.table1 (col1, col_bool)
VALUES ('one', 'two', 'three'), VALUES ('one', 'two'),
('1', '2', '3') ('1', '2')
ON CONFLICT ON CONSTRAINT idk_primary_key DO UPDATE ON CONFLICT ON CONSTRAINT idk_primary_key DO UPDATE
SET col_bool = FALSE::boolean, SET col_bool = FALSE::boolean,
col_int = 1, col_int = 1,
col_string_array = '{"one"}', (col1, col_bool) = ROW(2, 'two'::text)
(col1, col_bool, col_string_array) = ROW(2, 'two'::text, '{"two"}')
WHERE table1.col1 > 2 WHERE table1.col1 > 2
RETURNING table1.col1 AS "table1.col1", RETURNING table1.col1 AS "table1.col1",
table1.col_bool AS "table1.col_bool", table1.col_bool AS "table1.col_bool";
table1.col_string_array AS "table1.col_string_array";
`) `)
} }

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"
@ -11,11 +13,6 @@ func Bool(value bool) BoolExpression {
return CAST(jet.Bool(value)).AS_BOOL() return CAST(jet.Bool(value)).AS_BOOL()
} }
// BoolArray creates new bool array literal expression
func BoolArray(elements []bool) BoolArrayExpression {
return jet.BoolArray(elements)
}
// Int is constructor for 64 bit signed integer expressions literals. // Int is constructor for 64 bit signed integer expressions literals.
var Int = jet.Int var Int = jet.Int
@ -73,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.
// //
@ -85,11 +85,6 @@ func String(value string) StringExpression {
return CAST(jet.String(value)).AS_TEXT() return CAST(jet.String(value)).AS_TEXT()
} }
// StringArray creates new string array literal expression
func StringArray(elements []string) StringArrayExpression {
return jet.StringArray(elements)
}
// Text is a parameter constructor for the PostgreSQL text type. This constructor also adds an // Text is a parameter constructor for the PostgreSQL text type. This constructor also adds an
// explicit placeholder type cast to text in the generated query, such as `$3::text`. // explicit placeholder type cast to text in the generated query, such as `$3::text`.
// Example usage: // Example usage:
@ -134,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 {
@ -195,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

@ -1,15 +1,13 @@
package postgres package postgres
import ( import (
"database/sql"
"github.com/lib/pq"
"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"
@ -46,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
@ -55,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)
@ -117,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
@ -259,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{}
@ -875,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)
}) })
} }
@ -1583,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',
@ -2250,7 +2238,7 @@ var allTypesRow0 = model.AllTypes{
TextArrayPtr: &pq.StringArray{"breakfast", "consulting"}, TextArrayPtr: &pq.StringArray{"breakfast", "consulting"},
TextArray: pq.StringArray{"breakfast", "consulting"}, TextArray: pq.StringArray{"breakfast", "consulting"},
JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`}, JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`},
TextMultiDimArrayPtr: testutils.StringPtr("{{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,
Mood: model.Mood_Happy, Mood: model.Mood_Happy,

View file

@ -0,0 +1,719 @@
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"), //newer postgres versions
ARRAY_POSITIONS(stringArray, String("text")).AS("position"),
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_POSITIONS($37::text[], $38::text) AS "position",
ARRAY_PREPEND($39::text, sample_arrays.text_array) AS "prepend",
ARRAY_REMOVE($40::boolean[], $41::boolean) AS "remove",
ARRAY_REPLACE(sample_arrays.varchar_array, $42::text, $43::text) AS "replace",
ARRAY_TO_STRING(sample_arrays.mood_enum_array, $44::text) AS "to_string",
ARRAY_UPPER(sample_arrays.int8_array, $45) AS "upper",
CARDINALITY(sample_arrays.double_array) AS "cardinality"
FROM test_sample.sample_arrays
WHERE sample_arrays.bool_array @> $46::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 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": [
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)

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

@ -2,8 +2,8 @@ package postgres
import ( import (
"context" "context"
"github.com/lib/pq"
"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"
@ -1007,7 +1007,6 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
type MyFloat32 float32 type MyFloat32 float32
type MyFloat64 float64 type MyFloat64 float64
type MyString string type MyString string
type MyStringArray pq.StringArray
type MyTime = time.Time type MyTime = time.Time
type film struct { type film struct {
@ -1022,13 +1021,12 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
ReplacementCost MyFloat64 ReplacementCost MyFloat64
Rating *model.MpaaRating Rating *model.MpaaRating
LastUpdate MyTime LastUpdate MyTime
SpecialFeatures MyStringArray SpecialFeatures pq.StringArray
Fulltext MyString Fulltext MyString
} }
// We'll skip special features, because it's a slice and it does not implement sql.Scanner
stmt := SELECT( stmt := SELECT(
Film.AllColumns.Except(Film.SpecialFeatures), Film.AllColumns,
).FROM( ).FROM(
Film, Film,
).ORDER_BY( ).ORDER_BY(

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

@ -2710,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",
@ -2730,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"
@ -2740,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 (
ORDER BY actor.actor_id ASC, film.film_id ASC; (language.name = $1::char(20))
` AND (category.name != $2::text)
AND (film.length > $3::integer)
stmt := SELECT( AND (film.rating != 'R')
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // list of all actor columns (equivalent to Actor.AllColumns) AND ($4::text = ANY(film.special_features))
Film.AllColumns, // list of all film columns (equivalent to Film.FilmID, Film.Title, ...)
Language.AllColumns,
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(
Language.Name.EQ(Char(20)("English")). // note that every column has type.
AND(Category.Name.NOT_EQ(Text("Action"))). // String column Language.Name and Category.Name can be compared only with string expression
AND(Film.Length.GT(Int32(180))). // Film.Length is integer column and can be compared only with integer expression
AND(Film.Rating.NOT_EQ(enum.MpaaRating.R)),
).ORDER_BY(
Actor.ActorID.ASC(),
Film.FilmID.ASC(),
) )
ORDER BY actor.actor_id ASC, film.film_id ASC;
`, "English", "Action", int32(180), "Trailers")
testutils.AssertDebugStatementSql(t, stmt, expectedSQL, "English", "Action", int32(180)) testutils.AssertDebugStatementSql(t, stmt, `
SELECT actor.actor_id AS "actor.actor_id",
actor.first_name AS "actor.first_name",
actor.last_name AS "actor.last_name",
actor.last_update AS "actor.last_update",
film.film_id AS "film.film_id",
film.title AS "film.title",
film.description AS "film.description",
film.release_year AS "film.release_year",
film.language_id AS "film.language_id",
film.rental_duration AS "film.rental_duration",
film.rental_rate AS "film.rental_rate",
film.length AS "film.length",
film.replacement_cost AS "film.replacement_cost",
film.rating AS "film.rating",
film.last_update AS "film.last_update",
film.special_features AS "film.special_features",
film.fulltext AS "film.fulltext",
language.language_id AS "language.language_id",
language.name AS "language.name",
category.category_id AS "category.category_id",
category.name AS "category.name",
category.last_update AS "category.last_update"
FROM dvds.actor
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
INNER JOIN dvds.language ON (language.language_id = film.language_id)
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
WHERE (
(language.name = '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
@ -2798,7 +2843,7 @@ 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-dest2.json") //testutils.SaveJSONFile(dest2, "./testdata/results/postgres/quick-start-dest2.json")
testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json") testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json")
} }
@ -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.

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