mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-12 17:39:42 +08:00
🐛 fix(metadata): 修复 Oracle 字段元数据显示缺失
- Oracle 元数据查询为字段名、类型、默认值、注释等列补齐稳定别名 - 新增字段定义归一化工具,兼容 name/Name/COLUMN_NAME 等返回形态 - 修复 DataGrid、DataViewer、QueryEditor、TableDesigner 对字段元数据的读取 - 补充 Oracle 字段注释、表头元数据和主键定位回归测试
This commit is contained in:
@@ -139,6 +139,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
RobotOutlined: Icon,
|
||||
SearchOutlined: Icon,
|
||||
LinkOutlined: Icon,
|
||||
AimOutlined: Icon,
|
||||
TableOutlined: Icon,
|
||||
AimOutlined: Icon,
|
||||
SortAscendingOutlined: Icon,
|
||||
@@ -664,7 +665,7 @@ describe('DataGrid DDL interactions', () => {
|
||||
storeState.queryOptions.showColumnType = true;
|
||||
backendApp.DBGetColumns.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ name: 'id', type: 'bigint', comment: '主键 ID' }],
|
||||
data: [{ Name: 'id', Type: 'bigint', Comment: '主键 ID' }],
|
||||
});
|
||||
|
||||
let renderer: ReactTestRenderer;
|
||||
@@ -703,6 +704,8 @@ describe('DataGrid DDL interactions', () => {
|
||||
expect(textContent(renderer!.root)).toContain('隐藏此字段');
|
||||
expect(textContent(renderer!.root)).toContain('隐藏字段类型');
|
||||
expect(textContent(renderer!.root)).toContain('隐藏字段备注');
|
||||
expect(textContent(renderer!.root)).toContain('bigint');
|
||||
expect(textContent(renderer!.root)).toContain('主键 ID');
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -104,6 +104,11 @@ import {
|
||||
resolveRowLocatorValues,
|
||||
type EditRowLocator,
|
||||
} from '../utils/rowLocator';
|
||||
import {
|
||||
getColumnDefinitionComment,
|
||||
getColumnDefinitionName,
|
||||
getColumnDefinitionType,
|
||||
} from '../utils/columnDefinition';
|
||||
import {
|
||||
V2CellContextMenuView,
|
||||
V2ColumnHeaderContextMenuView,
|
||||
@@ -2142,10 +2147,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}
|
||||
const nextMap: Record<string, ColumnMeta> = {};
|
||||
(res.data as ColumnDefinition[]).forEach((column: any) => {
|
||||
const name = String(column?.name ?? column?.Name ?? '').trim();
|
||||
const name = getColumnDefinitionName(column);
|
||||
if (!name) return;
|
||||
const type = String(column?.type ?? column?.Type ?? '').trim();
|
||||
const comment = String(column?.comment ?? column?.Comment ?? '').trim();
|
||||
const type = getColumnDefinitionType(column);
|
||||
const comment = getColumnDefinitionComment(column);
|
||||
nextMap[name] = { type, comment };
|
||||
});
|
||||
columnMetaCacheRef.current[cacheKey] = nextMap;
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('DataViewer safe editing locator', () => {
|
||||
it('enables table preview editing after primary keys are loaded', async () => {
|
||||
backendApp.DBGetColumns.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
|
||||
data: [{ Name: 'ID', Key: 'PRI' }, { Name: 'NAME', Key: '' }],
|
||||
});
|
||||
|
||||
const renderer = await renderAndReload();
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
type EditRowLocator,
|
||||
} from '../utils/rowLocator';
|
||||
import { isOracleLikeDialect } from '../utils/sqlDialect';
|
||||
import {
|
||||
getColumnDefinitionKey,
|
||||
getColumnDefinitionName,
|
||||
getColumnDefinitionType,
|
||||
} from '../utils/columnDefinition';
|
||||
|
||||
type ViewerPaginationState = {
|
||||
current: number;
|
||||
@@ -104,7 +109,7 @@ const formatDataViewerTableName = (dbName: string, tableName: string): string =>
|
||||
|
||||
const getTableColumnNames = (columns: ColumnDefinition[] | undefined): string[] => (
|
||||
(columns || [])
|
||||
.map((column) => String(column?.name || '').trim())
|
||||
.map(getColumnDefinitionName)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
@@ -572,8 +577,8 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
} else {
|
||||
const columnDefs = resCols.data as ColumnDefinition[];
|
||||
const primaryKeys = columnDefs
|
||||
.filter((column: any) => column?.key === 'PRI')
|
||||
.map((column: any) => String(column?.name || '').trim())
|
||||
.filter((column: any) => getColumnDefinitionKey(column) === 'PRI')
|
||||
.map(getColumnDefinitionName)
|
||||
.filter(Boolean);
|
||||
const indexes = resIndexes?.success && Array.isArray(resIndexes.data)
|
||||
? resIndexes.data as IndexDefinition[]
|
||||
@@ -721,10 +726,10 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
|
||||
if (resCols?.success && Array.isArray(resCols.data)) {
|
||||
const columnDefs = resCols.data as ColumnDefinition[];
|
||||
const selectParts = columnDefs.map((col) => {
|
||||
const colName = String(col?.name || '').trim();
|
||||
const colName = getColumnDefinitionName(col);
|
||||
if (!colName) return '';
|
||||
const quotedCol = quoteIdentPart(dbType, colName);
|
||||
if (isDuckDBComplexColumnType(col?.type)) {
|
||||
if (isDuckDBComplexColumnType(getColumnDefinitionType(col))) {
|
||||
return `CAST(${quotedCol} AS VARCHAR) AS ${quotedCol}`;
|
||||
}
|
||||
return quotedCol;
|
||||
|
||||
@@ -23,6 +23,10 @@ import { splitSidebarQualifiedName } from '../utils/sidebarLocate';
|
||||
import { normalizeSidebarViewName } from '../utils/sidebarMetadata';
|
||||
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
|
||||
import { ORACLE_ROWID_LOCATOR_COLUMN, type EditRowLocator } from '../utils/rowLocator';
|
||||
import {
|
||||
getColumnDefinitionKey,
|
||||
getColumnDefinitionName,
|
||||
} from '../utils/columnDefinition';
|
||||
|
||||
const SQL_KEYWORDS = [
|
||||
'SELECT', 'FROM', 'WHERE', 'LIMIT', 'INSERT', 'UPDATE', 'DELETE', 'JOIN', 'LEFT', 'RIGHT',
|
||||
@@ -1530,10 +1534,10 @@ const resolveQueryLocatorPlan = async ({
|
||||
}
|
||||
|
||||
const tableColumns = resCols.data as ColumnDefinition[];
|
||||
const tableColumnNames = tableColumns.map((column) => String(column?.name || '').trim()).filter(Boolean);
|
||||
const tableColumnNames = tableColumns.map(getColumnDefinitionName).filter(Boolean);
|
||||
const primaryKeys = tableColumns
|
||||
.filter((column: any) => column?.key === 'PRI')
|
||||
.map((column: any) => String(column?.name || '').trim())
|
||||
.filter((column: any) => getColumnDefinitionKey(column) === 'PRI')
|
||||
.map(getColumnDefinitionName)
|
||||
.filter(Boolean);
|
||||
const indexes = resIndexes?.success && Array.isArray(resIndexes.data)
|
||||
? resIndexes.data as IndexDefinition[]
|
||||
|
||||
@@ -15,6 +15,10 @@ import { normalizeSchemaStatementForExecution, parseTableCommentFromDDL, splitSc
|
||||
import TableDesignerSqlPreview from './TableDesignerSqlPreview';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import {
|
||||
getColumnDefinitionExtra,
|
||||
normalizeColumnDefinition,
|
||||
} from '../utils/columnDefinition';
|
||||
import {
|
||||
isMysqlFamilyDialect as isMysqlFamilySqlDialect,
|
||||
isOracleLikeDialect as isOracleLikeSqlDialect,
|
||||
@@ -804,9 +808,9 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
|
||||
if (colsRes.success) {
|
||||
const colsWithKey = (colsRes.data as ColumnDefinition[]).map((c, index) => ({
|
||||
...c,
|
||||
...normalizeColumnDefinition(c),
|
||||
_key: `col-${index}-${Date.now()}`,
|
||||
isAutoIncrement: c.extra && c.extra.toLowerCase().includes('auto_increment')
|
||||
isAutoIncrement: getColumnDefinitionExtra(c).toLowerCase().includes('auto_increment')
|
||||
}));
|
||||
setColumns(JSON.parse(JSON.stringify(colsWithKey)));
|
||||
setOriginalColumns(JSON.parse(JSON.stringify(colsWithKey)));
|
||||
|
||||
31
frontend/src/utils/columnDefinition.test.ts
Normal file
31
frontend/src/utils/columnDefinition.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getColumnDefinitionComment,
|
||||
getColumnDefinitionKey,
|
||||
getColumnDefinitionName,
|
||||
getColumnDefinitionType,
|
||||
normalizeColumnDefinition,
|
||||
} from './columnDefinition';
|
||||
|
||||
describe('columnDefinition metadata normalization', () => {
|
||||
it('reads Go/Wails style column metadata fields', () => {
|
||||
const column = { Name: 'UPDATED_AT', Type: 'TIMESTAMP', Key: 'PRI', Comment: '更新时间' };
|
||||
|
||||
expect(getColumnDefinitionName(column)).toBe('UPDATED_AT');
|
||||
expect(getColumnDefinitionType(column)).toBe('TIMESTAMP');
|
||||
expect(getColumnDefinitionKey(column)).toBe('PRI');
|
||||
expect(getColumnDefinitionComment(column)).toBe('更新时间');
|
||||
});
|
||||
|
||||
it('reads Oracle dictionary style column metadata aliases', () => {
|
||||
const column = { COLUMN_NAME: 'UPDATED_AT', DATA_TYPE: 'TIMESTAMP', COLUMN_KEY: 'PRI', COMMENTS: '更新时间' };
|
||||
|
||||
expect(normalizeColumnDefinition(column)).toMatchObject({
|
||||
name: 'UPDATED_AT',
|
||||
type: 'TIMESTAMP',
|
||||
key: 'PRI',
|
||||
comment: '更新时间',
|
||||
});
|
||||
});
|
||||
});
|
||||
59
frontend/src/utils/columnDefinition.ts
Normal file
59
frontend/src/utils/columnDefinition.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ColumnDefinition } from '../types';
|
||||
|
||||
const readStringProperty = (value: unknown, keys: string[]): string => {
|
||||
const source = value as Record<string, unknown> | null | undefined;
|
||||
if (!source || typeof source !== 'object') return '';
|
||||
|
||||
for (const key of keys) {
|
||||
const raw = source[key];
|
||||
if (raw !== undefined && raw !== null) {
|
||||
return String(raw).trim();
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sourceKey, raw] of Object.entries(source)) {
|
||||
if (keys.some((key) => sourceKey.toLowerCase() === key.toLowerCase())) {
|
||||
return raw === undefined || raw === null ? '' : String(raw).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
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 getColumnDefinitionKey = (column: unknown): string => (
|
||||
readStringProperty(column, ['key', 'Key', 'COLUMN_KEY', 'column_key'])
|
||||
);
|
||||
|
||||
export const getColumnDefinitionExtra = (column: unknown): string => (
|
||||
readStringProperty(column, ['extra', 'Extra'])
|
||||
);
|
||||
|
||||
export const getColumnDefinitionComment = (column: unknown): string => (
|
||||
readStringProperty(column, ['comment', 'Comment', 'COMMENTS', 'comments', 'COLUMN_COMMENT', 'column_comment'])
|
||||
);
|
||||
|
||||
export const normalizeColumnDefinition = (column: unknown): ColumnDefinition => {
|
||||
const source = (column && typeof column === 'object' ? column : {}) as Partial<ColumnDefinition>;
|
||||
return {
|
||||
...source,
|
||||
name: getColumnDefinitionName(column),
|
||||
type: getColumnDefinitionType(column),
|
||||
nullable: readStringProperty(column, ['nullable', 'Nullable', 'NULLABLE', 'is_nullable']),
|
||||
key: getColumnDefinitionKey(column),
|
||||
default: source.default,
|
||||
extra: getColumnDefinitionExtra(column),
|
||||
comment: getColumnDefinitionComment(column),
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeColumnDefinitions = (columns: unknown): ColumnDefinition[] => (
|
||||
Array.isArray(columns) ? columns.map(normalizeColumnDefinition) : []
|
||||
);
|
||||
@@ -104,6 +104,11 @@ 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"`} {
|
||||
if !strings.Contains(queries[0], want) {
|
||||
t.Fatalf("expected GetColumns query to contain stable alias %q, got %s", want, queries[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleGetCreateStatementAppendsTableAndColumnComments(t *testing.T) {
|
||||
|
||||
@@ -325,9 +325,9 @@ 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, c.data_type, c.nullable, c.data_default,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key,
|
||||
cc.comments AS comment
|
||||
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",
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS "COLUMN_KEY",
|
||||
cc.comments AS "COMMENT"
|
||||
FROM all_tab_columns c
|
||||
LEFT JOIN all_col_comments cc
|
||||
ON cc.owner = c.owner AND cc.table_name = c.table_name AND cc.column_name = c.column_name
|
||||
@@ -342,9 +342,9 @@ 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, c.data_type, c.nullable, c.data_default,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS column_key,
|
||||
cc.comments AS comment
|
||||
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",
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' ELSE '' END AS "COLUMN_KEY",
|
||||
cc.comments AS "COMMENT"
|
||||
FROM user_tab_columns c
|
||||
LEFT JOIN user_col_comments cc
|
||||
ON cc.table_name = c.table_name AND cc.column_name = c.column_name
|
||||
|
||||
Reference in New Issue
Block a user