mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 03:11:24 +08:00
✨ feat(editor): 完善 SQL 编辑与数据编辑交互
- 结果区状态按 SQL Tab 独立保存,快捷键可恢复手动隐藏面板 - 对象设计保留完整字段类型和可空信息,完善兼容驱动 DDL 元数据 - 数据编辑新增手动/自动提交设置和自动提交倒计时 - 修复 schema 视图定位时找不到左侧树节点的问题
This commit is contained in:
@@ -29,6 +29,37 @@ describe('columnDefinition metadata normalization', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers complete column type aliases over base data type', () => {
|
||||
const column = {
|
||||
COLUMN_NAME: 'USER_NAME',
|
||||
DATA_TYPE: 'varchar',
|
||||
COLUMN_TYPE: 'varchar(64)',
|
||||
IS_NULLABLE: 'NO',
|
||||
};
|
||||
|
||||
expect(normalizeColumnDefinition(column)).toMatchObject({
|
||||
name: 'USER_NAME',
|
||||
type: 'varchar(64)',
|
||||
nullable: 'NO',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds display type from base type and length metadata', () => {
|
||||
const column = {
|
||||
column_name: 'amount',
|
||||
data_type: 'decimal',
|
||||
numeric_precision: 10,
|
||||
numeric_scale: 2,
|
||||
is_nullable: 'YES',
|
||||
};
|
||||
|
||||
expect(normalizeColumnDefinition(column)).toMatchObject({
|
||||
name: 'amount',
|
||||
type: 'decimal(10,2)',
|
||||
nullable: 'YES',
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -48,13 +48,72 @@ const readBooleanProperty = (value: unknown, keys: string[]): boolean => {
|
||||
return text === '1' || text === 't' || text === 'true' || text === 'y' || text === 'yes' || text === 'pri' || text === 'primary';
|
||||
};
|
||||
|
||||
const readNumberProperty = (value: unknown, keys: string[]): number => {
|
||||
const raw = readProperty(value, keys);
|
||||
if (raw === undefined || raw === null || raw === '') return 0;
|
||||
const parsed = Number(raw);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0;
|
||||
};
|
||||
|
||||
export const getColumnDefinitionName = (column: unknown): string => (
|
||||
readStringProperty(column, ['name', 'Name', 'COLUMN_NAME', 'column_name', 'field', 'Field'])
|
||||
);
|
||||
|
||||
export const getColumnDefinitionType = (column: unknown): string => (
|
||||
readStringProperty(column, ['type', 'Type', 'DATA_TYPE', 'data_type'])
|
||||
);
|
||||
export const getColumnDefinitionType = (column: unknown): string => {
|
||||
const fullType = readStringProperty(column, [
|
||||
'COLUMN_TYPE',
|
||||
'column_type',
|
||||
'FULL_TYPE',
|
||||
'full_type',
|
||||
'FULL_DATA_TYPE',
|
||||
'full_data_type',
|
||||
'TYPE_NAME',
|
||||
'type_name',
|
||||
'Type',
|
||||
'type',
|
||||
]);
|
||||
if (fullType) return fullType;
|
||||
|
||||
const dataType = readStringProperty(column, ['DATA_TYPE', 'data_type']);
|
||||
if (!dataType || /\(.+\)/.test(dataType)) return dataType;
|
||||
|
||||
const upperType = dataType.toUpperCase();
|
||||
const charLength = readNumberProperty(column, [
|
||||
'CHARACTER_MAXIMUM_LENGTH',
|
||||
'character_maximum_length',
|
||||
'CHARACTER_MAX_LENGTH',
|
||||
'character_max_length',
|
||||
'CHAR_LENGTH',
|
||||
'char_length',
|
||||
'LENGTH',
|
||||
'length',
|
||||
]);
|
||||
if (charLength > 0 && /(CHAR|VARCHAR|BINARY|VARBINARY|NCHAR|NVARCHAR)/.test(upperType)) {
|
||||
return `${dataType}(${charLength})`;
|
||||
}
|
||||
|
||||
const precision = readNumberProperty(column, [
|
||||
'NUMERIC_PRECISION',
|
||||
'numeric_precision',
|
||||
'DATA_PRECISION',
|
||||
'data_precision',
|
||||
'PRECISION',
|
||||
'precision',
|
||||
]);
|
||||
if (precision > 0 && /(DECIMAL|NUMERIC|NUMBER)/.test(upperType)) {
|
||||
const scale = readNumberProperty(column, [
|
||||
'NUMERIC_SCALE',
|
||||
'numeric_scale',
|
||||
'DATA_SCALE',
|
||||
'data_scale',
|
||||
'SCALE',
|
||||
'scale',
|
||||
]);
|
||||
return scale > 0 ? `${dataType}(${precision},${scale})` : `${dataType}(${precision})`;
|
||||
}
|
||||
|
||||
return dataType;
|
||||
};
|
||||
|
||||
export const getColumnDefinitionKey = (column: unknown): string => {
|
||||
const key = readStringProperty(column, ['key', 'Key', 'COLUMN_KEY', 'column_key']);
|
||||
@@ -89,7 +148,7 @@ export const normalizeColumnDefinition = (column: unknown): ColumnDefinition =>
|
||||
...source,
|
||||
name: getColumnDefinitionName(column),
|
||||
type: getColumnDefinitionType(column),
|
||||
nullable: readStringProperty(column, ['nullable', 'Nullable', 'NULLABLE', 'is_nullable']),
|
||||
nullable: readStringProperty(column, ['nullable', 'Nullable', 'NULLABLE', 'is_nullable', 'IS_NULLABLE', 'Null', 'null']),
|
||||
key: getColumnDefinitionKey(column),
|
||||
default: source.default,
|
||||
extra: getColumnDefinitionExtra(column),
|
||||
|
||||
@@ -1041,6 +1041,70 @@ describe('sidebarLocate', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers the current database schema when bare view nodes keep schema metadata separately', () => {
|
||||
const target = resolveSidebarLocateTarget({
|
||||
tabId: 'conn-1-SYSDBA-view-V_ACCOUNT',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'SYSDBA',
|
||||
tableName: 'V_ACCOUNT',
|
||||
objectGroup: 'views',
|
||||
}, { groupBySchema: true });
|
||||
|
||||
const tree = [
|
||||
{
|
||||
key: 'conn-1',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA',
|
||||
dataRef: { id: 'conn-1', dbName: 'SYSDBA' },
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA-schema-REPORT',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA-schema-REPORT-views',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA-view-REPORT.V_ACCOUNT',
|
||||
title: 'V_ACCOUNT',
|
||||
type: 'view',
|
||||
dataRef: { id: 'conn-1', dbName: 'SYSDBA', viewName: 'V_ACCOUNT', schemaName: 'REPORT' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conn-1-SYSDBA-schema-SYSDBA',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA-schema-SYSDBA-views',
|
||||
children: [
|
||||
{
|
||||
key: 'conn-1-SYSDBA-view-SYSDBA.V_ACCOUNT',
|
||||
title: 'V_ACCOUNT',
|
||||
type: 'view',
|
||||
dataRef: { id: 'conn-1', dbName: 'SYSDBA', viewName: 'V_ACCOUNT', schemaName: 'SYSDBA' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(findSidebarNodePathForLocate(tree, target)).toEqual([
|
||||
'conn-1',
|
||||
'conn-1-SYSDBA',
|
||||
'conn-1-SYSDBA-schema-SYSDBA',
|
||||
'conn-1-SYSDBA-schema-SYSDBA-views',
|
||||
'conn-1-SYSDBA-view-SYSDBA.V_ACCOUNT',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not guess a schema-qualified view when no current-schema preference resolves ambiguity', () => {
|
||||
const target = resolveSidebarLocateTarget({
|
||||
tabId: 'conn-1-SYSDBA-view-V_ACCOUNT',
|
||||
|
||||
@@ -272,8 +272,6 @@ const matchesLocateObjectName = (
|
||||
const resolvedNodeSchema = toTrimmedString(nodeSchemaName) || nodeParsed.schemaName;
|
||||
const resolvedTargetSchema = toTrimmedString(target.schemaName) || targetParsed.schemaName;
|
||||
|
||||
if (normalizeLocateName(normalizedNodeName) === normalizeLocateName(target.tableName)) return true;
|
||||
|
||||
if (
|
||||
resolvedTargetSchema
|
||||
&& !resolvedNodeSchema
|
||||
@@ -542,8 +540,15 @@ export const findSidebarNodePathForLocate = (
|
||||
}
|
||||
}
|
||||
|
||||
const relaxedPaths = collectSidebarNodePathsForLocateByObject(
|
||||
nodes,
|
||||
target,
|
||||
{ allowUnqualifiedSchemaMatch: true },
|
||||
);
|
||||
const relaxedPath = selectPreferredSidebarLocatePath(relaxedPaths, target);
|
||||
if (relaxedPath) return relaxedPath;
|
||||
|
||||
if (hasLocateTargetSchema(target)) return null;
|
||||
|
||||
const relaxedPaths = collectSidebarNodePathsForLocateByObject(nodes, target, { allowUnqualifiedSchemaMatch: true });
|
||||
return selectPreferredSidebarLocatePath(relaxedPaths, target);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } from './sidebarMetadata';
|
||||
import {
|
||||
buildMySQLCompatibleViewMetadataSqls,
|
||||
isSidebarViewTableType,
|
||||
normalizeSidebarViewMetadataEntry,
|
||||
normalizeSidebarViewName,
|
||||
resolveSidebarMetadataDialect,
|
||||
} from './sidebarMetadata';
|
||||
|
||||
describe('sidebarMetadata', () => {
|
||||
it('normalizes MySQL-compatible view names without schema prefixes', () => {
|
||||
expect(normalizeSidebarViewName('mysql', 'SYSDBA', 'SYSDBA', 'SYSDBA.V_ACCOUNT')).toBe('V_ACCOUNT');
|
||||
});
|
||||
|
||||
it('keeps MySQL-compatible view schema metadata after display-name normalization', () => {
|
||||
expect(normalizeSidebarViewMetadataEntry('mysql', 'SYSDBA', 'SYSDBA', 'SYSDBA.V_ACCOUNT')).toEqual({
|
||||
viewName: 'V_ACCOUNT',
|
||||
schemaName: 'SYSDBA',
|
||||
});
|
||||
expect(normalizeSidebarViewMetadataEntry('mysql', 'GDB_APP', 'SYSDBA', 'V_ACCOUNT')).toEqual({
|
||||
viewName: 'V_ACCOUNT',
|
||||
schemaName: 'SYSDBA',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses MySQL metadata queries for custom MySQL-compatible domestic drivers', () => {
|
||||
expect(resolveSidebarMetadataDialect('custom', 'gdb')).toBe('mysql');
|
||||
expect(resolveSidebarMetadataDialect('custom', 'goldendb')).toBe('mysql');
|
||||
|
||||
@@ -60,6 +60,28 @@ export const normalizeSidebarViewName = (dialect: string, dbName: string, schema
|
||||
return `${normalizedSchemaName}.${normalizedViewName}`;
|
||||
};
|
||||
|
||||
export interface SidebarViewMetadataEntry {
|
||||
viewName: string;
|
||||
schemaName: string;
|
||||
}
|
||||
|
||||
export const normalizeSidebarViewMetadataEntry = (
|
||||
dialect: string,
|
||||
dbName: string,
|
||||
schemaName: string,
|
||||
viewName: string,
|
||||
): SidebarViewMetadataEntry | null => {
|
||||
const normalizedViewName = normalizeSidebarViewName(dialect, dbName, schemaName, viewName);
|
||||
if (!normalizedViewName) return null;
|
||||
|
||||
const parsedViewName = splitQualifiedNameLast(viewName);
|
||||
const parsedNormalizedViewName = splitQualifiedNameLast(normalizedViewName);
|
||||
return {
|
||||
viewName: normalizedViewName,
|
||||
schemaName: String(schemaName || parsedNormalizedViewName.parentPath || parsedViewName.parentPath || '').trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const isSidebarViewTableType = (tableType: unknown): boolean => {
|
||||
const normalizedType = String(tableType ?? '').trim().toUpperCase();
|
||||
if (!normalizedType) return true;
|
||||
|
||||
Reference in New Issue
Block a user