fix(kingbase): reconfigure editor language and completion by mode (#12834)

This commit is contained in:
Hand Sonic
2026-10-20 15:07:37 +08:00
committed by GitHub
parent f1cac02678
commit e7a10baccd
13 changed files with 248 additions and 26 deletions
@@ -147,16 +147,25 @@ import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { isMacOS } from "@/lib/backend/platform";
import { resolveSqlDialectId } from "@/lib/sql/semantic/dialect";
import { kingbaseSqlDatabaseType } from "@/lib/database/jdbcDialect";
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionReferencedTable } from "@/lib/sql/sqlCompletion";
const props = defineProps<QueryEditorProps>();
function sqlBehaviorDialect(): "mysql" | "postgres" | "sqlserver" | undefined {
const mode = kingbaseSqlType.value;
if (props.databaseType === "kingbase") return mode === "oracle" ? undefined : (mode ?? "postgres");
// clickhouse and soql ride the SQL editor but have no matching behavior dialect;
// fall back to the connection's dialect (undefined for Salesforce).
if (props.syntaxDialect === "oracle") return undefined;
return props.syntaxDialect === "clickhouse" || props.syntaxDialect === "soql" ? props.dialect : (props.syntaxDialect ?? props.dialect);
}
function sqlSyntaxDialect() {
if (props.databaseType === "kingbase") return kingbaseSqlType.value ?? "postgres";
return props.syntaxDialect ?? props.dialect;
}
function queryEditorSelectionLanguage(): "sql" | "text" {
const databaseType = props.databaseType;
return databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "solr" || databaseType === "victoriametrics" ? "text" : "sql";
@@ -247,12 +256,14 @@ let contextMenuDoc: Text | null = null;
let contextMenuDocText = "";
const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const kingbaseConfig = computed(() => (props.databaseType === "kingbase" && props.connectionId ? connectionStore.getConfig(props.connectionId) : undefined));
const kingbaseSqlType = computed(() => kingbaseSqlDatabaseType(kingbaseConfig.value));
function sqlStatementParameterOptions() {
const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType, settingsStore.editorSettings.sqlVariableSubstitutionEnabled);
return {
databaseType: props.databaseType,
compatibilityMode: props.databaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(props.connectionId, props.database) : undefined,
databaseType: kingbaseSqlType.value ?? props.databaseType,
compatibilityMode: props.databaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(props.connectionId, props.database) : kingbaseConfig.value?.database_info?.compatibilityMode,
enabledSyntaxes: enabledSqlParameterSyntaxes(toggles),
};
}
@@ -511,6 +522,7 @@ const statementBoundaries = useQueryEditorStatementBoundaries({
props,
view,
sqlStatementParameterOptions,
syntaxDialect: sqlSyntaxDialect,
runtime: codeMirrorRuntime,
driverProfile: () => sqlDriverProfile.value,
cache: {
@@ -534,6 +546,7 @@ const completionMetadata = useQueryEditorCompletionMetadata({
view,
connectionStore,
sqlBehaviorDialect,
syntaxDialect: sqlSyntaxDialect,
remoteLatencyBudgetMs: COMPLETION_REMOTE_LATENCY_BUDGET_MS,
maxCompletionTables: MAX_COMPLETION_TABLES,
onDemandMinPrefix: PRESTO_ON_DEMAND_TABLE_COMPLETION_MIN_PREFIX,
@@ -1694,6 +1707,7 @@ const { sqlErrorDecorationRange, sqlSemanticDecorationRanges, reconfigureDiagnos
sqlDriverProfile,
sqlStatementParameterOptions,
sqlBehaviorDialect,
sqlSyntaxDialect,
queryEditorSelectionLanguage,
semanticCompletionEnabled: SEMANTIC_SQL_COMPLETION_ENABLED,
maxCompletionTables: MAX_COMPLETION_TABLES,
@@ -2025,6 +2039,8 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
t,
sqlBehaviorDialect,
sqlDriverProfile,
sqlFunctionDatabaseType: () => kingbaseSqlType.value ?? props.databaseType,
sqlSyntaxDialect,
sqlStatementParameterOptions,
queryEditorSelectionLanguage,
cache: {
@@ -2589,8 +2605,11 @@ watch([() => props.clientSessionId, () => props.completionContextVersion], () =>
});
watch(
[() => props.tabId, () => props.databaseType, () => props.dialect, () => props.syntaxDialect, sqlDriverProfile],
[() => props.tabId, () => props.databaseType, () => props.dialect, sqlSyntaxDialect, sqlDriverProfile, () => kingbaseConfig.value?.database_info?.compatibilityMode],
() => {
completion.invalidateRequests();
refreshCompletionCache();
setSemanticDiagnostics([]);
executableStatementRangeCache = null;
statementBoundaries.invalidate();
if (!view.value || !codeMirrorRuntime.sqlLanguageComp || !codeMirrorRuntime.buildSqlLanguageExtension || !codeMirrorRuntime.sqlSemanticHighlightComp || !codeMirrorRuntime.buildSqlSemanticHighlightExtension || !codeMirrorRuntime.sqlSignatureComp || !codeMirrorRuntime.buildSqlSignatureExtension)
@@ -2604,6 +2623,7 @@ watch(
],
});
applyEditorCompletionExtension();
scheduleSemanticDiagnostics(0);
},
{ flush: "post" },
);
@@ -0,0 +1,126 @@
// @vitest-environment happy-dom
import { createApp, h, nextTick, shallowRef } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { createI18n } from "vue-i18n";
import { syntaxTree } from "@codemirror/language";
import { EditorView, showTooltip } from "@codemirror/view";
import { completionStatus, currentCompletions, startCompletion } from "@codemirror/autocomplete";
import { afterEach, describe, expect, it } from "vitest";
import QueryEditor from "../QueryEditor.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
const cleanups: Array<() => void> = [];
afterEach(() => {
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
});
async function mountEditor(mode?: string) {
const pinia = createPinia();
setActivePinia(pinia);
const settings = useSettingsStore();
settings.editorSettings.showExecutionTargetPicker = false;
settings.editorSettings.completionTriggerMode = "manual";
settings.editorSettings.sqlSemanticDiagnosticsEnabled = false;
const store = useConnectionStore();
store.connections.push({ id: "kingbase-synthetic", name: "Synthetic Kingbase", db_type: "kingbase", host: "example.test", port: 54321, username: "synthetic", password: "", database_info: { compatibilityMode: mode } });
const sql = shallowRef("SELECT ");
const host = document.createElement("div");
document.body.append(host);
const app = createApp({
render: () =>
h(QueryEditor, {
modelValue: sql.value,
connectionId: "kingbase-synthetic",
databaseType: "kingbase",
dialect: "postgres",
autoFocus: false,
"onUpdate:modelValue": (value: string) => {
sql.value = value;
},
}),
});
app.use(pinia);
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false }));
app.mount(host);
cleanups.push(() => {
app.unmount();
host.remove();
});
await expect.poll(() => host.querySelector(".cm-editor")).not.toBeNull();
const view = EditorView.findFromDOM(host.querySelector(".cm-editor") as HTMLElement)!;
return { view, store, host, sql };
}
function signatureText(view: EditorView): string {
return view.state
.facet(showTooltip)
.flatMap((tooltip) => (tooltip ? [tooltip.create(view).dom.textContent ?? ""] : []))
.join("\n");
}
describe("Kingbase editor mode integration (synthetic metadata)", () => {
it("invalidates active completion results when the reported mode changes", async () => {
const { view, store, sql } = await mountEditor("pg");
sql.value = "SELECT IF";
await nextTick();
view.dispatch({ selection: { anchor: view.state.doc.length } });
startCompletion(view);
await expect.poll(() => completionStatus(view.state)).toBe("active");
expect(currentCompletions(view.state).map((option) => option.label)).not.toContain("IFNULL");
store.getConfig("kingbase-synthetic")!.database_info = { compatibilityMode: "mysql" };
await nextTick();
startCompletion(view);
await expect.poll(() => currentCompletions(view.state).map((option) => option.label)).toContain("IFNULL");
});
it.each([
["pg", "JSONB_BUILD_OBJECT"],
["oracle", "NVL"],
["mysql", "IFNULL"],
["sqlserver", "GETDATE"],
])("uses %s function completion and signatures without changing the native connection", async (mode, functionName) => {
const { view, store, sql } = await mountEditor(mode);
sql.value = `SELECT ${functionName.slice(0, 3)}`;
await nextTick();
view.dispatch({ selection: { anchor: view.state.doc.length } });
expect(startCompletion(view)).toBe(true);
await expect.poll(() => completionStatus(view.state)).toBe("active");
expect(currentCompletions(view.state).map((option) => option.label)).toContain(functionName);
sql.value = `SELECT ${functionName}(`;
await nextTick();
view.dispatch({ selection: { anchor: view.state.doc.length } });
await expect.poll(() => signatureText(view)).toContain(`${functionName}(`);
expect(store.getConfig("kingbase-synthetic")?.db_type).toBe("kingbase");
});
it("removes a previous mode's function signature when the mode changes", async () => {
const { view, store, sql } = await mountEditor("mysql");
sql.value = "SELECT IFNULL(";
await nextTick();
view.dispatch({ selection: { anchor: view.state.doc.length } });
await expect.poll(() => signatureText(view)).toContain("IFNULL(");
store.getConfig("kingbase-synthetic")!.database_info = { compatibilityMode: "pg" };
await nextTick();
await expect.poll(() => signatureText(view)).not.toContain("IFNULL(");
});
it("reconfigures the same mounted editor when mode metadata arrives and changes", async () => {
const { view, store, host, sql } = await mountEditor();
sql.value = "SELECT `name` FROM t";
await nextTick();
const tokenStart = sql.value.indexOf("`name`") + 1;
expect(syntaxTree(view.state).resolveInner(tokenStart).name).not.toBe("QuotedIdentifier");
store.getConfig("kingbase-synthetic")!.database_info = { compatibilityMode: "mysql" };
await nextTick();
expect(syntaxTree(view.state).resolveInner(tokenStart).name).toBe("QuotedIdentifier");
store.getConfig("kingbase-synthetic")!.database_info = { compatibilityMode: "sqlserver" };
sql.value = "SELECT [name] FROM #temp";
await nextTick();
expect(syntaxTree(view.state).resolveInner(sql.value.indexOf("#temp") + 1).name).toBe("SpecialVar");
const word = view.state.wordAt(sql.value.indexOf("#temp") + 1)!;
expect(view.state.sliceDoc(word.from, word.to)).toBe("#temp");
expect(host.querySelector(".cm-editor")).toBe(view.dom);
});
});
@@ -35,7 +35,7 @@ afterEach(() => {
vi.clearAllMocks();
});
function createHarness(overrides: Partial<QueryEditorProps> = {}) {
function createHarness(overrides: Partial<QueryEditorProps> = {}, syntaxDialect?: QueryEditorProps["syntaxDialect"]) {
const props = reactive<QueryEditorProps>({ modelValue: "SELECT missing;", databaseType: "mysql", dialect: "mysql", connectionId: "connection", database: "demo", ...overrides });
const parent = document.createElement("div");
document.body.append(parent);
@@ -62,10 +62,11 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
view,
runtime,
settingsStore,
connectionStore: { connectionIdentifierQuote: () => "`" } as Options["connectionStore"],
connectionStore: { connectionIdentifierQuote: () => (props.databaseType === "kingbase" && syntaxDialect !== "mysql" ? '"' : "`") } as Options["connectionStore"],
sqlDriverProfile: computed(() => undefined),
sqlStatementParameterOptions: () => ({}),
sqlBehaviorDialect: () => props.dialect,
sqlSyntaxDialect: () => syntaxDialect,
semanticCompletionEnabled: false,
maxCompletionTables: 100,
metadata,
@@ -89,6 +90,13 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
}
describe("QueryEditor diagnostic scheduling ownership", () => {
it.each(["oracle", "postgres", "mysql", "sqlserver"] as const)("uses the detected Kingbase %s mode for reference analysis", async (mode) => {
const { diagnostics } = createHarness({ databaseType: "kingbase", dialect: "postgres", formatDialect: "postgresql" }, mode);
diagnostics.scheduleSemanticDiagnostics(0);
await vi.advanceTimersByTimeAsync(0);
expect(analyze).toHaveBeenCalledWith("SELECT missing", mode);
});
it.each(graphQueryDatabases)("does not send %s editor text to SQL semantic diagnostics", async (databaseType) => {
const { diagnostics, metadata } = createHarness({ databaseType });
diagnostics.scheduleSemanticDiagnostics(0);
@@ -43,6 +43,8 @@ interface QueryEditorSqlExtensionsOptions {
t: Composer["t"];
sqlBehaviorDialect: () => QueryEditorProps["dialect"];
sqlDriverProfile: ComputedRef<string | undefined>;
sqlFunctionDatabaseType?: () => DatabaseType | undefined;
sqlSyntaxDialect?: () => QueryEditorProps["syntaxDialect"];
sqlStatementParameterOptions: () => SqlParameterOptions;
queryEditorSelectionLanguage: () => "sql" | "text";
cache: { value: ExecutableStatementRangeCache | null };
@@ -305,7 +307,7 @@ export function configureQueryEditorSqlExtensions(options: QueryEditorSqlExtensi
const source = currentState.doc.sliceString(windowFrom, cursor);
const signature = isGraphQueryDatabase(props.databaseType)
? getGraphFunctionSignatureHelp(source, cursor - windowFrom, props.databaseType, { truncatedPrefix: windowFrom > 0 })
: getSqlFunctionSignatureHelp(source, cursor - windowFrom, props.databaseType, sqlDriverProfile.value, { truncatedPrefix: windowFrom > 0 });
: getSqlFunctionSignatureHelp(source, cursor - windowFrom, options.sqlFunctionDatabaseType?.() ?? props.databaseType, sqlDriverProfile.value, { truncatedPrefix: windowFrom > 0 });
if (!signature) return null;
return {
pos: cursor,
@@ -341,13 +343,13 @@ export function configureQueryEditorSqlExtensions(options: QueryEditorSqlExtensi
? graphQueryLanguage(props.databaseType)
: supportsQueryEditorSqlLanguage(props.databaseType)
? langSql.sql({
dialect: createDbxCodeMirrorSqlDialect(langSql, props.syntaxDialect ?? props.dialect, props.databaseType, sqlDriverProfile.value),
dialect: createDbxCodeMirrorSqlDialect(langSql, options.sqlSyntaxDialect?.() ?? props.syntaxDialect ?? props.dialect, props.databaseType, sqlDriverProfile.value),
})
: [],
// Non-SQL editors (MongoDB shell) keep the SQL grammar for highlighting, so override the
// comment marker that toggleLineComment reads from language data.
Prec.highest(EditorState.languageData.of(() => [{ commentTokens: queryEditorCommentTokens(props.databaseType) }])),
Prec.highest(EditorState.languageData.of(() => queryEditorWordLanguageData(props.databaseType))),
Prec.highest(EditorState.languageData.of(() => queryEditorWordLanguageData(options.sqlFunctionDatabaseType?.() ?? props.databaseType))),
// The SQL grammar does not tokenize `//`, so those comments are highlighted by hand.
queryEditorLineCommentToken(props.databaseType) === "//" ? shellLineCommentHighlightPlugin : [],
];
@@ -34,6 +34,7 @@ interface QueryEditorCompletionMetadataOptions {
view: ShallowRef<EditorView | null>;
connectionStore: ReturnType<typeof useConnectionStore>;
sqlBehaviorDialect: () => QueryEditorProps["dialect"];
syntaxDialect?: () => QueryEditorProps["syntaxDialect"];
remoteLatencyBudgetMs: number;
maxCompletionTables: number;
onDemandMinPrefix: number;
@@ -121,6 +122,7 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
const dialect = sqlBehaviorDialect();
const doc = editorState?.doc;
const driverProfile = props.connectionId ? connectionStore.getConfig(props.connectionId)?.driver_profile : undefined;
const syntaxDialect = options.syntaxDialect?.() ?? props.syntaxDialect;
if (
doc &&
editorCompletionAnalysisCache?.doc === doc &&
@@ -129,7 +131,7 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
editorCompletionAnalysisCache.databaseType === props.databaseType &&
editorCompletionAnalysisCache.dialect === dialect &&
editorCompletionAnalysisCache.driverProfile === driverProfile &&
editorCompletionAnalysisCache.syntaxDialect === props.syntaxDialect
editorCompletionAnalysisCache.syntaxDialect === syntaxDialect
) {
return editorCompletionAnalysisCache.result;
}
@@ -140,7 +142,7 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
databaseType: props.databaseType,
dialect,
semanticCompletionEnabled: SEMANTIC_SQL_COMPLETION_ENABLED,
syntaxDialect: props.syntaxDialect,
syntaxDialect,
driverProfile,
};
const result = editorState && shouldUseQueryEditorLargeDocumentModeForSize(editorState.doc.length, editorState.doc.lines) ? completionAnalysisWorker.analyze(request, editorState) : Promise.resolve(analyzeSqlCompletion(request, editorState));
@@ -152,7 +154,7 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
databaseType: props.databaseType,
dialect,
driverProfile,
syntaxDialect: props.syntaxDialect,
syntaxDialect,
result,
};
}
@@ -70,6 +70,7 @@ interface QueryEditorDiagnosticsOptions {
sqlDriverProfile: ComputedRef<string | undefined>;
sqlStatementParameterOptions: () => SqlParameterOptions;
sqlBehaviorDialect: () => QueryEditorProps["dialect"];
sqlSyntaxDialect?: () => QueryEditorProps["syntaxDialect"];
queryEditorSelectionLanguage: () => "sql" | "text";
semanticCompletionEnabled: boolean;
maxCompletionTables: number;
@@ -441,7 +442,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
sqlReferenceAnalysisDialectFor({
databaseType: props.databaseType,
identifierQuote: connectionStore.connectionIdentifierQuote(props.connectionId),
fallbackDialect: props.formatDialect ?? props.dialect ?? "generic",
fallbackDialect: props.databaseType === "kingbase" ? (options.sqlSyntaxDialect?.() ?? sqlBehaviorDialect() ?? "postgres") : (props.formatDialect ?? props.dialect ?? "generic"),
}),
);
if (runId !== semanticDiagnosticRunId) return;
@@ -583,7 +584,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
sqlReferenceAnalysisDialectFor({
databaseType: props.databaseType,
identifierQuote: connectionStore.connectionIdentifierQuote(props.connectionId),
fallbackDialect: props.formatDialect ?? props.dialect ?? "generic",
fallbackDialect: props.databaseType === "kingbase" ? (options.sqlSyntaxDialect?.() ?? sqlBehaviorDialect() ?? "postgres") : (props.formatDialect ?? props.dialect ?? "generic"),
}),
);
if (currentView.state.doc !== doc) return [];
@@ -23,6 +23,7 @@ interface QueryEditorStatementBoundariesOptions {
cache: { value: ExecutableStatementRangeCache | null };
runtime: QueryEditorCodeMirrorRuntime;
driverProfile?: () => string | undefined;
syntaxDialect?: () => QueryEditorProps["syntaxDialect"];
}
export function useQueryEditorStatementBoundaries(options: QueryEditorStatementBoundariesOptions) {
@@ -85,15 +86,17 @@ export function useQueryEditorStatementBoundaries(options: QueryEditorStatementB
function ensureStatementCache(state: EditorState): Promise<ExecutableStatementRangeCache | null> {
if (pendingAnalysis?.doc === state.doc && pendingAnalysis.generation === statementBoundariesGeneration) return pendingAnalysis.result;
const generation = statementBoundariesGeneration;
const result = analysisWorker.analyze({ sql: state.doc.toString(), databaseType: props.databaseType, parameterOptions: sqlStatementParameterOptions(), includeFolds: true, syntaxDialect: props.syntaxDialect ?? props.dialect, driverProfile: options.driverProfile?.() }).then((analysis) => {
const currentView = view.value ?? trackedView;
if (!analysis || generation !== statementBoundariesGeneration || currentView?.state.doc !== state.doc) return null;
const resolved = { ...analysis, doc: state.doc };
installStatementBoundaries(state, resolved, analysis.folds);
const refreshEffect = codeMirrorRuntime.statementBoundariesRefreshEffect;
if (refreshEffect) currentView.dispatch({ effects: refreshEffect.of(null) });
return resolved;
});
const result = analysisWorker
.analyze({ sql: state.doc.toString(), databaseType: props.databaseType, parameterOptions: sqlStatementParameterOptions(), includeFolds: true, syntaxDialect: options.syntaxDialect?.() ?? props.syntaxDialect ?? props.dialect, driverProfile: options.driverProfile?.() })
.then((analysis) => {
const currentView = view.value ?? trackedView;
if (!analysis || generation !== statementBoundariesGeneration || currentView?.state.doc !== state.doc) return null;
const resolved = { ...analysis, doc: state.doc };
installStatementBoundaries(state, resolved, analysis.folds);
const refreshEffect = codeMirrorRuntime.statementBoundariesRefreshEffect;
if (refreshEffect) currentView.dispatch({ effects: refreshEffect.of(null) });
return resolved;
});
pendingAnalysis = { doc: state.doc, generation, result };
return result;
}
@@ -187,7 +187,7 @@ import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
import { codeMirrorSqlDialect, codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { codeMirrorSqlDialect, codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection, kingbaseSqlDatabaseType } from "@/lib/database/jdbcDialect";
import { supportsTableImport } from "@/lib/database/databaseFeatureSupport";
import { chartableColumnIndexes } from "@/lib/dataGrid/chartData";
import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elasticsearchJsonResponse";
@@ -485,7 +485,7 @@ const activeTabDimension = computed(() => {
return meta && "dimension" in meta ? (meta as VectorCollectionMeta).dimension : undefined;
});
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => sqlFormatDialectForDbType(activeEffectiveDatabaseType.value));
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => sqlFormatDialectForDbType(kingbaseSqlDatabaseType(props.activeConnection) ?? activeEffectiveDatabaseType.value));
const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => codeMirrorSqlDialect(activeEffectiveDatabaseType.value));
const editorSyntaxDialect = computed<CodeMirrorSqlDialectName>(() => codeMirrorSqlDialectForConnection(props.activeConnection));
@@ -31,10 +31,39 @@ import {
setGaussdbTargetServerType,
supportsGaussdbIdentifierQuoteStyle,
transferDatabaseTypeForConnection,
codeMirrorSqlDialectForConnection,
sqlSnippetDatabaseTypeForConnection,
kingbaseSqlDatabaseType,
} from "@/lib/database/jdbcDialect";
import { supportsTransfer } from "@/lib/database/databaseFeatureSupport";
import { transferObjectKindsForDatabase } from "@/lib/database/transferObjectKinds";
describe("Kingbase compatibility mode routing", () => {
it.each([undefined, "", "future_mode"])("does not infer a mode from a version, URL, or unknown mode (%s)", (mode) => {
const config = { db_type: "kingbase" as const, connection_string: "jdbc:kingbase8://example.test/demo", database_info: { compatibilityMode: mode, productVersion: "KingbaseES V009R001" } };
expect(kingbaseSqlDatabaseType(config)).toBeUndefined();
expect(codeMirrorSqlDialectForConnection(config)).toBe("postgres");
expect(effectiveDatabaseTypeForConnection(config)).toBe("kingbase");
});
it.each(["postgres", "mysql", "oracle", "sqlserver", "jdbc"] as const)("ignores Kingbase mode metadata on a %s connection", (db_type) => {
expect(kingbaseSqlDatabaseType({ db_type, database_info: { compatibilityMode: "oracle" } })).toBeUndefined();
});
it.each([
["pg", "postgres", "postgres"],
["postgresql", "postgres", "postgres"],
["oracle", "oracle", "oracle"],
["mysql", "mysql", "mysql"],
["sqlserver", "sqlserver", "sqlserver"],
] as const)("keeps the %s editor profile separate from the physical database", (mode, type, syntax) => {
const connection = { db_type: "kingbase" as const, database_info: { compatibilityMode: mode } };
expect(sqlSnippetDatabaseTypeForConnection(connection)).toBe(type);
expect(codeMirrorSqlDialectForConnection(connection)).toBe(syntax);
expect(effectiveDatabaseTypeForConnection(connection)).toBe("kingbase");
});
});
describe("jdbc dialect inference", () => {
it("detects InterSystems IRIS and Caché JDBC connections", () => {
expect(
@@ -14,6 +14,15 @@ function nodeNameAt(dialect: langSql.SQLDialect, statement: string, word: string
}
describe("codemirrorSqlDialect", () => {
it.each([
["oracle", "SELECT NVL(\"name\", 'n/a') FROM t", '"name"', "QuotedIdentifier"],
["mysql", "SELECT IFNULL(`name`, 'n/a') FROM t", "`name`", "QuotedIdentifier"],
["postgres", "SELECT now(), data #>> '{name}' FROM t", "now", "Builtin"],
["sqlserver", "SELECT GETDATE() FROM #temp", "#temp", "SpecialVar"],
] as const)("uses the %s lexer for Kingbase without replacing its physical type", (mode, sql, token, expected) => {
expect(nodeNameAt(createDbxCodeMirrorSqlDialect(langSql, mode, "kingbase"), sql, token)).toBe(expected);
});
it("keeps common PostgreSQL identifier names out of keyword highlighting", () => {
const keywords = new Set(postgresKeywordSyntaxTerms(langSql.PostgreSQL.spec.keywords || "").split(/\s+/));
+6 -1
View File
@@ -244,6 +244,8 @@ export function sqlSnippetDatabaseTypeForConnection(connection?: JdbcDialectConn
// ASE uses T-SQL snippets, but mapping it globally to SQL Server would also
// enable incompatible SQL Server metadata and pagination behavior.
if (isJdbcAseProfile(connection)) return "sqlserver";
const kingbaseType = kingbaseSqlDatabaseType(connection);
if (kingbaseType) return kingbaseType;
return effectiveDatabaseTypeForConnection(connection);
}
@@ -388,14 +390,17 @@ export function connectionTableSqlSchema(connection: JdbcDialectConnection | und
/** Maps a database type to the corresponding CodeMirror SQL dialect name used by QueryEditor and DdlViewDialog. */
export function codeMirrorSqlDialect(dbType: DatabaseType | undefined): "mysql" | "postgres" | "sqlserver" {
if (dbType === "postgres" || dbType === "gaussdb" || dbType === "kwdb" || dbType === "opengauss") return "postgres";
if (dbType === "postgres" || dbType === "gaussdb" || dbType === "kwdb" || dbType === "opengauss" || dbType === "kingbase") return "postgres";
if (dbType === "sqlserver") return "sqlserver";
return "mysql";
}
export function codeMirrorSqlDialectForConnection(connection?: JdbcDialectConnection): CodeMirrorSqlDialectName {
if (isJdbcAseProfile(connection)) return "sqlserver";
const kingbaseType = kingbaseSqlDatabaseType(connection);
if (kingbaseType) return kingbaseType;
const databaseType = effectiveDatabaseTypeForConnection(connection);
if (databaseType === "kingbase") return "postgres";
if (databaseType === "clickhouse") return "clickhouse";
return codeMirrorSqlDialect(databaseType);
}
@@ -11,7 +11,7 @@ import { driverProfileSqlBuiltinTerms } from "@/lib/database/driverProfileExtens
// identically to unquoted identifiers.
const SQL_DIALECT_LANGUAGE_PROPS = [styleTags({ QuotedIdentifier: tags.name })];
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse" | "soql";
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "oracle" | "clickhouse" | "soql";
export function supportsQueryEditorSqlLanguage(databaseType?: DatabaseType): boolean {
return databaseType !== "qdrant";
@@ -351,6 +351,12 @@ function standardSqlKeywordSyntaxTerms(langSql: CodeMirrorSqlLanguageModule): st
function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName, databaseType?: DatabaseType): SQLDialect {
if (databaseType) {
if (databaseType === "kingbase") {
if (dialectName === "oracle") return langSql.PLSQL;
if (dialectName === "mysql") return langSql.MySQL;
if (dialectName === "sqlserver") return langSql.MSSQL;
return langSql.PostgreSQL;
}
if (databaseType === "clickhouse") return langSql.StandardSQL;
if (MYSQL_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.MySQL;
if (POSTGRES_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.PostgreSQL;
@@ -362,6 +368,7 @@ function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName
return langSql.StandardSQL;
}
if (dialectName === "clickhouse") return langSql.StandardSQL;
if (dialectName === "oracle") return langSql.PLSQL;
return dialectName === "postgres" ? langSql.PostgreSQL : dialectName === "sqlserver" ? langSql.MSSQL : langSql.MySQL;
}
+10
View File
@@ -140,3 +140,13 @@ test("keeps MySQL-compatible double-dash whitespace rules", () => {
assert.equal(countParsedNodes(dialect, "-- SELECT 1", "LineComment", "-- SELECT 1"), 1, databaseType);
}
});
test("selects Kingbase comment rules by reported mode while legacy callers default to PG", () => {
assert.equal(codeMirrorSqlDialect("kingbase"), "postgres");
for (const mode of [undefined, "pg", "oracle", "mysql", "sqlserver"]) {
const syntax = codeMirrorSqlDialectForConnection({ db_type: "kingbase", database_info: { compatibilityMode: mode } });
const dialect = createDbxCodeMirrorSqlDialect(langSql, syntax, "kingbase");
assert.equal(countParsedNodes(dialect, "--SELECT 1", "LineComment", "--SELECT 1"), mode === "mysql" ? 0 : 1, String(mode));
assert.equal(countParsedNodes(dialect, "-- SELECT 1", "LineComment", "-- SELECT 1"), 1, String(mode));
}
});