mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 19:19:35 +08:00
- DuckDB 显式唯一索引表达式返回字符串包裹标识符时,统一归一化为真实列名 - 补充 DuckDB 主键、唯一约束、显式唯一索引的真实驱动回归测试 - 将 duckdb_metadata.go 纳入 DuckDB driver-agent revision 计算,确保重装驱动后加载新元数据逻辑
612 lines
15 KiB
Go
612 lines
15 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
)
|
|
|
|
type duckDBObjectPath struct {
|
|
Catalog string
|
|
Schema string
|
|
Object string
|
|
}
|
|
|
|
func buildDuckDBConstraintMetadataQuery(path duckDBObjectPath, exact bool) string {
|
|
base := `
|
|
SELECT
|
|
database_name,
|
|
schema_name,
|
|
table_name,
|
|
constraint_name,
|
|
constraint_type,
|
|
constraint_column_names
|
|
FROM duckdb_constraints()
|
|
WHERE table_name = '%s'
|
|
AND constraint_type IN ('PRIMARY KEY', 'UNIQUE')`
|
|
args := []any{escapeDuckDBLiteral(path.Object)}
|
|
if exact && path.Schema != "" {
|
|
base += "\n AND schema_name = '%s'"
|
|
args = append(args, escapeDuckDBLiteral(path.Schema))
|
|
}
|
|
if exact && path.Catalog != "" {
|
|
base += "\n AND database_name = '%s'"
|
|
args = append(args, escapeDuckDBLiteral(path.Catalog))
|
|
}
|
|
base += "\nORDER BY database_name, schema_name, table_name, constraint_type, constraint_name"
|
|
return fmt.Sprintf(base, args...)
|
|
}
|
|
|
|
func buildDuckDBIndexMetadataQuery(path duckDBObjectPath, exact bool) string {
|
|
base := `
|
|
SELECT
|
|
database_name,
|
|
schema_name,
|
|
table_name,
|
|
index_name,
|
|
is_unique,
|
|
expressions
|
|
FROM duckdb_indexes()
|
|
WHERE table_name = '%s'`
|
|
args := []any{escapeDuckDBLiteral(path.Object)}
|
|
if exact && path.Schema != "" {
|
|
base += "\n AND schema_name = '%s'"
|
|
args = append(args, escapeDuckDBLiteral(path.Schema))
|
|
}
|
|
if exact && path.Catalog != "" {
|
|
base += "\n AND database_name = '%s'"
|
|
args = append(args, escapeDuckDBLiteral(path.Catalog))
|
|
}
|
|
base += "\nORDER BY database_name, schema_name, table_name, index_name"
|
|
return fmt.Sprintf(base, args...)
|
|
}
|
|
|
|
func buildDuckDBColumnDefinitions(rows []map[string]interface{}, constraintRows []map[string]interface{}) []connection.ColumnDefinition {
|
|
primaryKeyColumns := make(map[string]struct{})
|
|
uniqueColumns := make(map[string]struct{})
|
|
|
|
for _, row := range constraintRows {
|
|
columnNames := duckDBRowIdentifierList(row, "constraint_column_names")
|
|
switch strings.ToUpper(strings.TrimSpace(duckDBRowString(row, "constraint_type"))) {
|
|
case "PRIMARY KEY":
|
|
for _, columnName := range columnNames {
|
|
primaryKeyColumns[strings.ToLower(columnName)] = struct{}{}
|
|
}
|
|
case "UNIQUE":
|
|
for _, columnName := range columnNames {
|
|
uniqueColumns[strings.ToLower(columnName)] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
columns := make([]connection.ColumnDefinition, 0, len(rows))
|
|
for _, row := range rows {
|
|
columnName := strings.TrimSpace(duckDBRowString(row, "column_name"))
|
|
column := connection.ColumnDefinition{
|
|
Name: columnName,
|
|
Type: duckDBRowString(row, "data_type"),
|
|
Nullable: strings.ToUpper(strings.TrimSpace(duckDBRowString(row, "is_nullable"))),
|
|
Key: "",
|
|
Extra: "",
|
|
Comment: "",
|
|
}
|
|
if column.Nullable == "" {
|
|
column.Nullable = "YES"
|
|
}
|
|
if _, ok := primaryKeyColumns[strings.ToLower(columnName)]; ok {
|
|
column.Key = "PRI"
|
|
} else if _, ok := uniqueColumns[strings.ToLower(columnName)]; ok {
|
|
column.Key = "UNI"
|
|
}
|
|
if defaultVal := strings.TrimSpace(duckDBRowString(row, "column_default")); defaultVal != "" && defaultVal != "<nil>" {
|
|
def := defaultVal
|
|
column.Default = &def
|
|
}
|
|
columns = append(columns, column)
|
|
}
|
|
|
|
return columns
|
|
}
|
|
|
|
func buildDuckDBIndexDefinitions(constraintRows []map[string]interface{}, indexRows []map[string]interface{}) []connection.IndexDefinition {
|
|
indexes := make([]connection.IndexDefinition, 0, len(constraintRows)+len(indexRows))
|
|
|
|
for _, row := range constraintRows {
|
|
name := strings.TrimSpace(duckDBRowString(row, "constraint_name"))
|
|
constraintType := strings.ToUpper(strings.TrimSpace(duckDBRowString(row, "constraint_type")))
|
|
columnNames := duckDBRowIdentifierList(row, "constraint_column_names")
|
|
if name == "" || len(columnNames) == 0 {
|
|
continue
|
|
}
|
|
for idx, columnName := range columnNames {
|
|
indexes = append(indexes, connection.IndexDefinition{
|
|
Name: name,
|
|
ColumnName: columnName,
|
|
NonUnique: 0,
|
|
SeqInIndex: idx + 1,
|
|
IndexType: constraintType,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, row := range indexRows {
|
|
name := strings.TrimSpace(duckDBRowString(row, "index_name"))
|
|
columnNames := duckDBRowExpressionList(row, "expressions")
|
|
if name == "" || len(columnNames) == 0 {
|
|
continue
|
|
}
|
|
nonUnique := 1
|
|
if duckDBRowBool(row, "is_unique") {
|
|
nonUnique = 0
|
|
}
|
|
for idx, columnName := range columnNames {
|
|
indexes = append(indexes, connection.IndexDefinition{
|
|
Name: name,
|
|
ColumnName: columnName,
|
|
NonUnique: nonUnique,
|
|
SeqInIndex: idx + 1,
|
|
IndexType: "INDEX",
|
|
})
|
|
}
|
|
}
|
|
|
|
return indexes
|
|
}
|
|
|
|
func normalizeDuckDBObjectPath(dbName string, tableName string) duckDBObjectPath {
|
|
rawDB := strings.TrimSpace(dbName)
|
|
rawTable := strings.TrimSpace(tableName)
|
|
if rawTable == "" {
|
|
if rawDB == "" {
|
|
return duckDBObjectPath{Schema: "main"}
|
|
}
|
|
dbParts := splitDuckDBQualifiedName(rawDB)
|
|
switch len(dbParts) {
|
|
case 0:
|
|
return duckDBObjectPath{Schema: "main"}
|
|
case 1:
|
|
return duckDBObjectPath{Catalog: normalizeDuckDBIdentifier(dbParts[0])}
|
|
default:
|
|
return duckDBObjectPath{
|
|
Catalog: normalizeDuckDBIdentifier(dbParts[0]),
|
|
Schema: normalizeDuckDBIdentifier(dbParts[len(dbParts)-1]),
|
|
}
|
|
}
|
|
}
|
|
|
|
parts := splitDuckDBQualifiedName(rawTable)
|
|
switch len(parts) {
|
|
case 0:
|
|
return duckDBObjectPath{Schema: "main"}
|
|
case 1:
|
|
schema := "main"
|
|
if rawDB != "" {
|
|
dbParts := splitDuckDBQualifiedName(rawDB)
|
|
if len(dbParts) >= 2 {
|
|
return duckDBObjectPath{
|
|
Catalog: normalizeDuckDBIdentifier(dbParts[0]),
|
|
Schema: normalizeDuckDBIdentifier(dbParts[len(dbParts)-1]),
|
|
Object: normalizeDuckDBIdentifier(parts[0]),
|
|
}
|
|
}
|
|
schema = normalizeDuckDBIdentifier(rawDB)
|
|
}
|
|
return duckDBObjectPath{
|
|
Schema: schema,
|
|
Object: normalizeDuckDBIdentifier(parts[0]),
|
|
}
|
|
case 2:
|
|
if rawDB != "" {
|
|
dbParts := splitDuckDBQualifiedName(rawDB)
|
|
if len(dbParts) == 1 && (strings.EqualFold(dbParts[0], "main") || strings.EqualFold(dbParts[0], "memory")) {
|
|
return duckDBObjectPath{
|
|
Schema: normalizeDuckDBIdentifier(parts[0]),
|
|
Object: normalizeDuckDBIdentifier(parts[1]),
|
|
}
|
|
}
|
|
if len(dbParts) == 1 {
|
|
return duckDBObjectPath{
|
|
Catalog: normalizeDuckDBIdentifier(dbParts[0]),
|
|
Schema: normalizeDuckDBIdentifier(parts[0]),
|
|
Object: normalizeDuckDBIdentifier(parts[1]),
|
|
}
|
|
}
|
|
if len(dbParts) >= 2 {
|
|
return duckDBObjectPath{
|
|
Catalog: normalizeDuckDBIdentifier(dbParts[0]),
|
|
Schema: normalizeDuckDBIdentifier(parts[0]),
|
|
Object: normalizeDuckDBIdentifier(parts[1]),
|
|
}
|
|
}
|
|
}
|
|
return duckDBObjectPath{
|
|
Schema: normalizeDuckDBIdentifier(parts[0]),
|
|
Object: normalizeDuckDBIdentifier(parts[1]),
|
|
}
|
|
default:
|
|
return duckDBObjectPath{
|
|
Catalog: normalizeDuckDBIdentifier(parts[len(parts)-3]),
|
|
Schema: normalizeDuckDBIdentifier(parts[len(parts)-2]),
|
|
Object: normalizeDuckDBIdentifier(parts[len(parts)-1]),
|
|
}
|
|
}
|
|
}
|
|
|
|
func normalizeDuckDBSchemaAndTable(dbName string, tableName string) (string, string) {
|
|
path := normalizeDuckDBObjectPath(dbName, tableName)
|
|
schema := path.Schema
|
|
if schema == "" {
|
|
schema = "main"
|
|
}
|
|
return schema, path.Object
|
|
}
|
|
|
|
func normalizeDuckDBIdentifier(raw string) string {
|
|
text := strings.TrimSpace(normalizeSQLIdentifierEscapes(raw))
|
|
if len(text) >= 2 {
|
|
first := text[0]
|
|
last := text[len(text)-1]
|
|
if (first == '"' && last == '"') || (first == '`' && last == '`') {
|
|
text = strings.TrimSpace(text[1 : len(text)-1])
|
|
}
|
|
}
|
|
return text
|
|
}
|
|
|
|
func quoteDuckDBIdentifier(raw string) string {
|
|
text := normalizeDuckDBIdentifier(raw)
|
|
return `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
|
|
}
|
|
|
|
func quoteDuckDBQualifiedTable(schema string, table string) string {
|
|
s := strings.TrimSpace(schema)
|
|
t := strings.TrimSpace(table)
|
|
if s == "" {
|
|
return quoteDuckDBIdentifier(t)
|
|
}
|
|
return quoteDuckDBIdentifier(s) + "." + quoteDuckDBIdentifier(t)
|
|
}
|
|
|
|
func duckDBRowString(row map[string]interface{}, keys ...string) string {
|
|
for _, key := range keys {
|
|
for rowKey, value := range row {
|
|
if !strings.EqualFold(rowKey, key) || value == nil {
|
|
continue
|
|
}
|
|
return fmt.Sprintf("%v", value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func duckDBRowValue(row map[string]interface{}, keys ...string) interface{} {
|
|
for _, key := range keys {
|
|
for rowKey, value := range row {
|
|
if !strings.EqualFold(rowKey, key) {
|
|
continue
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func duckDBRowBool(row map[string]interface{}, keys ...string) bool {
|
|
value := strings.TrimSpace(strings.ToLower(duckDBRowString(row, keys...)))
|
|
return value == "true" || value == "1" || value == "yes"
|
|
}
|
|
|
|
func duckDBRowInt(row map[string]interface{}, keys ...string) int {
|
|
raw := strings.TrimSpace(duckDBRowString(row, keys...))
|
|
if raw == "" {
|
|
return 0
|
|
}
|
|
var value int
|
|
_, _ = fmt.Sscanf(raw, "%d", &value)
|
|
return value
|
|
}
|
|
|
|
func duckDBRowIdentifierList(row map[string]interface{}, keys ...string) []string {
|
|
return parseDuckDBListValue(duckDBRowValue(row, keys...), true)
|
|
}
|
|
|
|
func duckDBRowExpressionList(row map[string]interface{}, keys ...string) []string {
|
|
return parseDuckDBListValue(duckDBRowValue(row, keys...), false)
|
|
}
|
|
|
|
func parseDuckDBIdentifierList(raw string) []string {
|
|
return parseDuckDBList(raw, true)
|
|
}
|
|
|
|
func parseDuckDBExpressionList(raw string) []string {
|
|
values := parseDuckDBList(raw, false)
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
|
|
func parseDuckDBListValue(raw interface{}, normalize bool) []string {
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
|
|
switch typed := raw.(type) {
|
|
case []string:
|
|
values := append([]string(nil), typed...)
|
|
if normalize {
|
|
return normalizeDuckDBIdentifierEntries(values)
|
|
}
|
|
return normalizeDuckDBExpressionList(values)
|
|
case []interface{}:
|
|
values := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
values = append(values, strings.TrimSpace(fmt.Sprintf("%v", item)))
|
|
}
|
|
if normalize {
|
|
return normalizeDuckDBIdentifierEntries(values)
|
|
}
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
|
|
rv := reflect.ValueOf(raw)
|
|
if rv.IsValid() && rv.Kind() != reflect.String && rv.Kind() != reflect.Slice && rv.Kind() != reflect.Array {
|
|
values := parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
|
|
if !normalize {
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
return values
|
|
}
|
|
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) {
|
|
if rv.Kind() == reflect.Slice && rv.Type().Elem().Kind() == reflect.Uint8 {
|
|
values := parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
|
|
if !normalize {
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
return values
|
|
}
|
|
values := make([]string, 0, rv.Len())
|
|
for i := 0; i < rv.Len(); i++ {
|
|
values = append(values, strings.TrimSpace(fmt.Sprintf("%v", rv.Index(i).Interface())))
|
|
}
|
|
if normalize {
|
|
return normalizeDuckDBIdentifierEntries(values)
|
|
}
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
|
|
values := parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
|
|
if !normalize {
|
|
return normalizeDuckDBExpressionList(values)
|
|
}
|
|
return values
|
|
}
|
|
|
|
func normalizeDuckDBIdentifierEntries(values []string) []string {
|
|
normalized := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
normalized = append(normalized, normalizeDuckDBIdentifier(trimmed))
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func normalizeDuckDBExpressionList(values []string) []string {
|
|
if len(values) == 0 {
|
|
return values
|
|
}
|
|
normalized := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
trimmed = normalizeDuckDBExpressionIdentifierLiteral(trimmed)
|
|
switch {
|
|
case trimmed == "":
|
|
continue
|
|
case isDuckDBSimpleIdentifierExpression(trimmed):
|
|
normalized = append(normalized, normalizeDuckDBIdentifier(trimmed))
|
|
default:
|
|
normalized = append(normalized, trimmed)
|
|
}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func normalizeDuckDBExpressionIdentifierLiteral(raw string) string {
|
|
text := strings.TrimSpace(raw)
|
|
if len(text) < 2 || text[0] != '\'' || text[len(text)-1] != '\'' {
|
|
return text
|
|
}
|
|
|
|
inner := strings.TrimSpace(text[1 : len(text)-1])
|
|
inner = strings.ReplaceAll(inner, `''`, `'`)
|
|
inner = normalizeSQLIdentifierEscapes(inner)
|
|
if inner == "" {
|
|
return text
|
|
}
|
|
if strings.ContainsAny(inner, "() +-/*%") {
|
|
return text
|
|
}
|
|
if len(inner) >= 2 {
|
|
first := inner[0]
|
|
last := inner[len(inner)-1]
|
|
if (first == '"' && last == '"') || (first == '`' && last == '`') {
|
|
return inner
|
|
}
|
|
}
|
|
if strings.ContainsAny(inner, `"'`) {
|
|
return text
|
|
}
|
|
return inner
|
|
}
|
|
|
|
func parseDuckDBList(raw string, normalize bool) []string {
|
|
text := strings.TrimSpace(normalizeSQLIdentifierEscapes(raw))
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") {
|
|
text = text[1 : len(text)-1]
|
|
}
|
|
values := make([]string, 0)
|
|
var current strings.Builder
|
|
inDouble := false
|
|
inBacktick := false
|
|
depth := 0
|
|
|
|
flush := func() {
|
|
value := strings.TrimSpace(current.String())
|
|
current.Reset()
|
|
if value == "" {
|
|
return
|
|
}
|
|
if normalize {
|
|
value = normalizeDuckDBIdentifier(value)
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
|
|
for i := 0; i < len(text); i++ {
|
|
ch := text[i]
|
|
switch ch {
|
|
case '"':
|
|
current.WriteByte(ch)
|
|
if inDouble && i+1 < len(text) && text[i+1] == '"' {
|
|
current.WriteByte(text[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
if !inBacktick {
|
|
inDouble = !inDouble
|
|
}
|
|
case '`':
|
|
current.WriteByte(ch)
|
|
if inBacktick && i+1 < len(text) && text[i+1] == '`' {
|
|
current.WriteByte(text[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
if !inDouble {
|
|
inBacktick = !inBacktick
|
|
}
|
|
case '(':
|
|
current.WriteByte(ch)
|
|
if !inDouble && !inBacktick {
|
|
depth++
|
|
}
|
|
case ')':
|
|
current.WriteByte(ch)
|
|
if !inDouble && !inBacktick && depth > 0 {
|
|
depth--
|
|
}
|
|
case ',':
|
|
if !inDouble && !inBacktick && depth == 0 {
|
|
flush()
|
|
continue
|
|
}
|
|
current.WriteByte(ch)
|
|
default:
|
|
current.WriteByte(ch)
|
|
}
|
|
}
|
|
flush()
|
|
|
|
return values
|
|
}
|
|
|
|
func splitDuckDBQualifiedName(raw string) []string {
|
|
text := strings.TrimSpace(normalizeSQLIdentifierEscapes(raw))
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
parts := make([]string, 0, 3)
|
|
var current strings.Builder
|
|
inDouble := false
|
|
inBacktick := false
|
|
inBracket := false
|
|
|
|
flush := func() {
|
|
value := strings.TrimSpace(current.String())
|
|
current.Reset()
|
|
if value == "" {
|
|
return
|
|
}
|
|
parts = append(parts, value)
|
|
}
|
|
|
|
for i := 0; i < len(text); i++ {
|
|
ch := text[i]
|
|
if inDouble {
|
|
current.WriteByte(ch)
|
|
if ch == '"' && i+1 < len(text) && text[i+1] == '"' {
|
|
current.WriteByte(text[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
if ch == '"' {
|
|
inDouble = false
|
|
}
|
|
continue
|
|
}
|
|
if inBacktick {
|
|
current.WriteByte(ch)
|
|
if ch == '`' && i+1 < len(text) && text[i+1] == '`' {
|
|
current.WriteByte(text[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
if ch == '`' {
|
|
inBacktick = false
|
|
}
|
|
continue
|
|
}
|
|
if inBracket {
|
|
current.WriteByte(ch)
|
|
if ch == ']' && i+1 < len(text) && text[i+1] == ']' {
|
|
current.WriteByte(text[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
if ch == ']' {
|
|
inBracket = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch ch {
|
|
case '"':
|
|
inDouble = true
|
|
current.WriteByte(ch)
|
|
case '`':
|
|
inBacktick = true
|
|
current.WriteByte(ch)
|
|
case '[':
|
|
inBracket = true
|
|
current.WriteByte(ch)
|
|
case '.':
|
|
flush()
|
|
default:
|
|
current.WriteByte(ch)
|
|
}
|
|
}
|
|
flush()
|
|
|
|
return parts
|
|
}
|
|
|
|
func isDuckDBSimpleIdentifierExpression(raw string) bool {
|
|
text := strings.TrimSpace(raw)
|
|
if text == "" {
|
|
return false
|
|
}
|
|
if strings.ContainsAny(text, "() +-/*%") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func escapeDuckDBLiteral(raw string) string {
|
|
return strings.ReplaceAll(raw, "'", "''")
|
|
}
|