feat(sidebar): 支持 Oracle 序列和存储包对象树

- 新增 Oracle/Dameng 序列与存储包元数据加载

- 同步对象树节点、V2 筛选、搜索、复制和拖拽支持

- 补充多语言文案和侧边栏回归测试
This commit is contained in:
Syngnat
2026-06-24 22:26:39 +08:00
parent 5725e78931
commit 322d0a7cb8
23 changed files with 465 additions and 25 deletions

View File

@@ -642,7 +642,7 @@ describe('Sidebar locate toolbar', () => {
const source = readSidebarSource();
const commandSearchRunSource = source.slice(
source.indexOf("if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view')"),
source.indexOf("if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine')"),
source.indexOf("if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' || node.type === 'sequence' || node.type === 'package')"),
);
expect(commandSearchRunSource).toContain("tabId: String(node.key || '')");
@@ -824,12 +824,16 @@ describe('Sidebar locate toolbar', () => {
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.all'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.tables'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.views'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.sequences'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.routines'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.packages'");
expect(objectKindSource).toContain("labelKey: 'sidebar.command_search.object_kind.events'");
expect(objectKindSource).not.toContain("label: '全部'");
expect(objectKindSource).not.toContain("label: '表'");
expect(objectKindSource).not.toContain("label: '视图'");
expect(objectKindSource).not.toContain("label: '序列'");
expect(objectKindSource).not.toContain("label: '函数'");
expect(objectKindSource).not.toContain("label: '存储包'");
expect(objectKindSource).not.toContain("label: '事件'");
expect(searchScopeSource).toContain("labelKey: 'sidebar.command_search.scope.smart'");
@@ -875,7 +879,9 @@ describe('Sidebar locate toolbar', () => {
'sidebar.command_search.object_kind.all',
'sidebar.command_search.object_kind.tables',
'sidebar.command_search.object_kind.views',
'sidebar.command_search.object_kind.sequences',
'sidebar.command_search.object_kind.routines',
'sidebar.command_search.object_kind.packages',
'sidebar.command_search.object_kind.events',
'sidebar.command_search.scope.smart',
'sidebar.command_search.scope.object',
@@ -913,7 +919,9 @@ describe('Sidebar locate toolbar', () => {
'sidebar.command_search.object_kind.all': 'All',
'sidebar.command_search.object_kind.tables': 'Tables',
'sidebar.command_search.object_kind.views': 'Views',
'sidebar.command_search.object_kind.sequences': 'Sequences',
'sidebar.command_search.object_kind.routines': 'Routines',
'sidebar.command_search.object_kind.packages': 'Packages',
'sidebar.command_search.object_kind.events': 'Events',
'table_overview.section.pinned': 'Pinned',
'table_overview.section.all': 'All',
@@ -927,8 +935,8 @@ describe('Sidebar locate toolbar', () => {
t('sidebar.search.scope.host', undefined, 'zh-CN'),
t('sidebar.search.scope.tag', undefined, 'zh-CN'),
]);
expect(buildV2ExplorerFilterOptions(translate).map((option) => option.label)).toEqual(['All', 'Tables', 'Views', 'Routines', 'Events']);
expect(V2_UTILS_EXPLORER_FILTER_OPTIONS.map((option) => option.label)).toEqual(['全部', '表', '视图', '函数', '事件']);
expect(buildV2ExplorerFilterOptions(translate).map((option) => option.label)).toEqual(['All', 'Tables', 'Views', 'Sequences', 'Routines', 'Packages', 'Events']);
expect(V2_UTILS_EXPLORER_FILTER_OPTIONS.map((option) => option.label)).toEqual(['全部', '表', '视图', '序列', '函数', '存储包', '事件']);
const tableNodes = [
{ title: 'orders', key: 'orders', type: 'table' as const, dataRef: { pinnedSidebarTable: true } },
@@ -994,6 +1002,13 @@ describe('Sidebar locate toolbar', () => {
expect(css).not.toContain('.gn-v2-active-connection-trigger:hover');
});
it('keeps v2 explorer filter tabs on a single line when Oracle object filters are present', () => {
const css = readV2ThemeCss();
expect(css).toMatch(/\.gn-v2-explorer-filter-tabs \{[^}]*flex-wrap: nowrap;[^}]*overflow-x: auto;[^}]*overflow-y: hidden;/s);
expect(css).toMatch(/\.gn-v2-explorer-filter-tabs button \{[^}]*flex: 0 0 auto;[^}]*white-space: nowrap;/s);
});
it('keeps v2 tree status dots circular while using virtual horizontal scroll for long labels', () => {
const css = readV2ThemeCss();
const source = readSidebarSource();
@@ -1370,6 +1385,13 @@ describe('Sidebar locate toolbar', () => {
dataRef: { groupKey: 'views' },
children: [{ title: 'v_users', key: 'v_users', type: 'view' as const }],
},
{
title: '序列',
key: 'conn-main-sequences',
type: 'object-group' as const,
dataRef: { groupKey: 'sequences' },
children: [{ title: 'seq_person_id', key: 'seq_person_id', type: 'sequence' as const }],
},
{
title: '函数',
key: 'conn-main-routines',
@@ -1377,6 +1399,13 @@ describe('Sidebar locate toolbar', () => {
dataRef: { groupKey: 'routines' },
children: [{ title: 'calc_total', key: 'calc_total', type: 'routine' as const }],
},
{
title: '存储包',
key: 'conn-main-packages',
type: 'object-group' as const,
dataRef: { groupKey: 'packages' },
children: [{ title: 'pkg_person', key: 'pkg_person', type: 'package' as const }],
},
{
title: '事件',
key: 'conn-main-events',
@@ -1391,12 +1420,16 @@ describe('Sidebar locate toolbar', () => {
'conn-main-queries',
'conn-main-tables',
'conn-main-views',
'conn-main-sequences',
'conn-main-routines',
'conn-main-packages',
'conn-main-events',
]);
expect(filterV2ExplorerTreeByKind(tree, 'tables')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-tables']);
expect(filterV2ExplorerTreeByKind(tree, 'views')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-views']);
expect(filterV2ExplorerTreeByKind(tree, 'sequences')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-sequences']);
expect(filterV2ExplorerTreeByKind(tree, 'routines')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-routines']);
expect(filterV2ExplorerTreeByKind(tree, 'packages')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-packages']);
expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-events']);
});
@@ -2609,7 +2642,9 @@ describe('Sidebar locate toolbar', () => {
const objectGroupTitleCases = [
['tables', 'sidebar.v2_table_group_menu.title', '表'],
['views', 'sidebar.object_group.views', '视图'],
['sequences', 'sidebar.object_group.sequences', '序列'],
['routines', 'sidebar.object_group.routines', '函数'],
['packages', 'sidebar.object_group.packages', '存储包'],
['triggers', 'sidebar.object_group.triggers', '触发器'],
['events', 'sidebar.object_group.events', '事件'],
['materializedViews', 'sidebar.object_group.materialized_views', '物化视图'],
@@ -2684,7 +2719,9 @@ describe('Sidebar locate toolbar', () => {
[
"if (groupKey === 'tables') return t('sidebar.v2_table_group_menu.title');",
"if (groupKey === 'views') return t('sidebar.object_group.views');",
"if (groupKey === 'sequences') return t('sidebar.object_group.sequences');",
"if (groupKey === 'routines') return t('sidebar.object_group.routines');",
"if (groupKey === 'packages') return t('sidebar.object_group.packages');",
"if (groupKey === 'triggers') return t('sidebar.object_group.triggers');",
"if (groupKey === 'events') return t('sidebar.object_group.events');",
"if (groupKey === 'materializedViews') return t('sidebar.object_group.materialized_views');",

View File

@@ -228,7 +228,9 @@ const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: strin
{ key: 'all', labelKey: 'sidebar.command_search.object_kind.all' },
{ key: 'tables', labelKey: 'sidebar.command_search.object_kind.tables' },
{ key: 'views', labelKey: 'sidebar.command_search.object_kind.views' },
{ key: 'sequences', labelKey: 'sidebar.command_search.object_kind.sequences' },
{ key: 'routines', labelKey: 'sidebar.command_search.object_kind.routines' },
{ key: 'packages', labelKey: 'sidebar.command_search.object_kind.packages' },
{ key: 'events', labelKey: 'sidebar.command_search.object_kind.events' },
];
@@ -1483,7 +1485,7 @@ const Sidebar: React.FC<{
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
} else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
} else if (type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
} else if (type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
} else if (type === 'saved-query') {
setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
@@ -1545,7 +1547,7 @@ const Sidebar: React.FC<{
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
} else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
} else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
} else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
} else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
else if (type === 'redis-db') setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` });
@@ -1633,6 +1635,8 @@ const Sidebar: React.FC<{
routineType
});
return;
} else if (node.type === 'sequence' || node.type === 'package') {
return;
} else if (node.type === 'jvm-mode') {
const { providerMode, id } = node.dataRef;
const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;

View File

@@ -1,13 +1,18 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = [
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8'),
].join('\n');
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
const requiredKeys = [
'sidebar.copy_object_name.label.table',
'sidebar.copy_object_name.label.view',
'sidebar.copy_object_name.label.materialized_view',
'sidebar.copy_object_name.label.sequence',
'sidebar.copy_object_name.label.package',
'sidebar.copy_object_name.label.event',
'sidebar.copy_object_name.empty',
'sidebar.copy_object_name.copied',
@@ -27,6 +32,8 @@ describe('Sidebar copy object name i18n', () => {
expect(source).not.toContain('`复制${label}失败: `');
expect(source).toContain("t('sidebar.copy_object_name.label.view')");
expect(source).toContain("t('sidebar.copy_object_name.label.materialized_view')");
expect(source).toContain("t('sidebar.copy_object_name.label.sequence')");
expect(source).toContain("t('sidebar.copy_object_name.label.package')");
expect(source).toContain("t('sidebar.copy_object_name.label.event')");
expect(source).toContain("t('sidebar.copy_object_name.label.table')");
expect(source).toContain("t('sidebar.copy_object_name.empty'");

View File

@@ -1,7 +1,10 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const source = [
readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('./sidebar/useSidebarTreeLoaders.tsx', import.meta.url), 'utf8'),
].join('\n');
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
const requiredKeys = [
@@ -9,7 +12,9 @@ const requiredKeys = [
'sidebar.object_group.tables',
'sidebar.object_group.views',
'sidebar.object_group.materialized_views',
'sidebar.object_group.sequences',
'sidebar.object_group.routines',
'sidebar.object_group.packages',
'sidebar.object_group.triggers',
'sidebar.object_group.events',
];
@@ -21,13 +26,17 @@ describe('Sidebar object group i18n', () => {
"buildObjectGroup(schemaNodeKey, 'tables', '表'",
"buildObjectGroup(schemaNodeKey, 'views', '视图'",
"buildObjectGroup(schemaNodeKey, 'materializedViews', '物化视图'",
"buildObjectGroup(schemaNodeKey, 'sequences', '序列'",
"buildObjectGroup(schemaNodeKey, 'routines', '函数'",
"buildObjectGroup(schemaNodeKey, 'packages', '存储包'",
"buildObjectGroup(schemaNodeKey, 'triggers', '触发器'",
"buildObjectGroup(schemaNodeKey, 'events', '事件'",
"buildObjectGroup(key as string, 'tables', '表'",
"buildObjectGroup(key as string, 'views', '视图'",
"buildObjectGroup(key as string, 'materializedViews', '物化视图'",
"buildObjectGroup(key as string, 'sequences', '序列'",
"buildObjectGroup(key as string, 'routines', '函数'",
"buildObjectGroup(key as string, 'packages', '存储包'",
"buildObjectGroup(key as string, 'triggers', '触发器'",
"buildObjectGroup(key as string, 'events', '事件'",
].forEach((snippet) => {

View File

@@ -55,9 +55,11 @@ export const renderSidebarV2TreeTitle = ({
const isMono = node.type === 'table'
|| node.type === 'view'
|| node.type === 'materialized-view'
|| node.type === 'sequence'
|| node.type === 'db-trigger'
|| node.type === 'db-event'
|| node.type === 'routine'
|| node.type === 'package'
|| node.type === 'saved-query'
|| node.type === 'external-sql-file';
const titleClassName = [

View File

@@ -18,7 +18,7 @@ export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-conn
// === 共享类型 ===
/** V2 资源管理器过滤维度 */
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events';
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'sequences' | 'routines' | 'packages' | 'events';
// === 纯函数 ===
@@ -94,9 +94,11 @@ export const isV2SidebarObjectNode = (
return node?.type === 'table'
|| node?.type === 'view'
|| node?.type === 'materialized-view'
|| node?.type === 'sequence'
|| node?.type === 'db-trigger'
|| node?.type === 'db-event'
|| node?.type === 'routine';
|| node?.type === 'routine'
|| node?.type === 'package';
};
// === 第二期:依赖 i18n 但不依赖 TreeNode 内部类型的工具函数 ===
@@ -125,7 +127,9 @@ export const resolveV2ObjectGroupTitle = (
const groupKey = String(node?.dataRef?.groupKey || '');
if (groupKey === 'tables') return t('sidebar.v2_table_group_menu.title');
if (groupKey === 'views') return t('sidebar.object_group.views');
if (groupKey === 'sequences') return t('sidebar.object_group.sequences');
if (groupKey === 'routines') return t('sidebar.object_group.routines');
if (groupKey === 'packages') return t('sidebar.object_group.packages');
if (groupKey === 'triggers') return t('sidebar.object_group.triggers');
if (groupKey === 'events') return t('sidebar.object_group.events');
if (groupKey === 'materializedViews') return t('sidebar.object_group.materialized_views');
@@ -139,7 +143,7 @@ export const resolveV2ObjectGroupTitle = (
export const resolveSidebarTableNameForCopy = (
node: Pick<SidebarNodeLike, 'title' | 'dataRef'> | null | undefined,
): string => {
return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || node?.title || '').trim();
return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || node?.title || '').trim();
};
// === 命令搜索相关类型与解析V2 Command Search===

View File

@@ -5,7 +5,14 @@ vi.mock("../../../wailsjs/go/app/App", () => ({
}));
import { DBQuery } from "../../../wailsjs/go/app/App";
import { buildSchemasMetadataQuerySpecs, loadViews } from "./sidebarMetadataLoaders";
import {
buildPackagesMetadataQuerySpecs,
buildSchemasMetadataQuerySpecs,
buildSequencesMetadataQuerySpecs,
loadPackages,
loadSequences,
loadViews,
} from "./sidebarMetadataLoaders";
const mockedDBQuery = vi.mocked(DBQuery);
@@ -67,3 +74,61 @@ describe("buildSchemasMetadataQuerySpecs", () => {
]);
});
});
describe("Oracle object metadata loaders", () => {
it("builds owner-scoped sequence and package queries for Oracle", () => {
expect(buildSequencesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([
"SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = 'MYCIMLED' ORDER BY SEQUENCE_NAME",
]);
expect(buildPackagesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([
"SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = 'MYCIMLED' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME",
]);
});
it("loads and deduplicates Oracle sequences and packages", async () => {
mockedDBQuery.mockImplementation(async (_config: unknown, _dbName: string, sql: string) => {
if (sql.includes("ALL_SEQUENCES")) {
return {
success: true,
message: "",
data: [
{ schema_name: "MYCIMLED", sequence_name: "SEQ_PERSON_ID" },
{ SEQUENCE_OWNER: "MYCIMLED", SEQUENCE_NAME: "SEQ_PERSON_ID" },
],
};
}
if (sql.includes("ALL_OBJECTS") && sql.includes("PACKAGE")) {
return {
success: true,
message: "",
data: [
{ schema_name: "MYCIMLED", package_name: "PKG_PERSON" },
{ OWNER: "MYCIMLED", OBJECT_NAME: "PKG_PERSON" },
],
};
}
return { success: false, message: "", data: [] };
});
await expect(loadSequences({ config: { type: "oracle" } }, "MYCIMLED")).resolves.toEqual({
supported: true,
sequences: [
{
displayName: "MYCIMLED.SEQ_PERSON_ID",
schemaName: "MYCIMLED",
sequenceName: "MYCIMLED.SEQ_PERSON_ID",
},
],
});
await expect(loadPackages({ config: { type: "oracle" } }, "MYCIMLED")).resolves.toEqual({
supported: true,
packages: [
{
displayName: "MYCIMLED.PKG_PERSON",
packageName: "MYCIMLED.PKG_PERSON",
schemaName: "MYCIMLED",
},
],
});
});
});

View File

@@ -604,6 +604,46 @@ const buildFunctionsMetadataQuerySpecs = (
}
};
const buildSequencesMetadataQuerySpecs = (
dialect: string,
dbName: string,
): MetadataQuerySpec[] => {
const safeDbName = escapeSQLLiteral(dbName);
switch (dialect) {
case "oracle":
case "dm":
return normalizeMetadataQuerySpecs([
{
sql: safeDbName
? `SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${safeDbName.toUpperCase()}' ORDER BY SEQUENCE_NAME`
: `SELECT SEQUENCE_NAME AS sequence_name FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME`,
},
]);
default:
return [];
}
};
const buildPackagesMetadataQuerySpecs = (
dialect: string,
dbName: string,
): MetadataQuerySpec[] => {
const safeDbName = escapeSQLLiteral(dbName);
switch (dialect) {
case "oracle":
case "dm":
return normalizeMetadataQuerySpecs([
{
sql: safeDbName
? `SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME`
: `SELECT OBJECT_NAME AS package_name FROM USER_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME`,
},
]);
default:
return [];
}
};
const buildEventsMetadataQuerySpecs = (
dialect: string,
dbName: string,
@@ -977,6 +1017,127 @@ const loadFunctions = async (
return { routines, supported: hasSuccessfulQuery };
};
const loadSequences = async (
conn: any,
dbName: string,
): Promise<{
sequences: Array<{
displayName: string;
sequenceName: string;
schemaName: string;
}>;
supported: boolean;
}> => {
const dialect = getMetadataDialect(conn as SavedConnection);
const querySpecs = buildSequencesMetadataQuerySpecs(dialect, dbName);
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(
conn,
dbName,
querySpecs,
);
const seen = new Set<string>();
const sequences: Array<{
displayName: string;
sequenceName: string;
schemaName: string;
}> = [];
results.forEach((queryResult) => {
queryResult.rows.forEach((row) => {
const rawSequenceName =
getCaseInsensitiveValue(row, [
"sequence_name",
"sequencename",
"object_name",
"name",
]) || getFirstRowValue(row);
if (!rawSequenceName) return;
const sequenceParts = splitQualifiedName(rawSequenceName);
const schemaName = (
getCaseInsensitiveValue(row, [
"schema_name",
"sequence_owner",
"owner",
]) ||
sequenceParts.schemaName ||
""
).trim();
const objectName = (sequenceParts.objectName || rawSequenceName).trim();
const fullName = buildQualifiedName(schemaName, objectName);
const uniqueKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}`;
if (!fullName || seen.has(uniqueKey)) return;
seen.add(uniqueKey);
sequences.push({
displayName: fullName,
sequenceName: fullName,
schemaName,
});
});
});
return { sequences, supported: hasSuccessfulQuery };
};
const loadPackages = async (
conn: any,
dbName: string,
): Promise<{
packages: Array<{
displayName: string;
packageName: string;
schemaName: string;
}>;
supported: boolean;
}> => {
const dialect = getMetadataDialect(conn as SavedConnection);
const querySpecs = buildPackagesMetadataQuerySpecs(dialect, dbName);
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(
conn,
dbName,
querySpecs,
);
const seen = new Set<string>();
const packages: Array<{
displayName: string;
packageName: string;
schemaName: string;
}> = [];
results.forEach((queryResult) => {
queryResult.rows.forEach((row) => {
const rawPackageName =
getCaseInsensitiveValue(row, [
"package_name",
"packagename",
"object_name",
"name",
]) || getFirstRowValue(row);
if (!rawPackageName) return;
const packageParts = splitQualifiedName(rawPackageName);
const schemaName = (
getCaseInsensitiveValue(row, [
"schema_name",
"owner",
]) ||
packageParts.schemaName ||
""
).trim();
const objectName = (packageParts.objectName || rawPackageName).trim();
const fullName = buildQualifiedName(schemaName, objectName);
const uniqueKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}`;
if (!fullName || seen.has(uniqueKey)) return;
seen.add(uniqueKey);
packages.push({
displayName: fullName,
packageName: fullName,
schemaName,
});
});
});
return { packages, supported: hasSuccessfulQuery };
};
const loadDatabaseEvents = async (
conn: any,
dbName: string,
@@ -1084,8 +1245,10 @@ export {
buildDuckDBMacroDDL,
buildEventsMetadataQuerySpecs,
buildFunctionsMetadataQuerySpecs,
buildPackagesMetadataQuerySpecs,
buildQualifiedName,
buildSchemasMetadataQuerySpecs,
buildSequencesMetadataQuerySpecs,
buildSidebarObjectKeyName,
buildSidebarTableStatusSQL,
buildTriggersMetadataQuerySpecs,
@@ -1102,7 +1265,9 @@ export {
loadDatabaseEvents,
loadDatabaseTriggers,
loadFunctions,
loadPackages,
loadSchemas,
loadSequences,
loadStarRocksMaterializedViews,
loadViews,
normalizeMetadataQuerySpecs,

View File

@@ -105,12 +105,14 @@ export const useSidebarCommandSearchRunner = ({
onDoubleClick(null, node);
return;
}
if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine') {
if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine' || node.type === 'sequence' || node.type === 'package') {
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
setSelectedKeys([node.key]);
selectedNodesRef.current = [node];
scrollSidebarTreeToKey(node.key);
onDoubleClick(null, node);
if (node.type !== 'sequence' && node.type !== 'package') {
onDoubleClick(null, node);
}
}
}, [
activeContext,

View File

@@ -108,6 +108,8 @@ type UseSidebarObjectActionsArgs = {
const resolveCopyObjectNameLabel = (node: any): string => {
if (node?.type === 'view') return t('sidebar.copy_object_name.label.view');
if (node?.type === 'materialized-view') return t('sidebar.copy_object_name.label.materialized_view');
if (node?.type === 'sequence') return t('sidebar.copy_object_name.label.sequence');
if (node?.type === 'package') return t('sidebar.copy_object_name.label.package');
if (node?.type === 'db-event') return t('sidebar.copy_object_name.label.event');
return t('sidebar.copy_object_name.label.table');
};

View File

@@ -9,6 +9,7 @@ import {
DatabaseOutlined,
EyeOutlined,
FilterOutlined,
KeyOutlined,
PlusOutlined,
RobotOutlined,
TableOutlined,
@@ -398,12 +399,14 @@ export const useSidebarSearchModel = ({
node.type === 'table'
|| node.type === 'view'
|| node.type === 'materialized-view'
|| node.type === 'sequence'
|| node.type === 'db-trigger'
|| node.type === 'db-event'
|| node.type === 'routine'
|| node.type === 'package'
) {
const conn = connectionById.get(String(dataRef.id || ''));
const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || node.title || '').trim();
const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.sequenceName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || dataRef.packageName || node.title || '').trim();
const displayName = String(node.title || extractObjectName(objectName) || objectName).trim();
result.push({
key: `node-${node.key}`,
@@ -412,7 +415,9 @@ export const useSidebarSearchModel = ({
meta: [conn?.name || dataRef.id, dataRef.dbName].filter(Boolean).join(' · '),
icon: node.type === 'table'
? <TableOutlined />
: (node.type === 'db-event' ? <ClockCircleOutlined /> : (node.type === 'routine' ? <CodeOutlined /> : <EyeOutlined />)),
: (node.type === 'sequence'
? <KeyOutlined />
: (node.type === 'db-event' ? <ClockCircleOutlined /> : ((node.type === 'routine' || node.type === 'package') ? <CodeOutlined /> : <EyeOutlined />))),
node,
});
}

View File

@@ -51,8 +51,8 @@ export const useSidebarTitleRender = ({
const displayTitle = String(node.title ?? '');
const dragText = resolveSidebarObjectDragText(node);
let hoverTitle = displayTitle;
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'db-event') {
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || '').trim();
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'sequence' || node.type === 'package' || node.type === 'db-event') {
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || '').trim();
const conn = node?.dataRef as SavedConnection | undefined;
if (rawTableName && shouldHideSchemaPrefix(conn)) {
if (splitQualifiedName(rawTableName).schemaName) {

View File

@@ -10,6 +10,7 @@ import {
FolderOpenOutlined,
FunctionOutlined,
HddOutlined,
KeyOutlined,
TableOutlined,
ThunderboltOutlined,
} from '@ant-design/icons';
@@ -32,7 +33,9 @@ import {
loadDatabaseEvents,
loadDatabaseTriggers,
loadFunctions,
loadPackages,
loadSchemas,
loadSequences,
loadStarRocksMaterializedViews,
loadViews,
parseMetadataRowCount,
@@ -505,18 +508,22 @@ export const useSidebarTreeLoaders = ({
};
});
const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult, eventsResult] = await Promise.all([
const [schemasResult, viewsResult, materializedViewsResult, triggersResult, routinesResult, sequencesResult, packagesResult, eventsResult] = await Promise.all([
loadSchemas(conn, conn.dbName),
loadViews(conn, conn.dbName),
loadStarRocksMaterializedViews(conn, conn.dbName),
loadDatabaseTriggers(conn, conn.dbName),
loadFunctions(conn, conn.dbName),
loadSequences(conn, conn.dbName),
loadPackages(conn, conn.dbName),
loadDatabaseEvents(conn, conn.dbName),
]);
const viewRows: SidebarViewMetadataEntry[] = Array.isArray(viewsResult.views) ? viewsResult.views : [];
const materializedViewRows: SidebarViewMetadataEntry[] = Array.isArray(materializedViewsResult.views) ? materializedViewsResult.views : [];
const triggerRows: any[] = Array.isArray(triggersResult.triggers) ? triggersResult.triggers : [];
const routineRows: any[] = Array.isArray(routinesResult.routines) ? routinesResult.routines : [];
const sequenceRows: any[] = Array.isArray(sequencesResult.sequences) ? sequencesResult.sequences : [];
const packageRows: any[] = Array.isArray(packagesResult.packages) ? packagesResult.packages : [];
const eventRows: any[] = Array.isArray(eventsResult.events) ? eventsResult.events : [];
const schemaRows: string[] = Array.isArray(schemasResult.schemas) ? schemasResult.schemas : [];
@@ -578,6 +585,24 @@ export const useSidebarTreeLoaders = ({
};
});
const sequenceEntries = sequenceRows.map((sequence: any) => {
const parsed = splitQualifiedName(sequence.sequenceName);
return {
...sequence,
schemaName: sequence.schemaName || parsed.schemaName,
displayName: parsed.objectName || sequence.sequenceName,
};
});
const packageEntries = packageRows.map((packageEntry: any) => {
const parsed = splitQualifiedName(packageEntry.packageName);
return {
...packageEntry,
schemaName: packageEntry.schemaName || parsed.schemaName,
displayName: parsed.objectName || packageEntry.packageName,
};
});
const eventEntries = eventRows.map((event: any) => ({
...event,
schemaName: String(event.schemaName || conn.dbName || '').trim(),
@@ -627,6 +652,10 @@ export const useSidebarTreeLoaders = ({
// Sort routines by display name (case-insensitive)
routineEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
sequenceEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
packageEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
eventEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string; rowCount?: number }): TreeNode => {
@@ -695,6 +724,30 @@ export const useSidebarTreeLoaders = ({
isLeaf: true,
});
const buildSequenceNode = (entry: { sequenceName: string; schemaName: string; displayName: string }): TreeNode => {
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.sequenceName);
return {
title: entry.displayName,
key: `${conn.id}-${conn.dbName}-sequence-${keyName}`,
icon: <KeyOutlined />,
type: 'sequence',
dataRef: { ...conn, sequenceName: entry.sequenceName, schemaName: entry.schemaName },
isLeaf: true,
};
};
const buildPackageNode = (entry: { packageName: string; schemaName: string; displayName: string }): TreeNode => {
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.packageName);
return {
title: entry.displayName,
key: `${conn.id}-${conn.dbName}-package-${keyName}`,
icon: <CodeOutlined />,
type: 'package',
dataRef: { ...conn, packageName: entry.packageName, schemaName: entry.schemaName },
isLeaf: true,
};
};
const buildEventNode = (entry: { eventName: string; schemaName: string; displayName: string; eventType?: string; status?: string }): TreeNode => ({
title: entry.displayName,
key: `${conn.id}-${conn.dbName}-event-${entry.schemaName}-${entry.eventName}`,
@@ -735,6 +788,8 @@ export const useSidebarTreeLoaders = ({
views: TreeNode[];
materializedViews: TreeNode[];
routines: TreeNode[];
sequences: TreeNode[];
packages: TreeNode[];
triggers: TreeNode[];
events: TreeNode[];
};
@@ -751,6 +806,8 @@ export const useSidebarTreeLoaders = ({
views: [],
materializedViews: [],
routines: [],
sequences: [],
packages: [],
triggers: [],
events: [],
};
@@ -764,12 +821,15 @@ export const useSidebarTreeLoaders = ({
viewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).views.push(buildViewNode(entry)));
materializedViewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).materializedViews.push(buildMaterializedViewNode(entry)));
routineEntries.forEach((entry) => getSchemaBucket(entry.schemaName).routines.push(buildRoutineNode(entry)));
sequenceEntries.forEach((entry) => getSchemaBucket(entry.schemaName).sequences.push(buildSequenceNode(entry)));
packageEntries.forEach((entry) => getSchemaBucket(entry.schemaName).packages.push(buildPackageNode(entry)));
triggerEntries.forEach((entry) => getSchemaBucket(entry.schemaName).triggers.push(buildTriggerNode(entry)));
eventEntries.forEach((entry) => getSchemaBucket(entry.schemaName).events.push(buildEventNode(entry)));
const dialect = getMetadataDialect(conn as SavedConnection);
const isOracleLike = (dialect === 'oracle' || dialect === 'dm');
const includeMaterializedViews = dialect === 'starrocks';
const includeOracleObjects = isOracleLike;
const includeEvents = supportsDatabaseEvents(conn as SavedConnection);
const schemaNodes: TreeNode[] = Array.from(schemaMap.values())
@@ -787,7 +847,9 @@ export const useSidebarTreeLoaders = ({
buildObjectGroup(schemaNodeKey, 'tables', t('sidebar.object_group.tables'), <TableOutlined />, bucket.tables, { schemaName: bucket.schemaName }),
buildObjectGroup(schemaNodeKey, 'views', t('sidebar.object_group.views'), <EyeOutlined />, bucket.views, { schemaName: bucket.schemaName }),
...(includeMaterializedViews ? [buildObjectGroup(schemaNodeKey, 'materializedViews', t('sidebar.object_group.materialized_views'), <ThunderboltOutlined />, bucket.materializedViews, { schemaName: bucket.schemaName })] : []),
...(includeOracleObjects ? [buildObjectGroup(schemaNodeKey, 'sequences', t('sidebar.object_group.sequences'), <KeyOutlined />, bucket.sequences, { schemaName: bucket.schemaName })] : []),
buildObjectGroup(schemaNodeKey, 'routines', t('sidebar.object_group.routines'), <CodeOutlined />, bucket.routines, { schemaName: bucket.schemaName }),
...(includeOracleObjects ? [buildObjectGroup(schemaNodeKey, 'packages', t('sidebar.object_group.packages'), <CodeOutlined />, bucket.packages, { schemaName: bucket.schemaName })] : []),
buildObjectGroup(schemaNodeKey, 'triggers', t('sidebar.object_group.triggers'), <FunctionOutlined />, bucket.triggers, { schemaName: bucket.schemaName }),
...(includeEvents ? [buildObjectGroup(schemaNodeKey, 'events', t('sidebar.object_group.events'), <ClockCircleOutlined />, bucket.events, { schemaName: bucket.schemaName })] : []),
];
@@ -805,13 +867,17 @@ export const useSidebarTreeLoaders = ({
replaceTreeNodeChildren(key, [queriesNode, ...schemaNodes]);
} else {
const includeMaterializedViews = getMetadataDialect(conn as SavedConnection) === 'starrocks';
const dialect = getMetadataDialect(conn as SavedConnection);
const includeMaterializedViews = dialect === 'starrocks';
const includeOracleObjects = dialect === 'oracle' || dialect === 'dm';
const includeEvents = supportsDatabaseEvents(conn as SavedConnection);
const groupedNodes: TreeNode[] = [
buildObjectGroup(key as string, 'tables', t('sidebar.object_group.tables'), <TableOutlined />, sortedTableEntries.map(buildTableNode)),
buildObjectGroup(key as string, 'views', t('sidebar.object_group.views'), <EyeOutlined />, viewEntries.map(buildViewNode)),
...(includeMaterializedViews ? [buildObjectGroup(key as string, 'materializedViews', t('sidebar.object_group.materialized_views'), <ThunderboltOutlined />, materializedViewEntries.map(buildMaterializedViewNode))] : []),
...(includeOracleObjects ? [buildObjectGroup(key as string, 'sequences', t('sidebar.object_group.sequences'), <KeyOutlined />, sequenceEntries.map(buildSequenceNode))] : []),
buildObjectGroup(key as string, 'routines', t('sidebar.object_group.routines'), <CodeOutlined />, routineEntries.map(buildRoutineNode)),
...(includeOracleObjects ? [buildObjectGroup(key as string, 'packages', t('sidebar.object_group.packages'), <CodeOutlined />, packageEntries.map(buildPackageNode))] : []),
buildObjectGroup(key as string, 'triggers', t('sidebar.object_group.triggers'), <FunctionOutlined />, triggerEntries.map(buildTriggerNode)),
...(includeEvents ? [buildObjectGroup(key as string, 'events', t('sidebar.object_group.events'), <ClockCircleOutlined />, eventEntries.map(buildEventNode))] : []),
];

View File

@@ -59,5 +59,15 @@ describe('sidebarCoreUtils', () => {
title: 'trg_users_audit',
dataRef: {},
})).toBe('trg_users_audit');
expect(resolveSidebarObjectDragText({
type: 'sequence',
title: 'SEQ_PERSON_ID',
dataRef: { sequenceName: 'MYCIMLED.SEQ_PERSON_ID' },
})).toBe('MYCIMLED.SEQ_PERSON_ID');
expect(resolveSidebarObjectDragText({
type: 'package',
title: 'PKG_PERSON',
dataRef: { packageName: 'MYCIMLED.PKG_PERSON' },
})).toBe('MYCIMLED.PKG_PERSON');
});
});

View File

@@ -58,9 +58,11 @@ export const isV2SidebarObjectNode = (node: Pick<SidebarObjectNodeLike, 'type'>
return node?.type === 'table'
|| node?.type === 'view'
|| node?.type === 'materialized-view'
|| node?.type === 'sequence'
|| node?.type === 'db-trigger'
|| node?.type === 'db-event'
|| node?.type === 'routine';
|| node?.type === 'routine'
|| node?.type === 'package';
};
export const resolveSidebarObjectDragText = (
@@ -69,8 +71,10 @@ export const resolveSidebarObjectDragText = (
const dataRef = node?.dataRef || {};
if (node?.type === 'table') return String(dataRef.tableName || node?.title || '').trim();
if (node?.type === 'view' || node?.type === 'materialized-view') return String(dataRef.viewName || dataRef.tableName || node?.title || '').trim();
if (node?.type === 'sequence') return String(dataRef.sequenceName || node?.title || '').trim();
if (node?.type === 'db-trigger') return String(dataRef.triggerName || node?.title || '').trim();
if (node?.type === 'routine') return String(dataRef.routineName || node?.title || '').trim();
if (node?.type === 'package') return String(dataRef.packageName || node?.title || '').trim();
if (node?.type === 'db-event') return String(dataRef.eventName || node?.title || '').trim();
return '';
};

View File

@@ -24,6 +24,8 @@ export type SidebarTreeNodeType =
| 'db-trigger'
| 'db-event'
| 'routine'
| 'sequence'
| 'package'
| 'object-group'
| 'v2-table-section'
| 'queries-folder'
@@ -76,7 +78,7 @@ export const shouldLoadSidebarNodeOnExpand = (
export const resolveSidebarTableNameForCopy = (
node: Pick<SidebarTreeNode, 'title' | 'dataRef'> | null | undefined,
): string => {
return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || node?.title || '').trim();
return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || node?.title || '').trim();
};
type SidebarTableSortPreference = 'name' | 'frequency';
@@ -294,7 +296,7 @@ export const getV2RailConnectionGroupBadgeText = (name: unknown, fallback = t('c
return trimmed.slice(0, 2);
};
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events';
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'sequences' | 'routines' | 'packages' | 'events';
export const buildV2ExplorerFilterOptions = (
translate: SidebarV2Translate = translateSidebarV2Current,
@@ -302,7 +304,9 @@ export const buildV2ExplorerFilterOptions = (
{ key: 'all', label: translate('sidebar.command_search.object_kind.all') },
{ key: 'tables', label: translate('sidebar.command_search.object_kind.tables') },
{ key: 'views', label: translate('sidebar.command_search.object_kind.views') },
{ key: 'sequences', label: translate('sidebar.command_search.object_kind.sequences') },
{ key: 'routines', label: translate('sidebar.command_search.object_kind.routines') },
{ key: 'packages', label: translate('sidebar.command_search.object_kind.packages') },
{ key: 'events', label: translate('sidebar.command_search.object_kind.events') },
];
@@ -311,7 +315,9 @@ export const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; label: s
const V2_EXPLORER_FILTER_GROUP_KEYS: Record<Exclude<V2ExplorerFilter, 'all'>, string[]> = {
tables: ['tables'],
views: ['views', 'materializedViews'],
sequences: ['sequences'],
routines: ['routines'],
packages: ['packages'],
events: ['events'],
};
@@ -365,7 +371,9 @@ export const filterV2ExplorerTreeByKind = (
const objectTypeMatches = (node: SidebarTreeNode): boolean => {
if (filter === 'tables') return node.type === 'table';
if (filter === 'views') return node.type === 'view' || node.type === 'materialized-view';
if (filter === 'sequences') return node.type === 'sequence';
if (filter === 'routines') return node.type === 'routine';
if (filter === 'packages') return node.type === 'package';
if (filter === 'events') return node.type === 'db-event';
return false;
};
@@ -483,7 +491,9 @@ export const parseV2CommandSearchQuery = (value: unknown): V2CommandSearchQuery
const isV2CommandSearchObjectNode = (node: SidebarTreeNode): boolean => {
return node.type === 'table'
|| node.type === 'view'
|| node.type === 'materialized-view';
|| node.type === 'materialized-view'
|| node.type === 'sequence'
|| node.type === 'package';
};
export const V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT = 24;
@@ -499,7 +509,7 @@ export const buildV2CommandSearchTreeIndex = (
const dataRef = item.node.dataRef || {};
const normalizedTitle = String(item.title || '').toLowerCase();
const normalizedPrimaryObjectText = String(
dataRef.tableName || dataRef.viewName || item.title || '',
dataRef.tableName || dataRef.viewName || dataRef.sequenceName || dataRef.packageName || item.title || '',
).toLowerCase();
return [{
@@ -509,6 +519,8 @@ export const buildV2CommandSearchTreeIndex = (
item.meta,
dataRef.tableName,
dataRef.viewName,
dataRef.sequenceName,
dataRef.packageName,
dataRef.dbName,
dataRef.name,
dataRef.config?.host,