🐛 fix(kingbase): 统一金仓标识符引用策略

- 标识符处理:下沉 Kingbase 引用逻辑,普通小写 schema/table 不再强制双引号包裹
- 表操作修复:修复截断、清空、导入、导出等路径生成异常双引号 SQL
- 同步链路修复:统一数据同步、预览、迁移建表中的 Kingbase schema.table 拼接规则
- 自定义驱动兼容:补齐 kingbase8/kingbasees/kingbasev8 别名归一与写入路径处理
- 回归覆盖:新增 ldf_server.andon_events、转义引号、保留字和大小写标识符测试
This commit is contained in:
Syngnat
2026-05-13 10:25:25 +08:00
parent 1f3cc2c686
commit bf7b9092df
18 changed files with 284 additions and 97 deletions

View File

@@ -5,6 +5,7 @@ import (
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
)
func normalizeRunConfig(config connection.ConnectionConfig, dbName string) connection.ConnectionConfig {
@@ -57,6 +58,16 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
return targetDB, rawTable
}
if dbType == "kingbase" {
schema, table := db.SplitKingbaseQualifiedName(rawTable)
if schema != "" && table != "" {
return schema, table
}
if table != "" {
return "public", table
}
}
if parts := strings.SplitN(rawTable, ".", 2); len(parts) == 2 {
schema := strings.TrimSpace(parts[0])
table := strings.TrimSpace(parts[1])

View File

@@ -50,6 +50,18 @@ func TestNormalizeSchemaAndTable_PostgresStillSplitsQualifiedName(t *testing.T)
}
}
func TestNormalizeSchemaAndTable_KingbaseNormalizesEscapedQualifiedName(t *testing.T) {
t.Parallel()
schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{
Type: "kingbase",
}, "demo_db", `\"Idf_server\".\"mes_bip_wip_finished\"`)
if schema != "Idf_server" || table != "mes_bip_wip_finished" {
t.Fatalf("expected kingbase qualified split to Idf_server.mes_bip_wip_finished, got %q.%q", schema, table)
}
}
func TestNormalizeRunConfig_OceanBaseOracleKeepsServiceName(t *testing.T) {
t.Parallel()
@@ -86,7 +98,7 @@ func TestQuoteTableIdentByType_KingbaseNormalizesQuotedQualifiedTable(t *testing
t.Fatalf("expected kingbase qualified split to Idf_server.mes_bip_wip_finished, got %q.%q", schema, table)
}
if got := quoteTableIdentByType("kingbase", schema, table); got != `"Idf_server"."mes_bip_wip_finished"` {
if got := quoteTableIdentByType("kingbase", schema, table); got != `"Idf_server".mes_bip_wip_finished` {
t.Fatalf("unexpected kingbase table identifier: %s", got)
}
}

View File

@@ -155,6 +155,12 @@ func resolveDDLDBType(config connection.ConnectionConfig) string {
if dbType == "mssql" || dbType == "sql_server" || dbType == "sql-server" {
return "sqlserver"
}
if dbType == "postgresql" {
return "postgres"
}
if dbType == "kingbase8" || dbType == "kingbasees" || dbType == "kingbasev8" {
return "kingbase"
}
if dbType == "oceanbase" && isOceanBaseOracleProtocol(config) {
return "oracle"
}
@@ -490,7 +496,7 @@ func (a *App) DBQueryWithCancel(config connection.ConnectionConfig, dbName strin
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
}
query = sanitizeSQLForPgLike(runConfig.Type, query)
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
timeoutSeconds := runConfig.Timeout
if timeoutSeconds <= 0 {
timeoutSeconds = 30
@@ -586,7 +592,7 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
}
query = sanitizeSQLForPgLike(runConfig.Type, query)
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
timeoutSeconds := runConfig.Timeout
if timeoutSeconds <= 0 {
timeoutSeconds = 30
@@ -785,7 +791,7 @@ func (a *App) DBQueryIsolated(config connection.ConnectionConfig, dbName string,
}
}()
query = sanitizeSQLForPgLike(runConfig.Type, query)
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
timeoutSeconds := runConfig.Timeout
if timeoutSeconds <= 0 {
timeoutSeconds = 30

View File

@@ -98,6 +98,14 @@ func TestResolveDDLDBType_OceanBaseOracleProtocol(t *testing.T) {
}
}
func TestResolveDDLDBType_KingbaseTypeAlias(t *testing.T) {
t.Parallel()
if got := resolveDDLDBType(connection.ConnectionConfig{Type: "kingbase8"}); got != "kingbase" {
t.Fatalf("expected kingbase8 type alias to resolve to kingbase, got %q", got)
}
}
func TestNormalizeSchemaAndTableByType_PGLikeQuotedQualifiedName(t *testing.T) {
t.Parallel()
@@ -109,6 +117,7 @@ func TestNormalizeSchemaAndTableByType_PGLikeQuotedQualifiedName(t *testing.T) {
wantTable string
}{
{name: "postgres quoted dots", dbType: "postgres", tableName: `"sales.schema"."order.items"`, wantSchema: "sales.schema", wantTable: "order.items"},
{name: "kingbase escaped lowercase", dbType: "kingbase", tableName: `\"ldf_server\".\"andon_events\"`, wantSchema: "ldf_server", wantTable: "andon_events"},
{name: "highgo escaped quoted", dbType: "highgo", tableName: `\"sales\".\"orders\"`, wantSchema: "sales", wantTable: "orders"},
{name: "vastbase quoted table only", dbType: "vastbase", tableName: `"order.items"`, wantSchema: "public", wantTable: "order.items"},
}
@@ -158,7 +167,7 @@ func TestResolveCreateStatementWithFallback_CustomKingbaseUsesPublicSchema(t *te
if dbInst.createSchema != "public" || dbInst.colsSchema != "public" {
t.Fatalf("expected fallback schema public, got create=%q columns=%q", dbInst.createSchema, dbInst.colsSchema)
}
if !strings.Contains(ddl, `CREATE TABLE "public"."orders"`) {
if !strings.Contains(ddl, `CREATE TABLE public.orders`) {
t.Fatalf("expected fallback DDL with public schema, got: %s", ddl)
}
}

View File

@@ -936,6 +936,7 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
return connection.QueryResult{Success: false, Message: err.Error()}
}
dbType := resolveDDLDBType(config)
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
columnTypeMap := map[string]string{}
if defs, colErr := dbInst.GetColumns(schemaName, pureTableName); colErr == nil {
@@ -948,7 +949,7 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
quotedCols := make([]string, len(columns))
for i, c := range columns {
quotedCols[i] = quoteIdentByType(runConfig.Type, c)
quotedCols[i] = quoteIdentByType(dbType, c)
}
for idx, row := range rows {
@@ -956,11 +957,11 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
for _, col := range columns {
val := row[col]
colType := columnTypeMap[normalizeColumnName(col)]
values = append(values, formatImportSQLValue(runConfig.Type, colType, val))
values = append(values, formatImportSQLValue(dbType, colType, val))
}
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
quoteQualifiedIdentByType(runConfig.Type, tableName),
quoteQualifiedIdentByType(dbType, tableName),
strings.Join(quotedCols, ", "),
strings.Join(values, ", "))
@@ -1034,7 +1035,8 @@ func (a *App) PreviewChanges(config connection.ConnectionConfig, dbName, tableNa
cp = ChangePreview{Deletes: deletes, Updates: updates, Inserts: inserts}
} else {
// 回退到通用生成,使用 quoteIdentByType 处理标识符转义
quoter := func(s string) string { return quoteIdentByType(runConfig.Type, s) }
dbType := resolveDDLDBType(config)
quoter := func(s string) string { return quoteIdentByType(dbType, s) }
deletes, updates, inserts := db.GenerateChangePreview(tableName, changes, quoter)
cp = ChangePreview{Deletes: deletes, Updates: updates, Inserts: inserts}
}
@@ -1083,7 +1085,8 @@ func (a *App) ExportTable(config connection.ConnectionConfig, dbName string, tab
return connection.QueryResult{Success: true, Message: "导出完成"}
}
query := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(runConfig.Type, tableName))
dbType := resolveDDLDBType(config)
query := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(dbType, tableName))
data, columns, err := queryDataForExport(dbInst, runConfig, query)
if err != nil {
@@ -1381,15 +1384,12 @@ func quoteIdentByType(dbType string, ident string) string {
return ident
}
dbType = resolveDDLDBType(connection.ConnectionConfig{Type: dbType})
switch dbType {
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "tdengine", "clickhouse":
return "`" + strings.ReplaceAll(ident, "`", "``") + "`"
case "kingbase":
cleaned := db.NormalizeKingbaseIdentifier(ident)
if cleaned == "" {
return `""`
}
return `"` + strings.ReplaceAll(cleaned, `"`, `""`) + `"`
return db.QuoteKingbaseIdentifier(ident)
case "sqlserver":
escaped := strings.ReplaceAll(ident, "]", "]]")
return "[" + escaped + "]"
@@ -1404,6 +1404,7 @@ func quoteQualifiedIdentByType(dbType string, ident string) string {
return raw
}
dbType = resolveDDLDBType(connection.ConnectionConfig{Type: dbType})
if dbType == "kingbase" {
schema, table := db.SplitKingbaseQualifiedName(raw)
if table == "" {
@@ -2085,7 +2086,8 @@ func dumpTableSQL(
}
qualified := qualifyTable(schemaName, pureTableName)
selectSQL := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(config.Type, qualified))
dbType := resolveDDLDBType(config)
selectSQL := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(dbType, qualified))
data, columns, err := queryDataForExport(dbInst, config, selectSQL)
if err != nil {
return err
@@ -2103,14 +2105,14 @@ func dumpTableSQL(
quotedCols := make([]string, 0, len(columns))
for _, c := range columns {
quotedCols = append(quotedCols, quoteIdentByType(config.Type, c))
quotedCols = append(quotedCols, quoteIdentByType(dbType, c))
}
quotedTable := quoteQualifiedIdentByType(config.Type, qualified)
quotedTable := quoteQualifiedIdentByType(dbType, qualified)
for _, row := range data {
values := make([]string, 0, len(columns))
for _, c := range columns {
values = append(values, formatImportSQLValue(config.Type, columnTypeMap[normalizeColumnName(c)], row[c]))
values = append(values, formatImportSQLValue(dbType, columnTypeMap[normalizeColumnName(c)], row[c]))
}
if _, err := w.WriteString(fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);\n", quotedTable, strings.Join(quotedCols, ", "), strings.Join(values, ", "))); err != nil {
return err
@@ -2179,7 +2181,7 @@ func (a *App) ExportQuery(config connection.ConnectionConfig, dbName string, que
return connection.QueryResult{Success: false, Message: err.Error()}
}
query = sanitizeSQLForPgLike(runConfig.Type, query)
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
if !looksLikeSelectOrWith(query) {
return connection.QueryResult{Success: false, Message: "仅支持 SELECT/WITH 查询导出"}
}

View File

@@ -54,7 +54,20 @@ func TestBuildTableDataClearSQL_KingbaseTruncateNormalizesQuotedQualifiedTable(t
t.Fatalf("buildTableDataClearSQL() unexpected error: %v", err)
}
if sql != `TRUNCATE TABLE "Idf_server"."mes_bip_wip_finished"` {
if sql != `TRUNCATE TABLE "Idf_server".mes_bip_wip_finished` {
t.Fatalf("unexpected kingbase truncate sql: %s", sql)
}
}
func TestBuildTableDataClearSQL_KingbaseTruncateLeavesLowercaseQualifiedTableUnquoted(t *testing.T) {
t.Parallel()
sql, err := buildTableDataClearSQL(connection.ConnectionConfig{Type: "kingbase"}, "ldf_server.andon_events", tableDataClearModeTruncate)
if err != nil {
t.Fatalf("buildTableDataClearSQL() unexpected error: %v", err)
}
if sql != "TRUNCATE TABLE ldf_server.andon_events" {
t.Fatalf("unexpected kingbase truncate sql: %s", sql)
}
}
@@ -67,7 +80,7 @@ func TestBuildTableDataClearSQL_KingbaseClearNormalizesQuotedQualifiedTable(t *t
t.Fatalf("buildTableDataClearSQL() unexpected error: %v", err)
}
if sql != `DELETE FROM "Idf_server"."mes_bip_wip_finished"` {
if sql != `DELETE FROM "Idf_server".mes_bip_wip_finished` {
t.Fatalf("unexpected kingbase clear sql: %s", sql)
}
}

View File

@@ -66,7 +66,15 @@ func isReadOnlySQLQuery(dbType string, query string) bool {
}
func sanitizeSQLForPgLike(dbType string, query string) string {
switch strings.ToLower(strings.TrimSpace(dbType)) {
normalizedType := strings.ToLower(strings.TrimSpace(dbType))
switch normalizedType {
case "postgresql":
normalizedType = "postgres"
case "kingbase8", "kingbasees", "kingbasev8":
normalizedType = "kingbase"
}
switch normalizedType {
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
// 有些情况下会出现多层重复引用(例如 """"schema"""" 或 ""schema"""),单次修复不一定收敛。
// 这里做有限次数的迭代,直到输出不再变化。

View File

@@ -11,6 +11,15 @@ func TestSanitizeSQLForPgLike_FixesBrokenDoubleDoubleQuotes(t *testing.T) {
}
}
func TestSanitizeSQLForPgLike_KingbaseAliasFixesBrokenDoubleDoubleQuotes(t *testing.T) {
in := `SELECT * FROM ""ldf_server"".""t_user"" LIMIT 1`
out := sanitizeSQLForPgLike("kingbase8", in)
want := `SELECT * FROM "ldf_server"."t_user" LIMIT 1`
if out != want {
t.Fatalf("unexpected sanitize output:\nIN: %s\nOUT: %s\nWANT: %s", in, out, want)
}
}
func TestSanitizeSQLForPgLike_FixesBrokenDoubleDoubleQuotes_WithExtraQuotes(t *testing.T) {
in := `SELECT * FROM ""ldf_server""".""t_user"" LIMIT 1`
out := sanitizeSQLForPgLike("kingbase", in)

View File

@@ -295,11 +295,15 @@ func (c *CustomDB) ApplyChanges(tableName string, changes connection.ChangeSet)
driver := strings.ToLower(strings.TrimSpace(c.driver))
isMySQL := strings.Contains(driver, "mysql")
isPostgres := strings.Contains(driver, "postgres") || strings.Contains(driver, "kingbase") || strings.Contains(driver, "pg")
isKingbase := strings.Contains(driver, "kingbase")
isPostgres := strings.Contains(driver, "postgres") || isKingbase || strings.Contains(driver, "pg")
isOracle := strings.Contains(driver, "oracle") || strings.Contains(driver, "ora") || strings.Contains(driver, "dm") || strings.Contains(driver, "dameng")
quoteIdent := func(name string) string {
n := strings.TrimSpace(name)
if isKingbase {
return QuoteKingbaseIdentifier(n)
}
if isMySQL {
n = strings.Trim(n, "`")
n = strings.ReplaceAll(n, "`", "``")
@@ -329,7 +333,9 @@ func (c *CustomDB) ApplyChanges(tableName string, changes connection.ChangeSet)
schema := ""
table := strings.TrimSpace(tableName)
if parts := strings.SplitN(table, ".", 2); len(parts) == 2 {
if isKingbase {
schema, table = SplitKingbaseQualifiedName(table)
} else if parts := strings.SplitN(table, ".", 2); len(parts) == 2 {
schema = strings.TrimSpace(parts[0])
table = strings.TrimSpace(parts[1])
}

View File

@@ -126,6 +126,8 @@ func normalizeDatabaseType(dbType string) string {
return "diros"
case "postgresql":
return "postgres"
case "kingbase8", "kingbasees", "kingbasev8":
return "kingbase"
case "opengauss", "open_gauss", "open-gauss":
return "opengauss"
default:

View File

@@ -55,6 +55,8 @@ func normalizeRuntimeDriverType(driverType string) string {
return "diros"
case "postgresql":
return "postgres"
case "kingbase8", "kingbasees", "kingbasev8":
return "kingbase"
case "opengauss", "open_gauss", "open-gauss":
return "opengauss"
default:

View File

@@ -42,6 +42,15 @@ func TestOptionalDriverAgentRevisionsGeneratedForOptionalDrivers(t *testing.T) {
}
}
func TestKingbaseRuntimeAliasesNormalizeToKingbase(t *testing.T) {
if got := normalizeRuntimeDriverType("kingbase8"); got != "kingbase" {
t.Fatalf("expected kingbase8 runtime alias to normalize to kingbase, got %q", got)
}
if got := normalizeDatabaseType("kingbasees"); got != "kingbase" {
t.Fatalf("expected kingbasees database alias to normalize to kingbase, got %q", got)
}
}
func TestManagedDriverRequiresInstallMarker(t *testing.T) {
tmpDir := t.TempDir()
SetExternalDriverDownloadDirectory(tmpDir)

View File

@@ -1,6 +1,9 @@
package db
import "strings"
import (
"regexp"
"strings"
)
func normalizeKingbaseIdentCommon(raw string) string {
value := strings.TrimSpace(raw)
@@ -83,6 +86,67 @@ func NormalizeKingbaseIdentifier(raw string) string {
return normalizeKingbaseIdentCommon(raw)
}
func normalizeKingbaseIdentifier(raw string) string {
return normalizeKingbaseIdentCommon(raw)
}
// QuoteKingbaseIdentifier quotes a Kingbase identifier only when the dialect requires it.
func QuoteKingbaseIdentifier(raw string) string {
return quoteKingbaseIdent(raw)
}
// kingbaseIdentNeedsQuote 判断标识符是否需要双引号包裹。
// 与前端 sql.ts 中 needsQuote 逻辑保持一致。
func kingbaseIdentNeedsQuote(ident string) bool {
if ident == "" {
return false
}
// 不是合法裸标识符格式(必须以字母或下划线开头,仅含字母、数字、下划线)
if matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, ident); !matched {
return true
}
// 包含大写字母时需要引号保护KingbaseES/PostgreSQL 默认将未加引号的标识符折叠为小写)
for _, r := range ident {
if r >= 'A' && r <= 'Z' {
return true
}
}
// 是 SQL 保留字
return isKingbaseReservedWord(ident)
}
// isKingbaseReservedWord 检查是否为常见 SQL 保留字(简化版,与前端保持一致)。
func isKingbaseReservedWord(ident string) bool {
switch strings.ToLower(ident) {
case "select", "from", "where", "table", "index", "user", "order", "group", "by",
"limit", "offset", "and", "or", "not", "null", "true", "false", "key",
"primary", "foreign", "references", "default", "constraint",
"create", "drop", "alter", "insert", "update", "delete", "set", "values", "into",
"join", "left", "right", "inner", "outer", "on", "as", "is", "in", "like",
"between", "case", "when", "then", "else", "end", "having", "distinct",
"all", "any", "exists", "union", "except", "intersect",
"column", "check", "unique", "with", "grant", "revoke", "trigger",
"begin", "commit", "rollback", "schema", "database", "view", "function",
"procedure", "sequence", "type", "domain", "role", "session", "current",
"authorization", "cross", "full", "natural", "some", "cast", "fetch",
"for", "to", "do", "if", "return", "returns", "declare", "cursor", "server", "owner":
return true
}
return false
}
func quoteKingbaseIdent(name string) string {
n := normalizeKingbaseIdentCommon(name)
if n == "" {
return "\"\""
}
if !kingbaseIdentNeedsQuote(n) {
return n
}
n = strings.ReplaceAll(n, `"`, `""`)
return `"` + n + `"`
}
// SplitKingbaseQualifiedName splits a Kingbase schema-qualified identifier safely.
func SplitKingbaseQualifiedName(raw string) (schema string, table string) {
return splitKingbaseQualifiedNameCommon(raw)

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"net"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
@@ -933,62 +932,6 @@ func (k *KingbaseDB) ApplyChanges(tableName string, changes connection.ChangeSet
return tx.Commit()
}
func normalizeKingbaseIdentifier(raw string) string {
return normalizeKingbaseIdentCommon(raw)
}
// kingbaseIdentNeedsQuote 判断标识符是否需要双引号包裹。
// 与前端 sql.ts 中 needsQuote 逻辑保持一致。
func kingbaseIdentNeedsQuote(ident string) bool {
if ident == "" {
return false
}
// 不是合法裸标识符格式(必须以字母或下划线开头,仅含字母、数字、下划线)
if matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, ident); !matched {
return true
}
// 包含大写字母时需要引号保护KingbaseES/PostgreSQL 默认将未加引号的标识符折叠为小写)
for _, r := range ident {
if r >= 'A' && r <= 'Z' {
return true
}
}
// 是 SQL 保留字
return isKingbaseReservedWord(ident)
}
// isKingbaseReservedWord 检查是否为常见 SQL 保留字(简化版,与前端保持一致)。
func isKingbaseReservedWord(ident string) bool {
switch strings.ToLower(ident) {
case "select", "from", "where", "table", "index", "user", "order", "group", "by",
"limit", "offset", "and", "or", "not", "null", "true", "false", "key",
"primary", "foreign", "references", "default", "constraint",
"create", "drop", "alter", "insert", "update", "delete", "set", "values", "into",
"join", "left", "right", "inner", "outer", "on", "as", "is", "in", "like",
"between", "case", "when", "then", "else", "end", "having", "distinct",
"all", "any", "exists", "union", "except", "intersect",
"column", "check", "unique", "with", "grant", "revoke", "trigger",
"begin", "commit", "rollback", "schema", "database", "view", "function",
"procedure", "sequence", "type", "domain", "role", "session", "current",
"authorization", "cross", "full", "natural", "some", "cast", "fetch",
"for", "to", "do", "if", "return", "returns", "declare", "cursor", "server", "owner":
return true
}
return false
}
func quoteKingbaseIdent(name string) string {
n := normalizeKingbaseIdentifier(name)
if n == "" {
return "\"\""
}
if !kingbaseIdentNeedsQuote(n) {
return n
}
n = strings.ReplaceAll(n, `"`, `""`)
return `"` + n + `"`
}
func splitKingbaseQualifiedTable(tableName string) (schema string, table string) {
return splitKingbaseQualifiedNameCommon(tableName)
}

View File

@@ -12,6 +12,8 @@ func normalizeMigrationDBType(dbType string) string {
return "diros"
case "postgresql":
return "postgres"
case "kingbase8", "kingbasees", "kingbasev8":
return "kingbase"
case "opengauss", "open_gauss", "open-gauss":
return "opengauss"
case "dm", "dm8":

View File

@@ -118,16 +118,16 @@ func TestBuildMySQLToKingbaseCreateTablePlan_GeneratesAndSkipsIndexes(t *testing
if err != nil {
t.Fatalf("buildMySQLToKingbaseCreateTablePlan returned error: %v", err)
}
if !strings.Contains(createSQL, `CREATE TABLE "public"."orders"`) {
if !strings.Contains(createSQL, `CREATE TABLE public.orders`) {
t.Fatalf("unexpected create SQL: %s", createSQL)
}
if !strings.Contains(createSQL, `PRIMARY KEY ("id")`) {
if !strings.Contains(createSQL, `PRIMARY KEY (id)`) {
t.Fatalf("create SQL missing primary key: %s", createSQL)
}
if idxCreate != 1 || idxSkip != 2 {
t.Fatalf("unexpected index summary: create=%d skip=%d", idxCreate, idxSkip)
}
if len(postSQL) != 1 || !strings.Contains(postSQL[0], `CREATE INDEX "idx_user_status"`) {
if len(postSQL) != 1 || !strings.Contains(postSQL[0], `CREATE INDEX idx_user_status`) {
t.Fatalf("unexpected post SQL: %v", postSQL)
}
if len(warnings) != 0 {
@@ -177,7 +177,7 @@ func TestBuildSchemaMigrationPlan_AutoCreateWhenTargetMissing(t *testing.T) {
if !strings.Contains(plan.PlannedAction, "自动建表") {
t.Fatalf("unexpected planned action: %s", plan.PlannedAction)
}
if !strings.Contains(plan.CreateTableSQL, `CREATE TABLE "public"."orders"`) {
if !strings.Contains(plan.CreateTableSQL, `CREATE TABLE public.orders`) {
t.Fatalf("unexpected create table SQL: %s", plan.CreateTableSQL)
}
}
@@ -665,13 +665,13 @@ func TestBuildTDengineToPGLikePlan_AutoCreateWhenTargetMissing(t *testing.T) {
if !plan.AutoCreate {
t.Fatalf("expected auto create enabled")
}
if !strings.Contains(plan.CreateTableSQL, `CREATE TABLE "public"."cpu"`) {
if !strings.Contains(plan.CreateTableSQL, `CREATE TABLE public.cpu`) {
t.Fatalf("unexpected create table sql: %s", plan.CreateTableSQL)
}
if !strings.Contains(plan.CreateTableSQL, `"ts" timestamp`) {
if !strings.Contains(plan.CreateTableSQL, `ts timestamp`) {
t.Fatalf("expected timestamp mapping, got: %s", plan.CreateTableSQL)
}
if !strings.Contains(plan.CreateTableSQL, `"payload" jsonb`) {
if !strings.Contains(plan.CreateTableSQL, `payload jsonb`) {
t.Fatalf("expected json mapping, got: %s", plan.CreateTableSQL)
}
if len(plan.Warnings) == 0 || !strings.Contains(strings.Join(plan.Warnings, " "), "TAG") {

View File

@@ -1,6 +1,10 @@
package sync
import "strings"
import (
"strings"
"GoNavi-Wails/internal/db"
)
func normalizeSyncMode(mode string) string {
m := strings.ToLower(strings.TrimSpace(mode))
@@ -21,9 +25,11 @@ func quoteIdentByType(dbType string, ident string) string {
return ident
}
switch dbType {
switch normalizeMigrationDBType(dbType) {
case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse", "tdengine":
return "`" + strings.ReplaceAll(ident, "`", "``") + "`"
case "kingbase":
return db.QuoteKingbaseIdentifier(ident)
case "sqlserver":
escaped := strings.ReplaceAll(ident, "]", "]]")
return "[" + escaped + "]"
@@ -38,9 +44,21 @@ func quoteQualifiedIdentByType(dbType string, ident string) string {
return raw
}
normalizedType := normalizeMigrationDBType(dbType)
if normalizedType == "kingbase" {
schema, table := db.SplitKingbaseQualifiedName(raw)
if table == "" {
return quoteIdentByType(normalizedType, raw)
}
if schema == "" {
return quoteIdentByType(normalizedType, table)
}
return quoteIdentByType(normalizedType, schema) + "." + quoteIdentByType(normalizedType, table)
}
parts := strings.Split(raw, ".")
if len(parts) <= 1 {
return quoteIdentByType(dbType, raw)
return quoteIdentByType(normalizedType, raw)
}
quotedParts := make([]string, 0, len(parts))
@@ -49,11 +67,11 @@ func quoteQualifiedIdentByType(dbType string, ident string) string {
if part == "" {
continue
}
quotedParts = append(quotedParts, quoteIdentByType(dbType, part))
quotedParts = append(quotedParts, quoteIdentByType(normalizedType, part))
}
if len(quotedParts) == 0 {
return quoteIdentByType(dbType, raw)
return quoteIdentByType(normalizedType, raw)
}
return strings.Join(quotedParts, ".")
}
@@ -65,6 +83,17 @@ func normalizeSchemaAndTable(dbType string, dbName string, tableName string) (st
return rawDB, rawTable
}
normalizedType := normalizeMigrationDBType(dbType)
if normalizedType == "kingbase" {
schema, table := db.SplitKingbaseQualifiedName(rawTable)
if schema != "" && table != "" {
return schema, table
}
if table != "" {
return "public", table
}
}
if parts := strings.SplitN(rawTable, ".", 2); len(parts) == 2 {
schema := strings.TrimSpace(parts[0])
table := strings.TrimSpace(parts[1])
@@ -73,7 +102,7 @@ func normalizeSchemaAndTable(dbType string, dbName string, tableName string) (st
}
}
switch strings.ToLower(strings.TrimSpace(dbType)) {
switch normalizedType {
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
return "public", rawTable
case "duckdb":
@@ -92,7 +121,7 @@ func qualifiedNameForQuery(dbType string, schema string, table string, original
return raw
}
switch strings.ToLower(strings.TrimSpace(dbType)) {
switch normalizeMigrationDBType(dbType) {
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
s := strings.TrimSpace(schema)
if s == "" {

View File

@@ -0,0 +1,60 @@
package sync
import "testing"
func TestQuoteQualifiedIdentByType_KingbaseLeavesLowercaseQualifiedTableUnquoted(t *testing.T) {
t.Parallel()
got := quoteQualifiedIdentByType("kingbase", "ldf_server.andon_events")
if got != "ldf_server.andon_events" {
t.Fatalf("unexpected kingbase qualified identifier: %s", got)
}
}
func TestQuoteQualifiedIdentByType_KingbaseNormalizesEscapedQuotedQualifiedTable(t *testing.T) {
t.Parallel()
got := quoteQualifiedIdentByType("kingbase", `\"Idf_server\".\"andon_events\"`)
if got != `"Idf_server".andon_events` {
t.Fatalf("unexpected kingbase qualified identifier: %s", got)
}
}
func TestQuoteQualifiedIdentByType_KingbaseAliasUsesKingbaseQuoting(t *testing.T) {
t.Parallel()
got := quoteQualifiedIdentByType("kingbase8", `\"ldf_server\".\"andon_events\"`)
if got != "ldf_server.andon_events" {
t.Fatalf("unexpected kingbase alias qualified identifier: %s", got)
}
}
func TestQuoteIdentByType_KingbaseStillQuotesReservedAndMixedCaseIdentifiers(t *testing.T) {
t.Parallel()
if got := quoteIdentByType("kingbase", "select"); got != `"select"` {
t.Fatalf("expected reserved word to stay quoted, got %s", got)
}
if got := quoteIdentByType("kingbase", "CamelName"); got != `"CamelName"` {
t.Fatalf("expected mixed-case identifier to stay quoted, got %s", got)
}
}
func TestNormalizeSchemaAndTable_KingbaseNormalizesEscapedQualifiedName(t *testing.T) {
t.Parallel()
schema, table := normalizeSchemaAndTable("kingbase", "demo", `\"Idf_server\".\"andon_events\"`)
if schema != "Idf_server" || table != "andon_events" {
t.Fatalf("unexpected kingbase schema/table: %q.%q", schema, table)
}
}
func TestNormalizeMigrationDBType_KingbaseAliases(t *testing.T) {
t.Parallel()
for _, in := range []string{"kingbase8", "kingbasees", "kingbasev8"} {
if got := normalizeMigrationDBType(in); got != "kingbase" {
t.Fatalf("normalizeMigrationDBType(%q)=%q, want kingbase", in, got)
}
}
}