mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 08:50:17 +08:00
🐛 fix(kingbase): 统一金仓标识符引用策略
- 标识符处理:下沉 Kingbase 引用逻辑,普通小写 schema/table 不再强制双引号包裹 - 表操作修复:修复截断、清空、导入、导出等路径生成异常双引号 SQL - 同步链路修复:统一数据同步、预览、迁移建表中的 Kingbase schema.table 拼接规则 - 自定义驱动兼容:补齐 kingbase8/kingbasees/kingbasev8 别名归一与写入路径处理 - 回归覆盖:新增 ldf_server.andon_events、转义引号、保留字和大小写标识符测试
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 查询导出"}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"""),单次修复不一定收敛。
|
||||
// 这里做有限次数的迭代,直到输出不再变化。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
60
internal/sync/sql_helpers_test.go
Normal file
60
internal/sync/sql_helpers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user