From 9e80321c3e25b93fb70a9797b86139dd874a2e13 Mon Sep 17 00:00:00 2001 From: CyJay Date: Thu, 14 May 2026 17:45:47 +0800 Subject: [PATCH 1/3] feat: no ErrNoRows when scan nil struct ptr --- internal/dbtest/db_test.go | 46 ++++++++++++++++++++++++++++++++++++++ model.go | 7 ++++-- query_base.go | 3 +++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/internal/dbtest/db_test.go b/internal/dbtest/db_test.go index e4266e2ea..802b20c12 100644 --- a/internal/dbtest/db_test.go +++ b/internal/dbtest/db_test.go @@ -262,6 +262,7 @@ func TestDB(t *testing.T) { {testSelectMap}, {testSelectMapSlice}, {testSelectStruct}, + {testSelectStructNilPtr}, {testSelectNestedStructValue}, {testSelectNestedStructPtr}, {testSelectStructSlice}, @@ -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 diff --git a/model.go b/model.go index cec755ef9..73f3b4e94 100644 --- a/model.go +++ b/model.go @@ -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: @@ -200,8 +204,7 @@ func validMap(typ reflect.Type) error { func isSingleRowModel(m Model) bool { switch m.(type) { - case *mapModel, - *structTableModel, + case *structTableModel, *scanModel: return true default: diff --git a/query_base.go b/query_base.go index 09c015395..7cd056c1c 100644 --- a/query_base.go +++ b/query_base.go @@ -630,6 +630,9 @@ func (q *baseQuery) _scan( } if numRow == 0 && hasDest && isSingleRowModel(model) { + if nm, ok := model.(*structTableModel); ok && nm.isNil() { + return driver.RowsAffected(numRow), nil + } return nil, sql.ErrNoRows } return driver.RowsAffected(numRow), nil From 62501dec60f88e5af36b600ca057c013c4158eb2 Mon Sep 17 00:00:00 2001 From: CyJay Date: Fri, 15 May 2026 23:52:02 +0800 Subject: [PATCH 2/3] feat: no ErrNoRows when scan nil map --- model.go | 9 ++++++++- model_map.go | 4 ++++ query_base.go | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/model.go b/model.go index 73f3b4e94..a179350fb 100644 --- a/model.go +++ b/model.go @@ -204,10 +204,17 @@ func validMap(typ reflect.Type) error { func isSingleRowModel(m Model) bool { switch m.(type) { - case *structTableModel, + case *mapModel, + *structTableModel, *scanModel: return true default: 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 +} diff --git a/model_map.go b/model_map.go index b2a034c16..ffa509c83 100644 --- a/model_map.go +++ b/model_map.go @@ -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() diff --git a/query_base.go b/query_base.go index 7cd056c1c..963284f81 100644 --- a/query_base.go +++ b/query_base.go @@ -630,7 +630,7 @@ func (q *baseQuery) _scan( } if numRow == 0 && hasDest && isSingleRowModel(model) { - if nm, ok := model.(*structTableModel); ok && nm.isNil() { + if nm, ok := model.(nilModel); ok && nm.isNil() { return driver.RowsAffected(numRow), nil } return nil, sql.ErrNoRows From 385b0a6b19362d1bff10b4f04f389bb75caa5ccd Mon Sep 17 00:00:00 2001 From: CyJay Date: Wed, 20 May 2026 18:11:47 +0800 Subject: [PATCH 3/3] feat: no ErrNoRows when scan nil builtin type --- model_scan.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/model_scan.go b/model_scan.go index 63ad6f300..eb497b8be 100644 --- a/model_scan.go +++ b/model_scan.go @@ -28,6 +28,19 @@ func (m *scanModel) Value() any { return m.dest } +func (m *scanModel) isNil() bool { + if len(m.dest) == 0 { + return false + } + for _, item := range m.dest { + itemVal := reflect.ValueOf(item).Elem() + if isNil := itemVal.Type().Kind() == reflect.Pointer && itemVal.IsNil(); !isNil { + return false + } + } + return true +} + func (m *scanModel) ScanRows(ctx context.Context, rows *sql.Rows) (int, error) { if !rows.Next() { return 0, rows.Err()