Add support for strict scan.

If there are unused columns in query result set Query method panics.
This commit is contained in:
go-jet 2025-03-11 10:50:06 +01:00
parent cfc264221b
commit d86f14e665
9 changed files with 402 additions and 207 deletions

View file

@ -10,6 +10,21 @@ import (
"reflect"
)
// Config holds the configuration settings for QRM scanning behavior.
type Config struct {
// StrictScan, when true, causes the scanning function to panic if it encounters any
// unused columns in the SQL query result. This ensures that every column is mapped
// to a field in the destination struct.
// Does not apply to statements build with SELECT_JSON_OBJ or SELECT_JSON_ARR
StrictScan bool
}
// GlobalConfig is the package-wide configuration for SQL scanning.
// This variable should be modified only once, for instance, during application initialization.
var GlobalConfig = Config{
StrictScan: false,
}
// ErrNoRows is returned by Query when query result set is empty
var ErrNoRows = errors.New("qrm: no rows in result set")
@ -199,12 +214,16 @@ func ScanOneRowToDest(scanContext *ScanContext, rows *sql.Rows, destPtr interfac
destValuePtr := reflect.ValueOf(destPtr)
scanContext.rowNum++
_, err = mapRowToStruct(scanContext, "", destValuePtr, nil)
if err != nil {
return fmt.Errorf("jet: failed to scan a row into destination, %w", err)
}
scanContext.EnsureEveryColumnRead() // can panic
return nil
}
@ -246,6 +265,10 @@ func queryToSlice(ctx context.Context, db Queryable, query string, args []interf
if err != nil {
return scanContext.rowNum, err
}
if scanContext.rowNum == 1 && GlobalConfig.StrictScan {
scanContext.EnsureEveryColumnRead()
}
}
err = rows.Close()

View file

@ -18,6 +18,8 @@ type ScanContext struct {
typeInfoMap map[string]typeInfo
typesVisited typeStack // to prevent circular dependency scan
columnAlias []string
columnIndexRead []bool
}
// NewScanContext creates new ScanContext from rows
@ -57,9 +59,26 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) {
typeInfoMap: make(map[string]typeInfo),
typesVisited: newTypeStack(),
columnAlias: aliases,
columnIndexRead: make([]bool, len(aliases)),
}, nil
}
func (s *ScanContext) EnsureEveryColumnRead() {
var neverUsedColumns []string
for index, read := range s.columnIndexRead {
if !read {
neverUsedColumns = append(neverUsedColumns, `'`+s.columnAlias[index]+`'`)
}
}
if len(neverUsedColumns) > 0 {
panic("jet: columns never used: " + strings.Join(neverUsedColumns, ", "))
}
}
func createScanSlice(columnCount int) []interface{} {
scanPtrSlice := make([]interface{}, columnCount)
@ -244,6 +263,9 @@ func (s *ScanContext) typeToColumnIndex(typeName, fieldName string) int {
// rowElemValue always returns non-ptr value,
// invalid value is nil
func (s *ScanContext) rowElemValue(index int) reflect.Value {
if s.rowNum == 1 {
s.columnIndexRead[index] = true
}
scannedValue := reflect.ValueOf(s.row[index])
return scannedValue.Elem().Elem() // no need to check validity of Elem, because s.row[index] always contains interface in interface
}

View file

@ -591,10 +591,12 @@ func TestExpressionCast(t *testing.T) {
Raw("current_database()"),
)
allowUnusedColumns(func() {
var dest []struct{}
err := query.Query(db, &dest)
require.NoError(t, err)
})
}
func TestStringOperators(t *testing.T) {
@ -673,10 +675,11 @@ func TestStringOperators(t *testing.T) {
TO_HEX(AllTypes.IntegerPtr),
)
allowUnusedColumns(func() {
var dest []struct{}
err := query.Query(db, &dest)
require.NoError(t, err)
})
}
func TestBytea(t *testing.T) {
@ -792,10 +795,12 @@ FROM test_sample.all_types;
`)
}
allowUnusedColumns(func() {
var dest []struct{}
err := stmt.Query(db, &dest)
require.NoError(t, err)
})
}
func TestBlobConversion(t *testing.T) {
@ -1185,9 +1190,11 @@ LIMIT $27;
common.AllTypesIntegerExpResult `alias:"."`
}
allowUnusedColumns(func() {
err := query.Query(db, &dest)
require.NoError(t, err)
})
//testutils.SaveJSONFile(dest, "./testdata/results/common/int_operators.json")
//testutils.PrintJson(dest)
@ -1271,9 +1278,12 @@ func TestTimeExpression(t *testing.T) {
// fmt.Println(query.DebugSql())
var dest []struct{}
allowUnusedColumns(func() {
err := query.Query(db, &dest)
require.NoError(t, err)
})
}
func TestTimeScan(t *testing.T) {
@ -1616,8 +1626,11 @@ SELECT INTERVAL '1 YEAR',
FROM test_sample.all_types;
`)
allowUnusedColumns(func() {
err := stmt.Query(db, &struct{}{})
require.NoError(t, err)
})
requireLogged(t, stmt)
}
@ -1677,8 +1690,10 @@ SELECT EXTRACT(CENTURY FROM all_types.timestampz),
FROM test_sample.all_types;
`)
allowUnusedColumns(func() {
err := stmt.Query(db, &struct{}{})
require.NoError(t, err)
})
}
func TestRowExpression(t *testing.T) {
@ -1717,8 +1732,10 @@ SELECT ROW($1::integer, $2::real, $3::text) AS "row",
ROW($26::timestamp with time zone) <= ROW($27::timestamp with time zone);
`)
allowUnusedColumns(func() {
err := stmt.Query(db, &struct{}{})
require.NoError(t, err)
})
}
func TestAllTypesSubQueryFrom(t *testing.T) {
@ -2048,7 +2065,11 @@ FROM`
testutils.AssertDebugStatementSql(t, stmt1, expectedSQL+expected.sql+";\n", expected.args...)
dest1 := []model.AllTypes{}
var dest1 []struct {
model.AllTypes
AliasedColumn []byte
}
err := stmt1.Query(db, &dest1)
require.NoError(t, err)
require.Equal(t, len(dest1), 2)
@ -2064,12 +2085,17 @@ FROM`
stmt2 := SELECT(
subQuery.AllColumns(),
).
FROM(subQuery)
).FROM(
subQuery,
)
testutils.AssertDebugStatementSql(t, stmt2, expectedSQL+expected.sql+";\n", expected.args...)
dest2 := []model.AllTypes{}
var dest2 []struct {
model.AllTypes
AliasedColumn []byte
}
err = stmt2.Query(db, &dest2)
require.NoError(t, err)

