From bd23595748af3454d14ddfa93f9a6b107cac0a8b Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 10 Jan 2026 21:40:54 +0800 Subject: [PATCH 1/2] fix: [#858] facades.DB().Insert() can not operate nil columns as expected --- database/db/query.go | 15 +++-- database/db/utils_test.go | 129 +++++++++++++++++++++++++++++++++++++- support/str/str_test.go | 1 + tests/db_test.go | 10 ++- 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/database/db/query.go b/database/db/query.go index d419ba17e..8e7c12eb1 100644 --- a/database/db/query.go +++ b/database/db/query.go @@ -1105,16 +1105,23 @@ func (r *Query) buildInsert(data []map[string]any) (sql string, args []any, err builder = builder.PlaceholderFormat(placeholderFormat) } - first := data[0] - cols := make([]string, 0, len(first)) - for col := range first { + // Collect all unique columns from all maps to avoid missing columns + colSet := make(map[string]bool) + for _, row := range data { + for col := range row { + colSet[col] = true + } + } + + cols := make([]string, 0, len(colSet)) + for col := range colSet { cols = append(cols, col) } sort.Strings(cols) builder = builder.Columns(cols...) for _, row := range data { - vals := make([]any, 0, len(first)) + vals := make([]any, 0, len(cols)) for _, col := range cols { vals = append(vals, row[col]) } diff --git a/database/db/utils_test.go b/database/db/utils_test.go index 1c2992716..28e1dc1ce 100644 --- a/database/db/utils_test.go +++ b/database/db/utils_test.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" "github.com/goravel/framework/support/carbon" + "github.com/goravel/framework/support/convert" ) type Body struct { @@ -20,11 +21,25 @@ type Body struct { leg int `db:"leg"` } +type House struct { + Address string `db:"address"` + Size *int `db:"size"` +} + +type Job struct { + Title string `db:"title"` + Salary *float64 `db:"salary"` +} + type User struct { - ID int `db:"id"` - Name string `db:"-"` - Email string + ID int `db:"id"` + Name string `db:"-"` + Email string + Avatar *string + Alias *int Body + House *House + Job Job TestSoftDeletes TestTimestamps } @@ -124,6 +139,114 @@ func TestConvertToSliceMap(t *testing.T) { }, want: []map[string]any{{"weight": "100kg", "Age": 25}, {"weight": "90kg", "Age": 20}}, }, + { + name: "user with nested struct pointer", + data: User{ + ID: 1, + Name: "John", + Email: "john@example.com", + Body: Body{Weight: "100kg", Head: &head, DateTime: *dateTime}, + House: &House{Address: "123 Main St", Size: nil}, + Job: Job{Title: "Engineer", Salary: nil}, + TestSoftDeletes: TestSoftDeletes{DeletedAt: deletedAt}, + TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, + }, + want: []map[string]any{ + { + "id": 1, "email": "john@example.com", "weight": "100kg", "head": &head, "date_time": *dateTime, + "house": &House{Address: "123 Main St", Size: nil}, + "job": Job{Title: "Engineer", Salary: nil}, + "created_at": dateTime, "updated_at": dateTime, "deleted_at": deletedAt, + }, + }, + }, + { + name: "user with nil nested struct pointer", + data: User{ + ID: 1, + Email: "john@example.com", + Body: Body{Weight: "100kg", DateTime: *dateTime}, + House: nil, + Job: Job{Title: "Engineer"}, + TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, + }, + want: []map[string]any{ + { + "id": 1, "email": "john@example.com", "weight": "100kg", "date_time": *dateTime, + "job": Job{Title: "Engineer"}, + "created_at": dateTime, "updated_at": dateTime, + }, + }, + }, + { + name: "user with pointer fields", + data: func() User { + avatar := "avatar.jpg" + alias := 42 + size := 100 + salary := 50000.0 + return User{ + ID: 1, + Email: "john@example.com", + Avatar: &avatar, + Alias: &alias, + Body: Body{Weight: "100kg", Head: &head, DateTime: *dateTime}, + House: &House{Address: "123 Main St", Size: &size}, + Job: Job{Title: "Engineer", Salary: &salary}, + TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, + } + }(), + want: []map[string]any{ + { + "id": 1, "email": "john@example.com", "avatar": convert.Pointer("avatar.jpg"), + "alias": convert.Pointer(42), "weight": "100kg", "head": &head, "date_time": *dateTime, + "house": &House{Address: "123 Main St", Size: convert.Pointer(100)}, + "job": Job{Title: "Engineer", Salary: convert.Pointer(50000.0)}, + "created_at": dateTime, "updated_at": dateTime, + }, + }, + }, + { + name: "user slice with mixed nested structs", + data: []User{ + { + ID: 1, + Email: "john@example.com", + Body: Body{Length: 10, Weight: "100kg", Head: &head, DateTime: *dateTime}, + House: &House{Address: "123 Main St"}, + Job: Job{Title: "Engineer"}, + TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, + }, + { + ID: 2, + Email: "jane@example.com", + Body: Body{Weight: "90kg", DateTime: *dateTime}, + House: nil, + Job: Job{Title: "Designer"}, + TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, + }, + }, + want: []map[string]any{ + { + "id": 1, "email": "john@example.com", "length": 10, "weight": "100kg", "head": &head, "date_time": *dateTime, + "house": &House{Address: "123 Main St"}, + "job": Job{Title: "Engineer"}, + "created_at": dateTime, "updated_at": dateTime, + }, + { + "id": 2, "email": "jane@example.com", "weight": "90kg", "date_time": *dateTime, + "job": Job{Title: "Designer"}, + "created_at": dateTime, "updated_at": dateTime, + }, + }, + }, + { + name: "user with all zero values", + data: User{}, + want: []map[string]any{ + {}, + }, + }, } for _, tt := range tests { diff --git a/support/str/str_test.go b/support/str/str_test.go index 0fb58e8bc..e14956660 100644 --- a/support/str/str_test.go +++ b/support/str/str_test.go @@ -555,6 +555,7 @@ func (s *StringTestSuite) TestSnake() { s.Equal("a", Of("A").Snake().String()) s.Equal("foo123_bar", Of("foo123Bar").Snake().String()) s.Equal("123foo", Of("123foo").Snake().String()) + s.Equal("id", Of("ID").Snake().String()) } func (s *StringTestSuite) TestSplit() { diff --git a/tests/db_test.go b/tests/db_test.go index f5b2cc275..3356de72d 100644 --- a/tests/db_test.go +++ b/tests/db_test.go @@ -480,7 +480,8 @@ func (s *DBTestSuite) TestInsert_First_Get() { }, }, { - Name: "multiple structs2", + Name: "multiple structs2", + Weight: convert.Pointer(1), }, }) s.NoError(err) @@ -490,8 +491,15 @@ func (s *DBTestSuite) TestInsert_First_Get() { err = query.DB().Table("products").Where("name", []string{"multiple structs1", "multiple structs2"}).Where("deleted_at", nil).Get(&products) s.NoError(err) s.Equal(2, len(products)) + s.True(products[0].ID > 0) s.Equal("multiple structs1", products[0].Name) + s.NotEmpty(products[0].CreatedAt) + s.NotEmpty(products[0].UpdatedAt) + s.True(products[1].ID > 0) s.Equal("multiple structs2", products[1].Name) + s.Equal(1, *products[1].Weight) + s.Empty(products[1].CreatedAt) + s.Empty(products[1].UpdatedAt) }) s.Run("single map", func() { From 17478dc81e6a02f5f1f63d73963a1e0b09f5db7b Mon Sep 17 00:00:00 2001 From: Bowen Date: Sat, 10 Jan 2026 21:49:58 +0800 Subject: [PATCH 2/2] optimize --- database/db/utils_test.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/database/db/utils_test.go b/database/db/utils_test.go index 28e1dc1ce..5cdd93feb 100644 --- a/database/db/utils_test.go +++ b/database/db/utils_test.go @@ -181,18 +181,14 @@ func TestConvertToSliceMap(t *testing.T) { { name: "user with pointer fields", data: func() User { - avatar := "avatar.jpg" - alias := 42 - size := 100 - salary := 50000.0 return User{ ID: 1, Email: "john@example.com", - Avatar: &avatar, - Alias: &alias, + Avatar: convert.Pointer("avatar.jpg"), + Alias: convert.Pointer(42), Body: Body{Weight: "100kg", Head: &head, DateTime: *dateTime}, - House: &House{Address: "123 Main St", Size: &size}, - Job: Job{Title: "Engineer", Salary: &salary}, + House: &House{Address: "123 Main St", Size: convert.Pointer(100)}, + Job: Job{Title: "Engineer", Salary: convert.Pointer(50000.0)}, TestTimestamps: TestTimestamps{CreatedAt: dateTime, UpdatedAt: dateTime}, } }(),