From 711d0ec57b85ae4b4ccf2561ef78de5601bb51dd Mon Sep 17 00:00:00 2001 From: LRcoding <38545827+LwClick@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:53:22 +0800 Subject: [PATCH] test(editor): cover context menu routing payloads --- .../agent/dameng/DamengAgentMetadataTest.java | 16 ++ apps/desktop/src/App.vue | 87 +++++++-- .../src/components/editor/QueryEditor.vue | 115 +++++++----- .../src/components/layout/ContentArea.vue | 32 ++-- .../metadata/completionTreeIndex.spec.ts | 25 ++- .../lib/__tests__/sql/sqlNavigation.spec.ts | 30 +++- .../src/lib/metadata/completionTreeIndex.ts | 2 +- .../src/lib/sql/queryCursorTableTarget.ts | 170 ++++++++---------- apps/desktop/src/lib/sql/sqlCompletion.ts | 3 +- apps/desktop/src/lib/sql/sqlNavigation.ts | 64 +++++-- apps/desktop/src/stores/connectionStore.ts | 30 ++-- .../app-tests/queryCursorTableTarget.test.ts | 81 ++++++++- .../queryEditorContextObjectRouting.test.ts | 26 +++ 13 files changed, 484 insertions(+), 197 deletions(-) create mode 100644 packages/app-tests/queryEditorContextObjectRouting.test.ts diff --git a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java index ebf109daa..3882bc1b1 100644 --- a/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java +++ b/agents/drivers/dameng/src/test/java/com/dbx/agent/dameng/DamengAgentMetadataTest.java @@ -159,6 +159,22 @@ class DamengAgentMetadataTest { Assertions.assertTrue(source.getSource().contains("CREATE MATERIALIZED VIEW"), source.getSource()); } + @Test + void readsViewSourceWithDbmsMetadataType() { + DamengAgent agent = new DamengAgent(); + List params = new ArrayList<>(); + TestSupport.setPrivateConnection( + agent, + objectSourceConnection(params, "CREATE VIEW \"APP\".\"V_PROCESSPLAN\" AS SELECT 1 AS ID FROM DUAL") + ); + + ObjectSource source = agent.getObjectSource("APP", "V_PROCESSPLAN", "VIEW"); + + Assertions.assertEquals(List.of("VIEW", "V_PROCESSPLAN", "APP"), params); + Assertions.assertEquals("VIEW", source.getObject_type()); + Assertions.assertTrue(source.getSource().contains("CREATE VIEW"), source.getSource()); + } + @Test void triggerMetadataDoesNotRequireOracleTriggerTypeColumn() { DamengAgent agent = new DamengAgent(); diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index c9d7ef80b..4c96f331e 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -39,7 +39,7 @@ import { connectionRedactedNameLabel } from "@/lib/connection/connectionPresenta import { quickConnectionOpenTarget } from "@/lib/connection/connectionOpenTarget"; import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase"; import { findTreeNodeById, resolveNewQueryTarget, resolveNewQueryInitialSql } from "@/lib/sql/newQueryContext"; -import { sqlObjectNavigationTableType, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation"; +import { sqlObjectNavigationSourceKind, sqlObjectNavigationTableType, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation"; import { buildExecutableObjectSourceStatements, executeObjectSourceSave } from "@/lib/table/objectSourceEditor"; import { resolveExecutableSql, resolveExecutableSqlWithBackend, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget"; import { uuid } from "@/lib/common/utils"; @@ -86,6 +86,7 @@ import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory"; import { initSavedSqlEditorPositions } from "@/lib/app/savedSqlEditorPosition"; import { isSchemaAware, isSingleDatabase, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; @@ -104,6 +105,7 @@ const UpdateDialog = defineAsyncComponent(() => import("@/components/layout/Upda const CloseActionPromptDialog = defineAsyncComponent(() => import("@/components/layout/CloseActionPromptDialog.vue")); const LoginPage = defineAsyncComponent(() => import("@/components/auth/LoginPage.vue")); const QuickOpenDialog = defineAsyncComponent(() => import("@/components/quick-open/QuickOpenDialog.vue")); +const QueryEditorObjectSourceDialog = defineAsyncComponent(() => import("@/components/objects/ObjectSourceDialog.vue")); type AiAssistantHandle = { triggerAction: (action: AiAction, instruction?: string) => void; @@ -135,6 +137,7 @@ const settingsPageTabOpen = ref(false); const settingsInitialTab = ref("appearance"); const settingsInitialSection = ref(undefined); const showQueryEditorDdlDialog = ref(false); +const showQueryEditorObjectSourceDialog = ref(false); const driverStoreTabOpen = ref(false); const driverStoreActive = ref(false); const driverStoreActiveTab = ref<"agent" | "jdbc" | "storage" | "runtime">("agent"); @@ -160,6 +163,7 @@ const formatSqlRequest = ref<{ id: number; tabId: string } | null>(null); const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); const newQueryContextSource = ref<"tab" | "sidebar">("tab"); const queryEditorDdlTarget = ref<{ connectionId: string; database: string; schema?: string; tableName: string; objectType?: ObjectSourceKind } | null>(null); +const queryEditorObjectSourceTarget = ref<{ connectionId: string; database: string; schema?: string; name: string; objectType: ObjectSourceKind; initialEditing: boolean } | null>(null); const showSaveSqlDialog = ref(false); const saveSqlName = ref(""); const saveSqlFolderId = ref(""); @@ -352,6 +356,12 @@ const queryEditorDdlDatabaseType = computed(() => { const queryEditorDdlDialect = computed(() => { return codeMirrorSqlDialect(queryEditorDdlDatabaseType.value); }); +const queryEditorObjectSourceDatabaseType = computed(() => { + if (!queryEditorObjectSourceTarget.value?.connectionId) return undefined; + return effectiveDatabaseTypeForConnection(connectionStore.getConfig(queryEditorObjectSourceTarget.value.connectionId)); +}); +const queryEditorObjectSourceDialect = computed(() => codeMirrorSqlDialect(queryEditorObjectSourceDatabaseType.value)); +const queryEditorObjectSourceFormatDialect = computed(() => sqlFormatDialectForDbType(queryEditorObjectSourceDatabaseType.value)); const connectionStats = computed(() => ({ total: connectionStore.connections.length, connected: connectionStore.connectedIds.size, @@ -1204,9 +1214,20 @@ function tableTargetFromActiveTab(table: string | SqlObjectNavigationTarget) { const tab = activeTab.value; if (!tab) return null; const connectionId = tab.connectionId; + if (typeof table !== "string") { + // Structured targets already separate qualifiers; reparsing would corrupt quoted object names that contain dots. + return { + connectionId, + database: table.database || tab.database, + schema: table.schema || tab.schema, + tableName: table.name, + tableType: table.type ? sqlObjectNavigationTableType(table) : undefined, + }; + } + let database = tab.database; - let schema = typeof table === "string" ? tab.schema : table.schema || tab.schema; - const tableName = typeof table === "string" ? table : table.name; + let schema = tab.schema; + const tableName = table; const parts = tableName.split(".").filter(Boolean); const rawTableName = parts[parts.length - 1] || tableName; @@ -1223,15 +1244,16 @@ function tableTargetFromActiveTab(table: string | SqlObjectNavigationTarget) { } } - return { connectionId, database, schema, tableName: rawTableName, tableType: typeof table === "string" ? undefined : sqlObjectNavigationTableType(table) }; + return { connectionId, database, schema, tableName: rawTableName, tableType: undefined }; } async function onClickTable(table: SqlObjectNavigationTarget) { const target = tableTargetFromActiveTab(table); if (!target) return; - if (table.type === "view") { + const objectType = sqlObjectNavigationSourceKind(table); + if (objectType) { // Definition navigation for views must not run the view query, which may be expensive or have side effects upstream. - queryEditorDdlTarget.value = { ...target, objectType: "VIEW" }; + queryEditorDdlTarget.value = { ...target, objectType }; showQueryEditorDdlDialog.value = true; return; } @@ -1242,8 +1264,8 @@ async function onClickTable(table: SqlObjectNavigationTarget) { } } -async function onViewTableData(tableName: string) { - const target = tableTargetFromActiveTab(tableName); +async function onViewTableData(table: SqlObjectNavigationTarget) { + const target = tableTargetFromActiveTab(table); if (!target) return; try { await openTableTarget(target); @@ -1252,19 +1274,41 @@ async function onViewTableData(tableName: string) { } } -function onViewTableDdl(tableName: string) { - const target = tableTargetFromActiveTab(tableName); +function onViewTableDdl(table: SqlObjectNavigationTarget) { + const target = tableTargetFromActiveTab(table); if (!target) return; - queryEditorDdlTarget.value = target; + queryEditorDdlTarget.value = { ...target, objectType: sqlObjectNavigationSourceKind(table) }; showQueryEditorDdlDialog.value = true; } -function onEditTableStructure(tableName: string) { - const target = tableTargetFromActiveTab(tableName); - if (!target) return; +function onEditTableStructure(table: SqlObjectNavigationTarget) { + const target = tableTargetFromActiveTab(table); + // Keep view-like objects out of the table editor even if a stale menu dispatches this event. + if (!target || sqlObjectNavigationSourceKind(table)) return; queryStore.openTableStructure(target.connectionId, target.database, target.schema, target.tableName); } +async function onOpenObjectSource(table: SqlObjectNavigationTarget, initialEditing: boolean) { + const target = tableTargetFromActiveTab(table); + const objectType = sqlObjectNavigationSourceKind(table); + if (!target || !objectType) return; + try { + await connectionStore.ensureConnected(target.connectionId); + connectionStore.activeConnectionId = target.connectionId; + queryEditorObjectSourceTarget.value = { connectionId: target.connectionId, database: target.database, schema: target.schema, name: target.tableName, objectType, initialEditing }; + showQueryEditorObjectSourceDialog.value = true; + } catch (e: any) { + toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000); + } +} + +function onQueryEditorObjectSourceSaved() { + const target = queryEditorObjectSourceTarget.value; + if (!target) return; + connectionStore.invalidateCompletionCache(target.connectionId, target.database); + contentAreaRef.value?.refreshQueryEditorCompletionCache(); +} + async function changeActiveConnection(connectionId: string) { const tab = activeTab.value; if (!tab) return; @@ -2004,6 +2048,7 @@ onUnmounted(() => { @view-table-data="onViewTableData" @edit-table-structure="onEditTableStructure" @view-table-ddl="onViewTableDdl" + @open-object-source="onOpenObjectSource" @open-object-table=" (target) => activeTab && @@ -2211,6 +2256,20 @@ onUnmounted(() => { :database-type="queryEditorDdlDatabaseType" :dialect="queryEditorDdlDialect" /> + diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index b061ba477..03838a71c 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1,6 +1,6 @@