diff --git a/go.mod b/go.mod index fa9d621..581d0d6 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,8 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -29,15 +31,18 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 // indirect + github.com/uptrace/bun/driver/sqliteshim v1.2.15 // indirect github.com/uptrace/bunrouter v1.0.23 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.0 // indirect ) diff --git a/go.sum b/go.sum index a40ec50..efe1dde 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -21,6 +22,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -50,6 +55,10 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.2.15 h1:Ut68XRBLDgp9qG9QBMa9ELWaZOmzHNdczHQdrOZbEFE= github.com/uptrace/bun v1.2.15/go.mod h1:Eghz7NonZMiTX/Z6oKYytJ0oaMEJ/eq3kEV4vSqG038= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15 h1:7upGMVjFRB1oI78GQw6ruNLblYn5CR+kxqcbbeBBils= +github.com/uptrace/bun/dialect/sqlitedialect v1.2.15/go.mod h1:c7YIDaPNS2CU2uI1p7umFuFWkuKbDcPDDvp+DLHZnkI= +github.com/uptrace/bun/driver/sqliteshim v1.2.15 h1:M/rZJSjOPV4OmfTVnDPtL+wJmdMTqDUn8cuk5ycfABA= +github.com/uptrace/bun/driver/sqliteshim v1.2.15/go.mod h1:YqwxFyvM992XOCpGJtXyKPkgkb+aZpIIMzGbpaw1hIk= github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU= github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -62,6 +71,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= +golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -77,9 +88,17 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= diff --git a/pkg/common/adapters/database/bun.go b/pkg/common/adapters/database/bun.go index 67e9c62..3c53e9f 100644 --- a/pkg/common/adapters/database/bun.go +++ b/pkg/common/adapters/database/bun.go @@ -307,12 +307,14 @@ func (b *BunSelectQuery) Exists(ctx context.Context) (bool, error) { // BunInsertQuery implements InsertQuery for Bun type BunInsertQuery struct { - query *bun.InsertQuery - values map[string]interface{} + query *bun.InsertQuery + values map[string]interface{} + hasModel bool } func (b *BunInsertQuery) Model(model interface{}) common.InsertQuery { b.query = b.query.Model(model) + b.hasModel = true return b } @@ -342,10 +344,16 @@ func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery { } func (b *BunInsertQuery) Exec(ctx context.Context) (common.Result, error) { - if b.values != nil { - // Use Value() for INSERT queries to set column values - for k, v := range b.values { - b.query = b.query.Value(k, "?", v) + if b.values != nil && len(b.values) > 0 { + if !b.hasModel { + // If no model was set, use the values map as the model + // Bun can insert map[string]interface{} directly + b.query = b.query.Model(&b.values) + } else { + // If model was set, use Value() to add individual values + for k, v := range b.values { + b.query = b.query.Value(k, "?", v) + } } } result, err := b.query.Exec(ctx) diff --git a/pkg/common/adapters/database/bun_insert_test.go b/pkg/common/adapters/database/bun_insert_test.go new file mode 100644 index 0000000..1bc5da6 --- /dev/null +++ b/pkg/common/adapters/database/bun_insert_test.go @@ -0,0 +1,213 @@ +package database + +import ( + "context" + "database/sql" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/sqlitedialect" + "github.com/uptrace/bun/driver/sqliteshim" +) + +// TestInsertModel is a test model for insert operations +type TestInsertModel struct { + bun.BaseModel `bun:"table:test_inserts"` + ID int64 `bun:"id,pk,autoincrement"` + Name string `bun:"name,notnull"` + Email string `bun:"email"` + Age int `bun:"age"` +} + +func setupBunTestDB(t *testing.T) *bun.DB { + sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared") + require.NoError(t, err, "Failed to open SQLite database") + + db := bun.NewDB(sqldb, sqlitedialect.New()) + + // Create test table + _, err = db.NewCreateTable(). + Model((*TestInsertModel)(nil)). + IfNotExists(). + Exec(context.Background()) + require.NoError(t, err, "Failed to create test table") + + return db +} + +func TestBunInsertQuery_Model(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test inserting with Model() + model := &TestInsertModel{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + + result, err := adapter.NewInsert(). + Model(model). + Returning("*"). + Exec(ctx) + + require.NoError(t, err, "Insert should succeed") + assert.Equal(t, int64(1), result.RowsAffected(), "Should insert 1 row") + + // Verify the data was inserted + var retrieved TestInsertModel + err = db.NewSelect(). + Model(&retrieved). + Where("id = ?", model.ID). + Scan(ctx) + + require.NoError(t, err, "Should retrieve inserted row") + assert.Equal(t, "John Doe", retrieved.Name) + assert.Equal(t, "john@example.com", retrieved.Email) + assert.Equal(t, 30, retrieved.Age) +} + +func TestBunInsertQuery_Value(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test inserting with Value() method - this was the bug + result, err := adapter.NewInsert(). + Table("test_inserts"). + Value("name", "Jane Smith"). + Value("email", "jane@example.com"). + Value("age", 25). + Exec(ctx) + + require.NoError(t, err, "Insert with Value() should succeed") + assert.Equal(t, int64(1), result.RowsAffected(), "Should insert 1 row") + + // Verify the data was inserted + var retrieved TestInsertModel + err = db.NewSelect(). + Model(&retrieved). + Where("name = ?", "Jane Smith"). + Scan(ctx) + + require.NoError(t, err, "Should retrieve inserted row") + assert.Equal(t, "Jane Smith", retrieved.Name) + assert.Equal(t, "jane@example.com", retrieved.Email) + assert.Equal(t, 25, retrieved.Age) +} + +func TestBunInsertQuery_MultipleValues(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test inserting multiple values + result, err := adapter.NewInsert(). + Table("test_inserts"). + Value("name", "Alice"). + Value("email", "alice@example.com"). + Value("age", 28). + Exec(ctx) + + require.NoError(t, err, "First insert should succeed") + assert.Equal(t, int64(1), result.RowsAffected()) + + result, err = adapter.NewInsert(). + Table("test_inserts"). + Value("name", "Bob"). + Value("email", "bob@example.com"). + Value("age", 35). + Exec(ctx) + + require.NoError(t, err, "Second insert should succeed") + assert.Equal(t, int64(1), result.RowsAffected()) + + // Verify both rows exist + var count int + count, err = db.NewSelect(). + Model((*TestInsertModel)(nil)). + Count(ctx) + + require.NoError(t, err, "Count should succeed") + assert.Equal(t, 2, count, "Should have 2 rows") +} + +func TestBunInsertQuery_ValueWithNil(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test inserting with nil value for nullable field + result, err := adapter.NewInsert(). + Table("test_inserts"). + Value("name", "Test User"). + Value("email", nil). // NULL email + Value("age", 20). + Exec(ctx) + + require.NoError(t, err, "Insert with nil value should succeed") + assert.Equal(t, int64(1), result.RowsAffected()) + + // Verify the data was inserted with NULL email + var retrieved TestInsertModel + err = db.NewSelect(). + Model(&retrieved). + Where("name = ?", "Test User"). + Scan(ctx) + + require.NoError(t, err, "Should retrieve inserted row") + assert.Equal(t, "Test User", retrieved.Name) + assert.Equal(t, "", retrieved.Email) // NULL becomes empty string + assert.Equal(t, 20, retrieved.Age) +} + +func TestBunInsertQuery_Returning(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test insert with RETURNING clause + // Note: SQLite has limited RETURNING support, but this tests the API + result, err := adapter.NewInsert(). + Table("test_inserts"). + Value("name", "Return Test"). + Value("email", "return@example.com"). + Value("age", 40). + Returning("*"). + Exec(ctx) + + require.NoError(t, err, "Insert with RETURNING should succeed") + assert.Equal(t, int64(1), result.RowsAffected()) +} + +func TestBunInsertQuery_EmptyValues(t *testing.T) { + db := setupBunTestDB(t) + defer db.Close() + + adapter := NewBunAdapter(db) + ctx := context.Background() + + // Test insert without calling Value() - should use Model() or fail gracefully + result, err := adapter.NewInsert(). + Table("test_inserts"). + Exec(ctx) + + // This should fail because no values are provided + assert.Error(t, err, "Insert without values should fail") + if result != nil { + assert.Equal(t, int64(0), result.RowsAffected()) + } +}