mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
🐛 fix(duckdb): 修复唯一索引识别与多库对象解析
- 合并 DuckDB 约束与索引元数据,恢复唯一索引表的可编辑判定 - 修复 attach 多库场景下 catalog/schema/table 定位混乱问题 - 统一前后端 qualified name 解析,支持带点和带引号对象名 - 补充 DuckDB 元数据与编辑链路回归测试
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
getColumnDefinitionName,
|
||||
getColumnDefinitionType,
|
||||
} from '../utils/columnDefinition';
|
||||
import { splitQualifiedNameLast, splitQualifiedNameSegments } from '../utils/qualifiedName';
|
||||
|
||||
type ViewerPaginationState = {
|
||||
current: number;
|
||||
@@ -171,33 +172,21 @@ const buildDataViewerBaseSelectSQL = (
|
||||
return `SELECT ${alias}.*, ${alias}.ROWID AS ${rowIDAlias} FROM ${quotedTableName} ${alias} ${whereSQL}`;
|
||||
};
|
||||
|
||||
const normalizeDuckDBIdentifier = (raw: string): string => {
|
||||
const text = String(raw || '').trim();
|
||||
if (text.length >= 2) {
|
||||
const first = text[0];
|
||||
const last = text[text.length - 1];
|
||||
if ((first === '"' && last === '"') || (first === '`' && last === '`')) {
|
||||
return text.slice(1, -1).trim();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const resolveDuckDBSchemaAndTable = (dbName: string, tableName: string) => {
|
||||
const rawTable = String(tableName || '').trim();
|
||||
if (!rawTable) return { schemaName: 'main', pureTableName: '' };
|
||||
|
||||
const parts = rawTable.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const pureTableName = normalizeDuckDBIdentifier(parts[parts.length - 1]);
|
||||
const schemaName = normalizeDuckDBIdentifier(parts[parts.length - 2]);
|
||||
if (schemaName && pureTableName) {
|
||||
return { schemaName, pureTableName };
|
||||
}
|
||||
const segments = splitQualifiedNameSegments(rawTable);
|
||||
if (segments.length >= 2) {
|
||||
return {
|
||||
schemaName: segments[segments.length - 2],
|
||||
pureTableName: segments[segments.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackSchema = normalizeDuckDBIdentifier(String(dbName || '').trim()) || 'main';
|
||||
return { schemaName: fallbackSchema, pureTableName: normalizeDuckDBIdentifier(rawTable) };
|
||||
const fallbackParsed = splitQualifiedNameLast(String(dbName || '').trim());
|
||||
const fallbackSchema = fallbackParsed.objectName || String(dbName || '').trim() || 'main';
|
||||
return { schemaName: fallbackSchema, pureTableName: segments[0] || rawTable };
|
||||
};
|
||||
|
||||
const escapeSQLLiteral = (value: string): string => String(value || '').replace(/'/g, "''");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useStore } from '../store';
|
||||
import { DBQuery } from '../../wailsjs/go/app/App';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
|
||||
interface DefinitionViewerProps {
|
||||
tab: TabData;
|
||||
@@ -63,12 +64,8 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
};
|
||||
|
||||
const parseSchemaAndName = (fullName: string): { schema: string; name: string } => {
|
||||
const raw = String(fullName || '').trim();
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx > 0 && idx < raw.length - 1) {
|
||||
return { schema: raw.substring(0, idx), name: raw.substring(idx + 1) };
|
||||
}
|
||||
return { schema: '', name: raw };
|
||||
const parsed = splitQualifiedNameLast(fullName);
|
||||
return { schema: parsed.parentPath, name: parsed.objectName };
|
||||
};
|
||||
|
||||
const getCaseInsensitiveRawValue = (row: Record<string, any>, candidateKeys: string[]): any => {
|
||||
|
||||
@@ -65,6 +65,7 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
import { resolveConnectionHostSummary, resolveConnectionHostTokens } from '../utils/tabDisplay';
|
||||
@@ -1796,9 +1797,8 @@ const Sidebar: React.FC<{
|
||||
const rawName = String(tableName || '').trim();
|
||||
if (!rawName) return rawName;
|
||||
if (!shouldHideSchemaPrefix(conn)) return rawName;
|
||||
const lastDotIndex = rawName.lastIndexOf('.');
|
||||
if (lastDotIndex <= 0 || lastDotIndex >= rawName.length - 1) return rawName;
|
||||
return rawName.substring(lastDotIndex + 1);
|
||||
const parsed = splitQualifiedName(rawName);
|
||||
return parsed.objectName || rawName;
|
||||
};
|
||||
|
||||
const getMetadataDialect = (conn: SavedConnection | undefined): string => {
|
||||
@@ -1984,15 +1984,10 @@ const Sidebar: React.FC<{
|
||||
};
|
||||
|
||||
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const raw = String(qualifiedName || '').trim();
|
||||
if (!raw) return { schemaName: '', objectName: '' };
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx <= 0 || idx >= raw.length - 1) {
|
||||
return { schemaName: '', objectName: raw };
|
||||
}
|
||||
const parsed = splitQualifiedNameLast(qualifiedName);
|
||||
return {
|
||||
schemaName: raw.substring(0, idx),
|
||||
objectName: raw.substring(idx + 1),
|
||||
schemaName: parsed.parentPath,
|
||||
objectName: parsed.objectName,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4785,12 +4780,7 @@ const Sidebar: React.FC<{
|
||||
};
|
||||
|
||||
const extractObjectName = (fullName: string) => {
|
||||
const raw = String(fullName || '').trim();
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx >= 0 && idx < raw.length - 1) {
|
||||
return raw.substring(idx + 1);
|
||||
}
|
||||
return raw;
|
||||
return splitQualifiedName(String(fullName || '').trim()).objectName || String(fullName || '').trim();
|
||||
};
|
||||
|
||||
const handleRenameDatabase = async () => {
|
||||
@@ -5012,9 +5002,9 @@ const Sidebar: React.FC<{
|
||||
query = `SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``;
|
||||
break;
|
||||
case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': {
|
||||
const parts = viewName.split('.');
|
||||
const schema = parts.length > 1 ? parts[0] : 'public';
|
||||
const name = parts.length > 1 ? parts[1] : viewName;
|
||||
const parts = splitQualifiedName(viewName);
|
||||
const schema = parts.schemaName || 'public';
|
||||
const name = parts.objectName || viewName;
|
||||
query = `SELECT pg_get_viewdef('${escapeSQLLiteral(schema)}.${escapeSQLLiteral(name)}'::regclass, true) AS view_definition`;
|
||||
break;
|
||||
}
|
||||
@@ -5133,7 +5123,10 @@ const Sidebar: React.FC<{
|
||||
const conn = node.dataRef;
|
||||
const { tableName, dbName, id } = conn;
|
||||
const safeTable = String(tableName || 'table_name').trim();
|
||||
const quotedTable = safeTable.includes('`') ? safeTable : safeTable.split('.').map(part => `\`${part.replace(/`/g, '``')}\``).join('.');
|
||||
const safeTableParts = [splitQualifiedName(safeTable).schemaName, splitQualifiedName(safeTable).objectName].filter(Boolean);
|
||||
const quotedTable = safeTable.includes('`')
|
||||
? safeTable
|
||||
: (safeTableParts.length > 0 ? safeTableParts : [safeTable]).map(part => `\`${part.replace(/`/g, '``')}\``).join('.');
|
||||
addTab({
|
||||
id: `query-create-starrocks-rollup-${Date.now()}`,
|
||||
title: '新增 Rollup',
|
||||
@@ -6504,7 +6497,7 @@ const Sidebar: React.FC<{
|
||||
const dbName = String(conn?.dbName || '').trim();
|
||||
const tableName = String(conn?.tableName || node?.title || '').trim();
|
||||
const objectName = extractObjectName(tableName);
|
||||
const schemaName = String(conn?.schemaName || (tableName.includes('.') ? tableName.split('.').slice(0, -1).join('.') : '')).trim();
|
||||
const schemaName = String(conn?.schemaName || splitQualifiedName(tableName).schemaName || '').trim();
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
@@ -8140,8 +8133,7 @@ const Sidebar: React.FC<{
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || '').trim();
|
||||
const conn = node?.dataRef as SavedConnection | undefined;
|
||||
if (rawTableName && shouldHideSchemaPrefix(conn)) {
|
||||
const lastDotIndex = rawTableName.lastIndexOf('.');
|
||||
if (lastDotIndex > 0 && lastDotIndex < rawTableName.length - 1) {
|
||||
if (splitQualifiedName(rawTableName).schemaName) {
|
||||
hoverTitle = rawTableName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
resolveColumnTypeOptions,
|
||||
resolveSqlDialect,
|
||||
} from '../utils/sqlDialect';
|
||||
import { splitQualifiedNameLast, stripIdentifierQuotes } from '../utils/qualifiedName';
|
||||
|
||||
interface EditableColumn extends ColumnDefinition {
|
||||
_key: string;
|
||||
@@ -1390,26 +1391,11 @@ ${selectedTrigger.statement}`;
|
||||
const escapeDoubleQuoteIdentifier = (name: string) => String(name || '').replace(/"/g, '""');
|
||||
const escapeSqlString = (value: string) => String(value || '').replace(/'/g, "''");
|
||||
|
||||
const stripIdentifierQuotes = (part: string): string => {
|
||||
const text = String(part || '').trim();
|
||||
if (!text) return '';
|
||||
if ((text.startsWith('`') && text.endsWith('`')) || (text.startsWith('"') && text.endsWith('"'))) {
|
||||
return text.slice(1, -1).trim();
|
||||
}
|
||||
if (text.startsWith('[') && text.endsWith(']')) {
|
||||
return text.slice(1, -1).trim();
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const raw = String(qualifiedName || '').trim();
|
||||
if (!raw) return { schemaName: '', objectName: '' };
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx <= 0 || idx >= raw.length - 1) return { schemaName: '', objectName: raw };
|
||||
const parsed = splitQualifiedNameLast(qualifiedName);
|
||||
return {
|
||||
schemaName: stripIdentifierQuotes(raw.substring(0, idx)),
|
||||
objectName: stripIdentifierQuotes(raw.substring(idx + 1)),
|
||||
schemaName: parsed.parentPath,
|
||||
objectName: parsed.objectName,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useStore } from '../store';
|
||||
import { DBQuery } from '../../wailsjs/go/app/App';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
|
||||
interface TriggerViewerProps {
|
||||
tab: TabData;
|
||||
@@ -25,12 +26,8 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
const parseSchemaAndName = (fullName: string): { schema: string; name: string } => {
|
||||
const raw = String(fullName || '').trim();
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx > 0 && idx < raw.length - 1) {
|
||||
return { schema: raw.substring(0, idx), name: raw.substring(idx + 1) };
|
||||
}
|
||||
return { schema: '', name: raw };
|
||||
const parsed = splitQualifiedNameLast(fullName);
|
||||
return { schema: parsed.parentPath, name: parsed.objectName };
|
||||
};
|
||||
|
||||
const getMetadataDialect = (conn: any): string => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
unquoteSqlIdentifierPart,
|
||||
unquoteSqlIdentifierPath,
|
||||
} from '../utils/sqlDialect';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
|
||||
export interface EditableColumnSnapshot {
|
||||
_key: string;
|
||||
@@ -83,13 +84,10 @@ const escapeSqlString = (value: string) => String(value || '').replace(/'/g, "''
|
||||
const stripIdentifierQuotes = unquoteSqlIdentifierPart;
|
||||
|
||||
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const raw = String(qualifiedName || '').trim();
|
||||
if (!raw) return { schemaName: '', objectName: '' };
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx <= 0 || idx >= raw.length - 1) return { schemaName: '', objectName: raw };
|
||||
const parsed = splitQualifiedNameLast(qualifiedName);
|
||||
return {
|
||||
schemaName: stripIdentifierQuotes(raw.substring(0, idx)),
|
||||
objectName: stripIdentifierQuotes(raw.substring(idx + 1)),
|
||||
schemaName: parsed.parentPath,
|
||||
objectName: parsed.objectName,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user