feat(dolt): add native profile, branch database discovery, and SQL completion
This commit is contained in:
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,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,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user