🐛 fix(db): 修复达梦DDL缺失字段注释 Fixes #615

- 补齐达梦列元数据查询中的字段注释读取
- 保留原始 CREATE TABLE 结果并追加 COMMENT ON COLUMN 语句
- 修正达梦列定义映射时 COMMENT 丢失的问题
- 新增达梦 DDL 注释补全回归测试
This commit is contained in:
Syngnat
2026-07-06 08:56:41 +08:00
parent 00d70d2934
commit 93dc696e90
4 changed files with 130 additions and 2 deletions

View File

@@ -1766,6 +1766,9 @@ func resolveCreateStatementWithFallbackWithText(dbInst db.Database, config conne
sqlStr, sourceErr := dbInst.GetCreateStatement(metadataSchemaName, metadataTableName)
if sourceErr == nil && !shouldFallbackCreateStatement(dbType, sqlStr) {
if strings.TrimSpace(sqlStr) != "" {
if columns, err := loadCreateStatementCommentColumns(dbInst, dbType, metadataSchemaName, metadataTableName); err == nil {
sqlStr = appendCreateStatementColumnComments(dbType, ddlSchemaName, ddlTableName, sqlStr, columns)
}
return sqlStr, nil
}
if isOceanBaseOracleProtocol(config) {
@@ -1916,6 +1919,51 @@ func buildFallbackCreateStatement(dbType string, schemaName string, tableName st
return buildFallbackCreateStatementWithText(dbType, schemaName, tableName, columns, nil, defaultDBBackendText)
}
func loadCreateStatementCommentColumns(dbInst db.Database, dbType string, schemaName string, tableName string) ([]connection.ColumnDefinition, error) {
if !shouldAppendColumnCommentsToCreateStatement(dbType) {
return nil, nil
}
return dbInst.GetColumns(schemaName, tableName)
}
func shouldAppendColumnCommentsToCreateStatement(dbType string) bool {
switch dbType {
case "dameng":
return true
default:
return false
}
}
func appendCreateStatementColumnComments(dbType string, schemaName string, tableName string, ddl string, columns []connection.ColumnDefinition) string {
if !shouldAppendColumnCommentsToCreateStatement(dbType) || strings.TrimSpace(ddl) == "" || len(columns) == 0 {
return ddl
}
qualifiedTable := quoteTableIdentByType(dbType, schemaName, tableName)
existingDDLUpper := strings.ToUpper(ddl)
commentStatements := make([]string, 0, len(columns))
for _, col := range columns {
commentSQL := buildFallbackColumnCommentStatement(dbType, schemaName, tableName, qualifiedTable, col.Name, col.Comment)
if commentSQL == "" {
continue
}
if strings.Contains(existingDDLUpper, strings.ToUpper(commentSQL)) {
continue
}
commentStatements = append(commentStatements, commentSQL)
}
if len(commentStatements) == 0 {
return ddl
}
trimmedDDL := strings.TrimRight(ddl, " \t\r\n")
if !strings.HasSuffix(trimmedDDL, ";") {
trimmedDDL += ";"
}
return trimmedDDL + "\n" + strings.Join(commentStatements, "\n")
}
func buildFallbackCreateStatementWithText(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition, indexes []connection.IndexDefinition, text func(string, map[string]any) string) (string, error) {
if text == nil {
text = defaultDBBackendText

View File

@@ -286,6 +286,39 @@ func TestResolveCreateStatementWithFallback_KingbaseIncludesColumnComments(t *te
}
}
func TestResolveCreateStatementWithFallback_DamengAppendsColumnComments(t *testing.T) {
t.Parallel()
dbInst := &fakeCreateStatementDB{
createSQL: `CREATE TABLE "APP"."ORDERS" ("ID" NUMBER NOT NULL, "STATUS" VARCHAR2(32));`,
columns: []connection.ColumnDefinition{
{Name: "ID", Type: "NUMBER", Nullable: "NO", Key: "PRI", Comment: "主键"},
{Name: "STATUS", Type: "VARCHAR2(32)", Nullable: "YES", Comment: "状态"},
},
}
ddl, err := resolveCreateStatementWithFallback(dbInst, connection.ConnectionConfig{
Type: "dameng",
}, "APP", "ORDERS")
if err != nil {
t.Fatalf("resolveCreateStatementWithFallback() unexpected error: %v", err)
}
if !strings.Contains(ddl, dbInst.createSQL) {
t.Fatalf("expected original dameng create SQL to be preserved, got: %s", ddl)
}
for _, want := range []string{
`COMMENT ON COLUMN "APP"."ORDERS"."ID" IS '主键';`,
`COMMENT ON COLUMN "APP"."ORDERS"."STATUS" IS '状态';`,
} {
if !strings.Contains(ddl, want) {
t.Fatalf("expected dameng DDL to contain %q, got: %s", want, ddl)
}
}
if dbInst.columnsCalls != 1 {
t.Fatalf("expected dameng path to load column metadata once, got %d", dbInst.columnsCalls)
}
}
func TestResolveCreateStatementWithFallback_KingbaseIncludesSecondaryIndexes(t *testing.T) {
t.Parallel()

View File

@@ -110,9 +110,11 @@ func buildDamengColumnsQuery(dbName, tableName string) string {
upperDBName := strings.ToUpper(strings.TrimSpace(dbName))
if upperDBName == "" {
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default,
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default, cc.comments AS comment,
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
FROM user_tab_columns c
LEFT JOIN user_col_comments cc
ON cc.table_name = c.table_name AND cc.column_name = c.column_name
LEFT JOIN (
SELECT cols.table_name, cols.column_name
FROM user_constraints cons
@@ -125,9 +127,11 @@ func buildDamengColumnsQuery(dbName, tableName string) string {
ORDER BY c.column_id`, upperTableName, upperTableName, upperTableName)
}
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default,
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default, cc.comments AS comment,
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
FROM all_tab_columns c
LEFT JOIN all_col_comments cc
ON cc.owner = c.owner AND cc.table_name = c.table_name AND cc.column_name = c.column_name
LEFT JOIN (
SELECT cols.owner, cols.table_name, cols.column_name
FROM all_constraints cons
@@ -243,6 +247,7 @@ func buildDamengColumnDefinitions(data []map[string]interface{}) []connection.Co
Type: formatDamengColumnType(row),
Nullable: normalizeDamengNullable(getDamengRowString(row, "NULLABLE")),
Key: getDamengRowString(row, "COLUMN_KEY"),
Comment: getDamengRowString(row, "COMMENT", "COMMENTS"),
}
defaultValue := getDamengRowString(row, "DATA_DEFAULT")

View File

@@ -3,6 +3,7 @@ package db
import (
"errors"
"reflect"
"strings"
"testing"
)
@@ -118,3 +119,44 @@ func TestCollectDamengDatabaseNames_FallbackToCurrentUser(t *testing.T) {
t.Fatalf("兜底查询应该返回当前用户, got=%v want=%v", got, want)
}
}
func TestBuildDamengColumnsQuery_IncludesColumnCommentsJoin(t *testing.T) {
t.Parallel()
userQuery := buildDamengColumnsQuery("", "orders")
if !strings.Contains(userQuery, "user_col_comments") {
t.Fatalf("expected user query to join user_col_comments, got: %s", userQuery)
}
if !strings.Contains(userQuery, "cc.comments AS comment") {
t.Fatalf("expected user query to select column comments, got: %s", userQuery)
}
allQuery := buildDamengColumnsQuery("app", "orders")
if !strings.Contains(allQuery, "all_col_comments") {
t.Fatalf("expected schema query to join all_col_comments, got: %s", allQuery)
}
if !strings.Contains(allQuery, "cc.comments AS comment") {
t.Fatalf("expected schema query to select column comments, got: %s", allQuery)
}
}
func TestBuildDamengColumnDefinitions_MapsComment(t *testing.T) {
t.Parallel()
columns := buildDamengColumnDefinitions([]map[string]interface{}{
{
"COLUMN_NAME": "ID",
"DATA_TYPE": "NUMBER",
"NULLABLE": "N",
"COLUMN_KEY": "PRI",
"COMMENT": "主键",
},
})
if len(columns) != 1 {
t.Fatalf("expected one column, got=%d", len(columns))
}
if columns[0].Comment != "主键" {
t.Fatalf("expected comment to be mapped, got=%q", columns[0].Comment)
}
}