View file

@ -320,7 +320,6 @@ func testJoinEverything(t require.TestingT) {
Track.AllColumns,
Genre.AllColumns,
MediaType.AllColumns,
PlaylistTrack.AllColumns,
Playlist.AllColumns,
Invoice.AllColumns,
Customer.AllColumns,
@ -365,8 +364,6 @@ SELECT "Artist"."ArtistId" AS "Artist.ArtistId",
"Genre"."Name" AS "Genre.Name",
"MediaType"."MediaTypeId" AS "MediaType.MediaTypeId",
"MediaType"."Name" AS "MediaType.Name",
"PlaylistTrack"."PlaylistId" AS "PlaylistTrack.PlaylistId",
"PlaylistTrack"."TrackId" AS "PlaylistTrack.TrackId",
"Playlist"."PlaylistId" AS "Playlist.PlaylistId",
"Playlist"."Name" AS "Playlist.Name",
"Invoice"."InvoiceId" AS "Invoice.InvoiceId",
@ -1295,7 +1292,7 @@ func TestAggregateFunc(t *testing.T) {
WITHIN_GROUP_ORDER_BY(Invoice.BillingAddress.DESC()).AS("percentile_disc_3"),
PERCENTILE_CONT(Float(0.3)).WITHIN_GROUP_ORDER_BY(Invoice.Total).AS("percentile_cont_1"),
PERCENTILE_CONT(Float(0.2)).WITHIN_GROUP_ORDER_BY(INTERVAL(1, HOUR).DESC()).AS("percentile_cont_int"),
PERCENTILE_CONT(Float(0.2)).WITHIN_GROUP_ORDER_BY(INTERVAL(1, HOUR).DESC()).AS("percentile_cont_interval"),
MODE().WITHIN_GROUP_ORDER_BY(Invoice.BillingPostalCode.DESC()).AS("mode_1"),
).FROM(
@ -1309,7 +1306,7 @@ SELECT PERCENTILE_DISC ($1::double precision) WITHIN GROUP (ORDER BY "Invoice"."
PERCENTILE_DISC ("Invoice"."Total" / $2) WITHIN GROUP (ORDER BY "Invoice"."InvoiceDate" ASC) AS "percentile_disc_2",
PERCENTILE_DISC ((select array_agg(s) from generate_series(0, 1, 0.2) as s)) WITHIN GROUP (ORDER BY "Invoice"."BillingAddress" DESC) AS "percentile_disc_3",
PERCENTILE_CONT ($3::double precision) WITHIN GROUP (ORDER BY "Invoice"."Total") AS "percentile_cont_1",
PERCENTILE_CONT ($4::double precision) WITHIN GROUP (ORDER BY INTERVAL '1 HOUR' DESC) AS "percentile_cont_int",
PERCENTILE_CONT ($4::double precision) WITHIN GROUP (ORDER BY INTERVAL '1 HOUR' DESC) AS "percentile_cont_interval",
MODE () WITHIN GROUP (ORDER BY "Invoice"."BillingPostalCode" DESC) AS "mode_1"
FROM chinook."Invoice"
GROUP BY "Invoice"."Total";
@ -1320,6 +1317,7 @@ GROUP BY "Invoice"."Total";
PercentileDisc2 string
PercentileDisc3 string
PercentileCont1 string
PercentileContInterval string
Mode1 string
}
@ -1332,6 +1330,7 @@ GROUP BY "Invoice"."Total";
"PercentileDisc2": "2009-01-19T00:00:00Z",
"PercentileDisc3": "{\"Via Degli Scipioni, 43\",\"Qe 7 Bloco G\",\"Berger Stra<72>e 10\",\"696 Osborne Street\",\"2211 W Berry Street\",\"1033 N Park Ave\"}",
"PercentileCont1": "0.99",
"PercentileContInterval": "01:00:00",
"Mode1": "X1A 1N6"
}
`)

View file

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"github.com/go-jet/jet/v2/qrm"
"github.com/go-jet/jet/v2/stmtcache"
"github.com/go-jet/jet/v2/tests/internal/utils/repo"
"github.com/jackc/pgx/v4/stdlib"
@ -49,6 +50,8 @@ func skipForCockroachDB(t *testing.T) {
func TestMain(m *testing.M) {
defer profile.Start().Stop()
qrm.GlobalConfig.StrictScan = true
for _, driverName := range []string{"postgres", "pgx"} {
fmt.Printf("\nRunning postgres tests for driver: %s, caching enabled: %t \n", driverName, withStatementCaching)
@ -95,6 +98,16 @@ func getConnectionString() string {
return dbconfig.PostgresConnectString
}
func allowUnusedColumns(f func()) {
defer func() {
qrm.GlobalConfig.StrictScan = true
}()
qrm.GlobalConfig.StrictScan = false
f()
}
var loggedSQL string
var loggedSQLArgs []interface{}
var loggedDebugSQL string

View file

@ -137,8 +137,10 @@ WHERE sample_ranges.date_range @> $36::date;
}
var dest sample
allowUnusedColumns(func() {
err := query.Query(db, &dest)
require.NoError(t, err)
})
expectedRow := sample{
SampleRanges: sampleRangeRow,
@ -219,7 +221,7 @@ func TestRangeSelectColumnsFromSubQuery(t *testing.T) {
int4Range := Int4RangeColumn("range4").From(subQuery)
stmt := SELECT(
subQuery.AllColumns(),
subQuery.AllColumns().Except(int4Range),
int4Range,
).FROM(
subQuery,
@ -232,7 +234,6 @@ SELECT sub_query."sample_ranges.date_range" AS "sample_ranges.date_range",
sub_query."sample_ranges.int4_range" AS "sample_ranges.int4_range",
sub_query."sample_ranges.int8_range" AS "sample_ranges.int8_range",
sub_query."sample_ranges.num_range" AS "sample_ranges.num_range",
sub_query.range4 AS "range4",
sub_query.range4 AS "range4"
FROM (
SELECT sample_ranges.date_range AS "sample_ranges.date_range",

View file

@ -54,8 +54,18 @@ func TestScanToInvalidDestination(t *testing.T) {
}
func TestScanToValidDestination(t *testing.T) {
t.Run("pointer to empty struct - non strict scan", func(t *testing.T) {
allowUnusedColumns(func() {
var dest []struct{}
err := oneInventoryQuery.Query(db, &dest)
require.NoError(t, err)
})
})
t.Run("pointer to struct", func(t *testing.T) {
dest := []struct{}{}
var dest model.Inventory
err := oneInventoryQuery.Query(db, &dest)
require.NoError(t, err)
@ -63,20 +73,24 @@ func TestScanToValidDestination(t *testing.T) {
t.Run("global query function scan", func(t *testing.T) {
queryStr, args := oneInventoryQuery.Sql()
dest := []struct{}{}
var dest model.Inventory
rowProcessed, err := qrm.Query(nil, db, queryStr, args, &dest)
require.Equal(t, rowProcessed, int64(1))
require.NoError(t, err)
})
t.Run("pointer to slice", func(t *testing.T) {
err := oneInventoryQuery.Query(db, &[]struct{}{})
var dest []model.Inventory
err := oneInventoryQuery.Query(db, &dest)
require.NoError(t, err)
})
t.Run("pointer to slice of pointer to structs", func(t *testing.T) {
err := oneInventoryQuery.Query(db, &[]*struct{}{})
var dest []*model.Inventory
err := oneInventoryQuery.Query(db, &dest)
require.NoError(t, err)
})
@ -84,7 +98,7 @@ func TestScanToValidDestination(t *testing.T) {
t.Run("pointer to slice of integers", func(t *testing.T) {
var dest []int32
err := oneInventoryQuery.Query(db, &dest)
err := Inventory.SELECT(Inventory.InventoryID).Query(db, &dest)
require.NoError(t, err)
require.Equal(t, dest[0], int32(1))
})
@ -92,7 +106,7 @@ func TestScanToValidDestination(t *testing.T) {
t.Run("pointer to slice integer pointers", func(t *testing.T) {
var dest []*int32
err := oneInventoryQuery.Query(db, &dest)
err := Inventory.SELECT(Inventory.InventoryID).Query(db, &dest)
require.NoError(t, err)
require.Equal(t, dest[0], ptr.Of(int32(1)))
})
@ -114,8 +128,6 @@ func TestScanToStruct(t *testing.T) {
SELECT(Inventory.AllColumns).
ORDER_BY(Inventory.InventoryID)
//fmt.Println(query.DebugSql())
t.Run("one struct", func(t *testing.T) {
dest := model.Inventory{}
err := query.LIMIT(1).Query(db, &dest)
@ -173,6 +185,7 @@ func TestScanToStruct(t *testing.T) {
InventoryID *int32 `sql:"primary_key"`
FilmID int16
StoreID *int16
LastUpdate time.Time
}
dest := Inventory{}
@ -184,12 +197,15 @@ func TestScanToStruct(t *testing.T) {
require.Equal(t, *dest.InventoryID, int32(1))
require.Equal(t, dest.FilmID, int16(1))
require.Equal(t, *dest.StoreID, int16(1))
require.NotEmpty(t, dest.LastUpdate)
})
t.Run("type convert int32 to int", func(t *testing.T) {
type Inventory struct {
InventoryID int
FilmID string
InventoryID *int32 `sql:"primary_key"`
FilmID int16
StoreID *int16
LastUpdate time.Time
}
dest := Inventory{}
@ -267,6 +283,8 @@ func TestScanToNestedStruct(t *testing.T) {
dest := struct {
model.Inventory
model.Actor //unused
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -280,6 +298,8 @@ func TestScanToNestedStruct(t *testing.T) {
dest := struct {
model.Inventory
*model.Actor //unused
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -293,6 +313,8 @@ func TestScanToNestedStruct(t *testing.T) {
dest := struct {
model.Inventory
Actor *model.Actor //unused
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -312,6 +334,8 @@ func TestScanToNestedStruct(t *testing.T) {
dest := struct {
model.Inventory
Actor *model.Actor //unused
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -327,6 +351,8 @@ func TestScanToNestedStruct(t *testing.T) {
Actor *struct {
model.Actor
} //unused
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -343,6 +369,8 @@ func TestScanToNestedStruct(t *testing.T) {
model.Actor //unused
model.Language //unesed
}
model.Film
model.Store
}{}
err := query.Query(db, &dest)
@ -362,6 +390,7 @@ func TestScanToNestedStruct(t *testing.T) {
model.Actor //unselected
model.Film //selected
}
model.Store
}{}
err := query.Query(db, &dest)
@ -382,6 +411,7 @@ func TestScanToNestedStruct(t *testing.T) {
*model.Film //selected
}
}
model.Store
}{}
err := query.Query(db, &dest)
@ -443,8 +473,13 @@ func TestScanToSlice(t *testing.T) {
ORDER_BY(Inventory.InventoryID).
LIMIT(10)
justIDs := Inventory.
SELECT(Inventory.InventoryID).
ORDER_BY(Inventory.InventoryID).
LIMIT(10)
t.Run("slice od inventory", func(t *testing.T) {
dest := []model.Inventory{}
var dest []model.Inventory
err := query.Query(db, &dest)
@ -454,44 +489,55 @@ func TestScanToSlice(t *testing.T) {
testutils.AssertDeepEqual(t, dest[1], inventory2)
})
t.Run("slice of ints", func(t *testing.T) {
t.Run("slice of int32 non strict scan", func(t *testing.T) {
allowUnusedColumns(func() {
var dest []int32
err := query.Query(db, &dest)
require.NoError(t, err)
testutils.AssertDeepEqual(t, dest, []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
})
})
t.Run("slice of int32 strict scan", func(t *testing.T) {
var dest []int32
err := justIDs.Query(db, &dest)
require.NoError(t, err)
testutils.AssertDeepEqual(t, dest, []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
})
t.Run("slice type convertible", func(t *testing.T) {
var dest []int
err := query.Query(db, &dest)
err := justIDs.Query(db, &dest)
require.NoError(t, err)
})
t.Run("slice type mismatch", func(t *testing.T) {
var dest []bool
err := query.Query(db, &dest)
err := justIDs.Query(db, &dest)
require.Error(t, err)
require.EqualError(t, err, `jet: can't append int64 to []bool slice: can't assign int64(2) to bool`)
})
})
t.Run("slice of complex structs", func(t *testing.T) {
query := Inventory.
INNER_JOIN(Film, Inventory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Store, Inventory.StoreID.EQ(Store.StoreID)).
SELECT(
query := SELECT(
Inventory.AllColumns,
Film.AllColumns,
Store.AllColumns,
).
ORDER_BY(Inventory.InventoryID).
LIMIT(10)
).FROM(
Inventory.
INNER_JOIN(Film, Inventory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Store, Inventory.StoreID.EQ(Store.StoreID)),
).ORDER_BY(
Inventory.InventoryID,
).LIMIT(10)
t.Run("struct with slice of ints", func(t *testing.T) {
t.Run("struct with slice of ints - non strict scan", func(t *testing.T) {
allowUnusedColumns(func() {
var dest struct {
model.Film
IDs []int32 `alias:"inventory.inventory_id"`
@ -503,8 +549,10 @@ func TestScanToSlice(t *testing.T) {
testutils.AssertDeepEqual(t, dest.Film, film1)
testutils.AssertDeepEqual(t, dest.IDs, []int32{1, 2, 3, 4, 5, 6, 7, 8})
})
})
t.Run("slice of structs with slice of ints", func(t *testing.T) {
allowUnusedColumns(func() {
var dest []struct {
model.Film
IDs []int32 `alias:"inventory.inventory_id"`
@ -519,8 +567,10 @@ func TestScanToSlice(t *testing.T) {
testutils.AssertDeepEqual(t, dest[1].Film, film2)
testutils.AssertDeepEqual(t, dest[1].IDs, []int32{9, 10})
})
})
t.Run("slice of structs with slice of pointer to ints", func(t *testing.T) {
allowUnusedColumns(func() {
var dest []struct {
model.Film
IDs []*int32 `alias:"inventory.inventory_id"`
@ -536,13 +586,14 @@ func TestScanToSlice(t *testing.T) {
testutils.AssertDeepEqual(t, dest[1].Film, film2)
testutils.AssertDeepEqual(t, dest[1].IDs, []*int32{ptr.Of(int32(9)), ptr.Of(int32(10))})
})
})
t.Run("complex struct 1", func(t *testing.T) {
dest := []struct {
var dest []struct {
model.Inventory
model.Film
model.Store
}{}
}
err := query.Query(db, &dest)
@ -619,6 +670,7 @@ func TestScanToSlice(t *testing.T) {
Inventories []struct {
model.Inventory
model.Store
Rentals *[]model.Rental
Rentals2 []model.Rental
@ -639,11 +691,11 @@ func TestScanToSlice(t *testing.T) {
})
t.Run("slice of complex structs 2", func(t *testing.T) {
query := Country.
query := SELECT(Country.AllColumns, City.AllColumns, Address.AllColumns, Customer.AllColumns).
FROM(Country.
INNER_JOIN(City, City.CountryID.EQ(Country.CountryID)).
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
SELECT(Country.AllColumns, City.AllColumns, Address.AllColumns, Customer.AllColumns).
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID))).
ORDER_BY(Country.CountryID.ASC(), City.CityID.ASC(), Address.AddressID.ASC(), Customer.CustomerID.ASC()).
LIMIT(1000)
@ -654,7 +706,7 @@ func TestScanToSlice(t *testing.T) {
Cities []struct {
model.City
Adresses []struct {
Addresses []struct {
model.Address
Customer model.Customer
@ -669,14 +721,14 @@ func TestScanToSlice(t *testing.T) {
testutils.AssertDeepEqual(t, dest[100].Country, countryUk)
require.Equal(t, len(dest[100].Cities), 8)
testutils.AssertDeepEqual(t, dest[100].Cities[2].City, cityLondon)
require.Equal(t, len(dest[100].Cities[2].Adresses), 2)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Adresses[0].Address, address256)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Adresses[0].Customer, customer256)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Adresses[1].Address, addres517)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Adresses[1].Customer, customer512)
require.Equal(t, len(dest[100].Cities[2].Addresses), 2)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Addresses[0].Address, address256)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Addresses[0].Customer, customer256)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Addresses[1].Address, addres517)
testutils.AssertDeepEqual(t, dest[100].Cities[2].Addresses[1].Customer, customer512)
})
t.Run("dest1", func(t *testing.T) {
t.Run("dest2", func(t *testing.T) {
var dest []*struct {
*model.Country
@ -707,7 +759,7 @@ func TestScanToSlice(t *testing.T) {
})
t.Run("dest1", func(t *testing.T) {
t.Run("dest3", func(t *testing.T) {
var dest []*struct {
*model.Country
@ -1074,6 +1126,47 @@ VALUES (1234, 0, 'Joe', '', NULL, 1, TRUE, '2020-02-02 10:00:00Z', NULL, 1);
testutils.AssertExecAndRollback(t, stmt, db)
}
func TestStrictScan(t *testing.T) {
stmt := SELECT(
Actor.AllColumns,
).FROM(
Actor,
).LIMIT(10)
type Actor struct {
ActorID int32 `sql:"primary_key"`
FirstName string
//LastName string
//LastUpdate time.Time
}
var dest []Actor
require.PanicsWithValue(t, "jet: columns never used: 'actor.last_name', 'actor.last_update'", func() {
err := stmt.Query(db, &dest)
require.NoError(t, err)
})
var dest2 []model.Actor
err := stmt.Query(db, &dest2)
require.NoError(t, err)
t.Run("using_rows", func(t *testing.T) {
rows, err := stmt.Rows(ctx, db)
require.NoError(t, err)
require.True(t, rows.Next())
require.PanicsWithValue(t, "jet: columns never used: 'actor.last_name', 'actor.last_update'", func() {
var actor Actor
err = rows.Scan(&actor)
require.NoError(t, err)
})
})
}
var address256 = model.Address{
AddressID: 256,
Address: "1497 Yuzhou Drive",

View file

@ -145,7 +145,11 @@ LIMIT 30;
testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(30))
var dest []model.Payment
var dest []struct {
model.Payment
Customer model.Customer
}
err := query.Query(db, &dest)
@ -246,8 +250,10 @@ LIMIT 12;
testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(1), int64(1), int64(10), int64(1), int64(2), int64(1), int64(12))
var dest []struct{}
allowUnusedColumns(func() {
err := query.Query(db, &dest)
require.NoError(t, err)
})
}
func TestSelectFetchFirst(t *testing.T) {
@ -369,10 +375,7 @@ OFFSET (
func TestSelectJoinQueryStruct(t *testing.T) {
expectedSQL := `
SELECT film_actor.actor_id AS "film_actor.actor_id",
film_actor.film_id AS "film_actor.film_id",
film_actor.last_update AS "film_actor.last_update",
film.film_id AS "film.film_id",
SELECT film.film_id AS "film.film_id",
film.title AS "film.title",
film.description AS "film.description",
film.release_year AS "film.release_year",
@ -420,7 +423,6 @@ LIMIT 1000;
INNER_JOIN(Inventory, Inventory.FilmID.EQ(Film.FilmID)).
INNER_JOIN(Rental, Rental.InventoryID.EQ(Inventory.InventoryID)).
SELECT(
FilmActor.AllColumns,
Film.AllColumns,
Language.AllColumns,
Actor.AllColumns,
@ -1235,6 +1237,8 @@ func TestSelectOrderByAscDesc(t *testing.T) {
func TestSelectOrderBy(t *testing.T) {
var destRentals []model.Rental
t.Run("default", func(t *testing.T) {
stmt := SELECT(
Rental.AllColumns,
@ -1256,7 +1260,8 @@ FROM dvds.rental
ORDER BY rental.return_date
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("NULLS FIRST", func(t *testing.T) {
@ -1280,7 +1285,7 @@ FROM dvds.rental
ORDER BY rental.return_date NULLS FIRST
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("NULLS LAST", func(t *testing.T) {
@ -1304,7 +1309,7 @@ FROM dvds.rental
ORDER BY rental.return_date NULLS LAST
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("ASC", func(t *testing.T) {
@ -1328,7 +1333,7 @@ FROM dvds.rental
ORDER BY rental.return_date ASC
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("ASC NULLS FIRST", func(t *testing.T) {
@ -1353,7 +1358,7 @@ ORDER BY rental.return_date ASC NULLS FIRST
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("ASC NULLS LAST", func(t *testing.T) {
@ -1379,7 +1384,7 @@ LIMIT 200
OFFSET 15800;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("DESC", func(t *testing.T) {
@ -1405,7 +1410,7 @@ LIMIT 200
OFFSET 15800;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("DESC NULLS LAST", func(t *testing.T) {
@ -1431,7 +1436,7 @@ LIMIT 200
OFFSET 15800;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
t.Run("DESC NULLS FIRST", func(t *testing.T) {
@ -1456,7 +1461,7 @@ ORDER BY rental.return_date DESC NULLS FIRST
LIMIT 200;
`)
require.NoError(t, stmt.Query(db, &struct{}{}))
require.NoError(t, stmt.Query(db, &destRentals))
})
}
@ -1552,14 +1557,14 @@ LIMIT 1000;
testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(1000))
var customerAddresCrosJoined []struct {
var customerAddersCrossJoined []struct {
model.Customer
model.Address
}
err := query.Query(db, &customerAddresCrosJoined)
err := query.Query(db, &customerAddersCrossJoined)
require.Equal(t, len(customerAddresCrosJoined), 1000)
require.Equal(t, len(customerAddersCrossJoined), 1000)
require.NoError(t, err)
}
@ -1682,7 +1687,6 @@ func TestSelectSubQuery(t *testing.T) {
SELECT(
rRatingFilms.AllColumns(),
Actor.AllColumns,
FilmActor.AllColumns,
).FROM(
rRatingFilms.
INNER_JOIN(FilmActor, FilmActor.FilmID.EQ(rFilmID)).
@ -1701,10 +1705,7 @@ SELECT "rFilms"."film.film_id" AS "film.film_id",
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_actor.actor_id AS "film_actor.actor_id",
film_actor.film_id AS "film_actor.film_id",
film_actor.last_update AS "film_actor.last_update"
actor.last_update AS "actor.last_update"
FROM (
SELECT film.film_id AS "film.film_id",
film.title AS "film.title",
@ -1871,9 +1872,7 @@ SELECT customer.customer_id AS "customer.customer_id",
customer.active AS "customer.active",
SUM(payment.amount) AS "amount.sum",
AVG(payment.amount) AS "amount.avg",
MAX(payment.payment_date) AS "amount.max_date",
MAX(payment.amount) AS "amount.max",
MIN(payment.payment_date) AS "amount.min_date",
MIN(payment.amount) AS "amount.min",
COUNT(payment.amount) AS "amount.count"
FROM dvds.payment
@ -1887,9 +1886,7 @@ ORDER BY customer.customer_id, SUM(payment.amount) ASC;
SUMf(Payment.Amount).AS("amount.sum"),
AVG(Payment.Amount).AS("amount.avg"),
MAX(Payment.PaymentDate).AS("amount.max_date"),
MAXf(Payment.Amount).AS("amount.max"),
MIN(Payment.PaymentDate).AS("amount.min_date"),
MINf(Payment.Amount).AS("amount.min"),
COUNT(Payment.Amount).AS("amount.count"),
).FROM(
@ -2795,8 +2792,10 @@ ORDER BY actor.actor_id ASC, film.film_id ASC;
Actors []model.Actor
}
allowUnusedColumns(func() {
err = stmt.Query(db, &dest2)
require.NoError(t, err)
})
//testutils.SaveJSONFile(dest2, "./testdata/results/postgres/quick-start-dest2.json")
testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json")
@ -2819,17 +2818,18 @@ func TestSelectQuickStartWithSubQueries(t *testing.T) {
categoryID := Category.CategoryID.From(categoriesNotAction)
stmt := Actor.
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).
INNER_JOIN(filmLogerThan180, filmID.EQ(FilmActor.FilmID)).
INNER_JOIN(Language, Language.LanguageID.EQ(filmLanguageID)).
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(filmID)).
INNER_JOIN(categoriesNotAction, categoryID.EQ(FilmCategory.CategoryID)).
SELECT(
stmt := SELECT(
Actor.AllColumns,
filmLogerThan180.AllColumns(),
Language.AllColumns,
categoriesNotAction.AllColumns(),
).FROM(
Actor.
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).
INNER_JOIN(filmLogerThan180, filmID.EQ(FilmActor.FilmID)).
INNER_JOIN(Language, Language.LanguageID.EQ(filmLanguageID)).
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(filmID)).
INNER_JOIN(categoriesNotAction, categoryID.EQ(FilmCategory.CategoryID)),
).ORDER_BY(
Actor.ActorID.ASC(),
filmID.ASC(),
@ -2860,8 +2860,10 @@ func TestSelectQuickStartWithSubQueries(t *testing.T) {
Actors []model.Actor
}
allowUnusedColumns(func() {
err = stmt.Query(db, &dest2)
require.NoError(t, err)
})
//jsonSave("./testdata/quick-start-dest2.json", dest2)
testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json")
@ -2892,9 +2894,10 @@ SELECT true,
'date';
`)
dest := []struct{}{}
err := query.Query(db, &dest)
allowUnusedColumns(func() {
err := query.Query(db, &[]struct{}{})
require.NoError(t, err)
})
}
func TestSelectWindowFunction(t *testing.T) {
@ -2928,8 +2931,7 @@ FROM dvds.payment
WHERE payment.payment_id < $3
GROUP BY payment.amount, payment.customer_id, payment.payment_date;
`
query := Payment.
SELECT(
query := SELECT(
AVG(Payment.Amount).OVER(),
AVG(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID)),
MAXf(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate.DESC())),
@ -2957,16 +2959,24 @@ GROUP BY payment.amount, payment.customer_id, payment.payment_date;
FIRST_VALUE(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)),
LAST_VALUE(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)),
NTH_VALUE(Payment.Amount, 3).OVER(ORDER_BY(Payment.PaymentDate)),
).GROUP_BY(Payment.Amount, Payment.CustomerID, Payment.PaymentDate).
WHERE(Payment.PaymentID.LT(Int(10)))
).FROM(
Payment,
).WHERE(
Payment.PaymentID.LT(Int(10)),
).GROUP_BY(
Payment.Amount,
Payment.CustomerID,
Payment.PaymentDate,
)
//fmt.Println(query.Sql())
testutils.AssertStatementSql(t, query, expectedSQL, 100, 100, int64(10))
dest := []struct{}{}
err := query.Query(db, &dest)
allowUnusedColumns(func() {
err := query.Query(db, &[]struct{}{})
require.NoError(t, err)
})
}
func TestSelectWindowClause(t *testing.T) {
@ -3001,10 +3011,11 @@ ORDER BY payment.customer_id;
testutils.AssertStatementSql(t, query, expectedSQL, int64(10))
dest := []struct{}{}
err := query.Query(db, &dest)
allowUnusedColumns(func() {
err := query.Query(db, &[]struct{}{})
require.NoError(t, err)
})
}
func TestSelectSimpleView(t *testing.T) {
@ -3050,12 +3061,14 @@ func TestSelectJoinViewWithTable(t *testing.T) {
query := SELECT(
view.CustomerList.AllColumns,
Rental.AllColumns,
).
FROM(view.CustomerList.
).FROM(
view.CustomerList.
INNER_JOIN(Rental, view.CustomerList.ID.EQ(Rental.CustomerID)),
).
ORDER_BY(view.CustomerList.ID).
WHERE(view.CustomerList.ID.LT_EQ(Int(2)))
).WHERE(
view.CustomerList.ID.LT_EQ(Int(2)),
).ORDER_BY(
view.CustomerList.ID,
)
var dest []struct {
model.CustomerList `sql:"primary_key=ID"`
@ -3162,7 +3175,7 @@ FROM dvds.customer
WHERE ($1::boolean AND (customer.customer_id = $2)) AND (customer.activebool = $3::boolean);
`, true, int64(1), true)
dest := []model.Customer{}
var dest []model.Customer
err := stmt.Query(db, &dest)
require.NoError(t, err)
require.Len(t, dest, 1)
@ -3710,8 +3723,10 @@ SELECT SUM((CASE WHEN staff.active IS TRUE THEN $1::smallint ELSE $2::integer EN
FROM dvds.staff;
`)
allowUnusedColumns(func() {
err := stmt.Query(db, &struct{}{})
require.NoError(t, err)
})
}
func GET_FILM_COUNT(lenFrom, lenTo IntegerExpression) IntegerExpression {

View file

@ -454,8 +454,11 @@ FROM cte2;
} `alias:"territories4.*"`
}
allowUnusedColumns(func() {
err := stmt.Query(db, &dest)
require.NoError(t, err)
})
require.Len(t, dest, 53)
require.Equal(t, dest[0].Territories1.Territories, model.Territories{
TerritoryID: "01581",