🐛 fix(duckdb): 修复元数据兼容与在线安装回退

This commit is contained in:
Syngnat
2026-06-04 08:27:25 +08:00
parent f5166ac3fc
commit 37a094c351
5 changed files with 198 additions and 5 deletions

View File

@@ -161,6 +161,32 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
it('keeps DuckDB table preview writable when unique index metadata arrives as a safe locator', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'slug', key: '' }, { name: 'name', key: '' }],
});
backendApp.DBGetIndexes.mockResolvedValue({
success: true,
data: [{ name: 'events_slug_key', columnName: 'slug', nonUnique: 0, seqInIndex: 1, indexType: 'UNIQUE' }],
});
const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-unique', dbName: 'main', tableName: 'main.events', title: 'events' }));
expect(dataGridState.latestProps?.pkColumns).toEqual([]);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'unique-key',
columns: ['slug'],
valueColumns: ['slug'],
readOnly: false,
});
expect(dataGridState.latestProps?.readOnly).toBe(false);
expect(messageApi.warning).not.toHaveBeenCalled();
renderer.unmount();
});
it('enables MongoDB table preview editing through the _id locator', async () => {
storeState.connections[0].config.type = 'mongodb';
storeState.connections[0].config.database = 'app';

View File

@@ -3847,6 +3847,9 @@ func shouldPreferSourceBuildBeforeDownloadForBuildType(buildType string, driverT
func shouldRequireSourceBuildBeforeDownloadForBuildType(buildType string, driverType string, selectedVersion string) bool {
_ = selectedVersion
if shouldUseDuckDBWindowsDynamicLibrary(driverType) {
return false
}
return shouldPreferDevelopmentDriverAgentSourceBuild(buildType, driverType)
}

View File

@@ -515,8 +515,11 @@ func TestShouldPreferSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T)
}
func TestShouldRequireSourceBuildBeforeDownloadForDevelopmentBuild(t *testing.T) {
if !shouldRequireSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build to require local DuckDB driver-agent source build")
if shouldRequireSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build to allow DuckDB release bundle fallback after local build failure")
}
if !shouldPreferSourceBuildBeforeDownloadForBuildType("dev", "duckdb", "2.5.6") {
t.Fatal("expected development build to still prefer local DuckDB driver-agent source build before bundle fallback")
}
if !shouldRequireSourceBuildBeforeDownloadForBuildType("development", "mariadb", "1.9.3") {
t.Fatal("expected development build alias to require local driver-agent source build")

View File

@@ -2,6 +2,7 @@ package db
import (
"fmt"
"reflect"
"strings"
"GoNavi-Wails/internal/connection"
@@ -67,7 +68,7 @@ func buildDuckDBColumnDefinitions(rows []map[string]interface{}, constraintRows
uniqueColumns := make(map[string]struct{})
for _, row := range constraintRows {
columnNames := parseDuckDBIdentifierList(duckDBRowString(row, "constraint_column_names"))
columnNames := duckDBRowIdentifierList(row, "constraint_column_names")
switch strings.ToUpper(strings.TrimSpace(duckDBRowString(row, "constraint_type"))) {
case "PRIMARY KEY":
for _, columnName := range columnNames {
@@ -115,7 +116,7 @@ func buildDuckDBIndexDefinitions(constraintRows []map[string]interface{}, indexR
for _, row := range constraintRows {
name := strings.TrimSpace(duckDBRowString(row, "constraint_name"))
constraintType := strings.ToUpper(strings.TrimSpace(duckDBRowString(row, "constraint_type")))
columnNames := parseDuckDBIdentifierList(duckDBRowString(row, "constraint_column_names"))
columnNames := duckDBRowIdentifierList(row, "constraint_column_names")
if name == "" || len(columnNames) == 0 {
continue
}
@@ -132,7 +133,7 @@ func buildDuckDBIndexDefinitions(constraintRows []map[string]interface{}, indexR
for _, row := range indexRows {
name := strings.TrimSpace(duckDBRowString(row, "index_name"))
columnNames := parseDuckDBExpressionList(duckDBRowString(row, "expressions"))
columnNames := duckDBRowExpressionList(row, "expressions")
if name == "" || len(columnNames) == 0 {
continue
}
@@ -274,6 +275,18 @@ func duckDBRowString(row map[string]interface{}, keys ...string) string {
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"
@@ -289,12 +302,80 @@ func duckDBRowInt(row map[string]interface{}, keys ...string) int {
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 {
return parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
}
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) {
if rv.Kind() == reflect.Slice && rv.Type().Elem().Kind() == reflect.Uint8 {
return parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
}
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)
}
return parseDuckDBList(strings.TrimSpace(fmt.Sprintf("%v", raw)), normalize)
}
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
}

View File

@@ -90,6 +90,52 @@ func TestBuildDuckDBColumnDefinitions_MarksPrimaryAndUniqueColumns(t *testing.T)
}
}
func TestBuildDuckDBColumnDefinitions_SupportsArrayConstraintColumns(t *testing.T) {
t.Parallel()
columns := buildDuckDBColumnDefinitions(
[]map[string]interface{}{
{
"column_name": "id",
"data_type": "BIGINT",
"is_nullable": "NO",
},
{
"column_name": "tenant_id",
"data_type": "BIGINT",
"is_nullable": "NO",
},
{
"column_name": "slug",
"data_type": "VARCHAR",
"is_nullable": "NO",
},
},
[]map[string]interface{}{
{
"constraint_name": "events_pkey",
"constraint_type": "PRIMARY KEY",
"constraint_column_names": []interface{}{"id", "tenant_id"},
},
{
"constraint_name": "events_slug_key",
"constraint_type": "UNIQUE",
"constraint_column_names": []string{"slug"},
},
},
)
if len(columns) != 3 {
t.Fatalf("unexpected column count: %d", len(columns))
}
if columns[0].Key != "PRI" || columns[1].Key != "PRI" {
t.Fatalf("复合主键列未正确标记: %+v", columns)
}
if columns[2].Key != "UNI" {
t.Fatalf("唯一键列未正确标记: %+v", columns[2])
}
}
func TestBuildDuckDBIndexDefinitions_MergesConstraintsAndUniqueIndexes(t *testing.T) {
t.Parallel()
@@ -132,6 +178,40 @@ func TestBuildDuckDBIndexDefinitions_MergesConstraintsAndUniqueIndexes(t *testin
}
}
func TestBuildDuckDBIndexDefinitions_SupportsArrayMetadataRows(t *testing.T) {
t.Parallel()
indexes := buildDuckDBIndexDefinitions(
[]map[string]interface{}{
{
"constraint_name": "events_business_key",
"constraint_type": "UNIQUE",
"constraint_column_names": []interface{}{"tenant_id", "slug"},
},
},
[]map[string]interface{}{
{
"index_name": "idx_events_expr",
"is_unique": true,
"expressions": []string{`lower("slug")`},
},
},
)
if len(indexes) != 3 {
t.Fatalf("unexpected index row count: %d", len(indexes))
}
if indexes[0].Name != "events_business_key" || indexes[0].ColumnName != "tenant_id" || indexes[0].SeqInIndex != 1 {
t.Fatalf("约束唯一索引首列映射异常: %+v", indexes[0])
}
if indexes[1].Name != "events_business_key" || indexes[1].ColumnName != "slug" || indexes[1].SeqInIndex != 2 {
t.Fatalf("约束唯一索引次列映射异常: %+v", indexes[1])
}
if indexes[2].Name != "idx_events_expr" || indexes[2].ColumnName != `lower("slug")` || indexes[2].NonUnique != 0 {
t.Fatalf("表达式唯一索引映射异常: %+v", indexes[2])
}
}
func TestNormalizeDuckDBObjectPath_PreservesCatalogSchemaAndQuotedDots(t *testing.T) {
t.Parallel()