feat(editor): 完善 SQL 编辑与数据编辑交互

- 结果区状态按 SQL Tab 独立保存,快捷键可恢复手动隐藏面板

- 对象设计保留完整字段类型和可空信息,完善兼容驱动 DDL 元数据

- 数据编辑新增手动/自动提交设置和自动提交倒计时

- 修复 schema 视图定位时找不到左侧树节点的问题
This commit is contained in:
Syngnat
2026-06-10 14:27:40 +08:00
parent 8ddd8a726d
commit c4153202ba
17 changed files with 890 additions and 126 deletions

View File

@@ -251,8 +251,8 @@ func (c *CustomDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi
schema = dbName
}
query := fmt.Sprintf(`SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
query := fmt.Sprintf(`SELECT column_name, data_type, character_maximum_length, numeric_precision, numeric_scale, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = '%s'`, tableName)
// Adjust for schema if likely supported
@@ -272,30 +272,97 @@ func (c *CustomDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi
var columns []connection.ColumnDefinition
for _, row := range data {
col := connection.ColumnDefinition{}
// flexible mapping
for k, v := range row {
kl := strings.ToLower(k)
val := fmt.Sprintf("%v", v)
if strings.Contains(kl, "field") || strings.Contains(kl, "column_name") {
col.Name = val
} else if strings.Contains(kl, "type") {
col.Type = val
} else if strings.Contains(kl, "null") || strings.Contains(kl, "nullable") {
col.Nullable = val
} else if strings.Contains(kl, "default") {
col.Default = &val
} else if strings.Contains(kl, "key") {
col.Key = val
} else if strings.Contains(kl, "comment") {
col.Comment = val
}
}
columns = append(columns, col)
columns = append(columns, buildCustomColumnDefinition(row))
}
return columns, nil
}
func buildCustomColumnDefinition(row map[string]interface{}) connection.ColumnDefinition {
col := connection.ColumnDefinition{
Name: customMetadataString(row, "Field", "field", "COLUMN_NAME", "column_name", "NAME", "name"),
Type: buildCustomColumnType(row),
Nullable: normalizeCustomNullable(customMetadataString(row, "Null", "null", "IS_NULLABLE", "is_nullable", "NULLABLE", "nullable")),
Key: customMetadataString(row, "Key", "key", "COLUMN_KEY", "column_key", "PRIMARY_KEY", "primary_key"),
Extra: customMetadataString(row, "Extra", "extra", "EXTRA"),
Comment: customMetadataString(row, "Comment", "comment", "COMMENTS", "comments", "COLUMN_COMMENT", "column_comment"),
}
if defaultValue, ok := customMetadataStringOK(row, "Default", "default", "COLUMN_DEFAULT", "column_default", "DATA_DEFAULT", "data_default"); ok {
col.Default = &defaultValue
}
return col
}
func buildCustomColumnType(row map[string]interface{}) string {
rawType := customMetadataString(
row,
"COLUMN_TYPE",
"column_type",
"FULL_TYPE",
"full_type",
"FULL_DATA_TYPE",
"full_data_type",
"TYPE_NAME",
"type_name",
"Type",
"type",
"DATA_TYPE",
"data_type",
)
if rawType == "" || strings.Contains(rawType, "(") {
return rawType
}
upperType := strings.ToUpper(rawType)
charLength := customMetadataInt(row, "CHARACTER_MAXIMUM_LENGTH", "character_maximum_length", "CHARACTER_MAX_LENGTH", "character_max_length", "CHAR_LENGTH", "char_length", "LENGTH", "length")
if charLength > 0 && strings.Contains(upperType, "CHAR") {
return fmt.Sprintf("%s(%d)", rawType, charLength)
}
precision := customMetadataInt(row, "NUMERIC_PRECISION", "numeric_precision", "DATA_PRECISION", "data_precision", "PRECISION", "precision")
if precision > 0 && (strings.Contains(upperType, "DECIMAL") || strings.Contains(upperType, "NUMERIC") || strings.Contains(upperType, "NUMBER")) {
scale := customMetadataInt(row, "NUMERIC_SCALE", "numeric_scale", "DATA_SCALE", "data_scale", "SCALE", "scale")
if scale > 0 {
return fmt.Sprintf("%s(%d,%d)", rawType, precision, scale)
}
return fmt.Sprintf("%s(%d)", rawType, precision)
}
return rawType
}
func customMetadataString(row map[string]interface{}, keys ...string) string {
value, _ := customMetadataStringOK(row, keys...)
return value
}
func customMetadataStringOK(row map[string]interface{}, keys ...string) (string, bool) {
for _, key := range keys {
for rowKey, raw := range row {
if !strings.EqualFold(rowKey, key) || raw == nil {
continue
}
return strings.TrimSpace(fmt.Sprintf("%v", raw)), true
}
}
return "", false
}
func customMetadataInt(row map[string]interface{}, keys ...string) int {
return parseMetadataInt(customMetadataString(row, keys...))
}
func normalizeCustomNullable(value string) string {
trimmed := strings.TrimSpace(value)
switch strings.ToLower(trimmed) {
case "n", "no", "false", "0":
return "NO"
case "y", "yes", "true", "1":
return "YES"
default:
return trimmed
}
}
func (c *CustomDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
return nil, fmt.Errorf("not implemented for custom")
}

View File

@@ -142,3 +142,44 @@ func TestCustomDBOnlyNormalizesBuiltInMySQLDriverDSN(t *testing.T) {
t.Fatalf("non-mysql custom driver DSN should stay untouched, got %q", customMySQLDSNRecordingLastDSN)
}
}
func TestBuildCustomColumnDefinitionPrefersCompleteColumnType(t *testing.T) {
col := buildCustomColumnDefinition(map[string]interface{}{
"COLUMN_NAME": "USER_NAME",
"DATA_TYPE": "varchar",
"COLUMN_TYPE": "varchar(64)",
"IS_NULLABLE": "NO",
})
if col.Name != "USER_NAME" {
t.Fatalf("expected name USER_NAME, got %q", col.Name)
}
if col.Type != "varchar(64)" {
t.Fatalf("expected complete type varchar(64), got %q", col.Type)
}
if col.Nullable != "NO" {
t.Fatalf("expected nullable NO, got %q", col.Nullable)
}
}
func TestBuildCustomColumnDefinitionBuildsTypeFromLengthAndPrecision(t *testing.T) {
nameCol := buildCustomColumnDefinition(map[string]interface{}{
"column_name": "display_name",
"data_type": "varchar",
"character_maximum_length": int64(128),
"is_nullable": "YES",
})
if nameCol.Type != "varchar(128)" {
t.Fatalf("expected varchar(128), got %q", nameCol.Type)
}
amountCol := buildCustomColumnDefinition(map[string]interface{}{
"column_name": "amount",
"data_type": "decimal",
"numeric_precision": float64(10),
"numeric_scale": float64(2),
})
if amountCol.Type != "decimal(10,2)" {
t.Fatalf("expected decimal(10,2), got %q", amountCol.Type)
}
}