From 8b48a605ad82ef73acb8dedd001d501ce9565bc6 Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 8 Dec 2025 17:57:50 +0800 Subject: [PATCH] up --- contrib/drivers/README.MD | 5 + contrib/drivers/dm/dm_z_unit_basic_test.go | 49 +++--- contrib/drivers/mssql/mssql_do_insert.go | 162 ++++++++++-------- .../drivers/mssql/mssql_z_unit_basic_test.go | 42 +++++ contrib/drivers/pgsql/pgsql_do_insert.go | 19 +- .../drivers/pgsql/pgsql_z_unit_model_test.go | 19 ++ 6 files changed, 197 insertions(+), 99 deletions(-) diff --git a/contrib/drivers/README.MD b/contrib/drivers/README.MD index ccd652bac..ceea19422 100644 --- a/contrib/drivers/README.MD +++ b/contrib/drivers/README.MD @@ -77,6 +77,7 @@ import _ "github.com/gogf/gf/contrib/drivers/mssql/v2" Note: +- `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. @@ -110,6 +111,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. diff --git a/contrib/drivers/dm/dm_z_unit_basic_test.go b/contrib/drivers/dm/dm_z_unit_basic_test.go index da9ab215b..7f2e99ba7 100644 --- a/contrib/drivers/dm/dm_z_unit_basic_test.go +++ b/contrib/drivers/dm/dm_z_unit_basic_test.go @@ -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) }) } diff --git a/contrib/drivers/mssql/mssql_do_insert.go b/contrib/drivers/mssql/mssql_do_insert.go index 48142a478..558c4500c 100644 --- a/contrib/drivers/mssql/mssql_do_insert.go +++ b/contrib/drivers/mssql/mssql_do_insert.go @@ -25,62 +25,84 @@ 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, - gdb.InsertOptionReplace: - // MSSQL does not support REPLACE INTO syntax. - // Convert Replace to Save operation, using MERGE statement. - // Auto-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 Replace operation`, - ) - } - foundPrimaryKey := false - for _, primaryKey := range primaryKeys { - if _, ok := list[0][primaryKey]; ok { - foundPrimaryKey = true - break - } - } - if !foundPrimaryKey { - return nil, gerror.NewCodef( - gcode.CodeMissingParameter, - `Save/Replace operation requires conflict detection: `+ - `either specify OnConflict() columns or ensure table '%s' has a primary key in the data`, - table, - ) - } - option.OnConflict = primaryKeys - } - // Convert to Save operation + case gdb.InsertOptionSave: return d.doSave(ctx, link, table, list, option) + case gdb.InsertOptionReplace: + // 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) { + return d.doMergeInsert(ctx, link, table, list, option, true) +} + +// 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 { + if _, ok := list[0][primaryKey]; ok { + foundPrimaryKey = true + 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 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) @@ -100,9 +122,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), @@ -111,8 +133,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 @@ -126,44 +150,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) } diff --git a/contrib/drivers/mssql/mssql_z_unit_basic_test.go b/contrib/drivers/mssql/mssql_z_unit_basic_test.go index 9933f751c..c5450db59 100644 --- a/contrib/drivers/mssql/mssql_z_unit_basic_test.go +++ b/contrib/drivers/mssql/mssql_z_unit_basic_test.go @@ -1286,3 +1286,45 @@ func Test_Empty_Slice_Argument(t *testing.T) { t.Assert(len(result), 0) }) } + +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) + }) +} diff --git a/contrib/drivers/pgsql/pgsql_do_insert.go b/contrib/drivers/pgsql/pgsql_do_insert.go index 55e6f761c..691063fa4 100644 --- a/contrib/drivers/pgsql/pgsql_do_insert.go +++ b/contrib/drivers/pgsql/pgsql_do_insert.go @@ -13,7 +13,6 @@ import ( "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/text/gstr" ) // DoInsert inserts or updates data for given table. @@ -57,16 +56,14 @@ func (d *Driver) DoInsert( // Treat Replace as Save operation option.InsertOption = gdb.InsertOptionSave - case gdb.InsertOptionDefault: - tableFields, err := d.GetCore().GetDB().TableFields(ctx, table) - if err == nil { - for _, field := range tableFields { - if gstr.Equal(field.Key, "pri") { - pkField := *field - ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField) - break - } - } + // pgsql support InsertIgnore natively, so no need to set primary key in context. + case gdb.InsertOptionIgnore, gdb.InsertOptionDefault: + primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table) + if err != nil { + return nil, err + } + if len(primaryKeys) > 0 { + ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, primaryKeys[0]) } default: diff --git a/contrib/drivers/pgsql/pgsql_z_unit_model_test.go b/contrib/drivers/pgsql/pgsql_z_unit_model_test.go index f971ff2f2..4ca729891 100644 --- a/contrib/drivers/pgsql/pgsql_z_unit_model_test.go +++ b/contrib/drivers/pgsql/pgsql_z_unit_model_test.go @@ -841,5 +841,24 @@ func Test_Model_InsertIgnore(t *testing.T) { 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) }) }