Files
gf/database/gdb/gdb_core_utility.go
John Guo 852c3dda62 feat(contrib/drivers/dm&pgsql&mssql&oracle): add Replace/LastInsertId features support for dm/pgsql/mssql/oracle (#4547)
This pull request introduces significant improvements to the handling of
the `Replace` and `Save` operations for multiple database drivers,
especially for MSSQL and PostgreSQL. The changes ensure that these
operations now auto-detect primary keys when conflict columns are not
explicitly provided, improving usability and aligning behavior across
drivers. Additionally, the pull request updates related tests to reflect
these enhancements and includes some minor documentation and code
cleanup.

**Key changes:**

### Enhanced Replace/Save Logic for Database Drivers

* **MSSQL Driver:**
- `Replace` and `Save` operations now auto-detect primary keys if
`OnConflict` is not specified, using the `MERGE` statement for upsert
functionality. If no primary key is found in the data, a detailed error
is returned.
[[1]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L23-R61)
[[2]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L43-L59)
- Updated tests to verify that `Replace` correctly updates or inserts
records, and that missing conflict columns are properly handled.
[[1]](diffhunk://#diff-bdbde9d7d6ee14c795343767b414740c4396f4dd3e97788b1f9d4e615405a42dL141-R151)
[[2]](diffhunk://#diff-26338e93e473300b1313936eb0f6826546473793442f24715fa294b595f7a805L2661-R2707)

* **PostgreSQL Driver:**
- Similar to MSSQL, `Replace` and `Save` now auto-detect primary keys
for conflict resolution if `OnConflict` is not set, and treat `Replace`
as a `Save` operation.
- Adjusted tests to ensure `Save` and `Replace` work as expected,
including verifying data replacement and insertion.
[[1]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048L93-R93)
[[2]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048R102)
[[3]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048L110-R130)

* **DM Driver:**
- Improved conflict detection: now checks that at least one primary key
exists in the provided data when `OnConflict` is not specified, and
provides clearer error messages.
- Refactored to use the core method for primary key detection and
removed redundant code.

### Minor Improvements and Documentation

* Added clarifying comments to `DoInsert` methods for ClickHouse, DM,
MSSQL, Oracle, and PostgreSQL drivers, specifying that the input list
must have at least one validated record.
[[1]](diffhunk://#diff-f2e003895041ed3c52b91bb8c270696adc3528d77c39d2f7137af3396267444cR19)
[[2]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eR23)
[[3]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L23-R61)
[[4]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cR24)
[[5]](diffhunk://#diff-c1dfed79aaa3a432057d2bd74d270e4b4094ebcf72984f1161d4972bea009410R16-R72)
* Minor code and comment cleanups, including improved formatting and
error handling.
[[1]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cR37)
[[2]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cL96-R98)
[[3]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cL106-L116)
[[4]](diffhunk://#diff-a17b44c76aaac53d1f164a2bb9440a5531659f4355e7ccfabdadff8dc8633c09L170-R171)
[[5]](diffhunk://#diff-56189fa9ae1df51716b50d34d7fe56bfe67a330e8ac2c6b0de7b958db6817ed5R83-R98)

### Workflow and Documentation Updates

* Updated example Docker commands in the CI workflow for consistency and
clarity.
[[1]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL57-R57)
[[2]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL78-R78)
[[3]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL92-R92)
[[4]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL106-R106)
[[5]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL153-R153)
[[6]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL164-R164)
* Removed outdated note about `Replace` support from the SQLite driver
documentation.

These changes improve the consistency, reliability, and developer
experience when performing upsert operations across different database
backends.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lance Add <1196661499@qq.com>
2025-12-09 15:46:41 +08:00

274 lines
8.0 KiB
Go

// 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.
//
package gdb
import (
"context"
"fmt"
"strings"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gutil"
)
// GetDB returns the underlying DB.
func (c *Core) GetDB() DB {
return c.db
}
// GetLink creates and returns the underlying database link object with transaction checks.
// The parameter `master` specifies whether using the master node if master-slave configured.
func (c *Core) GetLink(ctx context.Context, master bool, schema string) (Link, error) {
tx := TXFromCtx(ctx, c.db.GetGroup())
if tx != nil {
return &txLink{tx.GetSqlTX()}, nil
}
if master {
link, err := c.db.GetCore().MasterLink(schema)
if err != nil {
return nil, err
}
return link, nil
}
link, err := c.db.GetCore().SlaveLink(schema)
if err != nil {
return nil, err
}
return link, nil
}
// MasterLink acts like function Master but with additional `schema` parameter specifying
// the schema for the connection. It is defined for internal usage.
// Also see Master.
func (c *Core) MasterLink(schema ...string) (Link, error) {
db, err := c.db.Master(schema...)
if err != nil {
return nil, err
}
return &dbLink{
DB: db,
isOnMaster: true,
}, nil
}
// SlaveLink acts like function Slave but with additional `schema` parameter specifying
// the schema for the connection. It is defined for internal usage.
// Also see Slave.
func (c *Core) SlaveLink(schema ...string) (Link, error) {
db, err := c.db.Slave(schema...)
if err != nil {
return nil, err
}
return &dbLink{
DB: db,
isOnMaster: false,
}, nil
}
// QuoteWord checks given string `s` a word,
// if true it quotes `s` with security chars of the database
// and returns the quoted string; or else it returns `s` without any change.
//
// The meaning of a `word` can be considered as a column name.
func (c *Core) QuoteWord(s string) string {
s = gstr.Trim(s)
if s == "" {
return s
}
charLeft, charRight := c.db.GetChars()
return doQuoteWord(s, charLeft, charRight)
}
// QuoteString quotes string with quote chars. Strings like:
// "user", "user u", "user,user_detail", "user u, user_detail ut", "u.id asc".
//
// The meaning of a `string` can be considered as part of a statement string including columns.
func (c *Core) QuoteString(s string) string {
if !gregex.IsMatchString(regularFieldNameWithCommaRegPattern, s) {
return s
}
charLeft, charRight := c.db.GetChars()
return doQuoteString(s, charLeft, charRight)
}
// QuotePrefixTableName adds prefix string and quotes chars for the table.
// It handles table string like:
// "user", "user u",
// "user,user_detail",
// "user u, user_detail ut",
// "user as u, user_detail as ut".
//
// Note that, this will automatically checks the table prefix whether already added,
// if true it does nothing to the table name, or else adds the prefix to the table name.
func (c *Core) QuotePrefixTableName(table string) string {
charLeft, charRight := c.db.GetChars()
return doQuoteTableName(table, c.db.GetPrefix(), charLeft, charRight)
}
// GetChars returns the security char for current database.
// It does nothing in default.
func (c *Core) GetChars() (charLeft string, charRight string) {
return "", ""
}
// Tables retrieves and returns the tables of current schema.
// It's mainly used in cli tool chain for automatically generating the models.
func (c *Core) Tables(ctx context.Context, schema ...string) (tables []string, err error) {
return
}
// TableFields retrieves and returns the fields' information of specified table of current
// schema.
//
// The parameter `link` is optional, if given nil it automatically retrieves a raw sql connection
// as its link to proceed necessary sql query.
//
// Note that it returns a map containing the field name and its corresponding fields.
// As a map is unsorted, the TableField struct has an "Index" field marks its sequence in
// the fields.
//
// It's using cache feature to enhance the performance, which is never expired util the
// process restarts.
func (c *Core) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) {
return
}
// ClearTableFields removes certain cached table fields of current configuration group.
func (c *Core) ClearTableFields(ctx context.Context, table string, schema ...string) (err error) {
tableFieldsCacheKey := genTableFieldsCacheKey(
c.db.GetGroup(),
gutil.GetOrDefaultStr(c.db.GetSchema(), schema...),
table,
)
_, err = c.innerMemCache.Remove(ctx, tableFieldsCacheKey)
return
}
// ClearTableFieldsAll removes all cached table fields of current configuration group.
func (c *Core) ClearTableFieldsAll(ctx context.Context) (err error) {
var (
keys, _ = c.innerMemCache.KeyStrings(ctx)
cachePrefix = cachePrefixTableFields
removedKeys = make([]any, 0)
)
for _, key := range keys {
if gstr.HasPrefix(key, cachePrefix) {
removedKeys = append(removedKeys, key)
}
}
if len(removedKeys) > 0 {
err = c.innerMemCache.Removes(ctx, removedKeys)
}
return
}
// ClearCache removes cached sql result of certain table.
func (c *Core) ClearCache(ctx context.Context, table string) (err error) {
var (
keys, _ = c.db.GetCache().KeyStrings(ctx)
cachePrefix = fmt.Sprintf(`%s%s@`, cachePrefixSelectCache, table)
removedKeys = make([]any, 0)
)
for _, key := range keys {
if gstr.HasPrefix(key, cachePrefix) {
removedKeys = append(removedKeys, key)
}
}
if len(removedKeys) > 0 {
err = c.db.GetCache().Removes(ctx, removedKeys)
}
return
}
// ClearCacheAll removes all cached sql result from cache
func (c *Core) ClearCacheAll(ctx context.Context) (err error) {
if err = c.db.GetCache().Clear(ctx); err != nil {
return err
}
if err = c.GetInnerMemCache().Clear(ctx); err != nil {
return err
}
return
}
// HasField determine whether the field exists in the table.
func (c *Core) HasField(ctx context.Context, table, field string, schema ...string) (bool, error) {
table = c.guessPrimaryTableName(table)
tableFields, err := c.db.TableFields(ctx, table, schema...)
if err != nil {
return false, err
}
if len(tableFields) == 0 {
return false, gerror.NewCodef(
gcode.CodeNotFound,
`empty table fields for table "%s"`, table,
)
}
fieldsArray := make([]string, len(tableFields))
for k, v := range tableFields {
fieldsArray[v.Index] = k
}
charLeft, charRight := c.db.GetChars()
field = gstr.Trim(field, charLeft+charRight)
for _, f := range fieldsArray {
if f == field {
return true, nil
}
}
return false, nil
}
// guessPrimaryTableName parses and returns the primary table name.
func (c *Core) guessPrimaryTableName(tableStr string) string {
if tableStr == "" {
return ""
}
var (
guessedTableName string
array1 = gstr.SplitAndTrim(tableStr, ",")
array2 = gstr.SplitAndTrim(array1[0], " ")
array3 = gstr.SplitAndTrim(array2[0], ".")
)
if len(array3) >= 2 {
guessedTableName = array3[1]
} else {
guessedTableName = array3[0]
}
charL, charR := c.db.GetChars()
if charL != "" || charR != "" {
guessedTableName = gstr.Trim(guessedTableName, charL+charR)
}
if !gregex.IsMatchString(regularFieldNameRegPattern, guessedTableName) {
return ""
}
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
}