mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 01:03:51 +08:00
🐛 fix(oceanbase): 修复 Oracle 模式单元格事务提交
- 单元格变更提交与预览使用所选 schema 限定目标表 - 对限定表名按分段方式引用,避免将 schema 与表名整体转义 - 为全列定位条件中的日期和时间值补充显式 Oracle 转换 - 增加 OceanBase Oracle 事务提交与 SQL 预览回归测试
This commit is contained in:
@@ -9,12 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type fakeCreateDatabaseDB struct {
|
||||
connectConfig connection.ConnectionConfig
|
||||
execQueries []string
|
||||
applyChanges connection.ChangeSet
|
||||
previewDeletes []string
|
||||
previewUpdates []string
|
||||
previewInserts []string
|
||||
connectConfig connection.ConnectionConfig
|
||||
execQueries []string
|
||||
applyChanges connection.ChangeSet
|
||||
applyTableName string
|
||||
previewTableName string
|
||||
previewDeletes []string
|
||||
previewUpdates []string
|
||||
previewInserts []string
|
||||
}
|
||||
|
||||
func (f *fakeCreateDatabaseDB) Connect(config connection.ConnectionConfig) error {
|
||||
@@ -53,10 +55,12 @@ func (f *fakeCreateDatabaseDB) GetTriggers(dbName, tableName string) ([]connecti
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeCreateDatabaseDB) ApplyChanges(tableName string, changes connection.ChangeSet) error {
|
||||
f.applyTableName = tableName
|
||||
f.applyChanges = changes
|
||||
return nil
|
||||
}
|
||||
func (f *fakeCreateDatabaseDB) PreviewChanges(tableName string, changes connection.ChangeSet) (deletes, updates, inserts []string) {
|
||||
f.previewTableName = tableName
|
||||
return f.previewDeletes, f.previewUpdates, f.previewInserts
|
||||
}
|
||||
|
||||
|
||||
@@ -2465,8 +2465,9 @@ func (a *App) ApplyChanges(config connection.ConnectionConfig, dbName, tableName
|
||||
}
|
||||
|
||||
if applier, ok := dbInst.(db.BatchApplier); ok {
|
||||
preview := buildChangePreview(dbInst, config, tableName, changes)
|
||||
err := applier.ApplyChanges(tableName, changes)
|
||||
targetTableName := resolveChangeTargetTableName(config, dbName, tableName)
|
||||
preview := buildChangePreview(dbInst, config, targetTableName, changes)
|
||||
err := applier.ApplyChanges(targetTableName, changes)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), Data: preview}
|
||||
}
|
||||
@@ -2483,6 +2484,19 @@ type ChangePreview struct {
|
||||
Inserts []string `json:"inserts"`
|
||||
}
|
||||
|
||||
func resolveChangeTargetTableName(config connection.ConnectionConfig, dbName, tableName string) string {
|
||||
targetTableName := strings.TrimSpace(tableName)
|
||||
if resolveDDLDBType(config) != "oracle" {
|
||||
return targetTableName
|
||||
}
|
||||
|
||||
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, targetTableName)
|
||||
if strings.TrimSpace(schemaName) == "" || strings.TrimSpace(pureTableName) == "" {
|
||||
return targetTableName
|
||||
}
|
||||
return strings.TrimSpace(schemaName) + "." + strings.TrimSpace(pureTableName)
|
||||
}
|
||||
|
||||
func buildChangePreview(dbInst db.Database, config connection.ConnectionConfig, tableName string, changes connection.ChangeSet) ChangePreview {
|
||||
if previewer, ok := dbInst.(db.ChangePreviewer); ok {
|
||||
deletes, updates, inserts := previewer.PreviewChanges(tableName, changes)
|
||||
@@ -2491,7 +2505,8 @@ func buildChangePreview(dbInst db.Database, config connection.ConnectionConfig,
|
||||
|
||||
dbType := resolveDDLDBType(config)
|
||||
quoter := func(s string) string { return quoteIdentByType(dbType, s) }
|
||||
deletes, updates, inserts := db.GenerateChangePreview(tableName, changes, quoter)
|
||||
tableQuoter := func(s string) string { return quoteQualifiedIdentByType(dbType, s) }
|
||||
deletes, updates, inserts := db.GenerateChangePreviewWithTableQuoter(tableName, changes, quoter, tableQuoter)
|
||||
return ChangePreview{Deletes: deletes, Updates: updates, Inserts: inserts}
|
||||
}
|
||||
|
||||
@@ -2506,7 +2521,8 @@ func (a *App) PreviewChanges(config connection.ConnectionConfig, dbName, tableNa
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
return connection.QueryResult{Success: true, Data: buildChangePreview(dbInst, config, tableName, changes)}
|
||||
targetTableName := resolveChangeTargetTableName(config, dbName, tableName)
|
||||
return connection.QueryResult{Success: true, Data: buildChangePreview(dbInst, config, targetTableName, changes)}
|
||||
}
|
||||
|
||||
func (a *App) ExportTable(config connection.ConnectionConfig, dbName string, tableName string, format string) connection.QueryResult {
|
||||
|
||||
@@ -52,4 +52,89 @@ func TestApplyChangesReturnsDetailedSQLPreview(t *testing.T) {
|
||||
if len(fakeDB.applyChanges.Deletes) != 1 || len(fakeDB.applyChanges.Updates) != 1 || len(fakeDB.applyChanges.Inserts) != 1 {
|
||||
t.Fatalf("ApplyChanges did not send the full change set to the driver: %#v", fakeDB.applyChanges)
|
||||
}
|
||||
if fakeDB.previewTableName != "users" || fakeDB.applyTableName != "users" {
|
||||
t.Fatalf("non-Oracle target changed unexpectedly: preview=%q apply=%q", fakeDB.previewTableName, fakeDB.applyTableName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyChangesQualifiesOceanBaseOracleTargetWithSelectedSchema(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
||||
originalDriverRuntimeSupportStatusFunc := driverRuntimeSupportStatusFunc
|
||||
originalVerifyDriverAgentRevisionFunc := verifyDriverAgentRevisionFunc
|
||||
t.Cleanup(func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
||||
driverRuntimeSupportStatusFunc = originalDriverRuntimeSupportStatusFunc
|
||||
verifyDriverAgentRevisionFunc = originalVerifyDriverAgentRevisionFunc
|
||||
})
|
||||
|
||||
fakeDB := &fakeCreateDatabaseDB{}
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return fakeDB, nil
|
||||
}
|
||||
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
||||
return raw, nil
|
||||
}
|
||||
driverRuntimeSupportStatusFunc = func(driverType string) (bool, string) {
|
||||
return true, ""
|
||||
}
|
||||
verifyDriverAgentRevisionFunc = func(config connection.ConnectionConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := connection.ConnectionConfig{
|
||||
Type: "oceanbase",
|
||||
OceanBaseProtocol: "oracle",
|
||||
Host: "127.0.0.1",
|
||||
Port: 2881,
|
||||
}
|
||||
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
|
||||
result := app.ApplyChanges(
|
||||
config,
|
||||
"APP",
|
||||
"USERS",
|
||||
connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{"ID": 7},
|
||||
Values: map[string]interface{}{"UPDATED_AT": "2026-07-13 13:42:00"},
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
if !result.Success {
|
||||
t.Fatalf("ApplyChanges returned failure: %s", result.Message)
|
||||
}
|
||||
if fakeDB.previewTableName != "APP.USERS" {
|
||||
t.Fatalf("PreviewChanges table = %q, want APP.USERS", fakeDB.previewTableName)
|
||||
}
|
||||
if fakeDB.applyTableName != "APP.USERS" {
|
||||
t.Fatalf("ApplyChanges table = %q, want APP.USERS", fakeDB.applyTableName)
|
||||
}
|
||||
|
||||
fakeDB.previewTableName = ""
|
||||
previewResult := app.PreviewChanges(config, "APP", "USERS", connection.ChangeSet{})
|
||||
if !previewResult.Success {
|
||||
t.Fatalf("PreviewChanges returned failure: %s", previewResult.Message)
|
||||
}
|
||||
if fakeDB.previewTableName != "APP.USERS" {
|
||||
t.Fatalf("standalone PreviewChanges table = %q, want APP.USERS", fakeDB.previewTableName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildChangePreviewQuotesQualifiedOracleTargetBySegment(t *testing.T) {
|
||||
preview := buildChangePreview(
|
||||
&fakeBatchWriteDB{},
|
||||
connection.ConnectionConfig{Type: "oceanbase", OceanBaseProtocol: "oracle"},
|
||||
"APP.USERS",
|
||||
connection.ChangeSet{Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{"ID": 7},
|
||||
Values: map[string]interface{}{"STATUS": "0"},
|
||||
}}},
|
||||
)
|
||||
|
||||
want := `UPDATE "APP"."USERS" SET "STATUS" = '0' WHERE "ID" = 7;`
|
||||
if len(preview.Updates) != 1 || preview.Updates[0] != want {
|
||||
t.Fatalf("qualified Oracle preview = %#v, want %q", preview.Updates, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,30 @@ import (
|
||||
// GenerateChangePreview 将 ChangeSet 转为可读 SQL 语句(不执行)。
|
||||
// quoteIdent 用于引用列名/表名(MySQL: backtick, PostgreSQL: double quote)。
|
||||
func GenerateChangePreview(tableName string, changes connection.ChangeSet, quoteIdent func(string) string) (deletes, updates, inserts []string) {
|
||||
qt := quoteIdent
|
||||
return GenerateChangePreviewWithTableQuoter(tableName, changes, quoteIdent, quoteIdent)
|
||||
}
|
||||
|
||||
// GenerateChangePreviewWithTableQuoter allows qualified table names to be quoted
|
||||
// segment by segment while keeping column quoting unchanged.
|
||||
func GenerateChangePreviewWithTableQuoter(
|
||||
tableName string,
|
||||
changes connection.ChangeSet,
|
||||
quoteIdent func(string) string,
|
||||
quoteTable func(string) string,
|
||||
) (deletes, updates, inserts []string) {
|
||||
if quoteTable == nil {
|
||||
quoteTable = quoteIdent
|
||||
}
|
||||
|
||||
// Deletes
|
||||
for _, pk := range changes.Deletes {
|
||||
var conds []string
|
||||
for _, k := range sortedKeys(pk) {
|
||||
v := pk[k]
|
||||
conds = append(conds, fmt.Sprintf("%s = %s", qt(k), formatLiteral(v)))
|
||||
conds = append(conds, fmt.Sprintf("%s = %s", quoteIdent(k), formatLiteral(v)))
|
||||
}
|
||||
if len(conds) > 0 {
|
||||
deletes = append(deletes, fmt.Sprintf("DELETE FROM %s WHERE %s;", qt(tableName), strings.Join(conds, " AND ")))
|
||||
deletes = append(deletes, fmt.Sprintf("DELETE FROM %s WHERE %s;", quoteTable(tableName), strings.Join(conds, " AND ")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +44,7 @@ func GenerateChangePreview(tableName string, changes connection.ChangeSet, quote
|
||||
var sets []string
|
||||
for _, k := range sortedKeys(row.Values) {
|
||||
v := row.Values[k]
|
||||
sets = append(sets, fmt.Sprintf("%s = %s", qt(k), formatLiteral(v)))
|
||||
sets = append(sets, fmt.Sprintf("%s = %s", quoteIdent(k), formatLiteral(v)))
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
continue
|
||||
@@ -39,12 +52,12 @@ func GenerateChangePreview(tableName string, changes connection.ChangeSet, quote
|
||||
var conds []string
|
||||
for _, k := range sortedKeys(row.Keys) {
|
||||
v := row.Keys[k]
|
||||
conds = append(conds, fmt.Sprintf("%s = %s", qt(k), formatLiteral(v)))
|
||||
conds = append(conds, fmt.Sprintf("%s = %s", quoteIdent(k), formatLiteral(v)))
|
||||
}
|
||||
if len(conds) == 0 {
|
||||
continue
|
||||
}
|
||||
updates = append(updates, fmt.Sprintf("UPDATE %s SET %s WHERE %s;", qt(tableName), strings.Join(sets, ", "), strings.Join(conds, " AND ")))
|
||||
updates = append(updates, fmt.Sprintf("UPDATE %s SET %s WHERE %s;", quoteTable(tableName), strings.Join(sets, ", "), strings.Join(conds, " AND ")))
|
||||
}
|
||||
|
||||
// Inserts
|
||||
@@ -56,13 +69,13 @@ func GenerateChangePreview(tableName string, changes connection.ChangeSet, quote
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
cols = append(cols, qt(k))
|
||||
cols = append(cols, quoteIdent(k))
|
||||
vals = append(vals, formatLiteral(v))
|
||||
}
|
||||
if len(cols) == 0 {
|
||||
continue
|
||||
}
|
||||
inserts = append(inserts, fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);", qt(tableName), strings.Join(cols, ", "), strings.Join(vals, ", ")))
|
||||
inserts = append(inserts, fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);", quoteTable(tableName), strings.Join(cols, ", "), strings.Join(vals, ", ")))
|
||||
}
|
||||
|
||||
return deletes, updates, inserts
|
||||
|
||||
@@ -825,6 +825,43 @@ func TestOceanBaseOracleOBClientApplyChangesFormatsTemporalValuesExplicitly(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestOceanBaseOracleOBClientApplyChangesFormatsAllColumnsTemporalWhereValuesExplicitly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
oceanbaseDB := &OceanBaseDB{}
|
||||
oceanbaseDB.bindConnectedDatabase(dbConn, 0, oceanBaseProtocolOracle)
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
LocatorStrategy: "all-columns",
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"CREATED_AT": "2026-07-13",
|
||||
"UPDATED_AT": "2026-07-13 13:42:00.123456",
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"STATUS": "0",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if err := oceanbaseDB.ApplyChanges("APP.USERS", changes); err != nil {
|
||||
t.Fatalf("ApplyChanges() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
queries := state.snapshotExecQueries()
|
||||
if len(queries) != 1 {
|
||||
t.Fatalf("expected one exec query, got %#v", queries)
|
||||
}
|
||||
query := queries[0]
|
||||
if !strings.Contains(query, `"CREATED_AT" = TO_DATE(?, 'YYYY-MM-DD')`) {
|
||||
t.Fatalf("expected explicit TO_DATE binding in all-columns WHERE, got %q", query)
|
||||
}
|
||||
if !strings.Contains(query, `"UPDATED_AT" = TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS.FF')`) {
|
||||
t.Fatalf("expected explicit TO_TIMESTAMP binding in all-columns WHERE, got %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOceanBaseOracleGetCreateStatementFallsBackToShowCreateTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user