Files
MyGoNavi/internal/db/pg_metadata.go
Syngnat 02faa4586b 🐛 fix(metadata): 修复多数据源主键唯一索引识别
- 统一 PG-like 数据源字段和索引元数据查询,支持 search_path 可见表

- 兼容 snake_case、布尔别名和字符串唯一索引标记

- 修复 DuckDB main/memory 路径解析,避免误判外部 catalog

- 补充前后端回归测试,覆盖可编辑结果定位和元数据重试路径
2026-06-04 10:49:16 +08:00

160 lines
4.8 KiB
Go

package db
import (
"fmt"
"strings"
"GoNavi-Wails/internal/connection"
)
func normalizePGLikeMetadataTable(schemaName, tableName string) (string, string) {
schema := strings.TrimSpace(schemaName)
table := strings.TrimSpace(tableName)
if parsedSchema, parsedTable := SplitSQLQualifiedName(table); parsedSchema != "" && parsedTable != "" {
schema = parsedSchema
table = parsedTable
}
schema = strings.TrimSpace(normalizeSQLIdentifierEscapes(schema))
table = strings.TrimSpace(normalizeSQLIdentifierEscapes(table))
schema = strings.Trim(schema, `"`)
table = strings.Trim(table, `"`)
return schema, table
}
func escapePGLikeMetadataLiteral(raw string) string {
text := strings.TrimSpace(raw)
text = strings.Trim(text, `"`)
return strings.ReplaceAll(text, "'", "''")
}
func buildPGLikeVisibleRelationPredicate(alias string, schemaName string) string {
relAlias := strings.TrimSpace(alias)
if relAlias == "" {
relAlias = "c"
}
if strings.TrimSpace(schemaName) == "" {
return fmt.Sprintf("pg_catalog.pg_table_is_visible(%s.oid)", relAlias)
}
return fmt.Sprintf("n.nspname = '%s'", escapePGLikeMetadataLiteral(schemaName))
}
func buildPGLikeColumnsMetadataQuery(schemaName, tableName string) string {
return fmt.Sprintf(`
SELECT
a.attname AS column_name,
pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
col_description(a.attrelid, a.attnum) AS comment,
CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
LEFT JOIN (
SELECT i.indrelid, a3.attname
FROM pg_index i
JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
WHERE i.indisprimary
) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
WHERE c.relkind IN ('r', 'p')
AND %s
AND c.relname = '%s'
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum`, buildPGLikeVisibleRelationPredicate("c", schemaName), escapePGLikeMetadataLiteral(tableName))
}
func buildPGLikeIndexesMetadataQuery(schemaName, tableName string) string {
return fmt.Sprintf(`
SELECT
i.relname AS index_name,
a.attname AS column_name,
ix.indisunique AS is_unique,
x.ordinality AS seq_in_index,
am.amname AS index_type
FROM pg_class t
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON i.relam = am.oid
JOIN unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinality) ON TRUE
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 %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`, escapePGLikeMetadataLiteral(tableName), buildPGLikeVisibleRelationPredicate("t", schemaName))
}
func buildPGLikeColumnDefinitions(data []map[string]interface{}) []connection.ColumnDefinition {
columns := make([]connection.ColumnDefinition, 0, len(data))
for _, row := range data {
col := connection.ColumnDefinition{
Name: fmt.Sprintf("%v", row["column_name"]),
Type: fmt.Sprintf("%v", row["data_type"]),
Nullable: fmt.Sprintf("%v", row["is_nullable"]),
Key: fmt.Sprintf("%v", row["column_key"]),
Extra: "",
Comment: "",
}
if v, ok := row["comment"]; ok && v != nil {
col.Comment = fmt.Sprintf("%v", v)
}
if v, ok := row["column_default"]; ok && v != nil {
def := fmt.Sprintf("%v", v)
col.Default = &def
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
col.Extra = "auto_increment"
}
}
columns = append(columns, col)
}
return columns
}
func buildPGLikeIndexDefinitions(data []map[string]interface{}) []connection.IndexDefinition {
indexes := make([]connection.IndexDefinition, 0, len(data))
for _, row := range data {
isUnique := false
if v, ok := row["is_unique"]; ok && v != nil {
isUnique = parseMetadataBool(v)
}
nonUnique := 1
if isUnique {
nonUnique = 0
}
seq := 0
if v, ok := row["seq_in_index"]; ok && v != nil {
seq = parseMetadataInt(v)
}
indexType := ""
if v, ok := row["index_type"]; ok && v != nil {
indexType = strings.ToUpper(fmt.Sprintf("%v", v))
}
if indexType == "" {
indexType = "BTREE"
}
indexes = append(indexes, connection.IndexDefinition{
Name: fmt.Sprintf("%v", row["index_name"]),
ColumnName: fmt.Sprintf("%v", row["column_name"]),
NonUnique: nonUnique,
SeqInIndex: seq,
IndexType: indexType,
})
}
return indexes
}