feat(datasource): 支持 DuckDB Parquet 文件模式并优化弹窗打开链路

- 统一 DuckDB 文件库与 Parquet 文件接入能力
- 补充 URI、文件选择、只读挂载与连接缓存键处理
- 去掉数据源卡片点击前的同步驱动查询,修复打开卡顿
- refs #166
This commit is contained in:
杨国锋
2026-03-08 18:42:27 +08:00
parent e521d2125f
commit b85c7529ec
14 changed files with 35 additions and 342 deletions

View File

@@ -6,10 +6,8 @@ import (
"context"
"database/sql"
"fmt"
"path/filepath"
"strings"
"time"
"unicode"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/utils"
@@ -18,9 +16,6 @@ import (
type DuckDB struct {
conn *sql.DB
pingTimeout time.Duration
mode string
sourcePath string
mountedView string
}
func (d *DuckDB) Connect(config connection.ConnectionConfig) error {
@@ -28,18 +23,11 @@ func (d *DuckDB) Connect(config connection.ConnectionConfig) error {
return fmt.Errorf("DuckDB 驱动不可用:%s", reason)
}
sourcePath := strings.TrimSpace(config.Host)
if sourcePath == "" {
sourcePath = strings.TrimSpace(config.Database)
dsn := strings.TrimSpace(config.Host)
if dsn == "" {
dsn = strings.TrimSpace(config.Database)
}
mode := normalizeDuckDBConnectionMode(config.DuckDBMode, sourcePath)
dsn := sourcePath
if mode == "parquet" {
if strings.TrimSpace(sourcePath) == "" || sourcePath == ":memory:" {
return fmt.Errorf("Parquet 文件模式要求提供 .parquet 或 .parq 文件路径")
}
dsn = ":memory:"
} else if dsn == "" {
if dsn == "" {
dsn = ":memory:"
}
@@ -49,22 +37,12 @@ func (d *DuckDB) Connect(config connection.ConnectionConfig) error {
}
d.conn = db
d.pingTimeout = getConnectTimeout(config)
d.mode = mode
d.sourcePath = sourcePath
d.mountedView = ""
if err := d.Ping(); err != nil {
_ = db.Close()
d.conn = nil
return fmt.Errorf("连接建立后验证失败:%w", err)
}
if mode == "parquet" {
if err := d.mountParquetView(sourcePath); err != nil {
_ = db.Close()
d.conn = nil
return fmt.Errorf("连接建立后挂载 Parquet 失败:%w", err)
}
}
return nil
}
@@ -421,26 +399,6 @@ func (d *DuckDB) ApplyChanges(tableName string, changes connection.ChangeSet) er
return tx.Commit()
}
func (d *DuckDB) mountParquetView(sourcePath string) error {
if d.conn == nil {
return fmt.Errorf("connection not open")
}
viewName := deriveDuckDBParquetViewName(sourcePath)
if viewName == "" {
viewName = "parquet_data"
}
query := fmt.Sprintf(
"CREATE OR REPLACE VIEW %s AS SELECT * FROM read_parquet('%s')",
quoteDuckDBQualifiedTable("main", viewName),
escapeDuckDBLiteral(sourcePath),
)
if _, err := d.conn.Exec(query); err != nil {
return err
}
d.mountedView = viewName
return nil
}
func normalizeDuckDBSchemaAndTable(dbName string, tableName string) (string, string) {
schema := strings.TrimSpace(dbName)
table := strings.TrimSpace(tableName)
@@ -506,49 +464,3 @@ func duckDBRowString(row map[string]interface{}, keys ...string) string {
func escapeDuckDBLiteral(raw string) string {
return strings.ReplaceAll(raw, "'", "''")
}
func normalizeDuckDBConnectionMode(raw string, sourcePath string) string {
mode := strings.ToLower(strings.TrimSpace(raw))
if mode == "parquet" {
return "parquet"
}
if mode == "database" {
return "database"
}
lowerPath := strings.ToLower(strings.TrimSpace(sourcePath))
if strings.HasSuffix(lowerPath, ".parquet") || strings.HasSuffix(lowerPath, ".parq") {
return "parquet"
}
return "database"
}
func deriveDuckDBParquetViewName(sourcePath string) string {
baseName := strings.TrimSpace(filepath.Base(strings.TrimSpace(sourcePath)))
if ext := filepath.Ext(baseName); ext != "" {
baseName = strings.TrimSuffix(baseName, ext)
}
if baseName == "" {
return "parquet_data"
}
var builder strings.Builder
for _, r := range baseName {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r):
builder.WriteRune(unicode.ToLower(r))
case r == '_':
builder.WriteRune(r)
default:
builder.WriteRune('_')
}
}
name := strings.Trim(builder.String(), "_")
if name == "" {
name = "parquet_data"
}
if unicode.IsDigit(rune(name[0])) {
name = "parquet_" + name
}
return name
}