Merge remote-tracking branch 'upstream/master' into feat/strict-field-mapping
This commit is contained in:
commit
323c3a0597
11 changed files with 382 additions and 59 deletions
|
|
@ -633,7 +633,7 @@ Typically, two releases are published each year — one in early spring and anot
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2019-2025 Goran Bjelanovic
|
Copyright 2019-2026 Goran Bjelanovic
|
||||||
Licensed under the Apache License, Version 2.0.
|
Licensed under the Apache License, Version 2.0.
|
||||||
|
|
||||||
## Support the Project
|
## Support the Project
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "v2.14.0"
|
const version = "v2.14.1"
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ func (a *alias) fromImpl(subQuery SelectTable) Projection {
|
||||||
|
|
||||||
// This function is used to create dummy columns when exporting sub-query columns using subQuery.AllColumns()
|
// This function is used to create dummy columns when exporting sub-query columns using subQuery.AllColumns()
|
||||||
// In most case we don't care about type of the column, except when sub-query columns are used as SELECT_JSON projection.
|
// In most case we don't care about type of the column, except when sub-query columns are used as SELECT_JSON projection.
|
||||||
// We need to know type to encode value for json unmarshal. At the moment only bool, time and blob columns are of interest,
|
// We need to know type to encode value for json unmarshal.
|
||||||
// so we don't have to support every column type.
|
|
||||||
func newDummyColumnForExpression(exp Expression, name string) ColumnExpression {
|
func newDummyColumnForExpression(exp Expression, name string) ColumnExpression {
|
||||||
|
|
||||||
switch exp.(type) {
|
switch exp.(type) {
|
||||||
|
|
@ -54,6 +53,41 @@ func newDummyColumnForExpression(exp Expression, name string) ColumnExpression {
|
||||||
return IntervalColumn(name)
|
return IntervalColumn(name)
|
||||||
case StringExpression:
|
case StringExpression:
|
||||||
return StringColumn(name)
|
return StringColumn(name)
|
||||||
|
|
||||||
|
case Array[BoolExpression]:
|
||||||
|
return ArrayColumn[BoolExpression](name)
|
||||||
|
case Array[IntegerExpression]:
|
||||||
|
return ArrayColumn[IntegerExpression](name)
|
||||||
|
case Array[FloatExpression]:
|
||||||
|
return ArrayColumn[FloatExpression](name)
|
||||||
|
case Array[BlobExpression]:
|
||||||
|
return ArrayColumn[BlobExpression](name)
|
||||||
|
case Array[DateExpression]:
|
||||||
|
return ArrayColumn[DateExpression](name)
|
||||||
|
case Array[TimeExpression]:
|
||||||
|
return ArrayColumn[TimeExpression](name)
|
||||||
|
case Array[TimezExpression]:
|
||||||
|
return ArrayColumn[TimezExpression](name)
|
||||||
|
case Array[TimestampExpression]:
|
||||||
|
return ArrayColumn[TimestampExpression](name)
|
||||||
|
case Array[TimestampzExpression]:
|
||||||
|
return ArrayColumn[TimestampzExpression](name)
|
||||||
|
case Array[IntervalExpression]:
|
||||||
|
return ArrayColumn[IntervalExpression](name)
|
||||||
|
case Array[StringExpression]:
|
||||||
|
return ArrayColumn[StringExpression](name)
|
||||||
|
|
||||||
|
case Range[Int4Expression], Range[Int8Expression]:
|
||||||
|
return RangeColumn[IntegerExpression](name)
|
||||||
|
case Range[NumericExpression]:
|
||||||
|
return RangeColumn[NumericExpression](name)
|
||||||
|
case Range[DateExpression]:
|
||||||
|
return RangeColumn[DateExpression](name)
|
||||||
|
case Range[TimestampExpression]:
|
||||||
|
return RangeColumn[TimestampExpression](name)
|
||||||
|
case Range[TimestampzExpression]:
|
||||||
|
return RangeColumn[TimestampzExpression](name)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return StringColumn(name)
|
return StringColumn(name)
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,10 @@ type arrayExpressionWrapper[E Expression] struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArrayExpressionWrap[E Expression](expression Expression) Array[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
|
expression.setRoot(arrayExpressionWrapper)
|
||||||
|
return arrayExpressionWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArrayExp is array expression wrapper around arbitrary expression.
|
// ArrayExp is array expression wrapper around arbitrary expression.
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,10 @@ type ClauseOrderBy struct {
|
||||||
SkipNewLine bool
|
SkipNewLine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *ClauseOrderBy) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
o.Serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize serializes clause into SQLBuilder
|
// Serialize serializes clause into SQLBuilder
|
||||||
func (o *ClauseOrderBy) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
func (o *ClauseOrderBy) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
if o.List == nil {
|
if o.List == nil {
|
||||||
|
|
@ -219,6 +223,10 @@ type ClauseLimit struct {
|
||||||
Count int64
|
Count int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *ClauseLimit) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
o.Serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize serializes clause into SQLBuilder
|
// Serialize serializes clause into SQLBuilder
|
||||||
func (l *ClauseLimit) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
func (l *ClauseLimit) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
if l.Count >= 0 {
|
if l.Count >= 0 {
|
||||||
|
|
@ -233,6 +241,10 @@ type ClauseOffset struct {
|
||||||
Count IntegerExpression
|
Count IntegerExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *ClauseOffset) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
o.Serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize serializes clause into SQLBuilder
|
// Serialize serializes clause into SQLBuilder
|
||||||
func (o *ClauseOffset) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
func (o *ClauseOffset) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
if is.Nil(o.Count) {
|
if is.Nil(o.Count) {
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,20 @@ func SaveJSONFile(v interface{}, testRelativePath string) {
|
||||||
throw.OnError(err)
|
throw.OnError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReadJSONFile(t require.TestingT, testRelativePath string, dest any) {
|
||||||
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
return // skip assert for benchmarks
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := getFullPath(testRelativePath)
|
||||||
|
fileJSONData, err := os.ReadFile(filePath) // #nosec G304
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = json.Unmarshal(fileJSONData, dest)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
// AssertJSONFile check if data json representation is the same as json at testRelativePath
|
// AssertJSONFile check if data json representation is the same as json at testRelativePath
|
||||||
func AssertJSONFile(t require.TestingT, data interface{}, testRelativePath string) {
|
func AssertJSONFile(t require.TestingT, data interface{}, testRelativePath string) {
|
||||||
if _, ok := t.(*testing.B); ok {
|
if _, ok := t.(*testing.B); ok {
|
||||||
|
|
|
||||||
|
|
@ -30,26 +30,46 @@ func SELECT_JSON_OBJ(projections ...Projection) SelectJsonStatement {
|
||||||
|
|
||||||
type selectJsonStatement struct {
|
type selectJsonStatement struct {
|
||||||
*selectStatementImpl
|
*selectStatementImpl
|
||||||
|
|
||||||
|
projections []Projection
|
||||||
|
statementType jet.StatementType
|
||||||
|
|
||||||
|
// SELECT_JSON_ARR internal clauses
|
||||||
|
arrOrderBy *jet.ClauseOrderBy
|
||||||
|
arrLimit *jet.ClauseLimit
|
||||||
|
arrOffset *jet.ClauseOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSelectStatementJson(projections []Projection, statementType jet.StatementType) SelectJsonStatement {
|
func newSelectStatementJson(projections []Projection, statementType jet.StatementType) SelectJsonStatement {
|
||||||
newSelect := &selectJsonStatement{
|
newSelectJson := &selectJsonStatement{
|
||||||
selectStatementImpl: newSelectStatement(statementType, nil, nil),
|
selectStatementImpl: newSelectStatement(statementType, nil, nil),
|
||||||
|
|
||||||
|
projections: projections,
|
||||||
|
statementType: statementType,
|
||||||
|
|
||||||
|
arrOrderBy: &jet.ClauseOrderBy{},
|
||||||
|
arrLimit: &jet.ClauseLimit{Count: -1},
|
||||||
|
arrOffset: &jet.ClauseOffset{},
|
||||||
}
|
}
|
||||||
|
|
||||||
newSelect.Select.ProjectionList = ProjectionList{constructJsonFunc(projections, statementType).AS("json")}
|
newSelectJson.constructProjectionList()
|
||||||
|
|
||||||
return newSelect
|
return newSelectJson
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructJsonFunc(projections []Projection, statementType jet.StatementType) Expression {
|
func (s *selectJsonStatement) constructProjectionList() {
|
||||||
jsonObj := Func("JSON_OBJECT", CustomExpression(jet.JsonObjProjectionList(projections)))
|
jsonProjection := Func("JSON_OBJECT", CustomExpression(jet.JsonObjProjectionList(s.projections)))
|
||||||
|
|
||||||
if statementType == jet.SelectJsonArrStatementType {
|
if s.statementType == jet.SelectJsonArrStatementType {
|
||||||
return Func("JSON_ARRAYAGG", jsonObj)
|
jsonProjection = Func("JSON_ARRAYAGG", CustomExpression(
|
||||||
|
jsonProjection,
|
||||||
|
s.arrOrderBy,
|
||||||
|
s.arrLimit,
|
||||||
|
s.arrOffset,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonObj
|
s.Select.ProjectionList = ProjectionList{jsonProjection.AS("json")}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selectJsonStatement) FROM(table ReadableTable) SelectJsonStatement {
|
func (s *selectJsonStatement) FROM(table ReadableTable) SelectJsonStatement {
|
||||||
|
|
@ -63,17 +83,32 @@ func (s *selectJsonStatement) WHERE(condition BoolExpression) SelectJsonStatemen
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selectJsonStatement) ORDER_BY(orderByClauses ...OrderByClause) SelectJsonStatement {
|
func (s *selectJsonStatement) ORDER_BY(orderBy ...OrderByClause) SelectJsonStatement {
|
||||||
s.OrderBy.List = orderByClauses
|
if s.statementType == jet.SelectJsonArrStatementType {
|
||||||
|
s.arrOrderBy.List = orderBy
|
||||||
|
} else {
|
||||||
|
s.OrderBy.List = orderBy
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selectJsonStatement) LIMIT(limit int64) SelectJsonStatement {
|
func (s *selectJsonStatement) LIMIT(limit int64) SelectJsonStatement {
|
||||||
s.Limit.Count = limit
|
if s.statementType == jet.SelectJsonArrStatementType {
|
||||||
|
s.arrLimit.Count = limit
|
||||||
|
} else {
|
||||||
|
s.Limit.Count = limit
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selectJsonStatement) OFFSET(offset int64) SelectJsonStatement {
|
func (s *selectJsonStatement) OFFSET(offset int64) SelectJsonStatement {
|
||||||
s.Offset.Count = Int(offset)
|
if s.statementType == jet.SelectJsonArrStatementType {
|
||||||
|
s.arrOffset.Count = Int(offset)
|
||||||
|
} else {
|
||||||
|
s.Offset.Count = Int(offset)
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,10 @@ func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interfac
|
||||||
return fmt.Errorf("jet: failed to scan a row into destination, %w", err)
|
return fmt.Errorf("jet: failed to scan a row into destination, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scanContext.EnsureEveryColumnRead() // can panic
|
if scanContext.rowNum == 1 && GlobalConfig.StrictScan {
|
||||||
|
scanContext.EnsureEveryColumnRead() // can panic
|
||||||
|
}
|
||||||
|
|
||||||
if GlobalConfig.StrictFieldMapping {
|
if GlobalConfig.StrictFieldMapping {
|
||||||
scanContext.EnsureEveryFieldMapped() // can panic
|
scanContext.EnsureEveryFieldMapped() // can panic
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ package mysql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"github.com/go-jet/jet/v2/qrm"
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -127,32 +127,91 @@ WHERE actor.actor_id = ?;
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectJsonArr(t *testing.T) {
|
func TestSelectJsonArr(t *testing.T) {
|
||||||
stmt := SELECT_JSON_ARR(Actor.AllColumns).
|
onlyMariaDB(t)
|
||||||
FROM(Actor).
|
|
||||||
ORDER_BY(Actor.ActorID)
|
|
||||||
|
|
||||||
testutils.AssertDebugStatementSql(t, stmt, `
|
var savedActors []model.Actor
|
||||||
|
testutils.ReadJSONFile(t, "./testdata/results/mysql/all_actors.json", &savedActors)
|
||||||
|
|
||||||
|
t.Run("order by", func(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
ORDER_BY(
|
||||||
|
Actor.LastName.DESC(),
|
||||||
|
Actor.FirstName.ASC(),
|
||||||
|
Actor.ActorID.ASC(),
|
||||||
|
)
|
||||||
|
|
||||||
|
testutils.AssertDebugStatementSql(t, stmt, `
|
||||||
SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
||||||
'actorID', actor.actor_id,
|
'actorID', actor.actor_id,
|
||||||
'firstName', actor.first_name,
|
'firstName', actor.first_name,
|
||||||
'lastName', actor.last_name,
|
'lastName', actor.last_name,
|
||||||
'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
|
'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
|
||||||
)) AS "json"
|
)
|
||||||
FROM dvds.actor
|
ORDER BY actor.last_name DESC, actor.first_name ASC, actor.actor_id ASC) AS "json"
|
||||||
ORDER BY actor.actor_id;
|
FROM dvds.actor;
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var dest []model.Actor
|
var dest []model.Actor
|
||||||
|
|
||||||
err := stmt.Query(db, &dest)
|
err := stmt.Query(db, &dest)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// sort by actor.LastName desc
|
||||||
|
slices.SortFunc(savedActors, func(a, b model.Actor) int {
|
||||||
|
if l := strings.Compare(b.LastName, a.LastName); l != 0 {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
if f := strings.Compare(a.FirstName, b.FirstName); f != 0 {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(a.ActorID) - int(b.ActorID)
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, dest, savedActors)
|
||||||
|
requireLogged(t, stmt)
|
||||||
|
requireQueryLogged(t, stmt, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("order by, limit, offset", func(t *testing.T) {
|
||||||
|
stmt := SELECT_JSON_ARR(Actor.AllColumns).
|
||||||
|
FROM(Actor).
|
||||||
|
ORDER_BY(Actor.ActorID.DESC()).
|
||||||
|
LIMIT(5).
|
||||||
|
OFFSET(10)
|
||||||
|
|
||||||
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
|
SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
||||||
|
'actorID', actor.actor_id,
|
||||||
|
'firstName', actor.first_name,
|
||||||
|
'lastName', actor.last_name,
|
||||||
|
'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
|
||||||
|
)
|
||||||
|
ORDER BY actor.actor_id DESC
|
||||||
|
LIMIT ?
|
||||||
|
OFFSET ?) AS "json"
|
||||||
|
FROM dvds.actor;
|
||||||
|
`)
|
||||||
|
|
||||||
|
var dest []model.Actor
|
||||||
|
|
||||||
|
err := stmt.Query(db, &dest)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
slices.SortFunc(savedActors, func(a, b model.Actor) int {
|
||||||
|
return int(b.ActorID) - int(a.ActorID)
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, dest, savedActors[10:15])
|
||||||
|
})
|
||||||
|
|
||||||
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/all_actors.json")
|
|
||||||
requireLogged(t, stmt)
|
|
||||||
requireQueryLogged(t, stmt, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectJsonArr_NestedArr(t *testing.T) {
|
func TestSelectJsonArr_NestedArr(t *testing.T) {
|
||||||
|
onlyMariaDB(t)
|
||||||
|
|
||||||
stmt := SELECT_JSON_ARR(
|
stmt := SELECT_JSON_ARR(
|
||||||
Actor.AllColumns,
|
Actor.AllColumns,
|
||||||
|
|
||||||
|
|
@ -198,16 +257,16 @@ SELECT JSON_ARRAYAGG(JSON_OBJECT(
|
||||||
'rating', film.rating,
|
'rating', film.rating,
|
||||||
'specialFeatures', film.special_features,
|
'specialFeatures', film.special_features,
|
||||||
'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
|
'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
|
||||||
)) AS "json"
|
)
|
||||||
|
ORDER BY film.length DESC) AS "json"
|
||||||
FROM dvds.film_actor
|
FROM dvds.film_actor
|
||||||
INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id))
|
INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id))
|
||||||
WHERE (film.film_id % 17) = 0
|
WHERE (film.film_id % 17) = 0
|
||||||
ORDER BY film.length DESC
|
|
||||||
)
|
)
|
||||||
)) AS "json"
|
)
|
||||||
|
ORDER BY actor.actor_id) AS "json"
|
||||||
FROM dvds.actor
|
FROM dvds.actor
|
||||||
WHERE actor.actor_id BETWEEN 1 AND 3
|
WHERE actor.actor_id BETWEEN 1 AND 3;
|
||||||
ORDER BY actor.actor_id;
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var dest []struct {
|
var dest []struct {
|
||||||
|
|
@ -217,8 +276,8 @@ ORDER BY actor.actor_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
err := stmt.QueryContext(ctx, db, &dest)
|
err := stmt.QueryContext(ctx, db, &dest)
|
||||||
fmt.Println(err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, err)
|
|
||||||
testutils.AssertJSON(t, dest, `
|
testutils.AssertJSON(t, dest, `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -234,21 +293,6 @@ ORDER BY actor.actor_id;
|
||||||
"LastName": "WAHLBERG",
|
"LastName": "WAHLBERG",
|
||||||
"LastUpdate": "2006-02-15T04:34:33Z",
|
"LastUpdate": "2006-02-15T04:34:33Z",
|
||||||
"Films": [
|
"Films": [
|
||||||
{
|
|
||||||
"FilmID": 357,
|
|
||||||
"Title": "GILBERT PELICAN",
|
|
||||||
"Description": "A Fateful Tale of a Man And a Feminist who must Conquer a Crocodile in A Manhattan Penthouse",
|
|
||||||
"ReleaseYear": 2006,
|
|
||||||
"LanguageID": 1,
|
|
||||||
"OriginalLanguageID": null,
|
|
||||||
"RentalDuration": 7,
|
|
||||||
"RentalRate": 0.99,
|
|
||||||
"Length": 114,
|
|
||||||
"ReplacementCost": 13.99,
|
|
||||||
"Rating": "G",
|
|
||||||
"SpecialFeatures": "Trailers,Commentaries",
|
|
||||||
"LastUpdate": "2006-02-15T05:03:42Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"FilmID": 561,
|
"FilmID": 561,
|
||||||
"Title": "MASK PEACH",
|
"Title": "MASK PEACH",
|
||||||
|
|
@ -263,6 +307,21 @@ ORDER BY actor.actor_id;
|
||||||
"Rating": "NC-17",
|
"Rating": "NC-17",
|
||||||
"SpecialFeatures": "Commentaries,Deleted Scenes",
|
"SpecialFeatures": "Commentaries,Deleted Scenes",
|
||||||
"LastUpdate": "2006-02-15T05:03:42Z"
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FilmID": 357,
|
||||||
|
"Title": "GILBERT PELICAN",
|
||||||
|
"Description": "A Fateful Tale of a Man And a Feminist who must Conquer a Crocodile in A Manhattan Penthouse",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"OriginalLanguageID": null,
|
||||||
|
"RentalDuration": 7,
|
||||||
|
"RentalRate": 0.99,
|
||||||
|
"Length": 114,
|
||||||
|
"ReplacementCost": 13.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"SpecialFeatures": "Trailers,Commentaries",
|
||||||
|
"LastUpdate": "2006-02-15T05:03:42Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"github.com/go-jet/jet/v2/internal/utils/ptr"
|
"github.com/go-jet/jet/v2/internal/utils/ptr"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/go-jet/jet/v2/qrm"
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -1728,6 +1728,135 @@ SELECT ROW($1::integer, $2::real, $3::text) AS "row",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubQueryAllExpTypes(t *testing.T) {
|
||||||
|
skipForCockroachDB(t)
|
||||||
|
|
||||||
|
subquery := SELECT(
|
||||||
|
Bool(true).AS("bool"),
|
||||||
|
Int32(11).AS("int"),
|
||||||
|
Text("doe").AS("text"),
|
||||||
|
Date(2000, 2, 2).AS("date"),
|
||||||
|
Time(11, 20, 40).AS("time"),
|
||||||
|
Timez(11, 20, 40, 200, "UTC").AS("timez"),
|
||||||
|
Timestamp(2030, 3, 4, 11, 20, 40).AS("timestamp"),
|
||||||
|
Timestampz(2023, 1, 2, 11, 20, 40, 200, "UTC").AS("timestampz"),
|
||||||
|
INTERVAL(100, HOUR).AS("interval"),
|
||||||
|
Bytea("bytes").AS("bytea"),
|
||||||
|
|
||||||
|
ARRAY(Bool(true)).AS("bool_arr"),
|
||||||
|
ARRAY(Int32(11)).AS("int_arr"),
|
||||||
|
ARRAY(Text("doe")).AS("text_arr"),
|
||||||
|
ARRAY(Date(2000, 2, 2)).AS("date_arr"),
|
||||||
|
ARRAY(Time(11, 20, 40)).AS("time_arr"),
|
||||||
|
ARRAY(Timez(11, 20, 40, 200, "UTC")).AS("timez_arr"),
|
||||||
|
ARRAY(Timestamp(2030, 3, 4, 11, 20, 40)).AS("timestamp_arr"),
|
||||||
|
ARRAY(Timestampz(2023, 1, 2, 11, 20, 40, 200, "UTC")).AS("timestampz_arr"),
|
||||||
|
ARRAY(INTERVAL(100, HOUR)).AS("interval_arr"),
|
||||||
|
ARRAY(Bytea("bytes")).AS("bytea_arr"),
|
||||||
|
|
||||||
|
INT4_RANGE(Int(1), Int(200)).AS("int4_range"),
|
||||||
|
DATE_RANGE(Date(2000, 2, 2), Date(2010, 3, 3)).AS("date_range"),
|
||||||
|
NUM_RANGE(Float(0.22), Float(22.1)).AS("num_range"),
|
||||||
|
TS_RANGE(LOCALTIMESTAMP(), LOCALTIMESTAMP().ADD(INTERVAL(1, HOUR))).AS("ts_range"),
|
||||||
|
TSTZ_RANGE(NOW(), NOW().ADD(INTERVAL(3, MONTH))).AS("tstz_range"),
|
||||||
|
).AsTable("sub")
|
||||||
|
|
||||||
|
var result = "\n"
|
||||||
|
for _, projection := range subquery.AllColumns() {
|
||||||
|
result += fmt.Sprintf("Column type: %T\n", projection)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, result, `
|
||||||
|
Column type: *jet.boolColumnImpl
|
||||||
|
Column type: *jet.integerColumnImpl
|
||||||
|
Column type: *jet.stringColumnImpl
|
||||||
|
Column type: *jet.dateColumnImpl
|
||||||
|
Column type: *jet.timeColumnImpl
|
||||||
|
Column type: *jet.timezColumnImpl
|
||||||
|
Column type: *jet.timestampColumnImpl
|
||||||
|
Column type: *jet.timestampzColumnImpl
|
||||||
|
Column type: *jet.intervalColumnImpl
|
||||||
|
Column type: *jet.blobColumnImpl
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.BoolExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.IntegerExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.StringExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.DateExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimeExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimezExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimestampExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimestampzExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.IntervalExpression]
|
||||||
|
Column type: *jet.arrayColumnImpl[github.com/go-jet/jet/v2/internal/jet.BlobExpression]
|
||||||
|
Column type: *jet.rangeColumnImpl[github.com/go-jet/jet/v2/internal/jet.IntegerExpression]
|
||||||
|
Column type: *jet.rangeColumnImpl[github.com/go-jet/jet/v2/internal/jet.DateExpression]
|
||||||
|
Column type: *jet.rangeColumnImpl[github.com/go-jet/jet/v2/internal/jet.NumericExpression]
|
||||||
|
Column type: *jet.rangeColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimestampExpression]
|
||||||
|
Column type: *jet.rangeColumnImpl[github.com/go-jet/jet/v2/internal/jet.TimestampzExpression]
|
||||||
|
`)
|
||||||
|
|
||||||
|
stmt := SELECT(
|
||||||
|
subquery.AllColumns(),
|
||||||
|
).FROM(subquery)
|
||||||
|
|
||||||
|
testutils.AssertStatementSql(t, stmt, `
|
||||||
|
SELECT sub.bool AS "bool",
|
||||||
|
sub.int AS "int",
|
||||||
|
sub.text AS "text",
|
||||||
|
sub.date AS "date",
|
||||||
|
sub.time AS "time",
|
||||||
|
sub.timez AS "timez",
|
||||||
|
sub.timestamp AS "timestamp",
|
||||||
|
sub.timestampz AS "timestampz",
|
||||||
|
sub.interval AS "interval",
|
||||||
|
sub.bytea AS "bytea",
|
||||||
|
sub.bool_arr AS "bool_arr",
|
||||||
|
sub.int_arr AS "int_arr",
|
||||||
|
sub.text_arr AS "text_arr",
|
||||||
|
sub.date_arr AS "date_arr",
|
||||||
|
sub.time_arr AS "time_arr",
|
||||||
|
sub.timez_arr AS "timez_arr",
|
||||||
|
sub.timestamp_arr AS "timestamp_arr",
|
||||||
|
sub.timestampz_arr AS "timestampz_arr",
|
||||||
|
sub.interval_arr AS "interval_arr",
|
||||||
|
sub.bytea_arr AS "bytea_arr",
|
||||||
|
sub.int4_range AS "int4_range",
|
||||||
|
sub.date_range AS "date_range",
|
||||||
|
sub.num_range AS "num_range",
|
||||||
|
sub.ts_range AS "ts_range",
|
||||||
|
sub.tstz_range AS "tstz_range"
|
||||||
|
FROM (
|
||||||
|
SELECT $1::boolean AS "bool",
|
||||||
|
$2::integer AS "int",
|
||||||
|
$3::text AS "text",
|
||||||
|
$4::date AS "date",
|
||||||
|
$5::time without time zone AS "time",
|
||||||
|
$6::time with time zone AS "timez",
|
||||||
|
$7::timestamp without time zone AS "timestamp",
|
||||||
|
$8::timestamp with time zone AS "timestampz",
|
||||||
|
INTERVAL '100 HOUR' AS "interval",
|
||||||
|
$9::bytea AS "bytea",
|
||||||
|
ARRAY[$10::boolean] AS "bool_arr",
|
||||||
|
ARRAY[$11::integer] AS "int_arr",
|
||||||
|
ARRAY[$12::text] AS "text_arr",
|
||||||
|
ARRAY[$13::date] AS "date_arr",
|
||||||
|
ARRAY[$14::time without time zone] AS "time_arr",
|
||||||
|
ARRAY[$15::time with time zone] AS "timez_arr",
|
||||||
|
ARRAY[$16::timestamp without time zone] AS "timestamp_arr",
|
||||||
|
ARRAY[$17::timestamp with time zone] AS "timestampz_arr",
|
||||||
|
ARRAY[INTERVAL '100 HOUR'] AS "interval_arr",
|
||||||
|
ARRAY[$18::bytea] AS "bytea_arr",
|
||||||
|
int4range($19, $20) AS "int4_range",
|
||||||
|
daterange($21::date, $22::date) AS "date_range",
|
||||||
|
numrange($23, $24) AS "num_range",
|
||||||
|
tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + INTERVAL '1 HOUR') AS "ts_range",
|
||||||
|
tstzrange(NOW(), NOW() + INTERVAL '3 MONTH') AS "tstz_range"
|
||||||
|
) AS sub;
|
||||||
|
`)
|
||||||
|
|
||||||
|
_, err := stmt.Exec(db)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAllTypesSubQueryFrom(t *testing.T) {
|
func TestAllTypesSubQueryFrom(t *testing.T) {
|
||||||
subQuery := SELECT(
|
subQuery := SELECT(
|
||||||
AllTypes.Boolean,
|
AllTypes.Boolean,
|
||||||
|
|
|
||||||
|
|
@ -841,6 +841,42 @@ func TestRowsScan(t *testing.T) {
|
||||||
requireQueryLogged(t, stmt, 0)
|
requireQueryLogged(t, stmt, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRowsNonStrictScan(t *testing.T) {
|
||||||
|
stmt := SELECT(
|
||||||
|
Inventory.AllColumns,
|
||||||
|
Store.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Inventory.INNER_JOIN(Store, Store.StoreID.EQ(Inventory.StoreID)),
|
||||||
|
).ORDER_BY(
|
||||||
|
Inventory.InventoryID.ASC(),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.PanicsWithValue(t, "jet: columns never used: 'store.store_id', 'store.manager_staff_id', 'store.address_id', 'store.last_update'", func() {
|
||||||
|
rows, err := stmt.Rows(context.Background(), db)
|
||||||
|
|
||||||
|
var dest model.Inventory
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
err = rows.Scan(&dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
allowUnusedColumns(func() {
|
||||||
|
rows, err := stmt.Rows(context.Background(), db)
|
||||||
|
|
||||||
|
var dest model.Inventory
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
err = rows.Scan(&dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, rows.Close())
|
||||||
|
require.NoError(t, rows.Err())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestScanNullColumn(t *testing.T) {
|
func TestScanNullColumn(t *testing.T) {
|
||||||
stmt := SELECT(
|
stmt := SELECT(
|
||||||
Address.AllColumns,
|
Address.AllColumns,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue