Merge remote-tracking branch 'upstream/master' into feat/strict-field-mapping

This commit is contained in:
k4n4ry 2026-01-30 22:44:12 +09:00
commit 323c3a0597
11 changed files with 382 additions and 59 deletions

View file

@ -633,7 +633,7 @@ Typically, two releases are published each year — one in early spring and anot
## License
Copyright 2019-2025 Goran Bjelanovic
Copyright 2019-2026 Goran Bjelanovic
Licensed under the Apache License, Version 2.0.
## Support the Project

View file

@ -1,3 +1,3 @@
package main
const version = "v2.14.0"
const version = "v2.14.1"

View file

@ -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()
// 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,
// so we don't have to support every column type.
// We need to know type to encode value for json unmarshal.
func newDummyColumnForExpression(exp Expression, name string) ColumnExpression {
switch exp.(type) {
@ -54,6 +53,41 @@ func newDummyColumnForExpression(exp Expression, name string) ColumnExpression {
return IntervalColumn(name)
case StringExpression:
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)

View file

@ -80,9 +80,10 @@ type arrayExpressionWrapper[E Expression] struct {
}
func newArrayExpressionWrap[E Expression](expression Expression) Array[E] {
arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression}
arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper
return &arrayExpressionWrapper
arrayExpressionWrapper := &arrayExpressionWrapper[E]{Expression: expression}
arrayExpressionWrapper.arrayInterfaceImpl.parent = arrayExpressionWrapper
expression.setRoot(arrayExpressionWrapper)
return arrayExpressionWrapper
}
// ArrayExp is array expression wrapper around arbitrary expression.

View file

@ -190,6 +190,10 @@ type ClauseOrderBy struct {
SkipNewLine bool
}
func (o *ClauseOrderBy) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
o.Serialize(statementType, out, options...)
}
// Serialize serializes clause into SQLBuilder
func (o *ClauseOrderBy) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
if o.List == nil {
@ -219,6 +223,10 @@ type ClauseLimit struct {
Count int64
}
func (o *ClauseLimit) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
o.Serialize(statementType, out, options...)
}
// Serialize serializes clause into SQLBuilder
func (l *ClauseLimit) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
if l.Count >= 0 {
@ -233,6 +241,10 @@ type ClauseOffset struct {
Count IntegerExpression
}
func (o *ClauseOffset) serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
o.Serialize(statementType, out, options...)
}
// Serialize serializes clause into SQLBuilder
func (o *ClauseOffset) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
if is.Nil(o.Count) {

View file

@ -136,6 +136,20 @@ func SaveJSONFile(v interface{}, testRelativePath string) {
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
func AssertJSONFile(t require.TestingT, data interface{}, testRelativePath string) {
if _, ok := t.(*testing.B); ok {

View file

@ -30,26 +30,46 @@ func SELECT_JSON_OBJ(projections ...Projection) SelectJsonStatement {
type selectJsonStatement struct {
*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 {
newSelect := &selectJsonStatement{
newSelectJson := &selectJsonStatement{
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 {
jsonObj := Func("JSON_OBJECT", CustomExpression(jet.JsonObjProjectionList(projections)))
func (s *selectJsonStatement) constructProjectionList() {
jsonProjection := Func("JSON_OBJECT", CustomExpression(jet.JsonObjProjectionList(s.projections)))
if statementType == jet.SelectJsonArrStatementType {
return Func("JSON_ARRAYAGG", jsonObj)
if s.statementType == jet.SelectJsonArrStatementType {
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 {
@ -63,17 +83,32 @@ func (s *selectJsonStatement) WHERE(condition BoolExpression) SelectJsonStatemen
return s
}
func (s *selectJsonStatement) ORDER_BY(orderByClauses ...OrderByClause) SelectJsonStatement {
s.OrderBy.List = orderByClauses
func (s *selectJsonStatement) ORDER_BY(orderBy ...OrderByClause) SelectJsonStatement {
if s.statementType == jet.SelectJsonArrStatementType {
s.arrOrderBy.List = orderBy
} else {
s.OrderBy.List = orderBy
}
return s
}
func (s *selectJsonStatement) LIMIT(limit int64) SelectJsonStatement {
if s.statementType == jet.SelectJsonArrStatementType {
s.arrLimit.Count = limit
} else {
s.Limit.Count = limit
}
return s
}
func (s *selectJsonStatement) OFFSET(offset int64) SelectJsonStatement {
if s.statementType == jet.SelectJsonArrStatementType {
s.arrOffset.Count = Int(offset)
} else {
s.Offset.Count = Int(offset)
}
return s
}

View file

@ -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)
}
if scanContext.rowNum == 1 && GlobalConfig.StrictScan {
scanContext.EnsureEveryColumnRead() // can panic
}
if GlobalConfig.StrictFieldMapping {
scanContext.EnsureEveryFieldMapped() // can panic
}

View file

@ -2,8 +2,8 @@ package mysql
import (
"context"
"fmt"
"github.com/go-jet/jet/v2/qrm"
"slices"
"strings"
"testing"
@ -127,9 +127,19 @@ WHERE actor.actor_id = ?;
}
func TestSelectJsonArr(t *testing.T) {
onlyMariaDB(t)
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.ActorID)
ORDER_BY(
Actor.LastName.DESC(),
Actor.FirstName.ASC(),
Actor.ActorID.ASC(),
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT JSON_ARRAYAGG(JSON_OBJECT(
@ -137,9 +147,9 @@ SELECT JSON_ARRAYAGG(JSON_OBJECT(
'firstName', actor.first_name,
'lastName', actor.last_name,
'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ')
)) AS "json"
FROM dvds.actor
ORDER BY actor.actor_id;
)
ORDER BY actor.last_name DESC, actor.first_name ASC, actor.actor_id ASC) AS "json"
FROM dvds.actor;
`)
var dest []model.Actor
@ -147,12 +157,61 @@ ORDER BY actor.actor_id;
err := stmt.Query(db, &dest)
require.Nil(t, err)
testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/all_actors.json")
// 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])
})
}
func TestSelectJsonArr_NestedArr(t *testing.T) {
onlyMariaDB(t)
stmt := SELECT_JSON_ARR(
Actor.AllColumns,
@ -198,16 +257,16 @@ SELECT JSON_ARRAYAGG(JSON_OBJECT(
'rating', film.rating,
'specialFeatures', film.special_features,
'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
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
ORDER BY film.length DESC
)
)) AS "json"
)
ORDER BY actor.actor_id) AS "json"
FROM dvds.actor
WHERE actor.actor_id BETWEEN 1 AND 3
ORDER BY actor.actor_id;
WHERE actor.actor_id BETWEEN 1 AND 3;
`)
var dest []struct {
@ -217,8 +276,8 @@ ORDER BY actor.actor_id;
}
err := stmt.QueryContext(ctx, db, &dest)
fmt.Println(err)
require.Nil(t, err)
require.NoError(t, err)
testutils.AssertJSON(t, dest, `
[
{
@ -234,21 +293,6 @@ ORDER BY actor.actor_id;
"LastName": "WAHLBERG",
"LastUpdate": "2006-02-15T04:34:33Z",
"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,
"Title": "MASK PEACH",
@ -263,6 +307,21 @@ ORDER BY actor.actor_id;
"Rating": "NC-17",
"SpecialFeatures": "Commentaries,Deleted Scenes",
"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"
}
]
},

View file

@ -2,12 +2,12 @@ package postgres
import (
"encoding/base64"
"fmt"
"github.com/go-jet/jet/v2/internal/utils/ptr"
"github.com/stretchr/testify/assert"
"math"
"github.com/go-jet/jet/v2/qrm"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"math"
"testing"
"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) {
subQuery := SELECT(
AllTypes.Boolean,

View file

@ -841,6 +841,42 @@ func TestRowsScan(t *testing.T) {
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) {
stmt := SELECT(
Address.AllColumns,