From cc9b802b49d5b0ae1ff33e30c1a199c6f5d38009 Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 13 Jul 2026 16:14:56 +0800 Subject: [PATCH] fix(editor): handle Oracle line comments in DDL viewer --- apps/desktop/src/App.vue | 18 +++++- .../src/components/editor/QueryEditor.vue | 19 +++--- .../src/components/objects/DdlViewDialog.vue | 8 ++- .../src/components/sidebar/TreeItem.vue | 11 ++-- .../src/lib/editor/codemirrorSqlDialect.ts | 29 +++++++-- .../app-tests/codemirrorSqlDialect.test.ts | 61 +++++++++++++++++++ 6 files changed, 122 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 269eef243..a4e010f7a 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -329,9 +329,12 @@ function closeDriverStorePage() { const toolbarAgentDriverUpdateCount = computed(() => (updateNotificationsEnabled.value ? agentDriverUpdateCount.value : 0)); const toolbarHasUpdateAvailable = computed(() => updateNotificationsEnabled.value && hasUpdateAvailable.value); const hasSqlFileConnections = computed(() => connectionStore.connections.some((c) => supportsSqlFileExecution(c.db_type))); +const queryEditorDdlDatabaseType = computed(() => { + if (!queryEditorDdlTarget.value?.connectionId) return undefined; + return effectiveDatabaseTypeForConnection(connectionStore.getConfig(queryEditorDdlTarget.value.connectionId)); +}); const queryEditorDdlDialect = computed(() => { - if (!queryEditorDdlTarget.value?.connectionId) return "mysql"; - return codeMirrorSqlDialect(effectiveDatabaseTypeForConnection(connectionStore.getConfig(queryEditorDdlTarget.value.connectionId))); + return codeMirrorSqlDialect(queryEditorDdlDatabaseType.value); }); const connectionStats = computed(() => ({ total: connectionStore.connections.length, @@ -2145,7 +2148,16 @@ onUnmounted(() => { - + diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index dd33aa9dd..a3da6b68e 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -209,6 +209,7 @@ let codeMirrorTheme: import("@codemirror/state").Compartment | null = null; let wordWrapComp: import("@codemirror/state").Compartment | null = null; let vimModeComp: import("@codemirror/state").Compartment | null = null; let closeBracketsComp: import("@codemirror/state").Compartment | null = null; +let sqlLanguageComp: import("@codemirror/state").Compartment | null = null; let codeMirrorCloseBrackets: typeof import("@codemirror/autocomplete").closeBrackets | null = null; let codeMirrorCloseBracketsKeymap: readonly import("@codemirror/view").KeyBinding[] | null = null; let readOnlyComp: import("@codemirror/state").Compartment | null = null; @@ -224,6 +225,7 @@ let dbxVimCommandsConfigured = false; let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | null = null; let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null; let buildSqlCompletionExtension: (() => import("@codemirror/state").Extension) | null = null; +let buildSqlLanguageExtension: (() => import("@codemirror/state").Extension) | null = null; let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion; let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null; let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null; @@ -2586,6 +2588,7 @@ onMounted(async () => { wordWrapComp = new Compartment(); vimModeComp = new Compartment(); closeBracketsComp = new Compartment(); + sqlLanguageComp = new Compartment(); codeMirrorCloseBrackets = closeBrackets; codeMirrorCloseBracketsKeymap = closeBracketsKeymap; readOnlyComp = new Compartment(); @@ -2700,7 +2703,7 @@ onMounted(async () => { override: [async (context: CompletionContext) => provideSqlCompletions(context)], }); - const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect); + buildSqlLanguageExtension = () => langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, props.dialect, props.databaseType) }); const initialSettings = settingsStore.editorSettings; const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors(), themePalette.value); @@ -2856,7 +2859,7 @@ onMounted(async () => { // Vim must be mounted before DBX/default keymaps so normal-mode keys are handled first. vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)), keymap.of([...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap]), - langSql.sql({ dialect }), + sqlLanguageComp.of(buildSqlLanguageExtension()), tooltips({ parent: document.body }), completionComp.of(buildSqlCompletionExtension()), sqlCompletionTheme(EditorView), @@ -3183,13 +3186,11 @@ watch( }, ); -watch( - () => props.databaseType, - () => { - executableStatementRangeCache = null; - view.value?.dispatch({}); - }, -); +watch([() => props.databaseType, () => props.dialect], () => { + executableStatementRangeCache = null; + if (!view.value || !sqlLanguageComp || !buildSqlLanguageExtension) return; + view.value.dispatch({ effects: sqlLanguageComp.reconfigure(buildSqlLanguageExtension()) }); +}); watch( () => props.forceWordWrap, diff --git a/apps/desktop/src/components/objects/DdlViewDialog.vue b/apps/desktop/src/components/objects/DdlViewDialog.vue index ac25a17c9..e1ba067e7 100644 --- a/apps/desktop/src/components/objects/DdlViewDialog.vue +++ b/apps/desktop/src/components/objects/DdlViewDialog.vue @@ -14,7 +14,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue"; import type { EditorView } from "@codemirror/view"; -import type { ObjectSourceKind } from "@/types/database"; +import type { DatabaseType, ObjectSourceKind } from "@/types/database"; const props = withDefaults( defineProps<{ @@ -24,7 +24,9 @@ const props = withDefaults( schema?: string; tableName: string; objectType?: ObjectSourceKind; - /** SQL dialect for syntax highlighting. Non-PG/non-MSSQL databases fall back to MySQL (same as QueryEditor's source viewer). */ + /** Effective database type selects database-specific syntax rules; older callers can still rely on the dialect fallback. */ + databaseType?: DatabaseType; + /** SQL dialect fallback for syntax highlighting when the effective database type is unavailable. */ dialect: "mysql" | "postgres" | "sqlserver"; /** SQL formatter dialect. Kept separate from the syntax-highlighting dialect because several PG-compatible DBs highlight as MySQL. */ formatDialect?: SqlFormatDialect; @@ -88,7 +90,7 @@ async function initDdlEditor(content: string) { const fontFamily = settingsStore.editorSettings.fontFamily; const themeExt = await loadEditorTheme(editorTheme, appAppearance, undefined, themePalette.value); const fontExt = editorFontTheme(EditorView, fontSize, fontFamily, { fixedHeight: true, scrollable: true }); - const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect); + const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect, props.databaseType); const state = EditorState.create({ doc: content, extensions: [ diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 9d4cfd75d..a93bf9ef8 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1808,13 +1808,15 @@ const pasteTableDataCopySupported = computed(() => supportsWholeRowTableDataCopy const ddlTarget = ref(null); const showDdlDialog = ref(false); +const ddlDatabaseType = computed(() => { + if (!ddlTarget.value?.connectionId) return undefined; + return effectiveDatabaseTypeForConnection(connectionStore.getConfig(ddlTarget.value.connectionId)); +}); const ddlDialect = computed(() => { - if (!ddlTarget.value?.connectionId) return "mysql"; - return codeMirrorSqlDialect(effectiveDatabaseTypeForConnection(connectionStore.getConfig(ddlTarget.value.connectionId))); + return codeMirrorSqlDialect(ddlDatabaseType.value); }); const ddlFormatDialect = computed(() => { - if (!ddlTarget.value?.connectionId) return "generic"; - return sqlFormatDialectForDbType(effectiveDatabaseTypeForConnection(connectionStore.getConfig(ddlTarget.value.connectionId))); + return sqlFormatDialectForDbType(ddlDatabaseType.value); }); const objectSourceTarget = ref<{ node: TreeNode; initialEditing: boolean } | null>(null); const showObjectSourceDialog = ref(false); @@ -5930,6 +5932,7 @@ function treeItemMenuItems(): ContextMenuItem[] { :schema="ddlTarget.schema" :table-name="ddlTarget.label" :object-type="tableDdlObjectTypeForNode(ddlTarget.type)" + :database-type="ddlDatabaseType" :dialect="ddlDialect" :format-dialect="ddlFormatDialect" v-model:open="showDdlDialog" diff --git a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts index 265db5739..19db8b898 100644 --- a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts +++ b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts @@ -1,8 +1,14 @@ import type { SQLDialect } from "@codemirror/lang-sql"; +import type { DatabaseType } from "@/types/database"; export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver"; -type CodeMirrorSqlLanguageModule = Pick; +type CodeMirrorSqlLanguageModule = Pick; + +const MYSQL_CODEMIRROR_DATABASE_TYPES = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]); +const POSTGRES_CODEMIRROR_DATABASE_TYPES = new Set(["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "vastbase", "opengauss", "questdb"]); +const ORACLE_CODEMIRROR_DATABASE_TYPES = new Set(["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]); +const SQLITE_CODEMIRROR_DATABASE_TYPES = new Set(["sqlite", "rqlite", "turso", "cloudflare-d1"]); const DBX_COMMON_SQL_KEYWORDS = [ "PIVOT", @@ -52,10 +58,23 @@ export function postgresKeywordSyntaxTerms(keywords: string): string { .join(" "); } -export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName = "mysql"): SQLDialect { - const baseDialect = dialectName === "postgres" ? langSql.PostgreSQL : dialectName === "sqlserver" ? langSql.MSSQL : langSql.MySQL; - const isPostgres = dialectName === "postgres"; - const isSqlServer = dialectName === "sqlserver"; +function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName, databaseType?: DatabaseType): SQLDialect { + if (databaseType) { + if (MYSQL_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.MySQL; + if (POSTGRES_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.PostgreSQL; + if (ORACLE_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.PLSQL; + if (SQLITE_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.SQLite; + if (databaseType === "sqlserver") return langSql.MSSQL; + if (databaseType === "cassandra") return langSql.Cassandra; + return langSql.StandardSQL; + } + return dialectName === "postgres" ? langSql.PostgreSQL : dialectName === "sqlserver" ? langSql.MSSQL : langSql.MySQL; +} + +export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName = "mysql", databaseType?: DatabaseType): SQLDialect { + const baseDialect = codeMirrorBaseDialect(langSql, dialectName, databaseType); + const isPostgres = baseDialect === langSql.PostgreSQL; + const isSqlServer = baseDialect === langSql.MSSQL; const baseKeywords = isPostgres ? postgresKeywordSyntaxTerms(baseDialect.spec.keywords || "") : baseDialect.spec.keywords || ""; return langSql.SQLDialect.define({ diff --git a/packages/app-tests/codemirrorSqlDialect.test.ts b/packages/app-tests/codemirrorSqlDialect.test.ts index 11fabddc0..f14a387f3 100644 --- a/packages/app-tests/codemirrorSqlDialect.test.ts +++ b/packages/app-tests/codemirrorSqlDialect.test.ts @@ -1,7 +1,10 @@ import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; import { test } from "vitest"; import * as langSql from "@codemirror/lang-sql"; import { createDbxCodeMirrorSqlDialect } from "../../apps/desktop/src/lib/editor/codemirrorSqlDialect.ts"; +import { codeMirrorSqlDialect } from "../../apps/desktop/src/lib/database/jdbcDialect.ts"; +import type { DatabaseType } from "../../apps/desktop/src/types/database.ts"; function hasKeyword(keywords: string | undefined, keyword: string): boolean { return new RegExp(`(?:^|\\s)${keyword}(?:\\s|$)`, "i").test(keywords || ""); @@ -31,3 +34,61 @@ test("keeps DBX PostgreSQL procedural dialect extensions", () => { assert.equal(hasKeyword(dialect.spec.types, "JSONB"), true); assert.equal(hasKeyword(dialect.spec.builtin, "TG_NAME"), true); }); + +test("treats compact double-dash comments as comments in non-MySQL SQL dialects", () => { + const databaseTypes: DatabaseType[] = [ + "oracle", + "dameng", + "yashandb", + "oscar", + "oceanbase-oracle", + "sqlite", + "rqlite", + "turso", + "cloudflare-d1", + "postgres", + "redshift", + "gaussdb", + "kwdb", + "kingbase", + "highgo", + "vastbase", + "opengauss", + "questdb", + "sqlserver", + "cassandra", + "clickhouse", + "duckdb", + "databend", + "db2", + "hive", + "spark", + ]; + + for (const databaseType of databaseTypes) { + const dialect = createDbxCodeMirrorSqlDialect(langSql, codeMirrorSqlDialect(databaseType), databaseType); + assert.equal(countParsedNodes(dialect, "--SELECT 1", "LineComment", "--SELECT 1"), 1, databaseType); + assert.equal(countParsedNodes(dialect, "--SELECT 1", "Keyword", "SELECT"), 0, databaseType); + } +}); + +test("keeps MySQL-compatible double-dash whitespace rules", () => { + const databaseTypes: DatabaseType[] = ["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]; + + for (const databaseType of databaseTypes) { + const dialect = createDbxCodeMirrorSqlDialect(langSql, codeMirrorSqlDialect(databaseType), databaseType); + assert.equal(countParsedNodes(dialect, "--SELECT 1", "LineComment", "--SELECT 1"), 0, databaseType); + assert.equal(countParsedNodes(dialect, "--SELECT 1", "Keyword", "SELECT"), 1, databaseType); + assert.equal(countParsedNodes(dialect, "-- SELECT 1", "LineComment", "-- SELECT 1"), 1, databaseType); + } +}); + +test("propagates database type to every DDL viewer entrypoint", () => { + const ddlViewDialog = readFileSync("apps/desktop/src/components/objects/DdlViewDialog.vue", "utf8"); + const treeItem = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8"); + const app = readFileSync("apps/desktop/src/App.vue", "utf8"); + + assert.match(ddlViewDialog, /createDbxCodeMirrorSqlDialect\(langSql, props\.dialect, props\.databaseType\)/); + assert.match(treeItem, /]*:database-type="queryEditorDdlDatabaseType"[^>]*\/>/); +});