mirror of
https://gitee.com/johng/gf
synced 2026-07-07 06:15:15 +08:00
fix(contrib/drivers/sqlite): infer save conflict from primary keys
This commit is contained in:
68
contrib/drivers/sqlite/sqlite_do_insert.go
Normal file
68
contrib/drivers/sqlite/sqlite_do_insert.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// This file implements SQLite insert behavior overrides for upsert conflict inference.
|
||||
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if option.InsertOption == gdb.InsertOptionSave && len(option.OnConflict) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for Save operation`,
|
||||
)
|
||||
}
|
||||
if !saveDataHasPrimaryKeys(list, primaryKeys) {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has primary keys in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
|
||||
// saveDataHasPrimaryKeys reports whether the first save record contains all primary keys.
|
||||
func saveDataHasPrimaryKeys(list gdb.List, primaryKeys []string) bool {
|
||||
if len(list) == 0 || len(primaryKeys) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, primaryKey := range primaryKeys {
|
||||
if !saveDataHasKey(list[0], primaryKey) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// saveDataHasKey reports whether the save data contains the given key case-insensitively.
|
||||
func saveDataHasKey(data gdb.Map, key string) bool {
|
||||
for dataKey := range data {
|
||||
if strings.EqualFold(dataKey, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -439,8 +439,26 @@ func Test_DB_Save(t *testing.T) {
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
result, err := db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNil(err)
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(rowsAffected, 1)
|
||||
|
||||
data["nickname"] = "T10-updated"
|
||||
result, err = db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNil(err)
|
||||
rowsAffected, err = result.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(rowsAffected, 1)
|
||||
|
||||
value, err := db.Model("t_user").Where("id", i).Value("nickname")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "T10-updated")
|
||||
|
||||
delete(data, "id")
|
||||
_, err = db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
68
contrib/drivers/sqlitecgo/sqlitecgo_do_insert.go
Normal file
68
contrib/drivers/sqlitecgo/sqlitecgo_do_insert.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
// This file implements SQLiteCGO insert behavior overrides for upsert conflict inference.
|
||||
|
||||
package sqlitecgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if option.InsertOption == gdb.InsertOptionSave && len(option.OnConflict) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for Save operation`,
|
||||
)
|
||||
}
|
||||
if !saveDataHasPrimaryKeys(list, primaryKeys) {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has primary keys in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
|
||||
// saveDataHasPrimaryKeys reports whether the first save record contains all primary keys.
|
||||
func saveDataHasPrimaryKeys(list gdb.List, primaryKeys []string) bool {
|
||||
if len(list) == 0 || len(primaryKeys) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, primaryKey := range primaryKeys {
|
||||
if !saveDataHasKey(list[0], primaryKey) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// saveDataHasKey reports whether the save data contains the given key case-insensitively.
|
||||
func saveDataHasKey(data gdb.Map, key string) bool {
|
||||
for dataKey := range data {
|
||||
if strings.EqualFold(dataKey, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -439,8 +439,26 @@ func Test_DB_Save(t *testing.T) {
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
result, err := db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNil(err)
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(rowsAffected, 1)
|
||||
|
||||
data["nickname"] = "T10-updated"
|
||||
result, err = db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNil(err)
|
||||
rowsAffected, err = result.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(rowsAffected, 1)
|
||||
|
||||
value, err := db.Model("t_user").Where("id", i).Value("nickname")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "T10-updated")
|
||||
|
||||
delete(data, "id")
|
||||
_, err = db.Save(ctx, "t_user", data, 10)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-08
|
||||
5
openspec/changes/sqlite-save-primary-conflict/design.md
Normal file
5
openspec/changes/sqlite-save-primary-conflict/design.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Design
|
||||
|
||||
The SQLite drivers will prepare `Save` insert options before delegating to the shared `gdb.Core.DoInsert` implementation. When `InsertOptionSave` is used without explicit conflict columns, each driver reads the table primary keys through `Core.GetPrimaryKeys`, verifies that the first save record includes those primary key fields, and sets `DoInsertOption.OnConflict` accordingly. Existing explicit `OnConflict(...)` behavior remains unchanged.
|
||||
|
||||
If no usable primary key can be found in the save data, the drivers will return a missing-parameter error with guidance to specify `OnConflict(...)` or include primary key values.
|
||||
@ -0,0 +1,3 @@
|
||||
# SQLite Save Primary Conflict
|
||||
|
||||
SQLite `Save` currently requires callers to specify `OnConflict(...)` explicitly, even when the target table has a primary key and the save data includes that primary key. This change aligns the SQLite and SQLiteCGO drivers with other upsert-capable drivers by automatically using table primary keys as the conflict target when `OnConflict(...)` is omitted.
|
||||
@ -0,0 +1,21 @@
|
||||
# sqlite-upsert Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: SQLite Save infers primary-key conflicts
|
||||
The SQLite and SQLiteCGO drivers SHALL infer the table primary key columns as the upsert conflict target for `Save` operations when callers do not explicitly provide `OnConflict(...)`.
|
||||
|
||||
#### Scenario: Save data includes the primary key
|
||||
- **WHEN** a caller executes `Save` against a SQLite table without `OnConflict(...)`
|
||||
- **AND** the table has primary key columns present in the save data
|
||||
- **THEN** the driver SHALL use those primary key columns as the conflict target
|
||||
- **AND** the save SHALL insert new rows or update existing rows using SQLite upsert syntax
|
||||
|
||||
#### Scenario: Save data has no usable primary key
|
||||
- **WHEN** a caller executes `Save` against a SQLite table without `OnConflict(...)`
|
||||
- **AND** the table does not have primary key columns present in the save data
|
||||
- **THEN** the driver SHALL return a missing-parameter error instructing the caller to specify `OnConflict(...)` or include primary key values
|
||||
|
||||
#### Scenario: Explicit conflict columns are provided
|
||||
- **WHEN** a caller executes `Save` against a SQLite table with `OnConflict(...)`
|
||||
- **THEN** the driver SHALL use the explicitly provided conflict columns
|
||||
3
openspec/changes/sqlite-save-primary-conflict/tasks.md
Normal file
3
openspec/changes/sqlite-save-primary-conflict/tasks.md
Normal file
@ -0,0 +1,3 @@
|
||||
## Feedback
|
||||
|
||||
- [x] **FB-1**: Allow SQLite and SQLiteCGO `Save` to infer primary-key conflict columns when `OnConflict` is omitted
|
||||
Reference in New Issue
Block a user