mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 09:41:22 +08:00
🐛 fix(workbench): 修复结果区切换与对象设计字段定义
- 修复手动隐藏结果区后快捷键无法再次显示的问题 - 补全 Dameng/Oracle-like 字段长度、精度和可空标识 - 增加 SQL 编辑器、字段元数据和对象设计回归测试
This commit is contained in:
@@ -741,6 +741,15 @@ describe('QueryEditor external SQL save', () => {
|
||||
});
|
||||
expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL');
|
||||
|
||||
const FakeNode = class {};
|
||||
const bodyNode = new FakeNode();
|
||||
const documentElement = new FakeNode();
|
||||
vi.stubGlobal('Node', FakeNode);
|
||||
vi.stubGlobal('document', {
|
||||
body: bodyNode,
|
||||
documentElement,
|
||||
});
|
||||
editorState.hasTextFocus = false;
|
||||
const isMacRuntime = /(Mac|iPhone|iPad|iPod)/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`);
|
||||
const toggleEvent = {
|
||||
ctrlKey: !isMacRuntime,
|
||||
@@ -748,7 +757,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
altKey: false,
|
||||
shiftKey: true,
|
||||
key: 'm',
|
||||
target: null,
|
||||
target: bodyNode,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -1766,6 +1766,16 @@ const resolveEventTargetNode = (target: EventTarget | null): Node | null => (
|
||||
typeof Node !== 'undefined' && target instanceof Node ? target : null
|
||||
);
|
||||
|
||||
const isDocumentLevelShortcutTarget = (targetNode: Node | null): boolean => {
|
||||
if (!targetNode) {
|
||||
return true;
|
||||
}
|
||||
if (typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return targetNode === document.body || targetNode === document.documentElement;
|
||||
};
|
||||
|
||||
const clearQueryEditorLinkDecorations = (
|
||||
editor: any,
|
||||
decorationIdsRef: React.MutableRefObject<string[]>,
|
||||
@@ -5004,7 +5014,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const targetNode = resolveEventTargetNode(event.target);
|
||||
const editorHasFocus = !!editor?.hasTextFocus?.();
|
||||
const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode));
|
||||
if (!editorHasFocus && !inQueryEditor) {
|
||||
if (!editorHasFocus && !inQueryEditor && !isDocumentLevelShortcutTarget(targetNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,21 @@ describe('columnDefinition metadata normalization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes Dameng style data length and nullable flags', () => {
|
||||
const column = {
|
||||
COLUMN_NAME: 'USER_NAME',
|
||||
DATA_TYPE: 'VARCHAR2',
|
||||
DATA_LENGTH: 64,
|
||||
NULLABLE: 'N',
|
||||
};
|
||||
|
||||
expect(normalizeColumnDefinition(column)).toMatchObject({
|
||||
name: 'USER_NAME',
|
||||
type: 'VARCHAR2(64)',
|
||||
nullable: 'NO',
|
||||
});
|
||||
});
|
||||
|
||||
it('maps boolean primary and unique metadata aliases to GoNavi keys', () => {
|
||||
expect(getColumnDefinitionKey({ column_name: 'id', isPrimary: true })).toBe('PRI');
|
||||
expect(getColumnDefinitionKey({ column_name: 'id', primary_key: 't' })).toBe('PRI');
|
||||
|
||||
@@ -55,6 +55,19 @@ const readNumberProperty = (value: unknown, keys: string[]): number => {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0;
|
||||
};
|
||||
|
||||
const normalizeNullable = (value: string): string => {
|
||||
const normalized = String(value || '').trim();
|
||||
if (!normalized) return '';
|
||||
const upper = normalized.toUpperCase();
|
||||
if (upper === 'N' || upper === 'NO' || upper === 'FALSE' || upper === '0' || upper === 'NOT NULL') {
|
||||
return 'NO';
|
||||
}
|
||||
if (upper === 'Y' || upper === 'YES' || upper === 'TRUE' || upper === '1' || upper === 'NULL' || upper === 'NULLABLE') {
|
||||
return 'YES';
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const getColumnDefinitionName = (column: unknown): string => (
|
||||
readStringProperty(column, ['name', 'Name', 'COLUMN_NAME', 'column_name', 'field', 'Field'])
|
||||
);
|
||||
@@ -85,6 +98,8 @@ export const getColumnDefinitionType = (column: unknown): string => {
|
||||
'character_max_length',
|
||||
'CHAR_LENGTH',
|
||||
'char_length',
|
||||
'DATA_LENGTH',
|
||||
'data_length',
|
||||
'LENGTH',
|
||||
'length',
|
||||
]);
|
||||
@@ -148,7 +163,7 @@ export const normalizeColumnDefinition = (column: unknown): ColumnDefinition =>
|
||||
...source,
|
||||
name: getColumnDefinitionName(column),
|
||||
type: getColumnDefinitionType(column),
|
||||
nullable: readStringProperty(column, ['nullable', 'Nullable', 'NULLABLE', 'is_nullable', 'IS_NULLABLE', 'Null', 'null']),
|
||||
nullable: normalizeNullable(readStringProperty(column, ['nullable', 'Nullable', 'NULLABLE', 'is_nullable', 'IS_NULLABLE', 'Null', 'null'])),
|
||||
key: getColumnDefinitionKey(column),
|
||||
default: source.default,
|
||||
extra: getColumnDefinitionExtra(column),
|
||||
|
||||
@@ -15,6 +15,11 @@ func TestBuildDamengColumnsQuery_IncludesPrimaryKeyMetadata(t *testing.T) {
|
||||
if !strings.Contains(ownerQuery, "AS column_key") {
|
||||
t.Fatalf("owner query 应返回 column_key, got=%s", ownerQuery)
|
||||
}
|
||||
for _, want := range []string{"c.data_length", "c.char_length", "c.data_precision", "c.data_scale"} {
|
||||
if !strings.Contains(ownerQuery, want) {
|
||||
t.Fatalf("owner query 应返回字段类型长度信息 %q, got=%s", want, ownerQuery)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(ownerQuery, "WHERE c.owner = 'BIZ' AND c.table_name = 'ORDERS'") {
|
||||
t.Fatalf("owner query 应按 owner/table 过滤, got=%s", ownerQuery)
|
||||
}
|
||||
@@ -42,22 +47,36 @@ func TestBuildDamengColumnDefinitions_MarksPrimaryKeyColumns(t *testing.T) {
|
||||
{
|
||||
"COLUMN_NAME": "NAME",
|
||||
"DATA_TYPE": "VARCHAR2",
|
||||
"DATA_LENGTH": 128,
|
||||
"CHAR_LENGTH": 64,
|
||||
"NULLABLE": "Y",
|
||||
"DATA_DEFAULT": "guest",
|
||||
"COLUMN_KEY": "",
|
||||
},
|
||||
{
|
||||
"COLUMN_NAME": "AMOUNT",
|
||||
"DATA_TYPE": "NUMBER",
|
||||
"DATA_PRECISION": 10,
|
||||
"DATA_SCALE": 2,
|
||||
"NULLABLE": "N",
|
||||
"DATA_DEFAULT": nil,
|
||||
"COLUMN_KEY": "",
|
||||
},
|
||||
})
|
||||
|
||||
if len(columns) != 2 {
|
||||
if len(columns) != 3 {
|
||||
t.Fatalf("unexpected column count: %d", len(columns))
|
||||
}
|
||||
if columns[0].Name != "ID" || columns[0].Key != "PRI" {
|
||||
if columns[0].Name != "ID" || columns[0].Key != "PRI" || columns[0].Nullable != "NO" {
|
||||
t.Fatalf("主键列未正确标记: %+v", columns[0])
|
||||
}
|
||||
if columns[1].Name != "NAME" || columns[1].Key != "" {
|
||||
if columns[1].Name != "NAME" || columns[1].Type != "VARCHAR2(64)" || columns[1].Nullable != "YES" || columns[1].Key != "" {
|
||||
t.Fatalf("非主键列标记异常: %+v", columns[1])
|
||||
}
|
||||
if columns[1].Default == nil || *columns[1].Default != "guest" {
|
||||
t.Fatalf("默认值未保留: %+v", columns[1])
|
||||
}
|
||||
if columns[2].Name != "AMOUNT" || columns[2].Type != "NUMBER(10,2)" || columns[2].Nullable != "NO" {
|
||||
t.Fatalf("数值字段定义异常: %+v", columns[2])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func buildDamengColumnsQuery(dbName, tableName string) string {
|
||||
upperDBName := strings.ToUpper(strings.TrimSpace(dbName))
|
||||
|
||||
if upperDBName == "" {
|
||||
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.nullable, c.data_default,
|
||||
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
|
||||
FROM user_tab_columns c
|
||||
LEFT JOIN (
|
||||
@@ -123,7 +123,7 @@ func buildDamengColumnsQuery(dbName, tableName string) string {
|
||||
ORDER BY c.column_id`, upperTableName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.nullable, c.data_default,
|
||||
return fmt.Sprintf(`SELECT c.column_name, c.data_type, c.data_length, c.char_length, c.data_precision, c.data_scale, c.nullable, c.data_default,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
|
||||
FROM all_tab_columns c
|
||||
LEFT JOIN (
|
||||
@@ -137,13 +137,77 @@ func buildDamengColumnsQuery(dbName, tableName string) string {
|
||||
ORDER BY c.column_id`, upperDBName, upperTableName)
|
||||
}
|
||||
|
||||
func getDamengRowInt(row map[string]interface{}, keys ...string) (int, bool) {
|
||||
for _, key := range keys {
|
||||
raw := getDamengRowString(row, key)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
var parsed int
|
||||
if _, err := fmt.Sscanf(raw, "%d", &parsed); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func normalizeDamengNullable(value string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "N", "NO", "FALSE", "0", "NOT NULL":
|
||||
return "NO"
|
||||
case "Y", "YES", "TRUE", "1", "NULL", "NULLABLE":
|
||||
return "YES"
|
||||
default:
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
|
||||
func isDamengLengthQualifiedType(upperType string) bool {
|
||||
switch strings.TrimSpace(upperType) {
|
||||
case "CHAR", "NCHAR", "VARCHAR", "VARCHAR2", "NVARCHAR", "NVARCHAR2", "RAW", "BINARY", "VARBINARY":
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(upperType, "CHARACTER")
|
||||
}
|
||||
}
|
||||
|
||||
func formatDamengColumnType(row map[string]interface{}) string {
|
||||
dataType := getDamengRowString(row, "DATA_TYPE")
|
||||
if dataType == "" || strings.Contains(dataType, "(") {
|
||||
return dataType
|
||||
}
|
||||
|
||||
upperType := strings.ToUpper(dataType)
|
||||
if isDamengLengthQualifiedType(upperType) {
|
||||
if charLength, ok := getDamengRowInt(row, "CHAR_LENGTH", "CHAR_COL_DECL_LENGTH"); ok && charLength > 0 {
|
||||
return fmt.Sprintf("%s(%d)", dataType, charLength)
|
||||
}
|
||||
if dataLength, ok := getDamengRowInt(row, "DATA_LENGTH"); ok && dataLength > 0 {
|
||||
return fmt.Sprintf("%s(%d)", dataType, dataLength)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(upperType, "NUMBER") || strings.Contains(upperType, "DECIMAL") || strings.Contains(upperType, "NUMERIC") {
|
||||
precision, hasPrecision := getDamengRowInt(row, "DATA_PRECISION", "NUMERIC_PRECISION")
|
||||
if hasPrecision && precision > 0 {
|
||||
scale, hasScale := getDamengRowInt(row, "DATA_SCALE", "NUMERIC_SCALE")
|
||||
if hasScale && scale > 0 {
|
||||
return fmt.Sprintf("%s(%d,%d)", dataType, precision, scale)
|
||||
}
|
||||
return fmt.Sprintf("%s(%d)", dataType, precision)
|
||||
}
|
||||
}
|
||||
|
||||
return dataType
|
||||
}
|
||||
|
||||
func buildDamengColumnDefinitions(data []map[string]interface{}) []connection.ColumnDefinition {
|
||||
columns := make([]connection.ColumnDefinition, 0, len(data))
|
||||
for _, row := range data {
|
||||
col := connection.ColumnDefinition{
|
||||
Name: getDamengRowString(row, "COLUMN_NAME"),
|
||||
Type: getDamengRowString(row, "DATA_TYPE"),
|
||||
Nullable: getDamengRowString(row, "NULLABLE"),
|
||||
Type: formatDamengColumnType(row),
|
||||
Nullable: normalizeDamengNullable(getDamengRowString(row, "NULLABLE")),
|
||||
Key: getDamengRowString(row, "COLUMN_KEY"),
|
||||
}
|
||||
|
||||
|
||||
@@ -104,13 +104,59 @@ func TestOracleGetColumnsIncludesColumnComments(t *testing.T) {
|
||||
if len(queries) == 0 || !strings.Contains(queries[0], "all_col_comments") {
|
||||
t.Fatalf("expected GetColumns to join all_col_comments, queries=%v", queries)
|
||||
}
|
||||
for _, want := range []string{`AS "COLUMN_NAME"`, `AS "DATA_TYPE"`, `AS "COMMENT"`} {
|
||||
for _, want := range []string{`AS "COLUMN_NAME"`, `AS "DATA_TYPE"`, `AS "DATA_LENGTH"`, `AS "CHAR_LENGTH"`, `AS "DATA_PRECISION"`, `AS "DATA_SCALE"`, `AS "COMMENT"`} {
|
||||
if !strings.Contains(queries[0], want) {
|
||||
t.Fatalf("expected GetColumns query to contain stable alias %q, got %s", want, queries[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatOracleColumnTypeIncludesLengthAndPrecision(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
row map[string]interface{}
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "varchar2 char length",
|
||||
row: map[string]interface{}{
|
||||
"DATA_TYPE": "VARCHAR2",
|
||||
"DATA_LENGTH": 256,
|
||||
"CHAR_LENGTH": 128,
|
||||
},
|
||||
want: "VARCHAR2(128)",
|
||||
},
|
||||
{
|
||||
name: "number precision scale",
|
||||
row: map[string]interface{}{
|
||||
"DATA_TYPE": "NUMBER",
|
||||
"DATA_PRECISION": 10,
|
||||
"DATA_SCALE": 2,
|
||||
},
|
||||
want: "NUMBER(10,2)",
|
||||
},
|
||||
{
|
||||
name: "date remains plain",
|
||||
row: map[string]interface{}{
|
||||
"DATA_TYPE": "DATE",
|
||||
},
|
||||
want: "DATE",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := formatOracleColumnType(tc.row); got != tc.want {
|
||||
t.Fatalf("expected %q, got %q", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleGetCreateStatementAppendsTableAndColumnComments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@ func (o *OracleDB) GetCreateStatement(dbName, tableName string) (string, error)
|
||||
func (o *OracleDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
|
||||
metadataTableName := escapeOracleMetadataLiteral(tableName)
|
||||
metadataSchemaName := escapeOracleMetadataLiteral(dbName)
|
||||
query := fmt.Sprintf(`SELECT c.column_name AS "COLUMN_NAME", c.data_type AS "DATA_TYPE", c.nullable AS "NULLABLE", c.data_default AS "DATA_DEFAULT",
|
||||
query := fmt.Sprintf(`SELECT c.column_name AS "COLUMN_NAME", c.data_type AS "DATA_TYPE", c.data_length AS "DATA_LENGTH", c.char_length AS "CHAR_LENGTH", c.data_precision AS "DATA_PRECISION", c.data_scale AS "DATA_SCALE", c.nullable AS "NULLABLE", c.data_default AS "DATA_DEFAULT",
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS "COLUMN_KEY",
|
||||
cc.comments AS "COMMENT"
|
||||
FROM all_tab_columns c
|
||||
@@ -342,7 +342,7 @@ func (o *OracleDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi
|
||||
ORDER BY c.column_id`, metadataSchemaName, metadataTableName)
|
||||
|
||||
if dbName == "" {
|
||||
query = fmt.Sprintf(`SELECT c.column_name AS "COLUMN_NAME", c.data_type AS "DATA_TYPE", c.nullable AS "NULLABLE", c.data_default AS "DATA_DEFAULT",
|
||||
query = fmt.Sprintf(`SELECT c.column_name AS "COLUMN_NAME", c.data_type AS "DATA_TYPE", c.data_length AS "DATA_LENGTH", c.char_length AS "CHAR_LENGTH", c.data_precision AS "DATA_PRECISION", c.data_scale AS "DATA_SCALE", c.nullable AS "NULLABLE", c.data_default AS "DATA_DEFAULT",
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS "COLUMN_KEY",
|
||||
cc.comments AS "COMMENT"
|
||||
FROM user_tab_columns c
|
||||
@@ -367,7 +367,7 @@ func (o *OracleDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi
|
||||
for _, row := range data {
|
||||
col := connection.ColumnDefinition{
|
||||
Name: oracleRowString(row, "COLUMN_NAME"),
|
||||
Type: oracleRowString(row, "DATA_TYPE"),
|
||||
Type: formatOracleColumnType(row),
|
||||
Nullable: oracleRowString(row, "NULLABLE"),
|
||||
Key: oracleRowString(row, "COLUMN_KEY"),
|
||||
Comment: oracleRowString(row, "COMMENT"),
|
||||
@@ -402,7 +402,58 @@ func oracleRowString(row map[string]interface{}, names ...string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v", value)
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
func oracleRowInt(row map[string]interface{}, names ...string) (int, bool) {
|
||||
raw := oracleRowString(row, names...)
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
func isOracleLengthQualifiedType(upperType string) bool {
|
||||
switch strings.TrimSpace(upperType) {
|
||||
case "CHAR", "NCHAR", "VARCHAR", "VARCHAR2", "NVARCHAR", "NVARCHAR2", "RAW", "BINARY", "VARBINARY":
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(upperType, "CHARACTER")
|
||||
}
|
||||
}
|
||||
|
||||
func formatOracleColumnType(row map[string]interface{}) string {
|
||||
dataType := oracleRowString(row, "DATA_TYPE")
|
||||
if dataType == "" || strings.Contains(dataType, "(") {
|
||||
return dataType
|
||||
}
|
||||
|
||||
upperType := strings.ToUpper(dataType)
|
||||
if isOracleLengthQualifiedType(upperType) {
|
||||
if charLength, ok := oracleRowInt(row, "CHAR_LENGTH", "CHAR_COL_DECL_LENGTH"); ok && charLength > 0 {
|
||||
return fmt.Sprintf("%s(%d)", dataType, charLength)
|
||||
}
|
||||
if dataLength, ok := oracleRowInt(row, "DATA_LENGTH"); ok && dataLength > 0 {
|
||||
return fmt.Sprintf("%s(%d)", dataType, dataLength)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(upperType, "NUMBER") || strings.Contains(upperType, "DECIMAL") || strings.Contains(upperType, "NUMERIC") {
|
||||
precision, hasPrecision := oracleRowInt(row, "DATA_PRECISION", "NUMERIC_PRECISION")
|
||||
if hasPrecision && precision > 0 {
|
||||
scale, hasScale := oracleRowInt(row, "DATA_SCALE", "NUMERIC_SCALE")
|
||||
if hasScale && scale > 0 {
|
||||
return fmt.Sprintf("%s(%d,%d)", dataType, precision, scale)
|
||||
}
|
||||
return fmt.Sprintf("%s(%d)", dataType, precision)
|
||||
}
|
||||
}
|
||||
|
||||
return dataType
|
||||
}
|
||||
|
||||
func (o *OracleDB) appendOracleCommentDDL(baseDDL string, dbName string, tableName string) string {
|
||||
|
||||
Reference in New Issue
Block a user