mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-30 02:31:23 +08:00
♻️ refactor(sidebar): 抽出元数据加载工具
This commit is contained in:
@@ -65,6 +65,7 @@ const readSidebarSource = () => [
|
||||
readSourceFile('./sidebar/SidebarConnectionRail.tsx'),
|
||||
readSourceFile('./sidebar/SidebarSearchPanel.tsx'),
|
||||
readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx'),
|
||||
readSourceFile('./sidebar/sidebarMetadataLoaders.ts'),
|
||||
readSourceFile('./sidebarV2Utils.ts'),
|
||||
].join('\n');
|
||||
const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx');
|
||||
|
||||
@@ -2,6 +2,30 @@
|
||||
import SidebarConnectionRail from './sidebar/SidebarConnectionRail';
|
||||
import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel';
|
||||
import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu';
|
||||
import {
|
||||
buildDuckDBMacroDDL,
|
||||
buildQualifiedName,
|
||||
buildSidebarObjectKeyName,
|
||||
buildSidebarTableStatusSQL,
|
||||
escapeSQLLiteral,
|
||||
extractSqlServerDefinitionRows,
|
||||
getCaseInsensitiveRawValue,
|
||||
getCaseInsensitiveValue,
|
||||
getMetadataDialect,
|
||||
getMySQLShowTablesName,
|
||||
getSidebarTableDisplayName,
|
||||
isSphinxConnection,
|
||||
loadDatabaseEvents,
|
||||
loadDatabaseTriggers,
|
||||
loadFunctions,
|
||||
loadSchemas,
|
||||
loadStarRocksMaterializedViews,
|
||||
loadViews,
|
||||
parseMetadataRowCount,
|
||||
shouldHideSchemaPrefix,
|
||||
splitQualifiedName,
|
||||
supportsDatabaseEvents,
|
||||
} from './sidebar/sidebarMetadataLoaders';
|
||||
import {
|
||||
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
|
||||
formatSidebarRowCount,
|
||||
@@ -96,16 +120,10 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { getDataSourceCapabilities, resolveDataSourceType } from '../utils/dataSourceCapabilities';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import {
|
||||
buildMySQLCompatibleViewMetadataSqls,
|
||||
isSidebarViewTableType,
|
||||
normalizeSidebarViewMetadataEntry,
|
||||
resolveSidebarMetadataDialect,
|
||||
resolveSidebarRuntimeDatabase,
|
||||
type SidebarViewMetadataEntry,
|
||||
} from '../utils/sidebarMetadata';
|
||||
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
||||
import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql';
|
||||
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
||||
import { resolveConnectionHostSummary, resolveConnectionHostTokens } from '../utils/tabDisplay';
|
||||
import {
|
||||
findSidebarNodePathByKey,
|
||||
@@ -1333,708 +1351,6 @@ const Sidebar: React.FC<{
|
||||
return null;
|
||||
};
|
||||
|
||||
const SIDEBAR_SCHEMA_DB_TYPES = new Set([
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'opengauss',
|
||||
'gaussdb',
|
||||
'open_gauss',
|
||||
'open-gauss',
|
||||
'sqlserver',
|
||||
'iris',
|
||||
'oracle',
|
||||
'dameng',
|
||||
]);
|
||||
|
||||
const SIDEBAR_SCHEMA_CUSTOM_DRIVERS = new Set([
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'opengauss',
|
||||
'gaussdb',
|
||||
'open_gauss',
|
||||
'open-gauss',
|
||||
'sqlserver',
|
||||
'iris',
|
||||
'oracle',
|
||||
'dm',
|
||||
]);
|
||||
|
||||
const shouldHideSchemaPrefix = (conn: SavedConnection | undefined): boolean => {
|
||||
const dbType = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (SIDEBAR_SCHEMA_DB_TYPES.has(dbType)) return true;
|
||||
if (dbType !== 'custom') return false;
|
||||
|
||||
const customDriver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return SIDEBAR_SCHEMA_CUSTOM_DRIVERS.has(customDriver);
|
||||
};
|
||||
|
||||
const getSidebarTableDisplayName = (conn: SavedConnection | undefined, tableName: string): string => {
|
||||
const rawName = String(tableName || '').trim();
|
||||
if (!rawName) return rawName;
|
||||
if (!shouldHideSchemaPrefix(conn)) return rawName;
|
||||
const parsed = splitQualifiedName(rawName);
|
||||
return parsed.objectName || rawName;
|
||||
};
|
||||
|
||||
const getMetadataDialect = (conn: SavedConnection | undefined): string => {
|
||||
return resolveSidebarMetadataDialect(
|
||||
conn?.config?.type || '',
|
||||
conn?.config?.driver || '',
|
||||
conn?.config?.oceanBaseProtocol,
|
||||
);
|
||||
};
|
||||
|
||||
const supportsDatabaseEvents = (conn: SavedConnection | undefined): boolean => {
|
||||
return getMetadataDialect(conn) === 'mysql';
|
||||
};
|
||||
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
|
||||
type MetadataQuerySpec = {
|
||||
sql: string;
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
type MetadataQueryResult = {
|
||||
rows: Record<string, any>[];
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
const isSphinxConnection = (conn: SavedConnection | undefined): boolean => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (type === 'sphinx') return true;
|
||||
if (type !== 'custom') return false;
|
||||
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return driver === 'sphinx' || driver === 'sphinxql';
|
||||
};
|
||||
|
||||
const normalizeMetadataQuerySpecs = (specs: MetadataQuerySpec[]): MetadataQuerySpec[] => {
|
||||
const seen = new Set<string>();
|
||||
const normalized: MetadataQuerySpec[] = [];
|
||||
specs.forEach((spec) => {
|
||||
const sql = String(spec.sql || '').trim();
|
||||
if (!sql) return;
|
||||
const key = `${spec.inferredType || ''}@@${sql}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
normalized.push({ sql, inferredType: spec.inferredType });
|
||||
});
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const getCaseInsensitiveValue = (row: Record<string, any>, candidateKeys: string[]): string => {
|
||||
const keyMap = new Map<string, any>();
|
||||
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
||||
for (const key of candidateKeys) {
|
||||
const value = keyMap.get(key.toLowerCase());
|
||||
if (value !== undefined && value !== null) {
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getCaseInsensitiveRawValue = (row: Record<string, any>, candidateKeys: string[]): any => {
|
||||
const keyMap = new Map<string, any>();
|
||||
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
||||
for (const key of candidateKeys) {
|
||||
const value = keyMap.get(key.toLowerCase());
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getFirstRowValue = (row: Record<string, any>): string => {
|
||||
for (const value of Object.values(row || {})) {
|
||||
if (value !== undefined && value !== null) {
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractSqlServerDefinitionRows = (rows: any[], definitionKeys: string[]): string => {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return '';
|
||||
const directDefinition = getCaseInsensitiveRawValue(rows[0] as Record<string, any>, definitionKeys);
|
||||
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
|
||||
return String(directDefinition);
|
||||
}
|
||||
return rows
|
||||
.map((row) => getCaseInsensitiveRawValue(row as Record<string, any>, ['Text', 'text']))
|
||||
.filter((value) => value !== undefined && value !== null)
|
||||
.map((value) => String(value))
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getMySQLShowTablesName = (row: Record<string, any>): string => {
|
||||
for (const key of Object.keys(row || {})) {
|
||||
if (!key.toLowerCase().startsWith('tables_in_')) continue;
|
||||
const value = row[key];
|
||||
if (value === undefined || value === null) continue;
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const parseMetadataRowCount = (row: Record<string, any>): number | undefined => {
|
||||
const rawValue = getCaseInsensitiveRawValue(row, ['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']);
|
||||
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(String(rawValue).replace(/,/g, ''));
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.round(parsed);
|
||||
};
|
||||
|
||||
const buildSidebarTableStatusSQL = (conn: SavedConnection, dbName: string): string => {
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
'SELECT TABLE_NAME AS table_name, TABLE_ROWS AS table_rows',
|
||||
'FROM information_schema.tables',
|
||||
`WHERE table_schema = '${safeDbName}'`,
|
||||
"AND table_type = 'BASE TABLE'",
|
||||
'ORDER BY table_name',
|
||||
].join('\n');
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'vastbase':
|
||||
case 'highgo':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [
|
||||
"SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows",
|
||||
'FROM pg_class c',
|
||||
'JOIN pg_namespace n ON n.oid = c.relnamespace',
|
||||
"WHERE c.relkind = 'r'",
|
||||
"AND n.nspname NOT IN ('information_schema', 'pg_catalog')",
|
||||
"AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'",
|
||||
'ORDER BY n.nspname, c.relname',
|
||||
].join('\n');
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName);
|
||||
return [
|
||||
'SELECT s.name + \'.\' + t.name AS table_name, SUM(p.rows) AS table_rows',
|
||||
`FROM ${safeDb}.sys.tables t`,
|
||||
`JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id`,
|
||||
`LEFT JOIN ${safeDb}.sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)`,
|
||||
'WHERE t.type = \'U\'',
|
||||
'GROUP BY s.name, t.name',
|
||||
'ORDER BY s.name, t.name',
|
||||
].join('\n');
|
||||
}
|
||||
case 'clickhouse':
|
||||
return [
|
||||
'SELECT name AS table_name, total_rows AS table_rows',
|
||||
'FROM system.tables',
|
||||
`WHERE database = '${safeDbName}'`,
|
||||
"AND engine NOT IN ('View', 'MaterializedView')",
|
||||
'ORDER BY name',
|
||||
].join('\n');
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const owner = escapeSQLLiteral(dbName).toUpperCase();
|
||||
return [
|
||||
'SELECT table_name, num_rows AS table_rows',
|
||||
'FROM all_tables',
|
||||
`WHERE owner = '${owner}'`,
|
||||
'ORDER BY table_name',
|
||||
].join('\n');
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const buildQualifiedName = (schemaName: string, objectName: string): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(objectName || '').trim();
|
||||
if (!name) return '';
|
||||
if (!schema) return name;
|
||||
if (name.includes('.')) return name;
|
||||
return `${schema}.${name}`;
|
||||
};
|
||||
|
||||
const buildSidebarObjectKeyName = (dbName: string, schemaName: string, objectName: string): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(objectName || '').trim();
|
||||
if (!schema || !name || name.includes('.')) return name;
|
||||
if (schema.toLowerCase() === String(dbName || '').trim().toLowerCase()) return name;
|
||||
return `${schema}.${name}`;
|
||||
};
|
||||
|
||||
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const parsed = splitQualifiedNameLast(qualifiedName);
|
||||
return {
|
||||
schemaName: parsed.parentPath,
|
||||
objectName: parsed.objectName,
|
||||
};
|
||||
};
|
||||
|
||||
const parseDuckDBParameterNames = (raw: any): string[] => {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map((item) => String(item ?? '').trim())
|
||||
.filter((item) => item !== '' && item.toLowerCase() !== '<nil>');
|
||||
}
|
||||
|
||||
const text = String(raw ?? '').trim();
|
||||
if (!text) return [];
|
||||
const normalized = text.startsWith('[') && text.endsWith(']')
|
||||
? text.slice(1, -1)
|
||||
: text;
|
||||
return normalized
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part !== '' && part.toLowerCase() !== '<nil>');
|
||||
};
|
||||
|
||||
const buildDuckDBMacroDDL = (
|
||||
schemaName: string,
|
||||
functionName: string,
|
||||
parametersRaw: any,
|
||||
macroDefinitionRaw: any
|
||||
): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(functionName || '').trim();
|
||||
const macroDefinition = String(macroDefinitionRaw || '').trim();
|
||||
if (!name || !macroDefinition) return '';
|
||||
|
||||
const parameters = parseDuckDBParameterNames(parametersRaw).join(', ');
|
||||
const qualifiedName = schema ? `${schema}.${name}` : name;
|
||||
const isTableMacro = !macroDefinition.startsWith('(');
|
||||
if (isTableMacro) {
|
||||
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS TABLE ${macroDefinition};`;
|
||||
}
|
||||
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS ${macroDefinition};`;
|
||||
};
|
||||
|
||||
const buildViewsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
return normalizeMetadataQuerySpecs(
|
||||
buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })),
|
||||
);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY schemaname, viewname` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, v.name AS view_name FROM ${safeDb}.sys.views v JOIN ${safeDb}.sys.schemas s ON v.schema_id = s.schema_id ORDER BY s.name, v.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{ sql: `SELECT VIEW_NAME AS view_name FROM USER_VIEWS ORDER BY VIEW_NAME` },
|
||||
{ sql: `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = USER ORDER BY VIEW_NAME` },
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME`
|
||||
: '',
|
||||
},
|
||||
]);
|
||||
case 'sqlite':
|
||||
return [{ sql: `SELECT name AS view_name FROM sqlite_master WHERE type = 'view' ORDER BY name` }];
|
||||
case 'duckdb':
|
||||
return [{ sql: `SELECT table_schema AS schema_name, table_name AS view_name FROM information_schema.views WHERE table_schema NOT IN ('information_schema', 'pg_catalog') ORDER BY table_schema, table_name` }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildTriggersMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TRIGGER_NAME AS trigger_name, EVENT_OBJECT_TABLE AS table_name, TRIGGER_SCHEMA AS schema_name FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW TRIGGERS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW TRIGGERS` },
|
||||
]);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY event_object_schema, event_object_table, trigger_name` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, t.name AS table_name, tr.name AS trigger_name FROM ${safeDb}.sys.triggers tr JOIN ${safeDb}.sys.tables t ON tr.parent_id = t.object_id JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id WHERE tr.parent_class = 1 ORDER BY s.name, t.name, tr.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return [{ sql: `SELECT TRIGGER_NAME AS trigger_name, TABLE_NAME AS table_name FROM USER_TRIGGERS ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
}
|
||||
return [{ sql: `SELECT OWNER AS schema_name, TABLE_NAME AS table_name, TRIGGER_NAME AS trigger_name FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
case 'sqlite':
|
||||
return [{ sql: `SELECT name AS trigger_name, tbl_name AS table_name FROM sqlite_master WHERE type = 'trigger' ORDER BY tbl_name, name` }];
|
||||
case 'duckdb':
|
||||
return [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildFunctionsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT ROUTINE_NAME AS routine_name, ROUTINE_TYPE AS routine_type, ROUTINE_SCHEMA AS schema_name FROM information_schema.routines WHERE routine_schema = '${safeDbName}' ORDER BY ROUTINE_TYPE, ROUTINE_NAME`
|
||||
: '',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW FUNCTION STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW FUNCTION STATUS`,
|
||||
inferredType: 'FUNCTION',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW PROCEDURE STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW PROCEDURE STATUS`,
|
||||
inferredType: 'PROCEDURE',
|
||||
},
|
||||
]);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
// PostgreSQL 11+ / 部分 PG-like:通过 prokind 区分 FUNCTION/PROCEDURE
|
||||
sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, routine_type, p.proname`,
|
||||
},
|
||||
{
|
||||
// PostgreSQL 10 / 不支持 prokind 的兼容路径
|
||||
sql: `SELECT r.routine_schema AS schema_name, r.routine_name AS routine_name, COALESCE(NULLIF(UPPER(r.routine_type), ''), 'FUNCTION') AS routine_type FROM information_schema.routines r WHERE r.routine_schema NOT IN ('pg_catalog', 'information_schema') AND r.routine_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY r.routine_schema, routine_type, r.routine_name`,
|
||||
},
|
||||
{
|
||||
// 最后兜底:仅函数列表,确保 prokind/routines 视图异常时仍可展示
|
||||
sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, 'FUNCTION' AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, p.proname`,
|
||||
},
|
||||
]);
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, o.name AS routine_name, CASE o.type WHEN 'P' THEN 'PROCEDURE' WHEN 'FN' THEN 'FUNCTION' WHEN 'IF' THEN 'FUNCTION' WHEN 'TF' THEN 'FUNCTION' END AS routine_type FROM ${safeDb}.sys.objects o JOIN ${safeDb}.sys.schemas s ON o.schema_id = s.schema_id WHERE o.type IN ('P','FN','IF','TF') ORDER BY o.type, s.name, o.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{ sql: `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` },
|
||||
{ sql: `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = USER AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` },
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`
|
||||
: '',
|
||||
},
|
||||
]);
|
||||
case 'duckdb':
|
||||
return [{
|
||||
sql: `SELECT schema_name, function_name AS routine_name, 'FUNCTION' AS routine_type FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND COALESCE(macro_definition, '') <> '' ORDER BY schema_name, function_name`,
|
||||
inferredType: 'FUNCTION',
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildEventsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
if (dialect !== 'mysql') {
|
||||
return [];
|
||||
}
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_TYPE AS event_type, STATUS AS status FROM information_schema.events WHERE event_schema = '${safeDbName}' ORDER BY EVENT_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW EVENTS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW EVENTS` },
|
||||
]);
|
||||
};
|
||||
|
||||
const buildSchemasMetadataQuerySpecs = (dialect: string): MetadataQuerySpec[] => {
|
||||
if (!isPostgresSchemaDialect(dialect)) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
sql: `SELECT nspname AS schema_name FROM pg_namespace WHERE nspname NOT IN ('pg_catalog', 'information_schema') AND nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY nspname`,
|
||||
}];
|
||||
};
|
||||
|
||||
const queryMetadataRowsBySpecs = async (
|
||||
conn: any,
|
||||
dbName: string,
|
||||
specs: MetadataQuerySpec[]
|
||||
): Promise<{ results: MetadataQueryResult[]; hasSuccessfulQuery: boolean }> => {
|
||||
const normalizedSpecs = normalizeMetadataQuerySpecs(specs);
|
||||
if (normalizedSpecs.length === 0) {
|
||||
return { results: [], hasSuccessfulQuery: false };
|
||||
}
|
||||
const config = buildRuntimeConfig(conn, dbName);
|
||||
const results: MetadataQueryResult[] = [];
|
||||
let hasSuccessfulQuery = false;
|
||||
|
||||
for (const spec of normalizedSpecs) {
|
||||
try {
|
||||
const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, spec.sql);
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
continue;
|
||||
}
|
||||
hasSuccessfulQuery = true;
|
||||
results.push({
|
||||
rows: result.data as Record<string, any>[],
|
||||
inferredType: spec.inferredType,
|
||||
});
|
||||
} catch {
|
||||
// 忽略单条查询失败,继续尝试后续回退语句
|
||||
}
|
||||
}
|
||||
return { results, hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadViews = async (conn: any, dbName: string): Promise<{ views: SidebarViewMetadataEntry[]; supported: boolean }> => {
|
||||
const savedConn = conn as SavedConnection;
|
||||
const dialect = getMetadataDialect(savedConn);
|
||||
const querySpecs = buildViewsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: SidebarViewMetadataEntry[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const tableType = getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']);
|
||||
if (!isSidebarViewTableType(tableType)) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema', 'db']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name'])
|
||||
|| getMySQLShowTablesName(row)
|
||||
|| getFirstRowValue(row);
|
||||
const entry = normalizeSidebarViewMetadataEntry(dialect, dbName, schemaName, viewName);
|
||||
if (!entry) return;
|
||||
const uniqueKey = `${entry.schemaName.toLowerCase()}@@${entry.viewName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
views.push(entry);
|
||||
});
|
||||
});
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadStarRocksMaterializedViews = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ views: SidebarViewMetadataEntry[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
if (dialect !== 'starrocks') {
|
||||
return { views: [], supported: false };
|
||||
}
|
||||
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
const querySpecs = normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name FROM information_schema.tables WHERE TABLE_SCHEMA = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%MATERIALIZED%' ORDER BY TABLE_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW MATERIALIZED VIEWS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW MATERIALIZED VIEWS` },
|
||||
]);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: SidebarViewMetadataEntry[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'table_schema', 'db', 'database']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['object_name', 'view_name', 'table_name', 'name', 'materialized_view_name', 'mv_name'])
|
||||
|| getFirstRowValue(row);
|
||||
const entry = normalizeSidebarViewMetadataEntry(dialect, dbName, schemaName, viewName);
|
||||
if (!entry) return;
|
||||
const uniqueKey = `${entry.schemaName.toLowerCase()}@@${entry.viewName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
views.push(entry);
|
||||
});
|
||||
});
|
||||
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseTriggers = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ triggers: Array<{ displayName: string; triggerName: string; tableName: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildTriggersMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const triggers: Array<{ displayName: string; triggerName: string; tableName: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawTriggerName = getCaseInsensitiveValue(row, ['trigger_name', 'triggername', 'trigger', 'name']) || getFirstRowValue(row);
|
||||
if (!rawTriggerName) return;
|
||||
|
||||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'event_object_schema', 'trigger_schema', 'db']);
|
||||
const rawTableName = getCaseInsensitiveValue(row, ['table_name', 'event_object_table', 'tbl_name', 'table']);
|
||||
|
||||
const triggerParts = splitQualifiedName(rawTriggerName);
|
||||
const tableParts = splitQualifiedName(rawTableName);
|
||||
|
||||
const resolvedSchema = (
|
||||
rawSchemaName
|
||||
|| tableParts.schemaName
|
||||
|| triggerParts.schemaName
|
||||
|| dbName
|
||||
).trim();
|
||||
const resolvedTriggerName = (triggerParts.objectName || rawTriggerName).trim();
|
||||
const resolvedTableName = (tableParts.objectName || rawTableName).trim();
|
||||
const fullTableName = buildQualifiedName(resolvedSchema, resolvedTableName);
|
||||
|
||||
// MySQL 下 trigger 名在同 schema 内唯一,直接按 schema+trigger 去重可彻底规避多元数据查询导致的重复
|
||||
const uniqueKey = dialect === 'mysql'
|
||||
? `${resolvedSchema.toLowerCase()}@@${resolvedTriggerName.toLowerCase()}`
|
||||
: `${resolvedSchema.toLowerCase()}@@${resolvedTriggerName.toLowerCase()}@@${resolvedTableName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const displayName = fullTableName ? `${resolvedTriggerName} (${fullTableName})` : resolvedTriggerName;
|
||||
triggers.push({ displayName, triggerName: resolvedTriggerName, tableName: fullTableName || resolvedTableName });
|
||||
});
|
||||
});
|
||||
return { triggers, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadFunctions = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ routines: Array<{ displayName: string; routineName: string; routineType: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildFunctionsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const routines: Array<{ displayName: string; routineName: string; routineType: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const routineName = getCaseInsensitiveValue(row, ['routine_name', 'object_name', 'proname', 'name']);
|
||||
if (!routineName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'owner', 'db', 'database']);
|
||||
const rawType = getCaseInsensitiveValue(row, ['routine_type', 'object_type', 'type']) || queryResult.inferredType || 'FUNCTION';
|
||||
const normalizedType = rawType.toUpperCase().includes('PROC') ? 'PROCEDURE' : 'FUNCTION';
|
||||
const fullName = buildQualifiedName(schemaName, routineName);
|
||||
const uniqueKey = `${fullName}@@${normalizedType}`;
|
||||
if (!fullName || seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const typeLabel = normalizedType === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: normalizedType });
|
||||
});
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseEvents = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildEventsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawEventName = getCaseInsensitiveValue(row, ['event_name', 'eventname', 'name', 'event']);
|
||||
if (!rawEventName) return;
|
||||
|
||||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'event_schema', 'db', 'database']);
|
||||
const parsed = splitQualifiedName(rawEventName);
|
||||
const schemaName = (rawSchemaName || parsed.schemaName || dbName).trim();
|
||||
const eventName = (parsed.objectName || rawEventName).trim();
|
||||
if (!eventName) return;
|
||||
|
||||
const uniqueKey = `${schemaName.toLowerCase()}@@${eventName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
|
||||
const eventType = getCaseInsensitiveValue(row, ['event_type', 'type']);
|
||||
const status = getCaseInsensitiveValue(row, ['status']);
|
||||
events.push({
|
||||
displayName: eventName,
|
||||
eventName,
|
||||
schemaName,
|
||||
eventType,
|
||||
status,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { events, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadSchemas = async (conn: any, dbName: string): Promise<{ schemas: string[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildSchemasMetadataQuerySpecs(dialect);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const schemas: string[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'schemaname']) || getFirstRowValue(row);
|
||||
if (!schemaName) return;
|
||||
const key = schemaName.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
schemas.push(schemaName);
|
||||
});
|
||||
});
|
||||
|
||||
return { schemas, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const fetchDriverStatusMap = async (): Promise<Record<string, DriverStatusSnapshot>> => {
|
||||
const cached = driverStatusCacheRef.current;
|
||||
if (cached && Date.now() - cached.fetchedAt < DRIVER_STATUS_CACHE_TTL_MS) {
|
||||
|
||||
763
frontend/src/components/sidebar/sidebarMetadataLoaders.ts
Normal file
763
frontend/src/components/sidebar/sidebarMetadataLoaders.ts
Normal file
@@ -0,0 +1,763 @@
|
||||
import { DBQuery } from '../../../wailsjs/go/app/App';
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { normalizeOceanBaseProtocol } from '../../utils/oceanBaseProtocol';
|
||||
import { splitQualifiedNameLast } from '../../utils/qualifiedName';
|
||||
import {
|
||||
buildMySQLCompatibleViewMetadataSqls,
|
||||
isSidebarViewTableType,
|
||||
normalizeSidebarViewMetadataEntry,
|
||||
resolveSidebarMetadataDialect,
|
||||
resolveSidebarRuntimeDatabase,
|
||||
type SidebarViewMetadataEntry,
|
||||
} from '../../utils/sidebarMetadata';
|
||||
import { isPostgresSchemaDialect } from '../sidebarCoreUtils';
|
||||
|
||||
export const buildSidebarRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {
|
||||
return buildRpcConnectionConfig(conn.config, {
|
||||
database: resolveSidebarRuntimeDatabase(
|
||||
conn?.config?.type,
|
||||
conn?.config?.driver,
|
||||
conn?.config?.database,
|
||||
overrideDatabase,
|
||||
clearDatabase,
|
||||
conn?.config?.oceanBaseProtocol,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const SIDEBAR_SCHEMA_DB_TYPES = new Set([
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'opengauss',
|
||||
'gaussdb',
|
||||
'open_gauss',
|
||||
'open-gauss',
|
||||
'sqlserver',
|
||||
'iris',
|
||||
'oracle',
|
||||
'dameng',
|
||||
]);
|
||||
|
||||
const SIDEBAR_SCHEMA_CUSTOM_DRIVERS = new Set([
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'opengauss',
|
||||
'gaussdb',
|
||||
'open_gauss',
|
||||
'open-gauss',
|
||||
'sqlserver',
|
||||
'iris',
|
||||
'oracle',
|
||||
'dm',
|
||||
]);
|
||||
|
||||
const shouldHideSchemaPrefix = (conn: SavedConnection | undefined): boolean => {
|
||||
const dbType = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (SIDEBAR_SCHEMA_DB_TYPES.has(dbType)) return true;
|
||||
if (dbType !== 'custom') return false;
|
||||
|
||||
const customDriver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return SIDEBAR_SCHEMA_CUSTOM_DRIVERS.has(customDriver);
|
||||
};
|
||||
|
||||
const getSidebarTableDisplayName = (conn: SavedConnection | undefined, tableName: string): string => {
|
||||
const rawName = String(tableName || '').trim();
|
||||
if (!rawName) return rawName;
|
||||
if (!shouldHideSchemaPrefix(conn)) return rawName;
|
||||
const parsed = splitQualifiedName(rawName);
|
||||
return parsed.objectName || rawName;
|
||||
};
|
||||
|
||||
const getMetadataDialect = (conn: SavedConnection | undefined): string => {
|
||||
return resolveSidebarMetadataDialect(
|
||||
conn?.config?.type || '',
|
||||
conn?.config?.driver || '',
|
||||
conn?.config?.oceanBaseProtocol,
|
||||
);
|
||||
};
|
||||
|
||||
const supportsDatabaseEvents = (conn: SavedConnection | undefined): boolean => {
|
||||
return getMetadataDialect(conn) === 'mysql';
|
||||
};
|
||||
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
|
||||
type MetadataQuerySpec = {
|
||||
sql: string;
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
type MetadataQueryResult = {
|
||||
rows: Record<string, any>[];
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
const isSphinxConnection = (conn: SavedConnection | undefined): boolean => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (type === 'sphinx') return true;
|
||||
if (type !== 'custom') return false;
|
||||
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return driver === 'sphinx' || driver === 'sphinxql';
|
||||
};
|
||||
|
||||
const normalizeMetadataQuerySpecs = (specs: MetadataQuerySpec[]): MetadataQuerySpec[] => {
|
||||
const seen = new Set<string>();
|
||||
const normalized: MetadataQuerySpec[] = [];
|
||||
specs.forEach((spec) => {
|
||||
const sql = String(spec.sql || '').trim();
|
||||
if (!sql) return;
|
||||
const key = `${spec.inferredType || ''}@@${sql}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
normalized.push({ sql, inferredType: spec.inferredType });
|
||||
});
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const getCaseInsensitiveValue = (row: Record<string, any>, candidateKeys: string[]): string => {
|
||||
const keyMap = new Map<string, any>();
|
||||
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
||||
for (const key of candidateKeys) {
|
||||
const value = keyMap.get(key.toLowerCase());
|
||||
if (value !== undefined && value !== null) {
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getCaseInsensitiveRawValue = (row: Record<string, any>, candidateKeys: string[]): any => {
|
||||
const keyMap = new Map<string, any>();
|
||||
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
||||
for (const key of candidateKeys) {
|
||||
const value = keyMap.get(key.toLowerCase());
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getFirstRowValue = (row: Record<string, any>): string => {
|
||||
for (const value of Object.values(row || {})) {
|
||||
if (value !== undefined && value !== null) {
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractSqlServerDefinitionRows = (rows: any[], definitionKeys: string[]): string => {
|
||||
if (!Array.isArray(rows) || rows.length === 0) return '';
|
||||
const directDefinition = getCaseInsensitiveRawValue(rows[0] as Record<string, any>, definitionKeys);
|
||||
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
|
||||
return String(directDefinition);
|
||||
}
|
||||
return rows
|
||||
.map((row) => getCaseInsensitiveRawValue(row as Record<string, any>, ['Text', 'text']))
|
||||
.filter((value) => value !== undefined && value !== null)
|
||||
.map((value) => String(value))
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getMySQLShowTablesName = (row: Record<string, any>): string => {
|
||||
for (const key of Object.keys(row || {})) {
|
||||
if (!key.toLowerCase().startsWith('tables_in_')) continue;
|
||||
const value = row[key];
|
||||
if (value === undefined || value === null) continue;
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const parseMetadataRowCount = (row: Record<string, any>): number | undefined => {
|
||||
const rawValue = getCaseInsensitiveRawValue(row, ['Rows', 'table_rows', 'TABLE_ROWS', 'num_rows', 'reltuples', 'total_rows']);
|
||||
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(String(rawValue).replace(/,/g, ''));
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.round(parsed);
|
||||
};
|
||||
|
||||
const buildSidebarTableStatusSQL = (conn: SavedConnection, dbName: string): string => {
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
'SELECT TABLE_NAME AS table_name, TABLE_ROWS AS table_rows',
|
||||
'FROM information_schema.tables',
|
||||
`WHERE table_schema = '${safeDbName}'`,
|
||||
"AND table_type = 'BASE TABLE'",
|
||||
'ORDER BY table_name',
|
||||
].join('\n');
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'vastbase':
|
||||
case 'highgo':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [
|
||||
"SELECT n.nspname || '.' || c.relname AS table_name, c.reltuples::bigint AS table_rows",
|
||||
'FROM pg_class c',
|
||||
'JOIN pg_namespace n ON n.oid = c.relnamespace',
|
||||
"WHERE c.relkind = 'r'",
|
||||
"AND n.nspname NOT IN ('information_schema', 'pg_catalog')",
|
||||
"AND n.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'",
|
||||
'ORDER BY n.nspname, c.relname',
|
||||
].join('\n');
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName);
|
||||
return [
|
||||
'SELECT s.name + \'.\' + t.name AS table_name, SUM(p.rows) AS table_rows',
|
||||
`FROM ${safeDb}.sys.tables t`,
|
||||
`JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id`,
|
||||
`LEFT JOIN ${safeDb}.sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)`,
|
||||
'WHERE t.type = \'U\'',
|
||||
'GROUP BY s.name, t.name',
|
||||
'ORDER BY s.name, t.name',
|
||||
].join('\n');
|
||||
}
|
||||
case 'clickhouse':
|
||||
return [
|
||||
'SELECT name AS table_name, total_rows AS table_rows',
|
||||
'FROM system.tables',
|
||||
`WHERE database = '${safeDbName}'`,
|
||||
"AND engine NOT IN ('View', 'MaterializedView')",
|
||||
'ORDER BY name',
|
||||
].join('\n');
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const owner = escapeSQLLiteral(dbName).toUpperCase();
|
||||
return [
|
||||
'SELECT table_name, num_rows AS table_rows',
|
||||
'FROM all_tables',
|
||||
`WHERE owner = '${owner}'`,
|
||||
'ORDER BY table_name',
|
||||
].join('\n');
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const buildQualifiedName = (schemaName: string, objectName: string): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(objectName || '').trim();
|
||||
if (!name) return '';
|
||||
if (!schema) return name;
|
||||
if (name.includes('.')) return name;
|
||||
return `${schema}.${name}`;
|
||||
};
|
||||
|
||||
const buildSidebarObjectKeyName = (dbName: string, schemaName: string, objectName: string): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(objectName || '').trim();
|
||||
if (!schema || !name || name.includes('.')) return name;
|
||||
if (schema.toLowerCase() === String(dbName || '').trim().toLowerCase()) return name;
|
||||
return `${schema}.${name}`;
|
||||
};
|
||||
|
||||
const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
|
||||
const parsed = splitQualifiedNameLast(qualifiedName);
|
||||
return {
|
||||
schemaName: parsed.parentPath,
|
||||
objectName: parsed.objectName,
|
||||
};
|
||||
};
|
||||
|
||||
const parseDuckDBParameterNames = (raw: any): string[] => {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map((item) => String(item ?? '').trim())
|
||||
.filter((item) => item !== '' && item.toLowerCase() !== '<nil>');
|
||||
}
|
||||
|
||||
const text = String(raw ?? '').trim();
|
||||
if (!text) return [];
|
||||
const normalized = text.startsWith('[') && text.endsWith(']')
|
||||
? text.slice(1, -1)
|
||||
: text;
|
||||
return normalized
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part !== '' && part.toLowerCase() !== '<nil>');
|
||||
};
|
||||
|
||||
const buildDuckDBMacroDDL = (
|
||||
schemaName: string,
|
||||
functionName: string,
|
||||
parametersRaw: any,
|
||||
macroDefinitionRaw: any
|
||||
): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(functionName || '').trim();
|
||||
const macroDefinition = String(macroDefinitionRaw || '').trim();
|
||||
if (!name || !macroDefinition) return '';
|
||||
|
||||
const parameters = parseDuckDBParameterNames(parametersRaw).join(', ');
|
||||
const qualifiedName = schema ? `${schema}.${name}` : name;
|
||||
const isTableMacro = !macroDefinition.startsWith('(');
|
||||
if (isTableMacro) {
|
||||
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS TABLE ${macroDefinition};`;
|
||||
}
|
||||
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS ${macroDefinition};`;
|
||||
};
|
||||
|
||||
const buildViewsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
return normalizeMetadataQuerySpecs(
|
||||
buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })),
|
||||
);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY schemaname, viewname` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, v.name AS view_name FROM ${safeDb}.sys.views v JOIN ${safeDb}.sys.schemas s ON v.schema_id = s.schema_id ORDER BY s.name, v.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{ sql: `SELECT VIEW_NAME AS view_name FROM USER_VIEWS ORDER BY VIEW_NAME` },
|
||||
{ sql: `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = USER ORDER BY VIEW_NAME` },
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME`
|
||||
: '',
|
||||
},
|
||||
]);
|
||||
case 'sqlite':
|
||||
return [{ sql: `SELECT name AS view_name FROM sqlite_master WHERE type = 'view' ORDER BY name` }];
|
||||
case 'duckdb':
|
||||
return [{ sql: `SELECT table_schema AS schema_name, table_name AS view_name FROM information_schema.views WHERE table_schema NOT IN ('information_schema', 'pg_catalog') ORDER BY table_schema, table_name` }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildTriggersMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TRIGGER_NAME AS trigger_name, EVENT_OBJECT_TABLE AS table_name, TRIGGER_SCHEMA AS schema_name FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW TRIGGERS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW TRIGGERS` },
|
||||
]);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY event_object_schema, event_object_table, trigger_name` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, t.name AS table_name, tr.name AS trigger_name FROM ${safeDb}.sys.triggers tr JOIN ${safeDb}.sys.tables t ON tr.parent_id = t.object_id JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id WHERE tr.parent_class = 1 ORDER BY s.name, t.name, tr.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return [{ sql: `SELECT TRIGGER_NAME AS trigger_name, TABLE_NAME AS table_name FROM USER_TRIGGERS ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
}
|
||||
return [{ sql: `SELECT OWNER AS schema_name, TABLE_NAME AS table_name, TRIGGER_NAME AS trigger_name FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
case 'sqlite':
|
||||
return [{ sql: `SELECT name AS trigger_name, tbl_name AS table_name FROM sqlite_master WHERE type = 'trigger' ORDER BY tbl_name, name` }];
|
||||
case 'duckdb':
|
||||
return [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildFunctionsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT ROUTINE_NAME AS routine_name, ROUTINE_TYPE AS routine_type, ROUTINE_SCHEMA AS schema_name FROM information_schema.routines WHERE routine_schema = '${safeDbName}' ORDER BY ROUTINE_TYPE, ROUTINE_NAME`
|
||||
: '',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW FUNCTION STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW FUNCTION STATUS`,
|
||||
inferredType: 'FUNCTION',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW PROCEDURE STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW PROCEDURE STATUS`,
|
||||
inferredType: 'PROCEDURE',
|
||||
},
|
||||
]);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
case 'opengauss':
|
||||
case 'gaussdb':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
// PostgreSQL 11+ / 部分 PG-like:通过 prokind 区分 FUNCTION/PROCEDURE
|
||||
sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, routine_type, p.proname`,
|
||||
},
|
||||
{
|
||||
// PostgreSQL 10 / 不支持 prokind 的兼容路径
|
||||
sql: `SELECT r.routine_schema AS schema_name, r.routine_name AS routine_name, COALESCE(NULLIF(UPPER(r.routine_type), ''), 'FUNCTION') AS routine_type FROM information_schema.routines r WHERE r.routine_schema NOT IN ('pg_catalog', 'information_schema') AND r.routine_schema NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY r.routine_schema, routine_type, r.routine_name`,
|
||||
},
|
||||
{
|
||||
// 最后兜底:仅函数列表,确保 prokind/routines 视图异常时仍可展示
|
||||
sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, 'FUNCTION' AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY n.nspname, p.proname`,
|
||||
},
|
||||
]);
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, o.name AS routine_name, CASE o.type WHEN 'P' THEN 'PROCEDURE' WHEN 'FN' THEN 'FUNCTION' WHEN 'IF' THEN 'FUNCTION' WHEN 'TF' THEN 'FUNCTION' END AS routine_type FROM ${safeDb}.sys.objects o JOIN ${safeDb}.sys.schemas s ON o.schema_id = s.schema_id WHERE o.type IN ('P','FN','IF','TF') ORDER BY o.type, s.name, o.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{ sql: `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` },
|
||||
{ sql: `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = USER AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` },
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`
|
||||
: '',
|
||||
},
|
||||
]);
|
||||
case 'duckdb':
|
||||
return [{
|
||||
sql: `SELECT schema_name, function_name AS routine_name, 'FUNCTION' AS routine_type FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND COALESCE(macro_definition, '') <> '' ORDER BY schema_name, function_name`,
|
||||
inferredType: 'FUNCTION',
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildEventsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
if (dialect !== 'mysql') {
|
||||
return [];
|
||||
}
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_TYPE AS event_type, STATUS AS status FROM information_schema.events WHERE event_schema = '${safeDbName}' ORDER BY EVENT_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW EVENTS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW EVENTS` },
|
||||
]);
|
||||
};
|
||||
|
||||
const buildSchemasMetadataQuerySpecs = (dialect: string): MetadataQuerySpec[] => {
|
||||
if (!isPostgresSchemaDialect(dialect)) {
|
||||
return [];
|
||||
}
|
||||
return [{
|
||||
sql: `SELECT nspname AS schema_name FROM pg_namespace WHERE nspname NOT IN ('pg_catalog', 'information_schema') AND nspname NOT LIKE 'pg|_%' ESCAPE '|' ORDER BY nspname`,
|
||||
}];
|
||||
};
|
||||
|
||||
const queryMetadataRowsBySpecs = async (
|
||||
conn: any,
|
||||
dbName: string,
|
||||
specs: MetadataQuerySpec[]
|
||||
): Promise<{ results: MetadataQueryResult[]; hasSuccessfulQuery: boolean }> => {
|
||||
const normalizedSpecs = normalizeMetadataQuerySpecs(specs);
|
||||
if (normalizedSpecs.length === 0) {
|
||||
return { results: [], hasSuccessfulQuery: false };
|
||||
}
|
||||
const config = buildSidebarRuntimeConfig(conn, dbName);
|
||||
const results: MetadataQueryResult[] = [];
|
||||
let hasSuccessfulQuery = false;
|
||||
|
||||
for (const spec of normalizedSpecs) {
|
||||
try {
|
||||
const result = await DBQuery(buildRpcConnectionConfig(config) as any, dbName, spec.sql);
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
continue;
|
||||
}
|
||||
hasSuccessfulQuery = true;
|
||||
results.push({
|
||||
rows: result.data as Record<string, any>[],
|
||||
inferredType: spec.inferredType,
|
||||
});
|
||||
} catch {
|
||||
// 忽略单条查询失败,继续尝试后续回退语句
|
||||
}
|
||||
}
|
||||
return { results, hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadViews = async (conn: any, dbName: string): Promise<{ views: SidebarViewMetadataEntry[]; supported: boolean }> => {
|
||||
const savedConn = conn as SavedConnection;
|
||||
const dialect = getMetadataDialect(savedConn);
|
||||
const querySpecs = buildViewsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: SidebarViewMetadataEntry[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const tableType = getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']);
|
||||
if (!isSidebarViewTableType(tableType)) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema', 'db']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name'])
|
||||
|| getMySQLShowTablesName(row)
|
||||
|| getFirstRowValue(row);
|
||||
const entry = normalizeSidebarViewMetadataEntry(dialect, dbName, schemaName, viewName);
|
||||
if (!entry) return;
|
||||
const uniqueKey = `${entry.schemaName.toLowerCase()}@@${entry.viewName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
views.push(entry);
|
||||
});
|
||||
});
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadStarRocksMaterializedViews = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ views: SidebarViewMetadataEntry[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
if (dialect !== 'starrocks') {
|
||||
return { views: [], supported: false };
|
||||
}
|
||||
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
const querySpecs = normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name FROM information_schema.tables WHERE TABLE_SCHEMA = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%MATERIALIZED%' ORDER BY TABLE_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW MATERIALIZED VIEWS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW MATERIALIZED VIEWS` },
|
||||
]);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: SidebarViewMetadataEntry[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'table_schema', 'db', 'database']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['object_name', 'view_name', 'table_name', 'name', 'materialized_view_name', 'mv_name'])
|
||||
|| getFirstRowValue(row);
|
||||
const entry = normalizeSidebarViewMetadataEntry(dialect, dbName, schemaName, viewName);
|
||||
if (!entry) return;
|
||||
const uniqueKey = `${entry.schemaName.toLowerCase()}@@${entry.viewName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
views.push(entry);
|
||||
});
|
||||
});
|
||||
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseTriggers = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ triggers: Array<{ displayName: string; triggerName: string; tableName: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildTriggersMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const triggers: Array<{ displayName: string; triggerName: string; tableName: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawTriggerName = getCaseInsensitiveValue(row, ['trigger_name', 'triggername', 'trigger', 'name']) || getFirstRowValue(row);
|
||||
if (!rawTriggerName) return;
|
||||
|
||||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'event_object_schema', 'trigger_schema', 'db']);
|
||||
const rawTableName = getCaseInsensitiveValue(row, ['table_name', 'event_object_table', 'tbl_name', 'table']);
|
||||
|
||||
const triggerParts = splitQualifiedName(rawTriggerName);
|
||||
const tableParts = splitQualifiedName(rawTableName);
|
||||
|
||||
const resolvedSchema = (
|
||||
rawSchemaName
|
||||
|| tableParts.schemaName
|
||||
|| triggerParts.schemaName
|
||||
|| dbName
|
||||
).trim();
|
||||
const resolvedTriggerName = (triggerParts.objectName || rawTriggerName).trim();
|
||||
const resolvedTableName = (tableParts.objectName || rawTableName).trim();
|
||||
const fullTableName = buildQualifiedName(resolvedSchema, resolvedTableName);
|
||||
|
||||
// MySQL 下 trigger 名在同 schema 内唯一,直接按 schema+trigger 去重可彻底规避多元数据查询导致的重复
|
||||
const uniqueKey = dialect === 'mysql'
|
||||
? `${resolvedSchema.toLowerCase()}@@${resolvedTriggerName.toLowerCase()}`
|
||||
: `${resolvedSchema.toLowerCase()}@@${resolvedTriggerName.toLowerCase()}@@${resolvedTableName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const displayName = fullTableName ? `${resolvedTriggerName} (${fullTableName})` : resolvedTriggerName;
|
||||
triggers.push({ displayName, triggerName: resolvedTriggerName, tableName: fullTableName || resolvedTableName });
|
||||
});
|
||||
});
|
||||
return { triggers, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadFunctions = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ routines: Array<{ displayName: string; routineName: string; routineType: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildFunctionsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const routines: Array<{ displayName: string; routineName: string; routineType: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const routineName = getCaseInsensitiveValue(row, ['routine_name', 'object_name', 'proname', 'name']);
|
||||
if (!routineName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'owner', 'db', 'database']);
|
||||
const rawType = getCaseInsensitiveValue(row, ['routine_type', 'object_type', 'type']) || queryResult.inferredType || 'FUNCTION';
|
||||
const normalizedType = rawType.toUpperCase().includes('PROC') ? 'PROCEDURE' : 'FUNCTION';
|
||||
const fullName = buildQualifiedName(schemaName, routineName);
|
||||
const uniqueKey = `${fullName}@@${normalizedType}`;
|
||||
if (!fullName || seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const typeLabel = normalizedType === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: normalizedType });
|
||||
});
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseEvents = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildEventsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const events: Array<{ displayName: string; eventName: string; schemaName: string; eventType: string; status: string }> = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawEventName = getCaseInsensitiveValue(row, ['event_name', 'eventname', 'name', 'event']);
|
||||
if (!rawEventName) return;
|
||||
|
||||
const rawSchemaName = getCaseInsensitiveValue(row, ['schema_name', 'event_schema', 'db', 'database']);
|
||||
const parsed = splitQualifiedName(rawEventName);
|
||||
const schemaName = (rawSchemaName || parsed.schemaName || dbName).trim();
|
||||
const eventName = (parsed.objectName || rawEventName).trim();
|
||||
if (!eventName) return;
|
||||
|
||||
const uniqueKey = `${schemaName.toLowerCase()}@@${eventName.toLowerCase()}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
|
||||
const eventType = getCaseInsensitiveValue(row, ['event_type', 'type']);
|
||||
const status = getCaseInsensitiveValue(row, ['status']);
|
||||
events.push({
|
||||
displayName: eventName,
|
||||
eventName,
|
||||
schemaName,
|
||||
eventType,
|
||||
status,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { events, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadSchemas = async (conn: any, dbName: string): Promise<{ schemas: string[]; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const querySpecs = buildSchemasMetadataQuerySpecs(dialect);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const schemas: string[] = [];
|
||||
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'schemaname']) || getFirstRowValue(row);
|
||||
if (!schemaName) return;
|
||||
const key = schemaName.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
schemas.push(schemaName);
|
||||
});
|
||||
});
|
||||
|
||||
return { schemas, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
export {
|
||||
buildDuckDBMacroDDL,
|
||||
buildEventsMetadataQuerySpecs,
|
||||
buildFunctionsMetadataQuerySpecs,
|
||||
buildQualifiedName,
|
||||
buildSchemasMetadataQuerySpecs,
|
||||
buildSidebarObjectKeyName,
|
||||
buildSidebarTableStatusSQL,
|
||||
buildTriggersMetadataQuerySpecs,
|
||||
buildViewsMetadataQuerySpecs,
|
||||
escapeSQLLiteral,
|
||||
extractSqlServerDefinitionRows,
|
||||
getCaseInsensitiveRawValue,
|
||||
getCaseInsensitiveValue,
|
||||
getFirstRowValue,
|
||||
getMetadataDialect,
|
||||
getMySQLShowTablesName,
|
||||
getSidebarTableDisplayName,
|
||||
isSphinxConnection,
|
||||
loadDatabaseEvents,
|
||||
loadDatabaseTriggers,
|
||||
loadFunctions,
|
||||
loadSchemas,
|
||||
loadStarRocksMaterializedViews,
|
||||
loadViews,
|
||||
normalizeMetadataQuerySpecs,
|
||||
parseDuckDBParameterNames,
|
||||
parseMetadataRowCount,
|
||||
quoteSqlServerIdentifier,
|
||||
shouldHideSchemaPrefix,
|
||||
splitQualifiedName,
|
||||
supportsDatabaseEvents,
|
||||
};
|
||||
Reference in New Issue
Block a user