mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
✨ feat(data-grid): 支持无主键表安全编辑
- 定位策略:新增主键、唯一索引和 Oracle ROWID 三类安全行定位能力 - 查询编辑器:简单单表 SELECT 自动补充隐藏定位列,复杂结果保持只读 - 表预览:无主键表可通过唯一索引或 Oracle ROWID 安全编辑 - 提交流程:移除无主键整行 WHERE fallback,隐藏定位列不参与展示和写入 - 后端保护:Oracle、MySQL、PostgreSQL 更新删除必须恰好影响 1 行 - 测试覆盖:补充 QueryEditor、DataViewer、DataGrid 和 ApplyChanges 相关用例 Refs #419
This commit is contained in:
@@ -184,9 +184,10 @@ type UpdateRow struct {
|
||||
|
||||
// ChangeSet 表示一组批量变更,包含新增、修改和删除操作。
|
||||
type ChangeSet struct {
|
||||
Inserts []map[string]interface{} `json:"inserts"`
|
||||
Updates []UpdateRow `json:"updates"`
|
||||
Deletes []map[string]interface{} `json:"deletes"`
|
||||
Inserts []map[string]interface{} `json:"inserts"`
|
||||
Updates []UpdateRow `json:"updates"`
|
||||
Deletes []map[string]interface{} `json:"deletes"`
|
||||
LocatorStrategy string `json:"locatorStrategy,omitempty"`
|
||||
}
|
||||
|
||||
// MongoMemberInfo 描述 MongoDB 副本集成员的信息。
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
@@ -64,6 +65,20 @@ type BatchApplier interface {
|
||||
ApplyChanges(tableName string, changes connection.ChangeSet) error
|
||||
}
|
||||
|
||||
func requireSingleRowAffected(result sql.Result, action string) error {
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s未生效:无法确认影响行数:%v", action, err)
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("%s未生效:未匹配到任何行", action)
|
||||
}
|
||||
if affected != 1 {
|
||||
return fmt.Errorf("%s未生效:影响了 %d 行,期望只影响 1 行", action, affected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type databaseFactory func() Database
|
||||
|
||||
var databaseFactories = map[string]databaseFactory{
|
||||
|
||||
@@ -624,8 +624,8 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除失败:%v", err)
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return fmt.Errorf("删除未生效:未匹配到任何行")
|
||||
if err := requireSingleRowAffected(res, "删除"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,8 +658,8 @@ func (m *MySQLDB) ApplyChanges(tableName string, changes connection.ChangeSet) e
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新失败:%v", err)
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return fmt.Errorf("更新未生效:未匹配到任何行")
|
||||
if err := requireSingleRowAffected(res, "更新"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,16 @@ var (
|
||||
)
|
||||
|
||||
type oracleRecordingState struct {
|
||||
mu sync.Mutex
|
||||
execArgs [][]driver.NamedValue
|
||||
mu sync.Mutex
|
||||
execQueries []string
|
||||
execArgs [][]driver.NamedValue
|
||||
rowsAffected int64
|
||||
}
|
||||
|
||||
func (s *oracleRecordingState) snapshotExecQueries() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]string(nil), s.execQueries...)
|
||||
}
|
||||
|
||||
func (s *oracleRecordingState) snapshotExecArgs() [][]driver.NamedValue {
|
||||
@@ -63,11 +71,12 @@ func (c *oracleRecordingConn) Close() error { return nil }
|
||||
|
||||
func (c *oracleRecordingConn) Begin() (driver.Tx, error) { return oracleRecordingTx{}, nil }
|
||||
|
||||
func (c *oracleRecordingConn) ExecContext(_ context.Context, _ string, args []driver.NamedValue) (driver.Result, error) {
|
||||
func (c *oracleRecordingConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
c.state.mu.Lock()
|
||||
defer c.state.mu.Unlock()
|
||||
c.state.execQueries = append(c.state.execQueries, query)
|
||||
c.state.execArgs = append(c.state.execArgs, append([]driver.NamedValue(nil), args...))
|
||||
return driver.RowsAffected(1), nil
|
||||
return driver.RowsAffected(c.state.rowsAffected), nil
|
||||
}
|
||||
|
||||
func (c *oracleRecordingConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
@@ -126,7 +135,7 @@ func openOracleRecordingDB(t *testing.T) (*sql.DB, *oracleRecordingState) {
|
||||
oracleRecordingDriverMu.Lock()
|
||||
oracleRecordingDriverSeq++
|
||||
dsn := fmt.Sprintf("oracle-recording-%d", oracleRecordingDriverSeq)
|
||||
state := &oracleRecordingState{}
|
||||
state := &oracleRecordingState{rowsAffected: 1}
|
||||
oracleRecordingDriverStates[dsn] = state
|
||||
oracleRecordingDriverMu.Unlock()
|
||||
|
||||
@@ -145,6 +154,82 @@ func openOracleRecordingDB(t *testing.T) (*sql.DB, *oracleRecordingState) {
|
||||
return dbConn, state
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenUpdateMatchesNoRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 0
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ID": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望更新未匹配到行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "更新未生效") {
|
||||
t.Fatalf("错误信息应提示更新未生效,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenUpdateAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ID": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望更新影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenDeleteAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Deletes: []map[string]interface{}{{
|
||||
"STATUS": "stale",
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望删除影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesNormalizesTemporalStringsForUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -181,3 +266,87 @@ func TestOracleApplyChangesNormalizesTemporalStringsForUpdate(t *testing.T) {
|
||||
t.Fatalf("日期主键字段应绑定为 time.Time,实际=%#v(%T)", args[1].Value, args[1].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesUsesUnquotedRowIDLocator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
LocatorStrategy: "oracle-rowid",
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ROWID": "AAAA",
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes); err != nil {
|
||||
t.Fatalf("ApplyChanges 返回错误: %v", err)
|
||||
}
|
||||
|
||||
executions := state.snapshotExecQueries()
|
||||
if len(executions) != 1 {
|
||||
t.Fatalf("期望执行 1 条更新,实际 %d 条", len(executions))
|
||||
}
|
||||
query := executions[0]
|
||||
if !strings.Contains(query, "ROWID = :2") {
|
||||
t.Fatalf("ROWID 定位条件不正确: %s", query)
|
||||
}
|
||||
if strings.Contains(query, "\"ROWID\" =") {
|
||||
t.Fatalf("ROWID 不应被当作普通列引用: %s", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLApplyChangesReturnsErrorWhenUpdateAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
mysqlDB := &MySQLDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"id": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"name": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := mysqlDB.ApplyChanges("users", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望 MySQL 更新影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresApplyChangesReturnsErrorWhenDeleteAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
postgresDB := &PostgresDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Deletes: []map[string]interface{}{{
|
||||
"id": 7,
|
||||
}},
|
||||
}
|
||||
|
||||
err := postgresDB.ApplyChanges("public.users", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望 PostgreSQL 删除影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,17 +319,31 @@ func (o *OracleDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi
|
||||
}
|
||||
|
||||
func (o *OracleDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
|
||||
query := fmt.Sprintf(`SELECT index_name, column_name, uniqueness
|
||||
FROM all_ind_columns
|
||||
JOIN all_indexes USING (index_name, owner)
|
||||
WHERE table_owner = '%s' AND table_name = '%s'`,
|
||||
strings.ToUpper(dbName), strings.ToUpper(tableName))
|
||||
esc := func(s string) string { return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(s)), "'", "''") }
|
||||
table := esc(tableName)
|
||||
if table == "" {
|
||||
return nil, fmt.Errorf("表名不能为空")
|
||||
}
|
||||
|
||||
if dbName == "" {
|
||||
query = fmt.Sprintf(`SELECT index_name, column_name, uniqueness
|
||||
FROM user_ind_columns
|
||||
JOIN user_indexes USING (index_name)
|
||||
WHERE table_name = '%s'`, strings.ToUpper(tableName))
|
||||
query := fmt.Sprintf(`SELECT c.index_name, c.column_name, i.uniqueness, c.column_position, i.index_type
|
||||
FROM all_ind_columns c
|
||||
JOIN all_indexes i ON i.owner = c.index_owner AND i.index_name = c.index_name
|
||||
WHERE c.table_owner = '%s'
|
||||
AND c.table_name = '%s'
|
||||
AND c.column_name IS NOT NULL
|
||||
AND c.column_name NOT LIKE 'SYS_NC%%$'
|
||||
AND i.index_type NOT LIKE 'FUNCTION-BASED%%'
|
||||
ORDER BY c.index_name, c.column_position`, esc(dbName), table)
|
||||
|
||||
if strings.TrimSpace(dbName) == "" {
|
||||
query = fmt.Sprintf(`SELECT c.index_name, c.column_name, i.uniqueness, c.column_position, i.index_type
|
||||
FROM user_ind_columns c
|
||||
JOIN user_indexes i ON i.index_name = c.index_name
|
||||
WHERE c.table_name = '%s'
|
||||
AND c.column_name IS NOT NULL
|
||||
AND c.column_name NOT LIKE 'SYS_NC%%$'
|
||||
AND i.index_type NOT LIKE 'FUNCTION-BASED%%'
|
||||
ORDER BY c.index_name, c.column_position`, table)
|
||||
}
|
||||
|
||||
data, _, err := o.Query(query)
|
||||
@@ -337,19 +351,46 @@ func (o *OracleDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
getValue := func(row map[string]interface{}, names ...string) interface{} {
|
||||
for _, name := range names {
|
||||
if value, ok := row[name]; ok {
|
||||
return value
|
||||
}
|
||||
for key, value := range row {
|
||||
if strings.EqualFold(key, name) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
parseInt := func(value interface{}) int {
|
||||
var n int
|
||||
_, _ = fmt.Sscanf(strings.TrimSpace(fmt.Sprintf("%v", value)), "%d", &n)
|
||||
return n
|
||||
}
|
||||
|
||||
var indexes []connection.IndexDefinition
|
||||
for _, row := range data {
|
||||
unique := 1
|
||||
if val, ok := row["UNIQUENESS"]; ok && val == "UNIQUE" {
|
||||
unique = 0
|
||||
uniqueness := strings.ToUpper(strings.TrimSpace(fmt.Sprintf("%v", getValue(row, "UNIQUENESS"))))
|
||||
nonUnique := 1
|
||||
if uniqueness == "UNIQUE" {
|
||||
nonUnique = 0
|
||||
}
|
||||
indexType := strings.ToUpper(strings.TrimSpace(fmt.Sprintf("%v", getValue(row, "INDEX_TYPE"))))
|
||||
if indexType == "" || indexType == "<NIL>" {
|
||||
indexType = "BTREE"
|
||||
}
|
||||
|
||||
idx := connection.IndexDefinition{
|
||||
Name: fmt.Sprintf("%v", row["INDEX_NAME"]),
|
||||
ColumnName: fmt.Sprintf("%v", row["COLUMN_NAME"]),
|
||||
NonUnique: unique,
|
||||
// SeqInIndex is harder to get in simple join, omitting or estimating
|
||||
IndexType: "BTREE", // Default assumption
|
||||
Name: strings.TrimSpace(fmt.Sprintf("%v", getValue(row, "INDEX_NAME"))),
|
||||
ColumnName: strings.TrimSpace(fmt.Sprintf("%v", getValue(row, "COLUMN_NAME"))),
|
||||
NonUnique: nonUnique,
|
||||
SeqInIndex: parseInt(getValue(row, "COLUMN_POSITION")),
|
||||
IndexType: indexType,
|
||||
}
|
||||
if idx.Name == "" || idx.ColumnName == "" || strings.EqualFold(idx.ColumnName, "<nil>") {
|
||||
continue
|
||||
}
|
||||
indexes = append(indexes, idx)
|
||||
}
|
||||
@@ -551,23 +592,38 @@ func (o *OracleDB) ApplyChanges(tableName string, changes connection.ChangeSet)
|
||||
qualifiedTable = quoteIdent(table)
|
||||
}
|
||||
|
||||
// 1. Deletes
|
||||
for _, pk := range changes.Deletes {
|
||||
isOracleRowIDLocator := strings.EqualFold(strings.TrimSpace(changes.LocatorStrategy), "oracle-rowid")
|
||||
buildWhere := func(keys map[string]interface{}, startIndex int) ([]string, []interface{}, int) {
|
||||
var wheres []string
|
||||
var args []interface{}
|
||||
idx := 0
|
||||
for k, v := range pk {
|
||||
idx := startIndex
|
||||
for k, v := range keys {
|
||||
idx++
|
||||
if isOracleRowIDLocator && strings.EqualFold(strings.TrimSpace(k), "ROWID") {
|
||||
wheres = append(wheres, fmt.Sprintf("ROWID = :%d", idx))
|
||||
args = append(args, v)
|
||||
continue
|
||||
}
|
||||
wheres = append(wheres, fmt.Sprintf("%s = :%d", quoteIdent(k), idx))
|
||||
args = append(args, normalizeOracleValueForWrite(k, v, columnTypeMap))
|
||||
}
|
||||
return wheres, args, idx
|
||||
}
|
||||
|
||||
// 1. Deletes
|
||||
for _, pk := range changes.Deletes {
|
||||
wheres, args, _ := buildWhere(pk, 0)
|
||||
if len(wheres) == 0 {
|
||||
continue
|
||||
}
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE %s", qualifiedTable, strings.Join(wheres, " AND "))
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
res, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除失败:%v", err)
|
||||
}
|
||||
if err := requireSingleRowAffected(res, "删除"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Updates
|
||||
@@ -586,21 +642,21 @@ func (o *OracleDB) ApplyChanges(tableName string, changes connection.ChangeSet)
|
||||
continue
|
||||
}
|
||||
|
||||
var wheres []string
|
||||
for k, v := range update.Keys {
|
||||
idx++
|
||||
wheres = append(wheres, fmt.Sprintf("%s = :%d", quoteIdent(k), idx))
|
||||
args = append(args, normalizeOracleValueForWrite(k, v, columnTypeMap))
|
||||
}
|
||||
wheres, whereArgs, _ := buildWhere(update.Keys, idx)
|
||||
args = append(args, whereArgs...)
|
||||
|
||||
if len(wheres) == 0 {
|
||||
return fmt.Errorf("更新操作需要主键条件")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("UPDATE %s SET %s WHERE %s", qualifiedTable, strings.Join(sets, ", "), strings.Join(wheres, " AND "))
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
res, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新失败:%v", err)
|
||||
}
|
||||
if err := requireSingleRowAffected(res, "更新"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inserts
|
||||
@@ -622,9 +678,13 @@ func (o *OracleDB) ApplyChanges(tableName string, changes connection.ChangeSet)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", qualifiedTable, strings.Join(cols, ", "), strings.Join(placeholders, ", "))
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
res, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入失败:%v", err)
|
||||
}
|
||||
if affected, err := res.RowsAffected(); err == nil && affected == 0 {
|
||||
return fmt.Errorf("插入未生效:未影响任何行")
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
|
||||
@@ -408,6 +408,12 @@ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
|
||||
WHERE t.relkind IN ('r', 'p')
|
||||
AND t.relname = '%s'
|
||||
AND n.nspname = '%s'
|
||||
AND ix.indisvalid
|
||||
AND ix.indpred IS NULL
|
||||
AND x.ordinality <= ix.indnkeyatts
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM unnest(ix.indkey) AS expr_key(attnum) WHERE expr_key.attnum <= 0
|
||||
)
|
||||
ORDER BY i.relname, x.ordinality`, esc(table), esc(schema))
|
||||
|
||||
data, _, err := p.Query(query)
|
||||
@@ -758,9 +764,13 @@ func (p *PostgresDB) ApplyChanges(tableName string, changes connection.ChangeSet
|
||||
continue
|
||||
}
|
||||
query := fmt.Sprintf("DELETE FROM %s WHERE %s", qualifiedTable, strings.Join(wheres, " AND "))
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
res, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除失败:%v", err)
|
||||
}
|
||||
if err := requireSingleRowAffected(res, "删除"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Updates
|
||||
@@ -791,9 +801,13 @@ func (p *PostgresDB) ApplyChanges(tableName string, changes connection.ChangeSet
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("UPDATE %s SET %s WHERE %s", qualifiedTable, strings.Join(sets, ", "), strings.Join(wheres, " AND "))
|
||||
if _, err := tx.Exec(query, args...); err != nil {
|
||||
res, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新失败:%v", err)
|
||||
}
|
||||
if err := requireSingleRowAffected(res, "更新"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Inserts
|
||||
|
||||
Reference in New Issue
Block a user