mirror of
https://gitee.com/johng/gf
synced 2026-06-12 04:03:22 +08:00
Compare commits
28 Commits
v2.9.7
...
copilot/su
| Author | SHA1 | Date | |
|---|---|---|---|
| e1282c2698 | |||
| bc3b20e64c | |||
| dddfe4647a | |||
| ce76575d71 | |||
| 3658a09b52 | |||
| 57e5126b44 | |||
| ffb3aacb5b | |||
| 141b5c1778 | |||
| 73b91ed763 | |||
| 36d63354e7 | |||
| 8b48a605ad | |||
| f6a7fd23f1 | |||
| 17906e31cb | |||
| bfe31a4be7 | |||
| 5f664f331a | |||
| 4080452ead | |||
| 67a8a28a18 | |||
| d8fa0a7922 | |||
| b7cd39a8b8 | |||
| 01cd4a3384 | |||
| 111f8b3264 | |||
| ba44475765 | |||
| 99536c8bef | |||
| 91e3f1eab1 | |||
| ffe65d9d4a | |||
| 8723999afc | |||
| 0d122d6fee | |||
| 63c2bb7c86 |
12
.github/workflows/ci-main.yml
vendored
12
.github/workflows/ci-main.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
# Service containers to run with `code-test`
|
||||
services:
|
||||
# Etcd service.
|
||||
# docker run -d --name etcd -p 2379:2379 -e ALLOW_NONE_AUTHENTICATION=yes bitnamilegacy/etcd:3.4.24
|
||||
# docker run -p 2379:2379 -e ALLOW_NONE_AUTHENTICATION=yes bitnamilegacy/etcd:3.4.24
|
||||
etcd:
|
||||
image: bitnamilegacy/etcd:3.4.24
|
||||
env:
|
||||
@ -75,7 +75,7 @@ jobs:
|
||||
- 6379:6379
|
||||
|
||||
# MySQL backend server.
|
||||
# docker run -d --name mysql \
|
||||
# docker run \
|
||||
# -p 3306:3306 \
|
||||
# -e MYSQL_DATABASE=test \
|
||||
# -e MYSQL_ROOT_PASSWORD=12345678 \
|
||||
@ -89,7 +89,7 @@ jobs:
|
||||
- 3306:3306
|
||||
|
||||
# MariaDb backend server.
|
||||
# docker run -d --name mariadb \
|
||||
# docker run \
|
||||
# -p 3307:3306 \
|
||||
# -e MYSQL_DATABASE=test \
|
||||
# -e MYSQL_ROOT_PASSWORD=12345678 \
|
||||
@ -103,7 +103,7 @@ jobs:
|
||||
- 3307:3306
|
||||
|
||||
# PostgreSQL backend server.
|
||||
# docker run -d --name postgres \
|
||||
# docker run \
|
||||
# -p 5432:5432 \
|
||||
# -e POSTGRES_PASSWORD=12345678 \
|
||||
# -e POSTGRES_USER=postgres \
|
||||
@ -150,7 +150,7 @@ jobs:
|
||||
--health-retries 10
|
||||
|
||||
# ClickHouse backend server.
|
||||
# docker run -d --name clickhouse \
|
||||
# docker run \
|
||||
# -p 9000:9000 -p 8123:8123 -p 9001:9001 \
|
||||
# clickhouse/clickhouse-server:24.11.1.2557-alpine
|
||||
clickhouse-server:
|
||||
@ -161,7 +161,7 @@ jobs:
|
||||
- 9001:9001
|
||||
|
||||
# Polaris backend server.
|
||||
# docker run -d --name polaris \
|
||||
# docker run \
|
||||
# -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 \
|
||||
# polarismesh/polaris-standalone:v1.17.2
|
||||
polaris:
|
||||
|
||||
@ -104,6 +104,10 @@ var (
|
||||
"smallmoney": {
|
||||
Type: "float64",
|
||||
},
|
||||
"uuid": {
|
||||
Type: "uuid.UUID",
|
||||
Import: "github.com/google/uuid",
|
||||
},
|
||||
}
|
||||
|
||||
// tablewriter Options
|
||||
|
||||
@ -57,7 +57,7 @@ import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
|
||||
#### cgo version
|
||||
|
||||
When the target is a 32-bit Windows system, the cgo version needs to be used.
|
||||
When the target is a `32-bit` Windows system, the `cgo` version needs to be used.
|
||||
|
||||
```go
|
||||
import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2"
|
||||
@ -69,10 +69,6 @@ import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2"
|
||||
import _ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
|
||||
### SQL Server
|
||||
|
||||
```go
|
||||
@ -81,9 +77,10 @@ import _ "github.com/gogf/gf/contrib/drivers/mssql/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
- It supports server version >= `SQL Server2005`
|
||||
- It ONLY supports datetime2 and datetimeoffset types for auto handling created_at/updated_at/deleted_at columns, because datetime type does not support microseconds precision when column value is passed as string.
|
||||
- It ONLY supports `datetime2` and `datetimeoffset` types for auto handling created_at/updated_at/deleted_at columns,
|
||||
because datetime type does not support microseconds precision when column value is passed as string.
|
||||
|
||||
### Oracle
|
||||
|
||||
@ -93,8 +90,8 @@ import _ "github.com/gogf/gf/contrib/drivers/oracle/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `Replace` features.
|
||||
- It does not support `LastInsertId`.
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
|
||||
### ClickHouse
|
||||
|
||||
@ -104,7 +101,7 @@ import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
|
||||
|
||||
Note:
|
||||
|
||||
- It does not support `InsertIgnore/InsertGetId` features.
|
||||
- It does not support `InsertIgnore/InsertAndGetId` features.
|
||||
- It does not support `Save/Replace` features.
|
||||
- It does not support `Transaction` feature.
|
||||
- It does not support `RowsAffected` feature.
|
||||
@ -115,6 +112,10 @@ Note:
|
||||
import _ "github.com/gogf/gf/contrib/drivers/dm/v2"
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
|
||||
|
||||
## Custom Drivers
|
||||
|
||||
It's quick and easy, please refer to current driver source.
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
|
||||
@ -20,6 +20,7 @@ import (
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
@ -60,16 +61,12 @@ func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link,
|
||||
table string,
|
||||
list gdb.List,
|
||||
option gdb.DoInsertOption,
|
||||
withUpdate bool,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
conflictKeys, err = d.getPrimaryKeys(ctx, table)
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
@ -77,29 +74,33 @@ func (d *Driver) doMergeInsert(
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
if len(conflictKeys) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Please specify conflict columns or ensure the table has a primary key`,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
opName := "Save"
|
||||
if !withUpdate {
|
||||
opName = "InsertIgnore"
|
||||
}
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidRequest, `%s operation list is empty by dm driver`, opName,
|
||||
)
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be merged
|
||||
@ -155,24 +156,6 @@ func (d *Driver) doMergeInsert(
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// getPrimaryKeys retrieves the primary key field names of the table as a slice of strings.
|
||||
// This method extracts primary key information from TableFields.
|
||||
func (d *Driver) getPrimaryKeys(ctx context.Context, table string) ([]string, error) {
|
||||
tableFields, err := d.TableFields(ctx, table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if field.Key == "PRI" {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
// parseSqlForMerge generates MERGE statement for DM database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
|
||||
@ -602,31 +602,38 @@ func Test_Model_InsertIgnore(t *testing.T) {
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := User{
|
||||
ID: int64(666),
|
||||
AccountName: fmt.Sprintf(`name_%d`, 666),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := User{
|
||||
ID: int64(666),
|
||||
AccountName: fmt.Sprintf(`name_%d`, 777),
|
||||
PwdReset: 0,
|
||||
AttrIndex: 99,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"account_name": fmt.Sprintf(`name_%d`, 777),
|
||||
"pwd_reset": 0,
|
||||
"attr_index": 777,
|
||||
"created_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("id", 666).One()
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["ACCOUNT_NAME"].String(), "name_666")
|
||||
t.Assert(one["ACCOUNT_NAME"].String(), "name_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
// "id": 1,
|
||||
"account_name": fmt.Sprintf(`name_%d`, 777),
|
||||
"pwd_reset": 0,
|
||||
"attr_index": 777,
|
||||
"created_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
@ -167,8 +167,8 @@ func (r *InsertResult) RowsAffected() (int64, error) {
|
||||
}
|
||||
|
||||
// GetInsertOutputSql gen get last_insert_id code
|
||||
func (m *Driver) GetInsertOutputSql(ctx context.Context, table string) string {
|
||||
fds, errFd := m.GetDB().TableFields(ctx, table)
|
||||
func (d *Driver) GetInsertOutputSql(ctx context.Context, table string) string {
|
||||
fds, errFd := d.GetDB().TableFields(ctx, table)
|
||||
if errFd != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -20,51 +20,94 @@ import (
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
switch option.InsertOption {
|
||||
case gdb.InsertOptionSave:
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionReplace:
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeNotSupported,
|
||||
`Replace operation is not supported by mssql driver`,
|
||||
)
|
||||
// MSSQL does not support REPLACE INTO syntax, use SAVE instead.
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionIgnore:
|
||||
// MSSQL does not support INSERT IGNORE syntax, use MERGE instead.
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
default:
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
}
|
||||
|
||||
// doSave support upsert for SQL server
|
||||
// doSave support upsert for MSSQL
|
||||
func (d *Driver) doSave(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if len(option.OnConflict) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeMissingParameter, `Please specify conflict columns`,
|
||||
)
|
||||
}
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeInvalidRequest, `Save operation list is empty by mssql driver`,
|
||||
)
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for MSSQL database.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for MSSQL database.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
|
||||
conflictKeys = option.OnConflict
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be upsert
|
||||
// queryValues: Handle data that need to be upsert
|
||||
// queryHolders: Handle data with Holder that need to be merged
|
||||
// queryValues: Handle data that need to be merged
|
||||
// insertKeys: Handle valid keys that need to be inserted
|
||||
// insertValues: Handle values that need to be inserted
|
||||
// updateValues: Handle values that need to be updated
|
||||
// updateValues: Handle values that need to be updated (only when withUpdate=true)
|
||||
queryHolders = make([]string, oneLen)
|
||||
queryValues = make([]any, oneLen)
|
||||
insertKeys = make([]string, oneLen)
|
||||
@ -84,9 +127,9 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
insertKeys[index] = charL + key + charR
|
||||
insertValues[index] = "T2." + charL + key + charR
|
||||
|
||||
// filter conflict keys in updateValues.
|
||||
// And the key is not a soft created field.
|
||||
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
// Build updateValues only when withUpdate is true
|
||||
// Filter conflict keys and soft created fields from updateValues
|
||||
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, charL+key+charR, charL+key+charR),
|
||||
@ -95,8 +138,10 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
index++
|
||||
}
|
||||
|
||||
batchResult := new(gdb.SqlResult)
|
||||
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
@ -110,41 +155,48 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForUpsert
|
||||
// MERGE INTO {{table}} T1
|
||||
// USING ( VALUES( {{queryHolders}}) T2 ({{insertKeyStr}})
|
||||
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
|
||||
// WHEN NOT MATCHED THEN
|
||||
// INSERT {{insertKeys}} VALUES {{insertValues}}
|
||||
// WHEN MATCHED THEN
|
||||
// UPDATE SET {{updateValues}}
|
||||
func parseSqlForUpsert(table string,
|
||||
// parseSqlForMerge generates MERGE statement for MSSQL database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
queryHolderStr = strings.Join(queryHolders, ",")
|
||||
insertKeyStr = strings.Join(insertKeys, ",")
|
||||
insertValueStr = strings.Join(insertValues, ",")
|
||||
updateValueStr = strings.Join(updateValues, ",")
|
||||
duplicateKeyStr string
|
||||
pattern = gstr.Trim(`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`)
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
for index, keys := range duplicateKey {
|
||||
if index != 0 {
|
||||
duplicateKeyStr += " AND "
|
||||
}
|
||||
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
duplicateKeyStr += duplicateTmp
|
||||
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(pattern,
|
||||
table,
|
||||
queryHolderStr,
|
||||
insertKeyStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
updateValueStr,
|
||||
// Build SQL based on whether UPDATE is needed
|
||||
pattern := gstr.Trim(
|
||||
`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
|
||||
)
|
||||
if len(updateValues) > 0 {
|
||||
// Upsert: INSERT or UPDATE
|
||||
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
|
||||
return fmt.Sprintf(
|
||||
pattern+";",
|
||||
table,
|
||||
queryHolderStr,
|
||||
insertKeyStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
strings.Join(updateValues, ","),
|
||||
)
|
||||
}
|
||||
// Insert Ignore: INSERT only
|
||||
return fmt.Sprintf(pattern+";", table, queryHolderStr, insertKeyStr, duplicateKeyStr, insertKeyStr, insertValueStr)
|
||||
}
|
||||
|
||||
@ -138,15 +138,17 @@ func TestDoInsert(t *testing.T) {
|
||||
|
||||
i := 10
|
||||
data := g.Map{
|
||||
"id": i,
|
||||
// "id": i,
|
||||
"passport": fmt.Sprintf(`t%d`, i),
|
||||
"password": fmt.Sprintf(`p%d`, i),
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
// Save without OnConflict should fail (missing conflict columns)
|
||||
_, err := db.Save(context.Background(), "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
|
||||
// Replace should fail because primary key 'id' is not in the data
|
||||
_, err = db.Replace(context.Background(), "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
})
|
||||
|
||||
@ -117,6 +117,48 @@ func Test_Model_Insert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "user_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Insert_KeyFieldNameMapping(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
@ -2658,14 +2700,53 @@ func Test_Model_Replace(t *testing.T) {
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
// Insert initial record
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "pass1",
|
||||
"nickname": "T1",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Replace with new data (should update existing record using MERGE)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t11",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T11",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Replace()
|
||||
t.Assert(err, "Replace operation is not supported by mssql driver")
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t11")
|
||||
t.Assert(one["NICKNAME"].String(), "T11")
|
||||
|
||||
// Replace with non-existing record (should insert new record)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "t222",
|
||||
"password": "pass2",
|
||||
"nickname": "T222",
|
||||
"create_time": "2018-10-24 11:00:00",
|
||||
}).Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1) // MERGE reports: 1 for insert
|
||||
|
||||
// Verify the new record was inserted
|
||||
one, err = db.Model(table).WherePri(2).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t222")
|
||||
t.Assert(one["NICKNAME"].String(), "T222")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// DoInsert inserts or updates data for given table.
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
@ -29,10 +30,14 @@ func (d *Driver) DoInsert(
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionReplace:
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeNotSupported,
|
||||
`Replace operation is not supported by oracle driver`,
|
||||
)
|
||||
// Oracle does not support REPLACE INTO syntax, use SAVE instead.
|
||||
return d.doSave(ctx, link, table, list, option)
|
||||
|
||||
case gdb.InsertOptionIgnore:
|
||||
// Oracle does not support INSERT IGNORE syntax, use MERGE instead.
|
||||
return d.doInsertIgnore(ctx, link, table, list, option)
|
||||
|
||||
default:
|
||||
}
|
||||
var (
|
||||
keys []string
|
||||
@ -97,24 +102,62 @@ func (d *Driver) DoInsert(
|
||||
func (d *Driver) doSave(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if len(option.OnConflict) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeMissingParameter, `Please specify conflict columns`,
|
||||
)
|
||||
}
|
||||
return d.doMergeInsert(ctx, link, table, list, option, true)
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeInvalidRequest, `Save operation list is empty by oracle driver`,
|
||||
)
|
||||
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database.
|
||||
// It only inserts records when there's no conflict on primary/unique keys.
|
||||
func (d *Driver) doInsertIgnore(ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
return d.doMergeInsert(ctx, link, table, list, option, false)
|
||||
}
|
||||
|
||||
// doMergeInsert implements MERGE-based insert operations for Oracle database.
|
||||
// When withUpdate is true, it performs upsert (insert or update).
|
||||
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
|
||||
func (d *Driver) doMergeInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
|
||||
) (result sql.Result, err error) {
|
||||
// If OnConflict is not specified, automatically get the primary key of the table
|
||||
conflictKeys := option.OnConflict
|
||||
if len(conflictKeys) == 0 {
|
||||
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
|
||||
if err != nil {
|
||||
return nil, gerror.WrapCode(
|
||||
gcode.CodeInternalError,
|
||||
err,
|
||||
`failed to get primary keys for table`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Replace/Save/InsertIgnore operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
conflictKeys = primaryKeys
|
||||
}
|
||||
|
||||
var (
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
|
||||
conflictKeys = option.OnConflict
|
||||
one = list[0]
|
||||
oneLen = len(one)
|
||||
charL, charR = d.GetChars()
|
||||
conflictKeySet = gset.New(false)
|
||||
|
||||
// queryHolders: Handle data with Holder that need to be upsert
|
||||
@ -142,9 +185,9 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
insertKeys[index] = keyWithChar
|
||||
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
|
||||
|
||||
// filter conflict keys in updateValues.
|
||||
// And the key is not a soft created field.
|
||||
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
// Build updateValues only when withUpdate is true
|
||||
// Filter conflict keys and soft created fields from updateValues
|
||||
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
|
||||
updateValues = append(
|
||||
updateValues,
|
||||
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
|
||||
@ -153,8 +196,10 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
index++
|
||||
}
|
||||
|
||||
batchResult := new(gdb.SqlResult)
|
||||
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
var (
|
||||
batchResult = new(gdb.SqlResult)
|
||||
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
|
||||
)
|
||||
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
|
||||
if err != nil {
|
||||
return r, err
|
||||
@ -168,40 +213,40 @@ func (d *Driver) doSave(ctx context.Context,
|
||||
return batchResult, nil
|
||||
}
|
||||
|
||||
// parseSqlForUpsert
|
||||
// MERGE INTO {{table}} T1
|
||||
// USING ( SELECT {{queryHolders}} FROM DUAL T2
|
||||
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
|
||||
// WHEN NOT MATCHED THEN
|
||||
// INSERT {{insertKeys}} VALUES {{insertValues}}
|
||||
// WHEN MATCHED THEN
|
||||
// UPDATE SET {{updateValues}}
|
||||
func parseSqlForUpsert(table string,
|
||||
// parseSqlForMerge generates MERGE statement for Oracle database.
|
||||
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
|
||||
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
|
||||
// Examples:
|
||||
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
|
||||
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
|
||||
func parseSqlForMerge(table string,
|
||||
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
|
||||
) (sqlStr string) {
|
||||
var (
|
||||
queryHolderStr = strings.Join(queryHolders, ",")
|
||||
insertKeyStr = strings.Join(insertKeys, ",")
|
||||
insertValueStr = strings.Join(insertValues, ",")
|
||||
updateValueStr = strings.Join(updateValues, ",")
|
||||
duplicateKeyStr string
|
||||
pattern = gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s`)
|
||||
)
|
||||
|
||||
// Build ON condition
|
||||
for index, keys := range duplicateKey {
|
||||
if index != 0 {
|
||||
duplicateKeyStr += " AND "
|
||||
}
|
||||
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
duplicateKeyStr += duplicateTmp
|
||||
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(pattern,
|
||||
table,
|
||||
queryHolderStr,
|
||||
duplicateKeyStr,
|
||||
insertKeyStr,
|
||||
insertValueStr,
|
||||
updateValueStr,
|
||||
)
|
||||
// Build SQL based on whether UPDATE is needed
|
||||
pattern := gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`)
|
||||
if len(updateValues) > 0 {
|
||||
// Upsert: INSERT or UPDATE
|
||||
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
|
||||
return fmt.Sprintf(
|
||||
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
|
||||
strings.Join(updateValues, ","),
|
||||
)
|
||||
}
|
||||
// Insert Ignore: INSERT only
|
||||
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
|
||||
}
|
||||
|
||||
@ -18,13 +18,23 @@ import (
|
||||
var (
|
||||
tableFieldsSqlTmp = `
|
||||
SELECT
|
||||
COLUMN_NAME AS FIELD,
|
||||
c.COLUMN_NAME AS FIELD,
|
||||
CASE
|
||||
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)=0) THEN 'INT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)>0) THEN 'FLOAT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
WHEN DATA_TYPE='FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
|
||||
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE,NULLABLE
|
||||
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)=0) THEN 'INT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)>0) THEN 'FLOAT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
WHEN c.DATA_TYPE='FLOAT' THEN c.DATA_TYPE||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
|
||||
ELSE c.DATA_TYPE||'('||c.DATA_LENGTH||')' END AS TYPE,
|
||||
c.NULLABLE,
|
||||
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRI' ELSE '' END AS KEY
|
||||
FROM USER_TAB_COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT cols.COLUMN_NAME
|
||||
FROM USER_CONSTRAINTS cons
|
||||
JOIN USER_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME
|
||||
WHERE cons.TABLE_NAME = '%s' AND cons.CONSTRAINT_TYPE = 'P'
|
||||
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = '%s'
|
||||
ORDER BY c.COLUMN_ID
|
||||
`
|
||||
)
|
||||
|
||||
@ -44,7 +54,8 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
result gdb.Result
|
||||
link gdb.Link
|
||||
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, strings.ToUpper(table))
|
||||
upperTable = strings.ToUpper(table)
|
||||
structureSql = fmt.Sprintf(tableFieldsSqlTmp, upperTable, upperTable)
|
||||
)
|
||||
if link, err = d.SlaveLink(usedSchema); err != nil {
|
||||
return nil, err
|
||||
@ -53,6 +64,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields = make(map[string]*gdb.TableField)
|
||||
for i, m := range result {
|
||||
isNull := false
|
||||
@ -65,6 +77,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
Name: m["FIELD"].String(),
|
||||
Type: m["TYPE"].String(),
|
||||
Null: isNull,
|
||||
Key: m["KEY"].String(),
|
||||
}
|
||||
}
|
||||
return fields, nil
|
||||
|
||||
@ -139,10 +139,10 @@ func Test_Do_Insert(t *testing.T) {
|
||||
"CREATE_TIME": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
_, err = db.Replace(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -233,6 +233,48 @@ func Test_Model_Insert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// db.SetDebug(true)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "user_1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"passport": fmt.Sprintf(`t%d`, 777),
|
||||
"password": fmt.Sprintf(`p%d`, 777),
|
||||
"nickname": fmt.Sprintf(`T%d`, 777),
|
||||
"create_time": gtime.Now(),
|
||||
}
|
||||
_, err := db.Model(table).Data(data).InsertIgnore()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/gogf/gf/issues/3286
|
||||
func Test_Model_Insert_Raw(t *testing.T) {
|
||||
table := createTable()
|
||||
@ -1179,14 +1221,73 @@ func Test_Model_Replace(t *testing.T) {
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
// Insert initial record
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "pass1",
|
||||
"nickname": "T1",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Replace with new data (should update existing record using MERGE)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t11",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T11",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).OnConflict("id").Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t11")
|
||||
t.Assert(one["PASSWORD"].String(), "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(one["NICKNAME"].String(), "T11")
|
||||
|
||||
// Replace with new ID (insert new record)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "t222",
|
||||
"password": "pass2",
|
||||
"nickname": "T222",
|
||||
"create_time": "2018-10-24 11:00:00",
|
||||
}).OnConflict("id").Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify new record was inserted
|
||||
one, err = db.Model(table).Where("id", 2).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["PASSPORT"].String(), "t222")
|
||||
t.Assert(one["NICKNAME"].String(), "T222")
|
||||
|
||||
// Replace without OnConflict should fail (no primary key detection yet)
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 3,
|
||||
"passport": "t3",
|
||||
"password": "pass3",
|
||||
"nickname": "T3",
|
||||
"create_time": "2018-10-24 12:00:00",
|
||||
}).Replace()
|
||||
t.Assert(err, "Replace operation is not supported by oracle driver")
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
// "id": 3,
|
||||
"passport": "t3",
|
||||
"password": "pass3",
|
||||
"nickname": "T3",
|
||||
"create_time": "2018-10-24 12:00:00",
|
||||
}).Replace()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ package pgsql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
@ -16,25 +17,67 @@ import (
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// The list parameter must contain at least one record, which was previously validated.
|
||||
func (d *Driver) DoInsert(
|
||||
ctx context.Context,
|
||||
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
switch option.InsertOption {
|
||||
case gdb.InsertOptionReplace:
|
||||
return nil, gerror.NewCode(
|
||||
gcode.CodeNotSupported,
|
||||
`Replace operation is not supported by pgsql driver`,
|
||||
)
|
||||
case
|
||||
gdb.InsertOptionSave,
|
||||
gdb.InsertOptionReplace:
|
||||
// PostgreSQL does not support REPLACE INTO syntax, use Save (ON CONFLICT ... DO UPDATE) instead.
|
||||
// Automatically detect primary keys if OnConflict is not specified.
|
||||
if 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/Replace operation`,
|
||||
)
|
||||
}
|
||||
foundPrimaryKey := false
|
||||
for _, primaryKey := range primaryKeys {
|
||||
for dataKey := range list[0] {
|
||||
if strings.EqualFold(dataKey, primaryKey) {
|
||||
foundPrimaryKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundPrimaryKey {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPrimaryKey {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeMissingParameter,
|
||||
`Replace/Save operation requires conflict detection: `+
|
||||
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
|
||||
table,
|
||||
)
|
||||
}
|
||||
option.OnConflict = primaryKeys
|
||||
}
|
||||
// Treat Replace as Save operation
|
||||
option.InsertOption = gdb.InsertOptionSave
|
||||
|
||||
case gdb.InsertOptionDefault:
|
||||
// pgsql support InsertIgnore natively, so no need to set primary key in context.
|
||||
case gdb.InsertOptionIgnore, gdb.InsertOptionDefault:
|
||||
// Get table fields to retrieve the primary key TableField object (not just the name)
|
||||
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
|
||||
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
|
||||
if err == nil {
|
||||
for _, field := range tableFields {
|
||||
if field.Key == "pri" {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
pkField := *field
|
||||
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
return d.Core.DoInsert(ctx, link, table, list, option)
|
||||
}
|
||||
|
||||
@ -80,10 +80,22 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
fieldType string
|
||||
dataType = m["type"].String()
|
||||
dataLength = m["length"].Int()
|
||||
)
|
||||
if dataLength > 0 {
|
||||
fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength)
|
||||
} else {
|
||||
fieldType = dataType
|
||||
}
|
||||
|
||||
fields[name] = &gdb.TableField{
|
||||
Index: index,
|
||||
Name: name,
|
||||
Type: m["type"].String(),
|
||||
Type: fieldType,
|
||||
Null: !m["null"].Bool(),
|
||||
Key: m["key"].String(),
|
||||
Default: m["default_value"].Val(),
|
||||
|
||||
@ -90,7 +90,7 @@ func Test_DB_Save(t *testing.T) {
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Save(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
gtest.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -99,6 +99,7 @@ func Test_DB_Replace(t *testing.T) {
|
||||
createTable("t_user")
|
||||
defer dropTable("t_user")
|
||||
|
||||
// Insert initial record
|
||||
i := 10
|
||||
data := g.Map{
|
||||
"id": i,
|
||||
@ -107,8 +108,26 @@ func Test_DB_Replace(t *testing.T) {
|
||||
"nickname": fmt.Sprintf(`T%d`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err := db.Replace(ctx, "t_user", data, 10)
|
||||
gtest.AssertNE(err, nil)
|
||||
_, err := db.Insert(ctx, "t_user", data)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
// Replace with new data
|
||||
data2 := g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`t%d_new`, i),
|
||||
"password": fmt.Sprintf(`p%d_new`, i),
|
||||
"nickname": fmt.Sprintf(`T%d_new`, i),
|
||||
"create_time": gtime.Now().String(),
|
||||
}
|
||||
_, err = db.Replace(ctx, "t_user", data2)
|
||||
gtest.AssertNil(err)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM t_user WHERE id=?"), i)
|
||||
gtest.AssertNil(err)
|
||||
gtest.Assert(one["passport"].String(), fmt.Sprintf(`t%d_new`, i))
|
||||
gtest.Assert(one["password"].String(), fmt.Sprintf(`p%d_new`, i))
|
||||
gtest.Assert(one["nickname"].String(), fmt.Sprintf(`T%d_new`, i))
|
||||
})
|
||||
}
|
||||
|
||||
@ -304,10 +323,10 @@ func Test_DB_TableFields(t *testing.T) {
|
||||
var expect = map[string][]any{
|
||||
// []string: Index Type Null Key Default Comment
|
||||
// id is bigserial so the default is a pgsql function
|
||||
"id": {0, "int8", false, "pri", fmt.Sprintf("nextval('%s_id_seq'::regclass)", table), ""},
|
||||
"passport": {1, "varchar", false, "", nil, ""},
|
||||
"password": {2, "varchar", false, "", nil, ""},
|
||||
"nickname": {3, "varchar", false, "", nil, ""},
|
||||
"id": {0, "int8(64)", false, "pri", fmt.Sprintf("nextval('%s_id_seq'::regclass)", table), ""},
|
||||
"passport": {1, "varchar(45)", false, "", nil, ""},
|
||||
"password": {2, "varchar(32)", false, "", nil, ""},
|
||||
"nickname": {3, "varchar(45)", false, "", nil, ""},
|
||||
"create_time": {4, "timestamp", false, "", nil, ""},
|
||||
}
|
||||
|
||||
@ -410,13 +429,13 @@ func Test_DB_TableFields_DuplicateConstraints(t *testing.T) {
|
||||
t.AssertNE(fields["id"], nil)
|
||||
t.Assert(fields["id"].Key, "pri")
|
||||
t.Assert(fields["id"].Name, "id")
|
||||
t.Assert(fields["id"].Type, "int8")
|
||||
t.Assert(fields["id"].Type, "int8(64)")
|
||||
|
||||
// Verify email field has unique constraint
|
||||
t.AssertNE(fields["email"], nil)
|
||||
t.Assert(fields["email"].Key, "uni")
|
||||
t.Assert(fields["email"].Name, "email")
|
||||
t.Assert(fields["email"].Type, "varchar")
|
||||
t.Assert(fields["email"].Type, "varchar(100)")
|
||||
|
||||
// Verify username field has no constraint
|
||||
t.AssertNE(fields["username"], nil)
|
||||
|
||||
@ -73,18 +73,18 @@ func Test_TableFields_Types(t *testing.T) {
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test integer type names
|
||||
t.Assert(fields["col_int2"].Type, "int2")
|
||||
t.Assert(fields["col_int4"].Type, "int4")
|
||||
t.Assert(fields["col_int8"].Type, "int8")
|
||||
t.Assert(fields["col_int2"].Type, "int2(16)")
|
||||
t.Assert(fields["col_int4"].Type, "int4(32)")
|
||||
t.Assert(fields["col_int8"].Type, "int8(64)")
|
||||
|
||||
// Test float type names
|
||||
t.Assert(fields["col_float4"].Type, "float4")
|
||||
t.Assert(fields["col_float8"].Type, "float8")
|
||||
t.Assert(fields["col_numeric"].Type, "numeric")
|
||||
t.Assert(fields["col_float4"].Type, "float4(24)")
|
||||
t.Assert(fields["col_float8"].Type, "float8(53)")
|
||||
t.Assert(fields["col_numeric"].Type, "numeric(10)")
|
||||
|
||||
// Test character type names
|
||||
t.Assert(fields["col_char"].Type, "bpchar")
|
||||
t.Assert(fields["col_varchar"].Type, "varchar")
|
||||
t.Assert(fields["col_char"].Type, "bpchar(10)")
|
||||
t.Assert(fields["col_varchar"].Type, "varchar(100)")
|
||||
t.Assert(fields["col_text"].Type, "text")
|
||||
|
||||
// Test boolean type name
|
||||
|
||||
@ -334,14 +334,53 @@ func Test_Model_Replace(t *testing.T) {
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
// Insert initial record
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t1",
|
||||
"password": "pass1",
|
||||
"nickname": "T1",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Replace with new data
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "t11",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "T11",
|
||||
"create_time": "2018-10-24 10:00:00",
|
||||
}).Replace()
|
||||
t.Assert(err, "Replace operation is not supported by pgsql driver")
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the data was replaced
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"].String(), "t11")
|
||||
t.Assert(one["password"].String(), "25d55ad283aa400af464c76d713c07ad")
|
||||
t.Assert(one["nickname"].String(), "T11")
|
||||
|
||||
// Replace with new ID (insert new record)
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 2,
|
||||
"passport": "t22",
|
||||
"password": "pass22",
|
||||
"nickname": "T22",
|
||||
"create_time": "2018-10-24 11:00:00",
|
||||
}).Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify new record was inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 2)
|
||||
})
|
||||
}
|
||||
|
||||
@ -757,3 +796,69 @@ func Test_ConvertSliceFloat64(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Model_InsertIgnore(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_1",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t1",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_1",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 0)
|
||||
|
||||
value, err := db.Model(table).Fields("passport").WherePri(1).Value()
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.String(), "t1")
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
// pgsql support ignore without primary key
|
||||
result, err = db.Model(table).Data(g.Map{
|
||||
// "id": 1,
|
||||
"uid": 1,
|
||||
"passport": "t2",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad",
|
||||
"nickname": "name_2",
|
||||
"create_time": gtime.Now().String(),
|
||||
}).InsertIgnore()
|
||||
t.AssertNil(err)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
@ -219,10 +219,10 @@ func Test_FormatUpsert_NoOnConflict(t *testing.T) {
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Try Save without OnConflict - should fail for pgsql
|
||||
// PostgreSQL requires OnConflict() for Save() operations, unlike MySQL
|
||||
// Try Save without OnConflict and without primary key in data - should fail
|
||||
// because driver cannot auto-detect conflict columns when primary key is missing
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
// "id": 1,
|
||||
"passport": "no_conflict_user",
|
||||
"password": "newpwd",
|
||||
"nickname": "newnick",
|
||||
|
||||
@ -10,6 +10,7 @@ package gdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@ -251,3 +252,22 @@ func (c *Core) guessPrimaryTableName(tableStr string) string {
|
||||
}
|
||||
return guessedTableName
|
||||
}
|
||||
|
||||
// GetPrimaryKeys retrieves and returns the primary key field names of the specified table.
|
||||
// This method extracts primary key information from TableFields.
|
||||
// The parameter `schema` is optional, if not specified it uses the default schema.
|
||||
func (c *Core) GetPrimaryKeys(ctx context.Context, table string, schema ...string) ([]string, error) {
|
||||
tableFields, err := c.db.TableFields(ctx, table, schema...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var primaryKeys []string
|
||||
for _, field := range tableFields {
|
||||
if strings.EqualFold(field.Key, "pri") {
|
||||
primaryKeys = append(primaryKeys, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return primaryKeys, nil
|
||||
}
|
||||
|
||||
@ -109,7 +109,17 @@ func (d *DriverWrapperDB) TableFields(
|
||||
// InsertOptionReplace: if there's unique/primary key in the data, it deletes it from table and inserts a new one;
|
||||
// InsertOptionSave: if there's unique/primary key in the data, it updates it or else inserts a new one;
|
||||
// InsertOptionIgnore: if there's unique/primary key in the data, it ignores the inserting;
|
||||
func (d *DriverWrapperDB) DoInsert(ctx context.Context, link Link, table string, list List, option DoInsertOption) (result sql.Result, err error) {
|
||||
func (d *DriverWrapperDB) DoInsert(
|
||||
ctx context.Context, link Link, table string, list List, option DoInsertOption,
|
||||
) (result sql.Result, err error) {
|
||||
if len(list) == 0 {
|
||||
return nil, gerror.NewCodef(
|
||||
gcode.CodeInvalidRequest,
|
||||
`data list is empty for %s operation`,
|
||||
GetInsertOperationByOption(option.InsertOption),
|
||||
)
|
||||
}
|
||||
|
||||
// Convert data type before commit it to underlying db driver.
|
||||
for i, item := range list {
|
||||
list[i], err = d.GetCore().ConvertDataForRecord(ctx, item, table)
|
||||
|
||||
Reference in New Issue
Block a user