mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
🐛 fix(duckdb): 修复元数据兼容与在线安装回退
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user