feat(dolt): add native profile, branch database discovery, and SQL completion

This commit is contained in:
hanxuanyu authored and GitHub committed 2026-08-10 00:37:34 +08:00
1 parent 56edbd3656
commit 681c6f35c5
19 files changed
+304 -23

No files matched your search

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { DOLT_SQL_ROUTINES, doltSqlBuiltinTerms, doltSqlRoutineSignatures, isDoltDriverProfile } from "@/lib/database/doltProfile";
describe("doltProfile", () => {
it("matches only the dedicated Dolt driver profile", () => {
expect(isDoltDriverProfile("dolt")).toBe(true);
expect(isDoltDriverProfile("DOLT")).toBe(true);
expect(isDoltDriverProfile("mysql")).toBe(false);
expect(isDoltDriverProfile()).toBe(false);
});
it("exposes Dolt routines only for Dolt connections", () => {
expect(doltSqlBuiltinTerms("mysql")).toBe("");
expect(doltSqlRoutineSignatures("mysql").size).toBe(0);
const terms = new Set(doltSqlBuiltinTerms("dolt").split(" "));
const signatures = doltSqlRoutineSignatures("dolt");
expect(terms.has("active_branch")).toBe(true);
expect(terms.has("dolt_branch")).toBe(true);
expect(terms.has("dolt_version")).toBe(true);
expect(signatures.get("DOLT_MERGE_BASE")).toEqual(["revision_a", "revision_b"]);
expect(signatures.size).toBe(DOLT_SQL_ROUTINES.length);
});
});
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { PostgreSQL } from "@codemirror/lang-sql";
import { postgresKeywordSyntaxTerms } from "@/lib/editor/codemirrorSqlDialect";
import * as langSql from "@codemirror/lang-sql";
import { createDbxCodeMirrorSqlDialect, postgresKeywordSyntaxTerms } from "@/lib/editor/codemirrorSqlDialect";
describe("codemirrorSqlDialect", () => {
it("keeps common PostgreSQL identifier names out of keyword highlighting", () => {
const keywords = new Set(postgresKeywordSyntaxTerms(PostgreSQL.spec.keywords || "").split(/\s+/));
const keywords = new Set(postgresKeywordSyntaxTerms(langSql.PostgreSQL.spec.keywords || "").split(/\s+/));
expect(keywords.has("select")).toBe(true);
expect(keywords.has("from")).toBe(true);
@@ -14,4 +14,13 @@ describe("codemirrorSqlDialect", () => {
expect(keywords.has("user")).toBe(false);
expect(keywords.has("count")).toBe(false);
});
it("adds Dolt routines to highlighting without changing standard MySQL", () => {
const doltBuiltins = new Set(createDbxCodeMirrorSqlDialect(langSql, "mysql", "mysql", "dolt").spec.builtin?.split(/\s+/));
const mysqlBuiltins = new Set(createDbxCodeMirrorSqlDialect(langSql, "mysql", "mysql", "mysql").spec.builtin?.split(/\s+/));
expect(doltBuiltins.has("dolt_branch")).toBe(true);
expect(doltBuiltins.has("dolt_merge")).toBe(true);
expect(mysqlBuiltins.has("dolt_branch")).toBe(false);
});
});
@@ -4,11 +4,11 @@ import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
describe("QueryEditor SQL signature refresh wiring", () => {
it("reconfigures signature help when the database dialect changes", () => {
it("reconfigures signature help when the database dialect or driver profile changes", () => {
expect(queryEditorSource).toContain('let sqlSignatureComp: import("@codemirror/state").Compartment | null = null;');
expect(queryEditorSource).toContain("sqlSignatureComp.of(buildSqlSignatureExtension())");
const watcherStart = queryEditorSource.indexOf("watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect]");
const watcherStart = queryEditorSource.indexOf("watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect, sqlDriverProfile]");
const watcherEnd = queryEditorSource.indexOf("\n});", watcherStart);
const dialectWatcher = queryEditorSource.slice(watcherStart, watcherEnd);
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { buildSqlCompletionItems } from "@/lib/sql/sqlCompletion";
function completionLabels(sql: string, driverProfile: string): string[] {
return buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [],
columnsByTable: new Map(),
databaseType: "mysql",
driverProfile,
}).map((item) => item.label);
}
describe("Dolt SQL completion", () => {
it("suggests Dolt routines for the Dolt profile", () => {
expect(completionLabels("DOLT_BR", "dolt")).toContain("DOLT_BRANCH");
expect(completionLabels("CALL DOLT_CH", "dolt")).toContain("DOLT_CHECKOUT");
});
it("does not expose Dolt routines to standard MySQL", () => {
expect(completionLabels("DOLT_BR", "mysql")).not.toContain("DOLT_BRANCH");
expect(completionLabels("CALL DOLT_CH", "mysql")).not.toContain("DOLT_CHECKOUT");
});
});
@@ -25,4 +25,14 @@ describe("ClickHouse signature help", () => {
expect(help?.overloads).toHaveLength(1);
expect(help?.overloads[0].parameterGroups).toEqual([["date", "INTERVAL expr unit"]]);
});
it("adds Dolt signature help only for the Dolt profile", () => {
const sql = "CALL DOLT_MERGE_BASE('main', ";
expect(getSqlFunctionSignatureHelp(sql, sql.length, "mysql", "mysql")).toBeNull();
const help = getSqlFunctionSignatureHelp(sql, sql.length, "mysql", "dolt");
expect(help?.name).toBe("DOLT_MERGE_BASE");
expect(help?.parameters).toEqual(["revision_a", "revision_b"]);
expect(help?.activeParameter).toBe(1);
});
});
@@ -0,0 +1,60 @@
export const DOLT_DRIVER_PROFILE = "dolt";
export type DoltSqlRoutineDefinition = {
name: string;
type: "procedure" | "function";
signature: string;
};
export const DOLT_SQL_ROUTINES: readonly DoltSqlRoutineDefinition[] = [
{ name: "ACTIVE_BRANCH", type: "function", signature: "" },
{ name: "DOLT_ADD", type: "procedure", signature: "arguments" },
{ name: "DOLT_BRANCH", type: "procedure", signature: "arguments" },
{ name: "DOLT_CHECKOUT", type: "procedure", signature: "arguments" },
{ name: "DOLT_CHERRY_PICK", type: "procedure", signature: "arguments" },
{ name: "DOLT_COMMIT", type: "procedure", signature: "arguments" },
{ name: "DOLT_FETCH", type: "procedure", signature: "arguments" },
{ name: "DOLT_MERGE", type: "procedure", signature: "arguments" },
{ name: "DOLT_PULL", type: "procedure", signature: "arguments" },
{ name: "DOLT_PUSH", type: "procedure", signature: "arguments" },
{ name: "DOLT_REBASE", type: "procedure", signature: "arguments" },
{ name: "DOLT_REMOTE", type: "procedure", signature: "arguments" },
{ name: "DOLT_RESET", type: "procedure", signature: "arguments" },
{ name: "DOLT_REVERT", type: "procedure", signature: "arguments" },
{ name: "DOLT_TAG", type: "procedure", signature: "arguments" },
{ name: "DOLT_DIFF", type: "function", signature: "arguments" },
{ name: "DOLT_DIFF_STAT", type: "function", signature: "arguments" },
{ name: "DOLT_DIFF_SUMMARY", type: "function", signature: "arguments" },
{ name: "DOLT_HASHOF", type: "function", signature: "revision" },
{ name: "DOLT_HASHOF_DB", type: "function", signature: "revision" },
{ name: "DOLT_HASHOF_TABLE", type: "function", signature: "table" },
{ name: "DOLT_LOG", type: "function", signature: "arguments" },
{ name: "DOLT_MERGE_BASE", type: "function", signature: "revision_a, revision_b" },
{ name: "DOLT_PATCH", type: "function", signature: "arguments" },
{ name: "DOLT_QUERY_DIFF", type: "function", signature: "query_a, query_b" },
{ name: "DOLT_REFLOG", type: "function", signature: "arguments" },
{ name: "DOLT_SCHEMA_DIFF", type: "function", signature: "arguments" },
{ name: "DOLT_VERSION", type: "function", signature: "" },
];
export function isDoltDriverProfile(driverProfile?: string): boolean {
return driverProfile?.toLowerCase() === DOLT_DRIVER_PROFILE;
}
export function doltSqlBuiltinTerms(driverProfile?: string): string {
if (!isDoltDriverProfile(driverProfile)) return "";
return DOLT_SQL_ROUTINES.map((routine) => routine.name.toLowerCase()).join(" ");
}
export function doltSqlRoutineSignatures(driverProfile?: string): Map<string, string[]> {
if (!isDoltDriverProfile(driverProfile)) return new Map();
return new Map(
DOLT_SQL_ROUTINES.map((routine) => [
routine.name,
routine.signature
.split(",")
.map((parameter) => parameter.trim())
.filter(Boolean),
]),
);
}
@@ -1,5 +1,6 @@
import type { SQLDialect } from "@codemirror/lang-sql";
import type { DatabaseType } from "@/types/database";
import { doltSqlBuiltinTerms } from "@/lib/database/doltProfile";
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse";
@@ -175,7 +176,7 @@ function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName
return dialectName === "postgres" ? langSql.PostgreSQL : dialectName === "sqlserver" ? langSql.MSSQL : langSql.MySQL;
}
export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName = "mysql", databaseType?: DatabaseType): SQLDialect {
export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName = "mysql", databaseType?: DatabaseType, driverProfile?: string): SQLDialect {
const baseDialect = codeMirrorBaseDialect(langSql, dialectName, databaseType);
const isPostgres = baseDialect === langSql.PostgreSQL;
const isSqlServer = baseDialect === langSql.MSSQL;
@@ -188,7 +189,7 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu
...baseDialect.spec,
keywords: [baseKeywords, commonKeywords, isClickHouse ? CLICKHOUSE_KEYWORDS : "", isPostgres ? POSTGRES_PLPGSQL_KEYWORDS : "", isSqlServer ? SQLSERVER_KEYWORDS : ""].filter(Boolean).join(" "),
types: [baseTypes, isClickHouse ? CLICKHOUSE_TYPES : "", isPostgres ? POSTGRES_PLPGSQL_TYPES : ""].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isClickHouse ? CLICKHOUSE_BUILTINS : "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : ""].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isClickHouse ? CLICKHOUSE_BUILTINS : "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : "", doltSqlBuiltinTerms(driverProfile)].filter(Boolean).join(" ") || undefined,
...(isClickHouse
? {
identifierQuotes: '"`',
+13 -8
View File
@@ -13,6 +13,7 @@ import { requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identifierSearch";
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { DOLT_SQL_ROUTINES, doltSqlRoutineSignatures, isDoltDriverProfile } from "@/lib/database/doltProfile";
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
@@ -1304,6 +1305,7 @@ export interface SqlCompletionProviderInput {
snippets?: SqlSnippet[];
dialect?: "mysql" | "postgres" | "sqlserver";
databaseType?: DatabaseType;
driverProfile?: string;
currentSchema?: string;
keywordCase?: SqlKeywordCase;
functionCase?: SqlKeywordCase;
@@ -1322,6 +1324,7 @@ export function buildSqlCompletionItems(
translations?: SqlCompletionTranslations;
dialect?: "mysql" | "postgres" | "sqlserver";
databaseType?: DatabaseType;
driverProfile?: string;
currentSchema?: string;
keywordCase?: SqlKeywordCase;
functionCase?: SqlKeywordCase;
@@ -1367,7 +1370,7 @@ class SqlCompletionProvider {
this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase, this.databaseType));
}
if (!preferReferencedColumns || context.suggestRoutines) {
const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType, context.openingParenAfterCursor, this.input.keywordCase, this.input.functionCase);
const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType, this.input.driverProfile, context.openingParenAfterCursor, this.input.keywordCase, this.input.functionCase);
this.items.push(...(preferReferencedColumns ? functionItems.filter((item) => item.label.toLowerCase().startsWith(context.prefix.toLowerCase())) : functionItems));
if (isOracleLikeDatabase(this.databaseType)) {
this.items.push(...buildOracleSystemValueItems(context.prefix, this.input.keywordCase));
@@ -1435,7 +1438,8 @@ class SqlCompletionProvider {
}
if (context.suggestRoutines || context.exclusiveRoutineSuggestions || context.oracleTableFunctionContext) {
this.items.push(...buildObjectItems(context, this.input.objects ?? [], this.dialect, this.databaseType, this.input.currentSchema));
const profileObjects = isDoltDriverProfile(this.input.driverProfile) ? DOLT_SQL_ROUTINES : [];
this.items.push(...buildObjectItems(context, [...(this.input.objects ?? []), ...profileObjects], this.dialect, this.databaseType, this.input.currentSchema));
}
if (context.comparisonLeftColumn && context.suggestKeywords) {
@@ -1654,7 +1658,7 @@ export function getSqlCompletionResultValidFor(sql: string, cursor: number): Reg
return undefined;
}
export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databaseType?: DatabaseType): SqlFunctionSignatureHelp | null {
export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databaseType?: DatabaseType, driverProfile?: string): SqlFunctionSignatureHelp | null {
const beforeCursor = sql.slice(0, cursor);
const call = findActiveFunctionCall(beforeCursor);
if (!call) return null;
@@ -1662,7 +1666,7 @@ export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databas
const observedParameter = countTopLevelCommas(call.groupText);
if (databaseType !== "clickhouse") {
const lookupName = call.name.toUpperCase();
const parameters = (databaseType ? DATABASE_FUNCTION_SIGNATURES[databaseType]?.get(lookupName) : undefined) ?? SQL_FUNCTION_SIGNATURES.get(lookupName);
const parameters = activeFunctionSignatures(databaseType, driverProfile).get(lookupName);
if (!parameters) return null;
const activeParameter = Math.min(observedParameter, Math.max(0, parameters.length - 1));
const signature = `${lookupName}(${parameters.join(", ")})`;
@@ -4167,13 +4171,14 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
});
}
function activeFunctionSignatures(databaseType?: DatabaseType): Map<string, string[]> {
function activeFunctionSignatures(databaseType?: DatabaseType, driverProfile?: string): Map<string, string[]> {
const commonFunctionNames = databaseType === "cloudflare-d1" ? CLOUDFLARE_D1_COMMON_FUNCTION_NAMES : COMMON_SQL_FUNCTION_NAMES;
const signatures = databaseType ? new Map(Array.from(SQL_FUNCTION_SIGNATURES.entries()).filter(([name]) => commonFunctionNames.has(name))) : new Map(SQL_FUNCTION_SIGNATURES);
const databaseSignatures = databaseType ? DATABASE_FUNCTION_SIGNATURES[databaseType] : undefined;
if (databaseSignatures) {
for (const [name, parameters] of databaseSignatures) signatures.set(name, parameters);
}
for (const [name, parameters] of doltSqlRoutineSignatures(driverProfile)) signatures.set(name, parameters);
return signatures;
}
@@ -4215,11 +4220,11 @@ function buildClickHouseFunctionItems(prefix: string, omitOpeningParen: boolean,
});
}
function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<string, string>, databaseType?: DatabaseType, omitOpeningParen = false, keywordCase?: SqlKeywordCase, functionCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<string, string>, databaseType?: DatabaseType, driverProfile?: string, omitOpeningParen = false, keywordCase?: SqlKeywordCase, functionCase?: SqlKeywordCase): SqlCompletionItem[] {
if (databaseType === "clickhouse") return buildClickHouseFunctionItems(prefix, omitOpeningParen);
const items: SqlCompletionItem[] = [];
for (const [name, parameters] of activeFunctionSignatures(databaseType).entries()) {
for (const [name, parameters] of activeFunctionSignatures(databaseType, driverProfile).entries()) {
if (!matchesPrefix(name, prefix)) continue;
const functionName = applySqlFunctionCase(name, functionCase);
const paramStr = parameters.length > 0 ? parameters.map((p) => `\${${applyGeneratedSqlTemplateKeywordCase(p, keywordCase)}}`).join(", ") : "";
@@ -4227,7 +4232,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<str
items.push({
label: functionName,
type: "function" as const,
detail: functionDescriptions.get(name) ?? "function",
detail: functionDescriptions.get(name) ?? (isDoltDriverProfile(driverProfile) && name.startsWith("DOLT_") ? "Dolt routine" : "function"),
apply: mysqlApply ? `${functionName}${applyGeneratedSqlTemplateKeywordCase(mysqlApply.slice(name.length), keywordCase)}` : `${functionName}(${paramStr})`,
boost: computeBoost(name, prefix) + 300,
});