From d86f14e665b30dcb646ff519d6b1f2e6c3aa8172 Mon Sep 17 00:00:00 2001 From: go-jet Date: Tue, 11 Mar 2025 10:50:06 +0100 Subject: [PATCH] Add support for strict scan. If there are unused columns in query result set Query method panics. --- qrm/qrm.go | 23 +++ qrm/scan_context.go | 24 ++- tests/postgres/alltypes_test.go | 74 ++++++--- tests/postgres/chinook_db_test.go | 19 ++- tests/postgres/main_test.go | 13 ++ tests/postgres/range_test.go | 9 +- tests/postgres/scan_test.go | 239 +++++++++++++++++++++--------- tests/postgres/select_test.go | 201 +++++++++++++------------ tests/postgres/with_test.go | 7 +- 9 files changed, 402 insertions(+), 207 deletions(-) diff --git a/qrm/qrm.go b/qrm/qrm.go index 4d49d46..192ad12 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -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() diff --git a/qrm/scan_context.go b/qrm/scan_context.go index e28c4f1..307dd60 100644 --- a/qrm/scan_context.go +++ b/qrm/scan_context.go @@ -17,7 +17,9 @@ type ScanContext struct { groupKeyInfoCache map[string]groupKeyInfo typeInfoMap map[string]typeInfo - typesVisited typeStack // to prevent circular dependency scan + 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 } diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index c2c6a5c..36c8d62 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -591,10 +591,12 @@ func TestExpressionCast(t *testing.T) { Raw("current_database()"), ) - var dest []struct{} - err := query.Query(db, &dest) + allowUnusedColumns(func() { + var dest []struct{} + err := query.Query(db, &dest) + require.NoError(t, err) + }) - require.NoError(t, err) } func TestStringOperators(t *testing.T) { @@ -673,10 +675,11 @@ func TestStringOperators(t *testing.T) { TO_HEX(AllTypes.IntegerPtr), ) - var dest []struct{} - err := query.Query(db, &dest) - - require.NoError(t, err) + 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; `) } - var dest []struct{} - err := stmt.Query(db, &dest) + allowUnusedColumns(func() { + var dest []struct{} + err := stmt.Query(db, &dest) - require.NoError(t, err) + require.NoError(t, err) + }) } func TestBlobConversion(t *testing.T) { @@ -1185,9 +1190,11 @@ LIMIT $27; common.AllTypesIntegerExpResult `alias:"."` } - err := query.Query(db, &dest) + allowUnusedColumns(func() { + err := query.Query(db, &dest) - require.NoError(t, err) + 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{} - err := query.Query(db, &dest) - require.NoError(t, err) + 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; `) - err := stmt.Query(db, &struct{}{}) - require.NoError(t, err) + 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; `) - err := stmt.Query(db, &struct{}{}) - require.NoError(t, err) + 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); `) - err := stmt.Query(db, &struct{}{}) - require.NoError(t, err) + 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) diff --git a/tests/postgres/chinook_db_test.go b/tests/postgres/chinook_db_test.go index 74f2097..c28623d 100644 --- a/tests/postgres/chinook_db_test.go +++ b/tests/postgres/chinook_db_test.go @@ -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,18 +1306,19 @@ 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"; `, 0.1, 100.0, 0.3, 0.2) var dest struct { - PercentileDisc1 string - PercentileDisc2 string - PercentileDisc3 string - PercentileCont1 string - Mode1 string + PercentileDisc1 string + PercentileDisc2 string + PercentileDisc3 string + PercentileCont1 string + PercentileContInterval string + Mode1 string } err := stmt.Query(db, &dest) @@ -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�e 10\",\"696 Osborne Street\",\"2211 W Berry Street\",\"1033 N Park Ave\"}", "PercentileCont1": "0.99", + "PercentileContInterval": "01:00:00", "Mode1": "X1A 1N6" } `) diff --git a/tests/postgres/main_test.go b/tests/postgres/main_test.go index 915387a..c4b06ba 100644 --- a/tests/postgres/main_test.go +++ b/tests/postgres/main_test.go @@ -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 diff --git a/tests/postgres/range_test.go b/tests/postgres/range_test.go index b2ef54c..ec75999 100644 --- a/tests/postgres/range_test.go +++ b/tests/postgres/range_test.go @@ -137,8 +137,10 @@ WHERE sample_ranges.date_range @> $36::date; } var dest sample - err := query.Query(db, &dest) - require.NoError(t, err) + 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", diff --git a/tests/postgres/scan_test.go b/tests/postgres/scan_test.go index a630145..37cbd8a 100644 --- a/tests/postgres/scan_test.go +++ b/tests/postgres/scan_test.go @@ -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,95 +489,111 @@ 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 := query.Query(db, &dest) + 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( - Inventory.AllColumns, - Film.AllColumns, - Store.AllColumns, - ). - ORDER_BY(Inventory.InventoryID). - LIMIT(10) + query := SELECT( + Inventory.AllColumns, + Film.AllColumns, + Store.AllColumns, + ).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) { - var dest struct { - model.Film - IDs []int32 `alias:"inventory.inventory_id"` - } + 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"` + } - err := query.Query(db, &dest) + err := query.Query(db, &dest) - require.NoError(t, err) - testutils.AssertDeepEqual(t, dest.Film, film1) - testutils.AssertDeepEqual(t, dest.IDs, []int32{1, 2, 3, 4, 5, 6, 7, 8}) + require.NoError(t, err) + 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) { - var dest []struct { - model.Film - IDs []int32 `alias:"inventory.inventory_id"` - } + allowUnusedColumns(func() { + var dest []struct { + model.Film + IDs []int32 `alias:"inventory.inventory_id"` + } - err := query.Query(db, &dest) + err := query.Query(db, &dest) - require.NoError(t, err) - require.Equal(t, len(dest), 2) - testutils.AssertDeepEqual(t, dest[0].Film, film1) - testutils.AssertDeepEqual(t, dest[0].IDs, []int32{1, 2, 3, 4, 5, 6, 7, 8}) - testutils.AssertDeepEqual(t, dest[1].Film, film2) - testutils.AssertDeepEqual(t, dest[1].IDs, []int32{9, 10}) + require.NoError(t, err) + require.Equal(t, len(dest), 2) + testutils.AssertDeepEqual(t, dest[0].Film, film1) + testutils.AssertDeepEqual(t, dest[0].IDs, []int32{1, 2, 3, 4, 5, 6, 7, 8}) + 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) { - var dest []struct { - model.Film - IDs []*int32 `alias:"inventory.inventory_id"` - } + allowUnusedColumns(func() { + var dest []struct { + model.Film + IDs []*int32 `alias:"inventory.inventory_id"` + } - err := query.Query(db, &dest) + err := query.Query(db, &dest) - require.NoError(t, err) - require.Equal(t, len(dest), 2) - testutils.AssertDeepEqual(t, dest[0].Film, film1) - testutils.AssertDeepEqual(t, dest[0].IDs, []*int32{ptr.Of(int32(1)), ptr.Of(int32(2)), ptr.Of(int32(3)), ptr.Of(int32(4)), - ptr.Of(int32(5)), ptr.Of(int32(6)), ptr.Of(int32(7)), ptr.Of(int32(8))}) - testutils.AssertDeepEqual(t, dest[1].Film, film2) - testutils.AssertDeepEqual(t, dest[1].IDs, []*int32{ptr.Of(int32(9)), ptr.Of(int32(10))}) + require.NoError(t, err) + require.Equal(t, len(dest), 2) + testutils.AssertDeepEqual(t, dest[0].Film, film1) + testutils.AssertDeepEqual(t, dest[0].IDs, []*int32{ptr.Of(int32(1)), ptr.Of(int32(2)), ptr.Of(int32(3)), ptr.Of(int32(4)), + ptr.Of(int32(5)), ptr.Of(int32(6)), ptr.Of(int32(7)), ptr.Of(int32(8))}) + 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. - 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). + 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))). 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", diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index 3716a70..dc7ca48 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -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{} - err := query.Query(db, &dest) - require.NoError(t, err) + 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 } - err = stmt.Query(db, &dest2) - require.NoError(t, err) + 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,18 +2818,19 @@ 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( - Actor.AllColumns, - filmLogerThan180.AllColumns(), - Language.AllColumns, - categoriesNotAction.AllColumns(), - ).ORDER_BY( + 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 } - err = stmt.Query(db, &dest2) - require.NoError(t, err) + 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) - require.NoError(t, err) + allowUnusedColumns(func() { + err := query.Query(db, &[]struct{}{}) + require.NoError(t, err) + }) } func TestSelectWindowFunction(t *testing.T) { @@ -2928,45 +2931,52 @@ FROM dvds.payment WHERE payment.payment_id < $3 GROUP BY payment.amount, payment.customer_id, payment.payment_date; ` - query := Payment. - SELECT( - AVG(Payment.Amount).OVER(), - AVG(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID)), - MAXf(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate.DESC())), - MINf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), - SUMf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID). - ORDER_BY(Payment.PaymentDate.DESC()).ROWS(PRECEDING(1), FOLLOWING(6))), - SUMf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID). - ORDER_BY(Payment.PaymentDate.DESC()).RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))), - MAXi(Payment.CustomerID).OVER(ORDER_BY(Payment.PaymentDate.DESC()).ROWS(CURRENT_ROW, FOLLOWING(UNBOUNDED))), - MINi(Payment.CustomerID).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), - SUMi(Payment.CustomerID).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), - ROW_NUMBER().OVER(ORDER_BY(Payment.PaymentDate)), - RANK().OVER(ORDER_BY(Payment.PaymentDate)), - DENSE_RANK().OVER(ORDER_BY(Payment.PaymentDate)), - CUME_DIST().OVER(ORDER_BY(Payment.PaymentDate)), - NTILE(11).OVER(ORDER_BY(Payment.PaymentDate)), - LAG(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), - LAG(Payment.Amount, 2).OVER(ORDER_BY(Payment.PaymentDate)), - LAG(Payment.Amount, 2, Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), - LAG(Payment.Amount, 2, 100).OVER(ORDER_BY(Payment.PaymentDate)), - LEAD(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), - LEAD(Payment.Amount, 2).OVER(ORDER_BY(Payment.PaymentDate)), - LEAD(Payment.Amount, 2, Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), - LEAD(Payment.Amount, 2, 100).OVER(ORDER_BY(Payment.PaymentDate)), - 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))) + query := SELECT( + AVG(Payment.Amount).OVER(), + AVG(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID)), + MAXf(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate.DESC())), + MINf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), + SUMf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID). + ORDER_BY(Payment.PaymentDate.DESC()).ROWS(PRECEDING(1), FOLLOWING(6))), + SUMf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID). + ORDER_BY(Payment.PaymentDate.DESC()).RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED))), + MAXi(Payment.CustomerID).OVER(ORDER_BY(Payment.PaymentDate.DESC()).ROWS(CURRENT_ROW, FOLLOWING(UNBOUNDED))), + MINi(Payment.CustomerID).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), + SUMi(Payment.CustomerID).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())), + ROW_NUMBER().OVER(ORDER_BY(Payment.PaymentDate)), + RANK().OVER(ORDER_BY(Payment.PaymentDate)), + DENSE_RANK().OVER(ORDER_BY(Payment.PaymentDate)), + CUME_DIST().OVER(ORDER_BY(Payment.PaymentDate)), + NTILE(11).OVER(ORDER_BY(Payment.PaymentDate)), + LAG(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), + LAG(Payment.Amount, 2).OVER(ORDER_BY(Payment.PaymentDate)), + LAG(Payment.Amount, 2, Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), + LAG(Payment.Amount, 2, 100).OVER(ORDER_BY(Payment.PaymentDate)), + LEAD(Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), + LEAD(Payment.Amount, 2).OVER(ORDER_BY(Payment.PaymentDate)), + LEAD(Payment.Amount, 2, Payment.Amount).OVER(ORDER_BY(Payment.PaymentDate)), + LEAD(Payment.Amount, 2, 100).OVER(ORDER_BY(Payment.PaymentDate)), + 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)), + ).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) - require.NoError(t, err) + 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) + 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; `) - err := stmt.Query(db, &struct{}{}) - require.NoError(t, err) + allowUnusedColumns(func() { + err := stmt.Query(db, &struct{}{}) + require.NoError(t, err) + }) } func GET_FILM_COUNT(lenFrom, lenTo IntegerExpression) IntegerExpression { diff --git a/tests/postgres/with_test.go b/tests/postgres/with_test.go index 92b5649..2348cd8 100644 --- a/tests/postgres/with_test.go +++ b/tests/postgres/with_test.go @@ -454,8 +454,11 @@ FROM cte2; } `alias:"territories4.*"` } - err := stmt.Query(db, &dest) - require.NoError(t, err) + 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",