feat(dolt): improve system tables and SQL completion

This commit is contained in:
hanxuanyu authored and GitHub committed 2026-08-14 22:18:28 +08:00
1 parent c5eea21821
commit a574459f8b
30 files changed
+789 -70

No files matched your search

@@ -48,6 +48,7 @@ import { mongodbAuthFailureHint, mongoUrlParam, mongoUrlParamIsTrue, normalizeMo
import { isMongoLegacyDriverProfile } from "@/lib/mongo/mongoCapabilities";
import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/database/mysqlConnectionOptions";
import { applyDamengSslUrlParams, damengSslFormConfig } from "@/lib/database/damengSslOptions";
import { doltSystemTablesVisible, isDoltDriverProfile, setDoltSystemTablesVisible } from "@/lib/database/doltProfile";
import { DamengJvmSystemPropertyError, damengJvmSystemPropertiesText, parseDamengJvmSystemProperties } from "@/lib/database/damengJvmOptions";
import { copyToClipboard } from "@/lib/common/clipboard";
import { configuredDatabaseProductName, connectionConfigFingerprint, databaseInfoCopyText, databaseInfoRows, normalizeDatabaseConnectionInfo, type DatabaseInfoField } from "@/lib/connection/connectionDatabaseInfo";
@@ -3065,6 +3066,11 @@ const showGenericUrlParamsHint = computed(() => form.value.db_type === "mysql" |
const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)));
const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value));
const supportsDoltSystemTables = computed(() => isDoltDriverProfile(form.value.driver_profile));
const showDoltSystemTables = computed({
get: () => doltSystemTablesVisible(form.value),
set: (visible: boolean) => setDoltSystemTablesVisible(form.value, visible),
});
const mysqlCleartextPasswordAuth = computed({
get: () => mysqlCleartextPasswordAuthEnabled(form.value.url_params),
set: (value: boolean) => {
@@ -3862,7 +3868,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
const style = gaussdbIdentifierQuoteStyle(config);
config.external_config = undefined;
setGaussdbIdentifierQuoteStyle(config, style);
} else {
} else if (!isDoltDriverProfile(config.driver_profile)) {
config.external_config = undefined;
}
if (config.db_type === "mongodb" && !mongoUseUrl.value) {
@@ -7869,6 +7875,13 @@ function openExternalUrl(url: string) {
<Input v-model.number="form.keepalive_interval_secs" type="number" min="1" max="3600" step="1" class="flex-1" :disabled="!keepaliveEnabled" />
</div>
</div>
<div v-if="supportsDoltSystemTables" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.doltShowSystemTables") }}</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
<input v-model="showDoltSystemTables" type="checkbox" class="mr-0" />
<span class="text-xs text-muted-foreground">{{ t("connection.doltShowSystemTablesHint") }}</span>
</label>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.readOnly") }}</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
@@ -50,6 +50,7 @@ import {
extractCteDefinitions,
} from "@/lib/sql/sqlCompletion";
import { originForSqlCompletionProvider, originForTypedSqlCompletionStart, shouldAllowSqlCompletionTrigger, type SqlCompletionTriggerFacts, type SqlCompletionTriggerOrigin } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { driverProfileHasCompletionCandidates } from "@/lib/database/driverProfileExtensions";
import { sqlCompletionContextFromSemantic, sqlSemanticSelectStarIsOnlyProjection, sqlSemanticSelectStarQualifierSql, sqlSemanticSelectStarTableSources } from "@/lib/sql/semantic/completion";
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
@@ -3765,7 +3766,14 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
}
}
if (tables.length === 0 && completionObjects.length === 0 && schemaNames.length === 0 && columnsByTable.size === 0 && (completionContext.exclusiveTableSuggestions || completionContext.exclusiveColumnSuggestions || completionContext.exclusiveRoutineSuggestions)) {
if (
tables.length === 0 &&
completionObjects.length === 0 &&
schemaNames.length === 0 &&
columnsByTable.size === 0 &&
!driverProfileHasCompletionCandidates(sqlDriverProfile.value, completionContext) &&
(completionContext.exclusiveTableSuggestions || completionContext.exclusiveColumnSuggestions || completionContext.exclusiveRoutineSuggestions)
) {
return null;
}
@@ -292,7 +292,7 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was
.catch(() => {});
});
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const simpleObjectParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema"]);
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "sequence", "synonym", "package", "package-body", "type", "type-body", "load-more"]);
@@ -1392,7 +1392,12 @@ async function ensureTableObjectGroupsLoaded(target: Extract<ActiveTabSidebarTar
function findTableObjectGroupNodes(nodes: TreeNode[], target: Extract<ActiveTabSidebarTarget, { type: "table" }>): TreeNode[] {
const matches: TreeNode[] = [];
for (const node of nodes) {
if ((node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views") && node.connectionId === target.connectionId && sameTreeName(node.database, target.database) && (!target.schema || sameTreeName(node.schema, target.schema))) {
if (
(node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views") &&
node.connectionId === target.connectionId &&
sameTreeName(node.database, target.database) &&
(!target.schema || sameTreeName(node.schema, target.schema))
) {
matches.push(node);
}
if (node.children) {
@@ -5219,7 +5219,7 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
const hasMongoCreateIndexAction = node.type === "group-indexes" && canCreateMongoIndex.value;
const hasMongoDropAllIndexesAction = node.type === "group-indexes" && canDropAllMongoIndexes.value;
const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database) || !!mysqlObjectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction;
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views";
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views";
if (node.type === "group-tables" && canCreateTable.value) {
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
if (canOpenTableImport.value) {
@@ -326,6 +326,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Columns3, colorClass: "text-muted-foreground" };
case "group-tables":
return { icon: Table, colorClass: "text-green-500" };
case "group-dolt-system-tables":
return { icon: Table, colorClass: "text-slate-500" };
case "group-views":
return { icon: Eye, colorClass: "text-purple-500" };
case "group-materialized-views":
@@ -1323,6 +1325,7 @@ function onKeydown(event: KeyboardEvent) {
<span
v-if="
(node.type === 'group-tables' ||
node.type === 'group-dolt-system-tables' ||
node.type === 'group-views' ||
node.type === 'group-materialized-views' ||
node.type === 'group-procedures' ||
+3
View File
@@ -1051,6 +1051,8 @@ export default {
keepaliveInterval: "Keepalive Interval (seconds)",
readOnly: "Read Only",
readOnlyHint: "Block all write operations (INSERT, UPDATE, DELETE, etc.)",
doltShowSystemTables: "Show Dolt System Tables",
doltShowSystemTablesHint: "Show Dolt's built-in version-control tables in table lists for this connection.",
showSystemSchemas: "Show System Schemas",
showSystemSchemasHint: "Show built-in and metadata schemas in the sidebar and schema pickers for this connection.",
docsNotesPath: "Notes file",
@@ -2972,6 +2974,7 @@ export default {
triggers: "Triggers",
constraints: "Constraints",
tables: "Tables",
doltSystemTables: "Dolt System Tables",
tableNameFilterActive: "filtered",
views: "Views",
materializedViews: "Materialized Views",
+3
View File
@@ -1111,6 +1111,8 @@ export default withEnglishFallback({
mqttClientCertPath: "Ruta del certificado de cliente",
mqttClientKeyPath: "Ruta de la clave privada del cliente",
mqttNoLocal: "Prohibir reenvío local",
doltShowSystemTables: "Mostrar tablas integradas de Dolt",
doltShowSystemTablesHint: "Muestra las tablas integradas de control de versiones de Dolt en la lista de tablas de la conexión actual.",
},
editor: {
duckdbDraining: "La consulta anterior de DuckDB aún se está deteniendo. Vuelva a intentarlo en breve.",
@@ -2917,6 +2919,7 @@ export default withEnglishFallback({
types: "tipo",
gridfs: "GridFS",
buckets: "Buckets",
doltSystemTables: "Tablas del sistema de Dolt",
},
userAdmin: {
title: "Usuarios y Privilegios",
+3
View File
@@ -1109,6 +1109,8 @@ export default withEnglishFallback({
mqttClientCertPath: "Percorso del certificato client",
mqttClientKeyPath: "Percorso della chiave privata client",
mqttNoLocal: "Disabilita inoltro locale",
doltShowSystemTables: "Mostra le tabelle integrate di Dolt",
doltShowSystemTablesHint: "Mostra le tabelle di controllo versione integrate di Dolt nell'elenco delle tabelle della connessione corrente.",
},
editor: {
duckdbDraining: "La query DuckDB precedente è ancora in fase di arresto. Riprova a breve.",
@@ -2915,6 +2917,7 @@ export default withEnglishFallback({
types: "Tipi",
gridfs: "GridFS",
buckets: "Buckets",
doltSystemTables: "Tabelle di sistema di Dolt",
},
userAdmin: {
title: "Utenti e Privilegi",
+3
View File
@@ -1130,6 +1130,8 @@ export default withEnglishFallback({
mqttClientCertPath: "クライアント証明書パス",
mqttClientKeyPath: "クライアント秘密鍵パス",
mqttNoLocal: "ローカル転送禁止",
doltShowSystemTables: "Dolt 組み込みテーブルを表示",
doltShowSystemTablesHint: "現在の接続のテーブルリストに Dolt 組み込みのバージョン管理テーブルを表示します。",
},
editor: {
duckdbDraining: "前回の DuckDB クエリはまだ停止処理中です。しばらくしてから再試行してください。",
@@ -2942,6 +2944,7 @@ export default withEnglishFallback({
types: "タイプ",
gridfs: "GridFS",
buckets: "バケット",
doltSystemTables: "Dolt システムテーブル",
},
consul,
sqlServerTrace,
+3
View File
@@ -1110,6 +1110,8 @@ export default withEnglishFallback({
mqttClientCertPath: "Caminho do certificado do cliente",
mqttClientKeyPath: "Caminho da chave privada do cliente",
mqttNoLocal: "Proibir encaminhamento local",
doltShowSystemTables: "Exibir tabelas internas do Dolt",
doltShowSystemTablesHint: "Exibir as tabelas de controle de versão internas do Dolt na lista de tabelas da conexão atual.",
},
editor: {
duckdbDraining: "A consulta anterior do DuckDB ainda está sendo interrompida. Tente novamente em breve.",
@@ -2917,6 +2919,7 @@ export default withEnglishFallback({
types: "Tipos",
gridfs: "GridFS",
buckets: "Buckets",
doltSystemTables: "Tabelas de sistema do Dolt",
},
userAdmin: {
title: "Usuários e Privilégios",
+3
View File
@@ -978,6 +978,8 @@ export default withEnglishFallback({
keepaliveInterval: "保持连接间隔(秒)",
readOnly: "只读模式",
readOnlyHint: "阻止所有写操作(INSERT、UPDATE、DELETE 等)",
doltShowSystemTables: "显示 Dolt 内置表",
doltShowSystemTablesHint: "在当前连接的表列表中显示 Dolt 内置的版本控制表。",
showSystemSchemas: "显示系统 Schema",
showSystemSchemasHint: "为当前连接在侧边栏和 Schema 选择器中显示内置/元数据 Schema。",
docsNotesPath: "笔记文件",
@@ -2896,6 +2898,7 @@ export default withEnglishFallback({
triggers: "触发器",
constraints: "约束",
tables: "表",
doltSystemTables: "Dolt 系统表",
tableNameFilterActive: "存在过滤",
views: "视图",
materializedViews: "物化视图",
+3
View File
@@ -1007,6 +1007,8 @@ export default withEnglishFallback({
rocketmqSecretKey: "Secret Key",
rocketmqAclAuth: "ACL",
jdbcMissingRuntimeDependencyHint: "目前 JDBC 驅動缺少執行依賴。請在「驅動管理」中使用 Maven 座標安裝,或一次匯入驅動及全部依賴 JAR。",
doltShowSystemTables: "顯示 Dolt 內建資料表",
doltShowSystemTablesHint: "在目前連線的資料表清單中顯示 Dolt 內建的版本控制資料表。",
showSystemSchemas: "顯示系統 Schema",
showSystemSchemasHint: "為目前連線在側邊欄和 Schema 選擇器中顯示內建/中繼資料 Schema。",
docsNotesPath: "筆記檔案",
@@ -2900,6 +2902,7 @@ export default withEnglishFallback({
triggers: "觸發器",
constraints: "約束",
tables: "資料表",
doltSystemTables: "Dolt 系統資料表",
tableNameFilterActive: "存在篩選",
views: "檢視",
procedures: "預存程序",
+111 -1
View File
@@ -1,5 +1,66 @@
import { describe, expect, it } from "vitest";
import { DOLT_SQL_ROUTINES, doltSqlBuiltinTerms, doltSqlRoutineSignatures, isDoltDriverProfile } from "@/lib/database/doltProfile";
import { DOLT_SQL_ROUTINES, doltObjectTreeProfileForConnection, doltSqlBuiltinTerms, doltSqlRoutineSignatures, doltSystemTablesVisible, isDoltDriverProfile, setDoltSystemTablesVisible } from "@/lib/database/doltProfile";
import type { ConnectionConfig } from "@/types/database";
function doltConfig(showDoltSystemTables = false): ConnectionConfig {
return {
id: "dolt-1",
name: "Dolt",
db_type: "mysql",
driver_profile: "dolt",
host: "127.0.0.1",
port: 3306,
username: "root",
password: "",
database: "app",
external_config: showDoltSystemTables ? { doltShowSystemTables: true } : undefined,
};
}
const documentedDoltProcedures = [
"DOLT_ADD",
"DOLT_BACKUP",
"DOLT_BRANCH",
"DOLT_CHECKOUT",
"DOLT_CHERRY_PICK",
"DOLT_CLEAN",
"DOLT_CLONE",
"DOLT_COMMIT",
"DOLT_COMMIT_HASH_OUT",
"DOLT_CONFLICTS_RESOLVE",
"DOLT_COUNT_COMMITS",
"DOLT_FETCH",
"DOLT_GC",
"DOLT_MERGE",
"DOLT_PULL",
"DOLT_PURGE_DROPPED_DATABASES",
"DOLT_PUSH",
"DOLT_REBASE",
"DOLT_REMOTE",
"DOLT_RESET",
"DOLT_REVERT",
"DOLT_RM",
"DOLT_SQUASH_HISTORY",
"DOLT_STASH",
"DOLT_TAG",
"DOLT_THREAD_DUMP",
"DOLT_UNDROP",
"DOLT_UPDATE_COLUMN_TAG",
"DOLT_VERIFY_CONSTRAINTS",
"DOLT_STATS_RESTART",
"DOLT_STATS_STOP",
"DOLT_STATS_PURGE",
"DOLT_STATS_ONCE",
"DOLT_STATS_WAIT",
"DOLT_STATS_FLUSH",
"DOLT_STATS_GC",
"DOLT_STATS_INFO",
"DOLT_STATS_TIMERS",
];
const documentedDoltScalarFunctions = ["ACTIVE_BRANCH", "DOLT_MERGE_BASE", "DOLT_HASHOF", "DOLT_HASHOF_DB", "DOLT_HASHOF_TABLE", "DOLT_STORAGE_FORMAT", "DOLT_VERSION", "HAS_ANCESTOR", "DOLT_JOIN_COST"];
const documentedDoltTableFunctions = ["DOLT_DIFF", "DOLT_DIFF_STAT", "DOLT_DIFF_SUMMARY", "DOLT_JSON_DIFF", "DOLT_LOG", "DOLT_PATCH", "DOLT_PREVIEW_MERGE_CONFLICTS_SUMMARY", "DOLT_PREVIEW_MERGE_CONFLICTS", "DOLT_REFLOG", "DOLT_SCHEMA_DIFF", "DOLT_QUERY_DIFF", "DOLT_BRANCH_STATUS", "DOLT_TEST_RUN"];
describe("doltProfile", () => {
it("matches only the dedicated Dolt driver profile", () => {
@@ -21,4 +82,53 @@ describe("doltProfile", () => {
expect(signatures.get("DOLT_MERGE_BASE")).toEqual(["revision_a", "revision_b"]);
expect(signatures.size).toBe(DOLT_SQL_ROUTINES.length);
});
it("keeps the documented Dolt routine catalog separated by SQL usage", () => {
expect(DOLT_SQL_ROUTINES.filter((routine) => routine.type === "procedure").map((routine) => routine.name)).toEqual(documentedDoltProcedures);
expect(DOLT_SQL_ROUTINES.filter((routine) => routine.type === "scalar-function").map((routine) => routine.name)).toEqual(documentedDoltScalarFunctions);
expect(DOLT_SQL_ROUTINES.filter((routine) => routine.type === "table-function").map((routine) => routine.name)).toEqual(documentedDoltTableFunctions);
});
it("stores system-table visibility in the existing profile config slot", () => {
const config = { ...doltConfig(), external_config: { retained: true } };
setDoltSystemTablesVisible(config, true);
expect(config.external_config).toEqual({ retained: true, doltShowSystemTables: true });
expect(doltSystemTablesVisible(config)).toBe(true);
setDoltSystemTablesVisible(config, false);
expect(config.external_config).toEqual({ retained: true });
expect(doltSystemTablesVisible(config)).toBe(false);
});
it("defines independent table ranges while Dolt system tables are visible", () => {
expect(doltObjectTreeProfileForConnection(doltConfig(true))).toEqual({
cacheKey: "dolt-system-tables-v1:shown",
groupOverrides: [
{
nodeType: "group-tables",
tableNameFilter: { includePatterns: [], excludePatterns: ["dolt%"] },
},
{
nodeType: "group-dolt-system-tables",
label: "tree.doltSystemTables",
tableNameFilter: { includePatterns: ["dolt%"], excludePatterns: [] },
},
],
});
});
it("uses a separate hidden cache scope without changing ordinary MySQL", () => {
expect(doltObjectTreeProfileForConnection(doltConfig())).toEqual({ cacheKey: "dolt-system-tables-v1:hidden", groupOverrides: [] });
expect(doltObjectTreeProfileForConnection({ ...doltConfig(), driver_profile: "mysql" })).toBeUndefined();
});
it("tracks the current Dolt procedure and scalar-function registries", () => {
const routines = new Map(DOLT_SQL_ROUTINES.map((routine) => [routine.name, routine.type]));
expect(routines.get("DOLT_COUNT_COMMITS")).toBe("procedure");
expect(routines.get("DOLT_THREAD_DUMP")).toBe("procedure");
expect(routines.get("DOLT_STATS_TIMERS")).toBe("procedure");
expect(routines.get("DOLT_STORAGE_FORMAT")).toBe("scalar-function");
expect(routines.has("LAST_INSERT_UUID")).toBe(false);
});
});
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileExtension, driverProfileObjectTreeProfileForConnection } from "@/lib/database/driverProfileExtensions";
import type { ConnectionConfig } from "@/types/database";
const completionContext = {
statementKind: "select" as const,
suggestTables: false,
suggestRoutines: true,
exclusiveRoutineSuggestions: false,
prefix: "DOLT_",
openingParenAfterCursor: false,
};
function connection(driverProfile: string): ConnectionConfig {
return {
id: driverProfile,
name: driverProfile,
db_type: "mysql",
driver_profile: driverProfile,
host: "127.0.0.1",
port: 3306,
username: "root",
password: "",
database: "app",
external_config: { doltShowSystemTables: true },
};
}
describe("driverProfileExtensions", () => {
it("resolves Dolt capabilities through the generic registry", () => {
expect(driverProfileExtension("DOLT")?.id).toBe("dolt");
expect(driverProfileObjectTreeProfileForConnection(connection("dolt"))?.groupOverrides.map((group) => group.nodeType)).toEqual(["group-tables", "group-dolt-system-tables"]);
expect(driverProfileCompletionObjects("dolt", completionContext).map((object) => object.name)).toContain("DOLT_HASHOF");
expect(driverProfileCompletionTables("dolt", { ...completionContext, suggestTables: true }).map((table) => table.name)).toContain("DOLT_LOG");
expect(driverProfileCompletionTableMetadata("dolt", "dolt_log")).toEqual({ detail: "Dolt system table", boost: -1200 });
});
it("leaves ordinary MySQL without profile capabilities", () => {
expect(driverProfileExtension("mysql")).toBeUndefined();
expect(driverProfileObjectTreeProfileForConnection(connection("mysql"))).toBeUndefined();
expect(driverProfileCompletionObjects("mysql", completionContext)).toEqual([]);
expect(driverProfileCompletionTables("mysql", { ...completionContext, suggestTables: true })).toEqual([]);
expect(driverProfileCompletionTableMetadata("mysql", "dolt_log")).toBeUndefined();
});
});
@@ -1,24 +1,73 @@
import { describe, expect, it } from "vitest";
import { buildSqlCompletionItems } from "@/lib/sql/sqlCompletion";
import { buildSqlCompletionItems, shouldAutoOpenSqlCompletion, type SqlCompletionObject, type SqlCompletionTable } from "@/lib/sql/sqlCompletion";
function completionLabels(sql: string, driverProfile: string): string[] {
function completionItems(sql: string, driverProfile: string, tables: SqlCompletionTable[] = [], objects: SqlCompletionObject[] = []) {
return buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [],
tables,
objects,
columnsByTable: new Map(),
databaseType: "mysql",
driverProfile,
}).map((item) => item.label);
});
}
function completionLabels(sql: string, driverProfile: string): string[] {
return completionItems(sql, 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");
expect(completionLabels("CALL DOLT_STASH", "dolt")).toContain("DOLT_STASH");
expect(completionLabels("HAS_ANC", "dolt")).toContain("HAS_ANCESTOR");
expect(completionLabels("DOLT_PREVIEW_MERGE_C", "dolt")).toContain("DOLT_PREVIEW_MERGE_CONFLICTS");
});
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");
});
it("filters Dolt routines by CALL, SELECT, and table contexts", () => {
expect(shouldAutoOpenSqlCompletion("CALL ", "CALL ".length, { databaseType: "mysql" })).toBe(true);
expect(completionLabels("CALL ", "dolt")).toContain("DOLT_CHECKOUT");
const callLabels = completionLabels("CALL DOLT_", "dolt");
expect(callLabels).toContain("DOLT_CHECKOUT");
expect(callLabels).not.toContain("DOLT_HASHOF");
expect(callLabels).not.toContain("DOLT_LOG");
const selectLabels = completionLabels("SELECT DOLT_", "dolt");
expect(selectLabels).toContain("DOLT_HASHOF");
expect(selectLabels).not.toContain("DOLT_CHECKOUT");
expect(selectLabels).not.toContain("DOLT_LOG");
const fromItems = completionItems("SELECT * FROM DOLT_", "dolt");
expect(fromItems.find((item) => item.label === "DOLT_LOG")).toMatchObject({ detail: "Dolt table function", type: "table" });
expect(fromItems.map((item) => item.label)).not.toContain("DOLT_CHECKOUT");
expect(fromItems.map((item) => item.label)).not.toContain("DOLT_HASHOF");
});
it("marks Dolt system tables and keeps user tables ahead of them", () => {
const items = completionItems("SELECT * FROM ", "dolt", [{ name: "dolt_log" }, { name: "orders" }]).filter((item) => item.type === "table");
const userTableIndex = items.findIndex((item) => item.label === "orders");
const systemTableIndex = items.findIndex((item) => item.label === "dolt_log");
expect(items[systemTableIndex]).toMatchObject({ detail: "Dolt system table" });
expect(userTableIndex).toBeGreaterThanOrEqual(0);
expect(systemTableIndex).toBeGreaterThan(userTableIndex);
expect(completionItems("SELECT * FROM dolt_", "mysql", [{ name: "dolt_log" }]).find((item) => item.label === "dolt_log")?.detail).not.toBe("Dolt system table");
});
it("does not change package completion for other database families", () => {
const labels = buildSqlCompletionItems("SELECT DBMS_", "SELECT DBMS_".length, {
tables: [],
objects: [{ name: "DBMS_OUTPUT", type: "package" }],
columnsByTable: new Map(),
databaseType: "oracle",
}).map((item) => item.label);
expect(labels).toContain("DBMS_OUTPUT");
});
});
@@ -34,5 +34,8 @@ describe("ClickHouse signature help", () => {
expect(help?.name).toBe("DOLT_MERGE_BASE");
expect(help?.parameters).toEqual(["revision_a", "revision_b"]);
expect(help?.activeParameter).toBe(1);
const procedureSql = "CALL DOLT_COMMIT_HASH_OUT(";
expect(getSqlFunctionSignatureHelp(procedureSql, procedureSql.length, "mysql", "dolt")?.parameters).toEqual(["out_hash", "arguments"]);
});
});
+153 -24
View File
@@ -1,46 +1,167 @@
import type { ConnectionConfig } from "@/types/database";
import type { DriverProfileExtensionDefinition, DriverProfileObjectTreeProfile, DriverProfileSqlCompletionContext } from "@/lib/database/driverProfileExtensions";
export const DOLT_DRIVER_PROFILE = "dolt";
const DOLT_SHOW_SYSTEM_TABLES_CONFIG_KEY = "doltShowSystemTables";
// Mirrors Dolt's IsSystemTable / HasDoltPrefix implementation. The reserved
// namespace comparison is case-insensitive and currently checks the "dolt" prefix.
export const DOLT_SYSTEM_TABLE_NAME_PATTERN = "dolt%";
export type DoltSqlRoutineDefinition = {
name: string;
type: "procedure" | "function";
type: "procedure" | "scalar-function" | "table-function";
signature: string;
};
// Keep this list aligned with Dolt's documented SQL procedures and functions:
// https://dolthub.com/docs/sql-reference/version-control/sql-extensions
export const DOLT_SQL_ROUTINES: readonly DoltSqlRoutineDefinition[] = [
{ name: "ACTIVE_BRANCH", type: "function", signature: "" },
{ name: "DOLT_ADD", type: "procedure", signature: "arguments" },
{ name: "DOLT_BACKUP", 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_CLEAN", type: "procedure", signature: "arguments" },
{ name: "DOLT_CLONE", type: "procedure", signature: "arguments" },
{ name: "DOLT_COMMIT", type: "procedure", signature: "arguments" },
{ name: "DOLT_COMMIT_HASH_OUT", type: "procedure", signature: "out_hash, arguments" },
{ name: "DOLT_CONFLICTS_RESOLVE", type: "procedure", signature: "arguments" },
{ name: "DOLT_COUNT_COMMITS", type: "procedure", signature: "revision_a, revision_b" },
{ name: "DOLT_FETCH", type: "procedure", signature: "arguments" },
{ name: "DOLT_GC", type: "procedure", signature: "arguments" },
{ name: "DOLT_MERGE", type: "procedure", signature: "arguments" },
{ name: "DOLT_PULL", type: "procedure", signature: "arguments" },
{ name: "DOLT_PURGE_DROPPED_DATABASES", 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_RM", type: "procedure", signature: "arguments" },
{ name: "DOLT_SQUASH_HISTORY", type: "procedure", signature: "arguments" },
{ name: "DOLT_STASH", 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: "" },
{ name: "DOLT_THREAD_DUMP", type: "procedure", signature: "" },
{ name: "DOLT_UNDROP", type: "procedure", signature: "arguments" },
{ name: "DOLT_UPDATE_COLUMN_TAG", type: "procedure", signature: "arguments" },
{ name: "DOLT_VERIFY_CONSTRAINTS", type: "procedure", signature: "arguments" },
{ name: "DOLT_STATS_RESTART", type: "procedure", signature: "" },
{ name: "DOLT_STATS_STOP", type: "procedure", signature: "" },
{ name: "DOLT_STATS_PURGE", type: "procedure", signature: "" },
{ name: "DOLT_STATS_ONCE", type: "procedure", signature: "" },
{ name: "DOLT_STATS_WAIT", type: "procedure", signature: "" },
{ name: "DOLT_STATS_FLUSH", type: "procedure", signature: "" },
{ name: "DOLT_STATS_GC", type: "procedure", signature: "" },
{ name: "DOLT_STATS_INFO", type: "procedure", signature: "arguments" },
{ name: "DOLT_STATS_TIMERS", type: "procedure", signature: "" },
{ name: "ACTIVE_BRANCH", type: "scalar-function", signature: "" },
{ name: "DOLT_MERGE_BASE", type: "scalar-function", signature: "revision_a, revision_b" },
{ name: "DOLT_HASHOF", type: "scalar-function", signature: "revision" },
{ name: "DOLT_HASHOF_DB", type: "scalar-function", signature: "revision" },
{ name: "DOLT_HASHOF_TABLE", type: "scalar-function", signature: "table" },
{ name: "DOLT_STORAGE_FORMAT", type: "scalar-function", signature: "" },
{ name: "DOLT_VERSION", type: "scalar-function", signature: "" },
{ name: "HAS_ANCESTOR", type: "scalar-function", signature: "target, ancestor" },
{ name: "DOLT_JOIN_COST", type: "scalar-function", signature: "query" },
{ name: "DOLT_DIFF", type: "table-function", signature: "from_revision, to_revision, table" },
{ name: "DOLT_DIFF_STAT", type: "table-function", signature: "from_revision, to_revision, table" },
{ name: "DOLT_DIFF_SUMMARY", type: "table-function", signature: "from_revision, to_revision, table" },
{ name: "DOLT_JSON_DIFF", type: "table-function", signature: "from_document, to_document" },
{ name: "DOLT_LOG", type: "table-function", signature: "arguments" },
{ name: "DOLT_PATCH", type: "table-function", signature: "from_revision, to_revision, table" },
{ name: "DOLT_PREVIEW_MERGE_CONFLICTS_SUMMARY", type: "table-function", signature: "base_branch, merge_branch" },
{ name: "DOLT_PREVIEW_MERGE_CONFLICTS", type: "table-function", signature: "base_branch, merge_branch, table" },
{ name: "DOLT_REFLOG", type: "table-function", signature: "arguments" },
{ name: "DOLT_SCHEMA_DIFF", type: "table-function", signature: "from_revision, to_revision, table" },
{ name: "DOLT_QUERY_DIFF", type: "table-function", signature: "query_a, query_b" },
{ name: "DOLT_BRANCH_STATUS", type: "table-function", signature: "base_refspec, target_refspecs" },
{ name: "DOLT_TEST_RUN", type: "table-function", signature: "arguments" },
];
function doltRoutineParameters(signature: string): string[] {
return signature
.split(",")
.map((parameter) => parameter.trim())
.filter(Boolean);
}
function doltRoutineApplyName(routine: DoltSqlRoutineDefinition, openingParenAfterCursor: boolean): string {
if (openingParenAfterCursor) return routine.name;
const parameters = doltRoutineParameters(routine.signature);
return `${routine.name}(${parameters.map((parameter, index) => `\${${index + 1}:${parameter}}`).join(", ")})`;
}
function doltCompletionObjects(context: DriverProfileSqlCompletionContext) {
const routineTypes = context.exclusiveRoutineSuggestions
? new Set<DoltSqlRoutineDefinition["type"]>(["procedure"])
: context.statementKind === "select"
? new Set<DoltSqlRoutineDefinition["type"]>(["scalar-function"])
: context.suggestTables
? new Set<DoltSqlRoutineDefinition["type"]>()
: new Set<DoltSqlRoutineDefinition["type"]>(["procedure", "scalar-function", "table-function"]);
return DOLT_SQL_ROUTINES.filter((routine) => routineTypes.has(routine.type)).map((routine) => ({
name: routine.name,
type: routine.type === "procedure" ? ("procedure" as const) : ("function" as const),
signature: routine.signature,
boost: routine.type === "table-function" ? -200 : 0,
}));
}
function doltCompletionTables(context: DriverProfileSqlCompletionContext) {
if (!context.suggestTables) return [];
return DOLT_SQL_ROUTINES.filter((routine) => routine.type === "table-function").map((routine) => ({
name: routine.name,
detail: "Dolt table function",
applyName: doltRoutineApplyName(routine, context.openingParenAfterCursor),
boost: 200,
}));
}
export function isDoltDriverProfile(driverProfile?: string): boolean {
return driverProfile?.toLowerCase() === DOLT_DRIVER_PROFILE;
}
type DoltConnectionConfig = Pick<ConnectionConfig, "driver_profile"> & Partial<Pick<ConnectionConfig, "external_config">>;
function externalConfigRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record<string, unknown>) } : {};
}
export function doltSystemTablesVisible(config?: DoltConnectionConfig): boolean {
if (!config || !isDoltDriverProfile(config.driver_profile)) return false;
return externalConfigRecord(config.external_config)[DOLT_SHOW_SYSTEM_TABLES_CONFIG_KEY] === true;
}
export function setDoltSystemTablesVisible(config: DoltConnectionConfig, visible: boolean): void {
if (!isDoltDriverProfile(config.driver_profile)) return;
const external = externalConfigRecord(config.external_config);
if (visible) external[DOLT_SHOW_SYSTEM_TABLES_CONFIG_KEY] = true;
else delete external[DOLT_SHOW_SYSTEM_TABLES_CONFIG_KEY];
config.external_config = Object.keys(external).length > 0 ? external : undefined;
}
export function doltObjectTreeProfileForConnection(config?: ConnectionConfig): DriverProfileObjectTreeProfile | undefined {
if (!config || !isDoltDriverProfile(config.driver_profile)) return undefined;
const systemTablesVisible = doltSystemTablesVisible(config);
return {
cacheKey: `dolt-system-tables-v1:${systemTablesVisible ? "shown" : "hidden"}`,
groupOverrides: systemTablesVisible
? [
{
nodeType: "group-tables",
tableNameFilter: { includePatterns: [], excludePatterns: [DOLT_SYSTEM_TABLE_NAME_PATTERN] },
},
{
nodeType: "group-dolt-system-tables",
label: "tree.doltSystemTables",
tableNameFilter: { includePatterns: [DOLT_SYSTEM_TABLE_NAME_PATTERN], excludePatterns: [] },
},
]
: [],
};
}
export function doltSqlBuiltinTerms(driverProfile?: string): string {
if (!isDoltDriverProfile(driverProfile)) return "";
return DOLT_SQL_ROUTINES.map((routine) => routine.name.toLowerCase()).join(" ");
@@ -48,13 +169,21 @@ export function doltSqlBuiltinTerms(driverProfile?: string): string {
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),
]),
);
return new Map(DOLT_SQL_ROUTINES.map((routine) => [routine.name, doltRoutineParameters(routine.signature)]));
}
export const DOLT_DRIVER_PROFILE_EXTENSION: DriverProfileExtensionDefinition = {
id: DOLT_DRIVER_PROFILE,
objectTreeProfile: doltObjectTreeProfileForConnection,
completionTableMetadata: (tableName) =>
tableName.toLowerCase().startsWith("dolt")
? {
detail: "Dolt system table",
boost: -1200,
}
: undefined,
completionObjects: doltCompletionObjects,
completionTables: doltCompletionTables,
sqlBuiltinTerms: () => DOLT_SQL_ROUTINES.map((routine) => routine.name.toLowerCase()).join(" "),
routineSignatures: () => new Map(DOLT_SQL_ROUTINES.map((routine) => [routine.name, doltRoutineParameters(routine.signature)])),
};
@@ -0,0 +1,71 @@
import { DOLT_DRIVER_PROFILE_EXTENSION } from "@/lib/database/doltProfile";
import type { SqlCompletionObject, SqlCompletionTable, SqlStatementKind } from "@/lib/sql/sqlCompletion";
import type { ConnectionConfig, TableNameFilter, TreeNodeType } from "@/types/database";
export type DriverProfileObjectTreeProfile = {
cacheKey: string;
groupOverrides: Array<{
nodeType: TreeNodeType;
label?: string;
tableNameFilter: TableNameFilter;
}>;
};
export type DriverProfileSqlCompletionContext = {
statementKind: SqlStatementKind;
suggestTables: boolean;
suggestRoutines: boolean;
exclusiveRoutineSuggestions: boolean;
prefix: string;
openingParenAfterCursor: boolean;
};
export type DriverProfileCompletionTableMetadata = Pick<SqlCompletionTable, "detail" | "boost">;
export type DriverProfileExtensionDefinition = {
id: string;
objectTreeProfile?: (config: ConnectionConfig) => DriverProfileObjectTreeProfile | undefined;
completionTableMetadata?: (tableName: string) => DriverProfileCompletionTableMetadata | undefined;
completionObjects?: (context: DriverProfileSqlCompletionContext) => SqlCompletionObject[];
completionTables?: (context: DriverProfileSqlCompletionContext) => SqlCompletionTable[];
sqlBuiltinTerms?: () => string;
routineSignatures?: () => Map<string, string[]>;
};
export const DRIVER_PROFILE_EXTENSIONS = [DOLT_DRIVER_PROFILE_EXTENSION] as const satisfies readonly DriverProfileExtensionDefinition[];
export function driverProfileExtension(driverProfile?: string): DriverProfileExtensionDefinition | undefined {
const normalized = driverProfile?.trim().toLowerCase();
if (!normalized) return undefined;
return DRIVER_PROFILE_EXTENSIONS.find((extension) => extension.id.toLowerCase() === normalized);
}
export function driverProfileObjectTreeProfileForConnection(config?: ConnectionConfig): DriverProfileObjectTreeProfile | undefined {
if (!config) return undefined;
return driverProfileExtension(config.driver_profile)?.objectTreeProfile?.(config);
}
export function driverProfileCompletionTableMetadata(driverProfile: string | undefined, tableName: string): DriverProfileCompletionTableMetadata | undefined {
return driverProfileExtension(driverProfile)?.completionTableMetadata?.(tableName);
}
export function driverProfileCompletionObjects(driverProfile: string | undefined, context: DriverProfileSqlCompletionContext): SqlCompletionObject[] {
return driverProfileExtension(driverProfile)?.completionObjects?.(context) ?? [];
}
export function driverProfileCompletionTables(driverProfile: string | undefined, context: DriverProfileSqlCompletionContext): SqlCompletionTable[] {
return driverProfileExtension(driverProfile)?.completionTables?.(context) ?? [];
}
export function driverProfileHasCompletionCandidates(driverProfile: string | undefined, context: DriverProfileSqlCompletionContext): boolean {
const extension = driverProfileExtension(driverProfile);
return Boolean(extension?.completionObjects?.(context).length || extension?.completionTables?.(context).length);
}
export function driverProfileSqlBuiltinTerms(driverProfile?: string): string {
return driverProfileExtension(driverProfile)?.sqlBuiltinTerms?.() ?? "";
}
export function driverProfileRoutineSignatures(driverProfile?: string): Map<string, string[]> {
return driverProfileExtension(driverProfile)?.routineSignatures?.() ?? new Map();
}
@@ -1,6 +1,6 @@
import type { SQLDialect } from "@codemirror/lang-sql";
import type { DatabaseType } from "@/types/database";
import { doltSqlBuiltinTerms } from "@/lib/database/doltProfile";
import { driverProfileSqlBuiltinTerms } from "@/lib/database/driverProfileExtensions";
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse";
@@ -189,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 : "", doltSqlBuiltinTerms(driverProfile)].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isClickHouse ? CLICKHOUSE_BUILTINS : "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : "", driverProfileSqlBuiltinTerms(driverProfile)].filter(Boolean).join(" ") || undefined,
...(isClickHouse
? {
identifierQuotes: '"`',
@@ -49,7 +49,7 @@ export function shouldOpenObjectBrowserOnSingleClick(type: TreeNodeType, enabled
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "trigger", "sequence", "synonym", "package", "package-body", "type", "type-body"]);
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
const tableChildGroupNodeTypes = new Set<TreeNodeType>(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-constraints", "group-partitions", "group-table-partitions", "group-table-subpartitions"]);
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const displayPathObjectNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger"]);
export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null {
@@ -9,6 +9,7 @@ const treeGroupNodeTypes = new Set<TreeNodeType>([
"group-table-partitions",
"group-table-subpartitions",
"group-tables",
"group-dolt-system-tables",
"group-views",
"group-materialized-views",
"group-procedures",
@@ -111,6 +111,8 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
return { icon: FileCode, colorClass: "text-cyan-400" };
case "group-tables":
return { icon: Table, colorClass: "text-green-500" };
case "group-dolt-system-tables":
return { icon: Table, colorClass: "text-slate-500" };
case "group-views":
return { icon: Eye, colorClass: "text-purple-500" };
case "group-materialized-views":
+22 -11
View File
@@ -13,7 +13,7 @@ import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@
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";
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
@@ -1373,6 +1373,18 @@ class SqlCompletionProvider {
build(): SqlCompletionItem[] {
const { context } = this;
const pendingJoinKeyword = isPendingJoinKeywordContext(context);
const completionTables = [
...this.input.tables.map((table) => {
const metadata = driverProfileCompletionTableMetadata(this.input.driverProfile, table.name);
if (!metadata) return table;
return {
...table,
detail: table.detail ?? metadata.detail,
boost: (table.boost ?? 0) + (metadata.boost ?? 0),
};
}),
...driverProfileCompletionTables(this.input.driverProfile, context),
];
if (this.databaseType === "mongodb") {
return dedupeAndSort(buildMongoCompletionItemsFromContext({ mode: "root", prefix: context.prefix, from: 0 }).map(mongoCompletionItemToSqlCompletionItem));
@@ -1385,7 +1397,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, this.input.driverProfile, context.openingParenAfterCursor, this.input.keywordCase, this.input.functionCase);
const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType, 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));
@@ -1443,8 +1455,8 @@ class SqlCompletionProvider {
}
if (!context.exclusiveColumnSuggestions && context.suggestTables) {
this.items.push(...buildForeignKeyRelatedTableItems(context, this.input.tables, this.input.foreignKeysByTable, this.dialect, !!this.input.autoAliasTables && context.autoAliasTableCompletions, this.databaseType, this.input.keywordCase, this.input.currentSchema));
this.items.push(...buildTableItems(context, this.input.tables, this.dialect, !!this.input.autoAliasTables && context.autoAliasTableCompletions, context.referencedTables, this.databaseType, this.input.currentSchema, this.input.keywordCase));
this.items.push(...buildForeignKeyRelatedTableItems(context, completionTables, this.input.foreignKeysByTable, this.dialect, !!this.input.autoAliasTables && context.autoAliasTableCompletions, this.databaseType, this.input.keywordCase, this.input.currentSchema));
this.items.push(...buildTableItems(context, completionTables, this.dialect, !!this.input.autoAliasTables && context.autoAliasTableCompletions, context.referencedTables, this.databaseType, this.input.currentSchema, this.input.keywordCase));
if (this.databaseType === "clickhouse") {
this.items.push(...buildClickHouseFunctionItems(context.prefix, context.openingParenAfterCursor, "table"));
}
@@ -1457,7 +1469,7 @@ class SqlCompletionProvider {
}
if (context.suggestRoutines || context.exclusiveRoutineSuggestions || context.oracleTableFunctionContext) {
const profileObjects = isDoltDriverProfile(this.input.driverProfile) ? DOLT_SQL_ROUTINES : [];
const profileObjects = driverProfileCompletionObjects(this.input.driverProfile, context);
this.items.push(...buildObjectItems(context, [...(this.input.objects ?? []), ...profileObjects], this.dialect, this.databaseType, this.input.currentSchema));
}
@@ -1889,7 +1901,7 @@ export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databas
const observedParameter = countTopLevelCommas(call.groupText);
if (databaseType !== "clickhouse") {
const lookupName = call.name.toUpperCase();
const parameters = activeFunctionSignatures(databaseType, driverProfile).get(lookupName);
const parameters = activeFunctionSignatures(databaseType).get(lookupName) ?? driverProfileRoutineSignatures(driverProfile).get(lookupName);
if (!parameters) return null;
const activeParameter = Math.min(observedParameter, Math.max(0, parameters.length - 1));
const signature = `${lookupName}(${parameters.join(", ")})`;
@@ -4455,14 +4467,13 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
});
}
function activeFunctionSignatures(databaseType?: DatabaseType, driverProfile?: string): Map<string, string[]> {
function activeFunctionSignatures(databaseType?: DatabaseType): 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;
}
@@ -4504,11 +4515,11 @@ function buildClickHouseFunctionItems(prefix: string, omitOpeningParen: boolean,
});
}
function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<string, string>, databaseType?: DatabaseType, driverProfile?: string, omitOpeningParen = false, keywordCase?: SqlKeywordCase, functionCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<string, string>, databaseType?: DatabaseType, omitOpeningParen = false, keywordCase?: SqlKeywordCase, functionCase?: SqlKeywordCase): SqlCompletionItem[] {
if (databaseType === "clickhouse") return buildClickHouseFunctionItems(prefix, omitOpeningParen);
const items: SqlCompletionItem[] = [];
for (const [name, parameters] of activeFunctionSignatures(databaseType, driverProfile).entries()) {
for (const [name, parameters] of activeFunctionSignatures(databaseType).entries()) {
if (!matchesPrefix(name, prefix)) continue;
const functionName = applySqlFunctionCase(name, functionCase);
const paramStr = parameters.length > 0 ? parameters.map((p) => `\${${applyGeneratedSqlTemplateKeywordCase(p, keywordCase)}}`).join(", ") : "";
@@ -4516,7 +4527,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<str
items.push({
label: functionName,
type: "function" as const,
detail: functionDescriptions.get(name) ?? (isDoltDriverProfile(driverProfile) && name.startsWith("DOLT_") ? "Dolt routine" : "function"),
detail: functionDescriptions.get(name) ?? "function",
apply: mysqlApply ? `${functionName}${applyGeneratedSqlTemplateKeywordCase(mysqlApply.slice(name.length), keywordCase)}` : `${functionName}(${paramStr})`,
boost: computeBoost(name, prefix) + 300,
});
+36 -15
View File
@@ -707,8 +707,10 @@ const groupDefs: Array<{
objectTypes: DatabaseObjectTreeKind[];
nodeType: TreeNodeType;
childType: TreeNodeType | ((objectType: DatabaseObjectTreeKind) => TreeNodeType);
profileOnly?: boolean;
}> = [
{ key: "__tables", label: "tree.tables", objectTypes: ["TABLE"], nodeType: "group-tables", childType: "table" },
{ key: "__dolt_system_tables", label: "tree.tables", objectTypes: ["TABLE"], nodeType: "group-dolt-system-tables", childType: "table", profileOnly: true },
{ key: "__views", label: "tree.views", objectTypes: ["VIEW"], nodeType: "group-views", childType: "view" },
{
key: "__materialized_views",
@@ -768,22 +770,40 @@ const groupDefs: Array<{
},
];
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
export function buildObjectGroupPlaceholderNodes({ nodeId, connectionId, database, schema, objectTypes }: { nodeId: string; connectionId: string; database: string; schema?: string; objectTypes: DatabaseObjectTreeKind[] }): TreeNode[] {
export function buildObjectGroupPlaceholderNodes({
nodeId,
connectionId,
database,
schema,
objectTypes,
groupOverrides = [],
}: {
nodeId: string;
connectionId: string;
database: string;
schema?: string;
objectTypes: DatabaseObjectTreeKind[];
groupOverrides?: Array<{ nodeType: TreeNodeType; label?: string }>;
}): TreeNode[] {
const supported = new Set(objectTypes);
const overrides = new Map(groupOverrides.map((override) => [override.nodeType, override]));
return groupDefs
.filter((def) => def.objectTypes.some((objectType) => supported.has(objectType)))
.map((def) => ({
id: `${nodeId}:${def.key}`,
label: def.label,
type: def.nodeType,
connectionId,
database,
schema,
isExpanded: false,
children: [],
}));
.filter((def) => def.objectTypes.some((objectType) => supported.has(objectType)) && (!def.profileOnly || overrides.has(def.nodeType)))
.map((def) => {
const override = overrides.get(def.nodeType);
return {
id: `${nodeId}:${def.key}`,
label: override?.label ?? def.label,
type: def.nodeType,
connectionId,
database,
schema,
isExpanded: false,
children: [],
};
});
}
export function objectGroupRefreshParentId(node: TreeNode): string | null {
@@ -797,7 +817,7 @@ export function objectTypesForGroupNode(type: TreeNodeType): DatabaseObjectTreeK
return groupDefs.find((def) => def.nodeType === type)?.objectTypes ?? null;
}
export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, schema, objects, databaseType }: { nodeId: string; connectionId: string; database: string; schema?: string; objects: ObjectInfo[]; databaseType?: DatabaseType }): TreeNode[] {
export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, schema, objects, databaseType, groupNodeType }: { nodeId: string; connectionId: string; database: string; schema?: string; objects: ObjectInfo[]; databaseType?: DatabaseType; groupNodeType?: TreeNodeType }): TreeNode[] {
const buckets = new Map<string, ObjectInfo[]>();
const seen = new Set<string>();
for (const obj of coalesceXuguPackageObjects(objects, databaseType)) {
@@ -816,9 +836,10 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc
const groups: TreeNode[] = [];
for (const def of groupDefs) {
if (groupNodeType ? def.nodeType !== groupNodeType : def.profileOnly) continue;
const items = def.objectTypes.flatMap((objectType) => buckets.get(objectType) ?? []);
if (!items?.length) continue;
const isExpandable = def.nodeType === "group-tables" || def.nodeType === "group-views" || def.nodeType === "group-materialized-views";
const isExpandable = def.nodeType === "group-tables" || def.nodeType === "group-dolt-system-tables" || def.nodeType === "group-views" || def.nodeType === "group-materialized-views";
const children = isExpandable
? buildObjectTreeEntries({
nodeId: `${nodeId}:${def.key}`,
@@ -38,6 +38,16 @@ function mysqlConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function doltConnection(): ConnectionConfig {
return {
...mysqlConnection(),
id: "dolt-1",
name: "Dolt",
driver_profile: "dolt",
external_config: { doltShowSystemTables: true },
} as ConnectionConfig;
}
function dorisConnection(): ConnectionConfig {
return {
...mysqlConnection(),
@@ -1086,6 +1096,67 @@ describe("connectionStore metadata loading", () => {
expect(tableGroup.children?.map((node) => node.label)).toEqual(["ads_pgc_report"]);
});
it("loads Dolt user and system table groups with independent pre-pagination filters", async () => {
const listTables = vi.fn(async (...args: unknown[]) => {
const tableNameFilter = args[8] as { includePatterns: string[]; excludePatterns: string[] } | undefined;
if (tableNameFilter?.includePatterns.includes("dolt%")) {
return [{ name: "dolt_log", table_type: "BASE TABLE", comment: null }];
}
return [{ name: "orders", table_type: "BASE TABLE", comment: null }];
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
listTables,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useConnectionStore();
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
const connection = doltConnection();
const databaseNode: TreeNode = {
id: `${connection.id}:app`,
label: "app",
type: "database",
connectionId: connection.id,
database: "app",
isExpanded: false,
children: [],
};
store.connections = [connection];
store.connectedIds.add(connection.id);
store.treeNodes = [
{
id: connection.id,
label: connection.name,
type: "connection",
connectionId: connection.id,
isExpanded: true,
children: [databaseNode],
},
];
await store.loadTables(connection.id, "app", undefined, { force: true });
const userTables = databaseNode.children?.find((node) => node.type === "group-tables");
const systemTables = databaseNode.children?.find((node) => node.type === "group-dolt-system-tables");
expect(systemTables?.label).toBe("tree.doltSystemTables");
await store.loadObjectGroupChildren(userTables!, { force: true });
await store.loadObjectGroupChildren(systemTables!, { force: true });
expect(userTables?.children?.map((node) => node.label)).toEqual(["orders"]);
expect(systemTables?.children?.map((node) => node.label)).toEqual(["dolt_log"]);
expect(listTables).toHaveBeenCalledWith(connection.id, "app", "app", undefined, 1001, 0, ["TABLE"], undefined, { includePatterns: [], excludePatterns: ["dolt%"] });
expect(listTables).toHaveBeenCalledWith(connection.id, "app", "app", undefined, 1001, 0, ["TABLE"], undefined, { includePatterns: ["dolt%"], excludePatterns: [] });
});
it("clears a stale connection error after a schema metadata retry succeeds", async () => {
const listSchemaInfos = vi
.fn()
+30 -4
View File
@@ -123,6 +123,7 @@ import { isXuguPublicSynonymScope, xuguSchemaDisplayName, XUGU_PUBLIC_SYNONYM_SC
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
import { driverProfileObjectTreeProfileForConnection } from "@/lib/database/driverProfileExtensions";
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
import { MetadataResultCache, type MetadataCacheInvalidation } from "@/lib/metadata/metadataResultCache";
@@ -1527,9 +1528,11 @@ export const useConnectionStore = defineStore("connection", () => {
function objectGroupCacheKey(node: TreeNode): string {
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
const objectTreeProfileCacheKey = driverProfileObjectTreeProfileForConnection(config)?.cacheKey;
// objects-v8: object-group listing SQL gained a pg_type branch for
// PostgreSQL-family user-defined types; older cached lists miss TYPE nodes.
const cacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "oracle" ? "objects-v7" : "objects-v8");
const baseCacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "oracle" ? "objects-v7" : "objects-v8");
const cacheVersion = objectTreeProfileCacheKey ? `${baseCacheVersion}:${objectTreeProfileCacheKey}` : baseCacheVersion;
return schemaCacheKey(node.connectionId || "", node.database || "", node.schema || "", node.type, cacheVersion);
}
@@ -1546,6 +1549,19 @@ export const useConnectionStore = defineStore("connection", () => {
return tableNameFilterIsEmpty(filter) ? undefined : filter;
}
function effectiveTableNameFilterForNode(node: TreeNode, userFilter?: TableNameFilter): TableNameFilter | undefined {
const config = node.connectionId ? getConfig(node.connectionId) : undefined;
const profileFilter = driverProfileObjectTreeProfileForConnection(config)?.groupOverrides.find((group) => group.nodeType === node.type)?.tableNameFilter;
if (!profileFilter) return userFilter;
if (!userFilter) return profileFilter;
return normalizeTableNameFilter({
// Dedicated groups own their include range. User-defined include patterns
// continue to apply to the default tables group, whose profile rule is an exclusion.
includePatterns: profileFilter.includePatterns.length > 0 ? profileFilter.includePatterns : userFilter.includePatterns,
excludePatterns: [...profileFilter.excludePatterns, ...userFilter.excludePatterns],
});
}
function tableNameFilterMetadataExtra(filter: TableNameFilter | undefined): MetadataScopeInput["extra"] {
return filter
? {
@@ -1698,6 +1714,7 @@ export const useConnectionStore = defineStore("connection", () => {
schema: options.effectiveSchema,
objects: options.objects.filter((object) => options.objectTypes.includes(normalizedObjectTreeKind(object.object_type))),
databaseType,
groupNodeType: options.node.type,
});
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
const children = refreshedGroup?.children ?? [];
@@ -1779,13 +1796,14 @@ export const useConnectionStore = defineStore("connection", () => {
return { children: [], objectCount: 0, hasMore: false, nextOffset: options.offset };
}
const searchFilter = (options.searchFilter ?? sidebarSearchQuery.value) || undefined;
const tableNameFilter = activeTableNameFilterForScope({
const userTableNameFilter = activeTableNameFilterForScope({
connectionId: options.node.connectionId,
database: options.node.database,
schema: options.node.schema,
nodeKind: options.node.type,
catalog: options.node.catalog,
});
const tableNameFilter = effectiveTableNameFilterForNode(options.node, userTableNameFilter);
const fetchLimit = searchFilter ? options.pageSize : options.pageSize + 1;
const fetchOffset = searchFilter ? undefined : options.offset;
const tables = await loadCachedMetadataListPage<TableInfo[]>(
@@ -4703,7 +4721,9 @@ export const useConnectionStore = defineStore("connection", () => {
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
const searchFilter = activeTreeLoadSearchFilter(options);
const config = getConfig(connectionId);
const cacheVersion = ownerAwareMetadataCacheVersion(config, simpleObjectDisplay ? "objects-simple-v8" : "objects-grouped-v8");
const objectTreeProfile = driverProfileObjectTreeProfileForConnection(config);
const baseCacheVersion = ownerAwareMetadataCacheVersion(config, simpleObjectDisplay ? "objects-simple-v8" : "objects-grouped-v8");
const cacheVersion = !simpleObjectDisplay && objectTreeProfile?.cacheKey ? `${baseCacheVersion}:${objectTreeProfile.cacheKey}` : baseCacheVersion;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", cacheVersion);
const querySchema = connectionObjectTreeQuerySchema(config, database, schema);
const effectiveSchema = connectionObjectTreeNodeSchema(config, database, schema);
@@ -4748,6 +4768,7 @@ export const useConnectionStore = defineStore("connection", () => {
database,
schema: effectiveSchema,
objectTypes: supportedSidebarObjectTypes(config),
groupOverrides: objectTreeProfile?.groupOverrides,
});
if (!schema && isPostgresLikeForExtensions(config?.db_type)) {
children.push(buildExtensionManagementNode(connectionId, database));
@@ -5165,7 +5186,12 @@ export const useConnectionStore = defineStore("connection", () => {
}
const matchingGroups = findTreeNodes(treeNodes.value, (node) => {
return (node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views") && node.connectionId === target.connectionId && sameSidebarObjectName(node.database, target.database) && (!target.schema || sameSidebarObjectName(node.schema, target.schema));
return (
(node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views") &&
node.connectionId === target.connectionId &&
sameSidebarObjectName(node.database, target.database) &&
(!target.schema || sameSidebarObjectName(node.schema, target.schema))
);
});
for (const group of matchingGroups) {
+1
View File
@@ -902,6 +902,7 @@ export type TreeNodeType =
| "group-table-partitions"
| "group-table-subpartitions"
| "group-tables"
| "group-dolt-system-tables"
| "group-views"
| "group-materialized-views"
| "group-procedures"