mirror of
https://gitee.com/johng/gf
synced 2026-06-07 02:12:11 +08:00
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>
274 lines
8.0 KiB
Go
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
|
|
}
|