Skip to content

Commit 9e80321

Browse files
committed
feat: no ErrNoRows when scan nil struct ptr
1 parent e91aa81 commit 9e80321

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

internal/dbtest/db_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ func TestDB(t *testing.T) {
262262
{testSelectMap},
263263
{testSelectMapSlice},
264264
{testSelectStruct},
265+
{testSelectStructNilPtr},
265266
{testSelectNestedStructValue},
266267
{testSelectNestedStructPtr},
267268
{testSelectStructSlice},
@@ -444,6 +445,51 @@ func testSelectStruct(t *testing.T, db *bun.DB) {
444445
require.Contains(t, err.Error(), "Model does not have column")
445446
}
446447

448+
func testSelectStructNilPtr(t *testing.T, db *bun.DB) {
449+
type Model struct {
450+
Num int
451+
Str string
452+
}
453+
454+
// Scan a row into a nil **Model: the inner pointer should be allocated and populated.
455+
var model *Model
456+
err := db.NewSelect().
457+
ColumnExpr("10 AS num, 'hello' AS str").
458+
Scan(ctx, &model)
459+
require.NoError(t, err)
460+
require.NotNil(t, model)
461+
require.Equal(t, 10, model.Num)
462+
require.Equal(t, "hello", model.Str)
463+
464+
// No rows + nil **Model: should NOT return ErrNoRows; inner pointer stays nil.
465+
var empty *Model
466+
err = db.NewSelect().
467+
TableExpr("(SELECT 42 AS num) AS t").
468+
Where("1 = 2").
469+
Scan(ctx, &empty)
470+
require.NoError(t, err)
471+
require.Nil(t, empty)
472+
473+
// Regression: a non-nil *Model (existing behaviour) must still return ErrNoRows.
474+
nonNil := new(Model)
475+
err = db.NewSelect().
476+
TableExpr("(SELECT 42 AS num) AS t").
477+
Where("1 = 2").
478+
Scan(ctx, nonNil)
479+
require.Equal(t, sql.ErrNoRows, err)
480+
481+
// Scan a row into a non-nil **Model: the existing inner pointer is reused.
482+
existing := &Model{}
483+
preserved := existing
484+
err = db.NewSelect().
485+
ColumnExpr("7 AS num, 'world' AS str").
486+
Scan(ctx, &existing)
487+
require.NoError(t, err)
488+
require.Same(t, preserved, existing)
489+
require.Equal(t, 7, existing.Num)
490+
require.Equal(t, "world", existing.Str)
491+
}
492+
447493
func testSelectNestedStructValue(t *testing.T, db *bun.DB) {
448494
type Model struct {
449495
Num int

model.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ func _newModel(db *DB, dest any, scan bool) (Model, error) {
124124
return newMapModel(db, mapPtr), nil
125125
case reflect.Struct:
126126
return newStructTableModelValue(db, dest, v), nil
127+
case reflect.Pointer:
128+
if v.Type().Elem().Kind() == reflect.Struct {
129+
return newStructTableModelValue(db, dest, v), nil
130+
}
127131
case reflect.Slice:
128132
switch elemType := sliceElemType(v); elemType.Kind() {
129133
case reflect.Struct:
@@ -200,8 +204,7 @@ func validMap(typ reflect.Type) error {
200204

201205
func isSingleRowModel(m Model) bool {
202206
switch m.(type) {
203-
case *mapModel,
204-
*structTableModel,
207+
case *structTableModel,
205208
*scanModel:
206209
return true
207210
default:

query_base.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,9 @@ func (q *baseQuery) _scan(
630630
}
631631

632632
if numRow == 0 && hasDest && isSingleRowModel(model) {
633+
if nm, ok := model.(*structTableModel); ok && nm.isNil() {
634+
return driver.RowsAffected(numRow), nil
635+
}
633636
return nil, sql.ErrNoRows
634637
}
635638
return driver.RowsAffected(numRow), nil

0 commit comments

Comments
 (0)