fix(editor): handle Oracle line comments in DDL viewer

This commit is contained in:
zipg 2026-07-13 16:14:56 +08:00 committed by GitHub
parent 29b351691a
commit cc9b802b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 122 additions and 24 deletions

View File

@ -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(() => {
</DialogFooter>
</DialogContent>
</Dialog>
<DdlViewDialog v-if="queryEditorDdlTarget" v-model:open="showQueryEditorDdlDialog" :connection-id="queryEditorDdlTarget.connectionId" :database="queryEditorDdlTarget.database" :schema="queryEditorDdlTarget.schema" :table-name="queryEditorDdlTarget.tableName" :dialect="queryEditorDdlDialect" />
<DdlViewDialog
v-if="queryEditorDdlTarget"
v-model:open="showQueryEditorDdlDialog"
:connection-id="queryEditorDdlTarget.connectionId"
:database="queryEditorDdlTarget.database"
:schema="queryEditorDdlTarget.schema"
:table-name="queryEditorDdlTarget.tableName"
:database-type="queryEditorDdlDatabaseType"
:dialect="queryEditorDdlDialect"
/>
</TooltipProvider>
</div>
</template>

View File

@ -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,

View File

@ -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: [

View File

@ -1808,13 +1808,15 @@ const pasteTableDataCopySupported = computed(() => supportsWholeRowTableDataCopy
const ddlTarget = ref<TreeNode | null>(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"

View File

@ -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<typeof import("@codemirror/lang-sql"), "MSSQL" | "MySQL" | "PostgreSQL" | "SQLDialect">;
type CodeMirrorSqlLanguageModule = Pick<typeof import("@codemirror/lang-sql"), "Cassandra" | "MSSQL" | "MySQL" | "PLSQL" | "PostgreSQL" | "SQLite" | "SQLDialect" | "StandardSQL">;
const MYSQL_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]);
const POSTGRES_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "vastbase", "opengauss", "questdb"]);
const ORACLE_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]);
const SQLITE_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["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({

View File

@ -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, /<DdlViewDialog[\s\S]*?:database-type="ddlDatabaseType"[\s\S]*?v-model:open="showDdlDialog"/);
assert.match(app, /<DdlViewDialog[^>]*:database-type="queryEditorDdlDatabaseType"[^>]*\/>/);
});