mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:36:47 +00:00
fix(kingbase): reconfigure editor language and completion by mode (#12834)
This commit is contained in:
@@ -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" },
|
||||
);
|
||||
|
||||
+126
@@ -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);
|
||||
});
|
||||
});
|
||||
+10
-2
@@ -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+/));
|
||||
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user