Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions internal/dbtest/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ func TestDB(t *testing.T) {
{testSelectMap},
{testSelectMapSlice},
{testSelectStruct},
{testSelectStructNilPtr},
{testSelectNestedStructValue},
{testSelectNestedStructPtr},
{testSelectStructSlice},
Expand Down Expand Up @@ -444,6 +445,51 @@ func testSelectStruct(t *testing.T, db *bun.DB) {
require.Contains(t, err.Error(), "Model does not have column")
}

func testSelectStructNilPtr(t *testing.T, db *bun.DB) {
type Model struct {
Num int
Str string
}

// Scan a row into a nil **Model: the inner pointer should be allocated and populated.
var model *Model
err := db.NewSelect().
ColumnExpr("10 AS num, 'hello' AS str").
Scan(ctx, &model)
require.NoError(t, err)
require.NotNil(t, model)
require.Equal(t, 10, model.Num)
require.Equal(t, "hello", model.Str)

// No rows + nil **Model: should NOT return ErrNoRows; inner pointer stays nil.
var empty *Model
err = db.NewSelect().
TableExpr("(SELECT 42 AS num) AS t").
Where("1 = 2").
Scan(ctx, &empty)
require.NoError(t, err)
require.Nil(t, empty)

// Regression: a non-nil *Model (existing behaviour) must still return ErrNoRows.
nonNil := new(Model)
err = db.NewSelect().
TableExpr("(SELECT 42 AS num) AS t").
Where("1 = 2").
Scan(ctx, nonNil)
require.Equal(t, sql.ErrNoRows, err)

// Scan a row into a non-nil **Model: the existing inner pointer is reused.
existing := &Model{}
preserved := existing
err = db.NewSelect().
ColumnExpr("7 AS num, 'world' AS str").
Scan(ctx, &existing)
require.NoError(t, err)
require.Same(t, preserved, existing)
require.Equal(t, 7, existing.Num)
require.Equal(t, "world", existing.Str)
}

func testSelectNestedStructValue(t *testing.T, db *bun.DB) {
type Model struct {
Num int
Expand Down
10 changes: 10 additions & 0 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ func _newModel(db *DB, dest any, scan bool) (Model, error) {
return newMapModel(db, mapPtr), nil
case reflect.Struct:
return newStructTableModelValue(db, dest, v), nil
case reflect.Pointer:
if v.Type().Elem().Kind() == reflect.Struct {
return newStructTableModelValue(db, dest, v), nil
}
case reflect.Slice:
switch elemType := sliceElemType(v); elemType.Kind() {
case reflect.Struct:
Expand Down Expand Up @@ -208,3 +212,9 @@ func isSingleRowModel(m Model) bool {
return false
}
}

// nilModel is implemented by models whose destination may be a nil pointer
// that should not be treated as "no rows" when an empty result is scanned.
type nilModel interface {
isNil() bool
}
Comment thread
CyJaySong marked this conversation as resolved.
4 changes: 4 additions & 0 deletions model_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func (m *mapModel) Value() any {
return m.dest
}

func (m *mapModel) isNil() bool {
return m.dest == nil || *m.dest == nil
}

func (m *mapModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) {
if !rows.Next() {
return 0, rows.Err()
Expand Down
3 changes: 3 additions & 0 deletions query_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,9 @@ func (q *baseQuery) _scan(
}

if numRow == 0 && hasDest && isSingleRowModel(model) {
Comment thread
CyJaySong marked this conversation as resolved.
if nm, ok := model.(nilModel); ok && nm.isNil() {
return driver.RowsAffected(numRow), nil
}
return nil, sql.ErrNoRows
}
return driver.RowsAffected(numRow), nil
Expand Down
Loading