fix(sqlserver): suggest database names in query completion
This commit is contained in:
parent
3285cb21a4
commit
79873443e4
|
|
@ -44,7 +44,7 @@ import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
|||
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
|
||||
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, mongoCompletionNeedsCollections, mongoCompletionNeedsFields, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
|
||||
import { resolveSqlCompletionRoutineLookupTarget, resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { mergeSqlCompletionQualifierNames, resolveSqlCompletionRoutineLookupTarget, resolveSqlCompletionSchemaLookupDatabase, resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { usesOracleSessionCompletionColumns as shouldUseOracleSessionCompletionColumns } from "@/lib/sql/oracleCompletionSession";
|
||||
import { extractIdentifierDetailsAt, isSqlKeyword, matchTable, mergeSqlObjectNavigationType, splitQualifiedIdentifier, sqlObjectHoverDetail, sqlObjectNavigationTarget, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
|
||||
import { buildHoverTableSql, hoverTableMatchesScope, quoteQualifiedName, reformatHoverDdl, scopeHoverTables, type HoverTableScope } from "@/lib/editor/hoverTableSql";
|
||||
|
|
@ -77,7 +77,7 @@ import { sqlSemanticTableNameSpansForSyntaxTree } from "@/lib/editor/codemirrorS
|
|||
import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorPointerSelection";
|
||||
import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recoverableNativePasteSuffix, shouldRecoverLargeTauriPaste } from "@/lib/editor/queryEditorLargePaste";
|
||||
import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation";
|
||||
import { isSchemaAware, isSingleDatabase, supportsDatabaseSchemaQualifier, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
|
||||
import { isSchemaAware, isSingleDatabase, supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
|
||||
import { metadataSchemaForConnection, sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy";
|
||||
import { loadTableMetadata, type TableMetadataLoadResult } from "@/lib/metadata/tableMetadataCache";
|
||||
|
|
@ -2496,21 +2496,25 @@ function buildCompletionResult(items: QueryCompletionItem[], from: number, valid
|
|||
};
|
||||
}
|
||||
|
||||
function mergeCompletionQualifierNames(primary: string[], secondary: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
for (const name of [...primary, ...secondary]) {
|
||||
const key = name.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(name);
|
||||
}
|
||||
return merged;
|
||||
function localCompletionDatabaseNames(completionContext: ReturnType<typeof getSqlCompletionContext>): string[] {
|
||||
if (!supportsDatabaseNameCompletion(props.databaseType) || !completionContext.suggestTables || completionContext.insertTable || !props.connectionId) return [];
|
||||
return connectionStore.lookupLocalCompletionDatabases(props.connectionId, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES);
|
||||
}
|
||||
|
||||
function localCompletionDatabaseNames(completionContext: ReturnType<typeof getSqlCompletionContext>): string[] {
|
||||
if (!supportsDatabaseQualifierCompletion() || !completionContext.suggestTables || completionContext.insertTable || !props.connectionId) return [];
|
||||
return connectionStore.lookupLocalCompletionDatabases(props.connectionId, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES);
|
||||
function mayCompleteDatabaseSchemaQualifier(completionContext: ReturnType<typeof getSqlCompletionContext>): boolean {
|
||||
if (!supportsDatabaseNameCompletion(props.databaseType) || !supportsDatabaseSchemaQualifierCompletion() || !completionContext.suggestTables || completionContext.insertTable) return false;
|
||||
return (completionContext.qualifierParts?.filter(Boolean).length ?? completionContext.qualifier?.split(".").filter(Boolean).length ?? 0) === 1;
|
||||
}
|
||||
|
||||
function localCompletionSchemasForDatabaseDisambiguation(completionContext: ReturnType<typeof getSqlCompletionContext>, databaseNames: string[]): string[] {
|
||||
if (!props.connectionId || props.database == null || !mayCompleteDatabaseSchemaQualifier(completionContext)) return [];
|
||||
const database = resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
if (!database) return [];
|
||||
return mergeSqlCompletionQualifierNames(props.schema ? [props.schema] : [], connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.qualifier, MAX_COMPLETION_TABLES));
|
||||
}
|
||||
|
||||
function shouldInsertSqlCompletionSpace(): boolean {
|
||||
|
|
@ -2836,7 +2840,14 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
|||
function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number) {
|
||||
if (!props.connectionId || props.database == null) return null;
|
||||
const databaseNames = localCompletionDatabaseNames(completionContext);
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
const currentDatabaseSchemaNames = localCompletionSchemasForDatabaseDisambiguation(completionContext, databaseNames);
|
||||
const schemaLookupDatabase = resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
knownSchemas: currentDatabaseSchemaNames,
|
||||
});
|
||||
const shouldLoadTables = !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)));
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: props.database,
|
||||
currentSchema: props.schema,
|
||||
|
|
@ -2846,13 +2857,19 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
|||
knownDatabases: databaseNames,
|
||||
});
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema, props.catalog) : cachedTables;
|
||||
const tables = schemaLookupDatabase ? [] : shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema, props.catalog) : cachedTables;
|
||||
|
||||
const shouldLoadObjects = shouldLoadCompletionObjects(completionContext);
|
||||
const completionObjects = shouldLoadObjects ? lookupLocalCompletionObjectsForContext(completionContext) : cachedCompletionObjects;
|
||||
|
||||
const schemaNames =
|
||||
completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable ? mergeCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames) : [];
|
||||
completionContext.suggestTables && !completionContext.insertTable
|
||||
? schemaLookupDatabase
|
||||
? connectionStore.lookupLocalCompletionSchemas(props.connectionId, schemaLookupDatabase, completionContext.prefix, MAX_COMPLETION_TABLES)
|
||||
: !completionContext.qualifier
|
||||
? mergeSqlCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames)
|
||||
: []
|
||||
: [];
|
||||
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
|
|
@ -2940,15 +2957,23 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
|||
const tableNameCompletion = isTableNameCompletionContext(completionContext);
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
const databaseNames = localCompletionDatabaseNames(completionContext);
|
||||
const currentDatabaseSchemaNames = localCompletionSchemasForDatabaseDisambiguation(completionContext, databaseNames);
|
||||
const schemaLookupDatabase = resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
knownSchemas: currentDatabaseSchemaNames,
|
||||
});
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: database,
|
||||
currentSchema: props.schema,
|
||||
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
|
||||
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: localCompletionDatabaseNames(completionContext),
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
if (!localOnlyMetadata && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||
if (!localOnlyMetadata && !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
void connectionStore
|
||||
.refreshCompletionTables(connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch, props.schema, props.catalog)
|
||||
|
|
@ -2971,10 +2996,14 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
|||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
if (!localOnlyMetadata && completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
void connectionStore.refreshCompletionSchemas(connectionId, database).catch(() => {});
|
||||
if (supportsDatabaseQualifierCompletion()) {
|
||||
void connectionStore.refreshCompletionDatabases(connectionId).catch(() => {});
|
||||
if (!localOnlyMetadata && completionContext.suggestTables && !completionContext.insertTable) {
|
||||
if (schemaLookupDatabase) {
|
||||
void connectionStore.refreshCompletionSchemas(connectionId, schemaLookupDatabase).catch(() => {});
|
||||
} else if (!completionContext.qualifier) {
|
||||
void connectionStore.refreshCompletionSchemas(connectionId, database).catch(() => {});
|
||||
if (supportsDatabaseNameCompletion(props.databaseType)) {
|
||||
void connectionStore.refreshCompletionDatabases(connectionId).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!onDemandOnlyColumns && completionContext.insertTable) {
|
||||
|
|
@ -3129,16 +3158,22 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
}
|
||||
}
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
let databaseNames = localCompletionDatabaseNames(completionContext);
|
||||
if (!localOnlyMetadata && supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable && !completionContext.qualifier) {
|
||||
try {
|
||||
databaseNames = await connectionStore.listCompletionDatabases(props.connectionId!);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
databaseNames = [];
|
||||
}
|
||||
let currentDatabaseSchemaNames = localCompletionSchemasForDatabaseDisambiguation(completionContext, databaseNames);
|
||||
const mayCompleteDatabaseSchema = mayCompleteDatabaseSchemaQualifier(completionContext);
|
||||
if (!localOnlyMetadata && supportsDatabaseNameCompletion(props.databaseType) && completionContext.suggestTables && !completionContext.insertTable && (!completionContext.qualifier || mayCompleteDatabaseSchema)) {
|
||||
const [databasesResult, schemasResult] = await Promise.allSettled([connectionStore.listCompletionDatabases(props.connectionId!), mayCompleteDatabaseSchema ? connectionStore.listCompletionSchemas(props.connectionId!, props.database!) : Promise.resolve(currentDatabaseSchemaNames)]);
|
||||
databaseNames = databasesResult.status === "fulfilled" ? databasesResult.value : [];
|
||||
if (schemasResult.status === "fulfilled") currentDatabaseSchemaNames = mergeSqlCompletionQualifierNames(props.schema ? [props.schema] : [], schemasResult.value);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
}
|
||||
const schemaLookupDatabase = resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
knownSchemas: currentDatabaseSchemaNames,
|
||||
});
|
||||
const shouldLoadTables = !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)));
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: props.database!,
|
||||
currentSchema: props.schema,
|
||||
|
|
@ -3148,11 +3183,13 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
knownDatabases: databaseNames,
|
||||
});
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
let tables = shouldLoadTables
|
||||
? localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema, props.catalog)
|
||||
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch)
|
||||
: cachedTables;
|
||||
let tables = schemaLookupDatabase
|
||||
? []
|
||||
: shouldLoadTables
|
||||
? localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema, props.catalog)
|
||||
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch)
|
||||
: cachedTables;
|
||||
if (localOnlyMetadata && tables.length === 0 && supportsDatabaseSchemaQualifierCompletion() && (completionContext.qualifierParts?.length ?? 0) >= 2 && allowsOnDemandQualifiedTableCompletion(completionContext.prefix)) {
|
||||
tables = await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, PRESTO_ON_DEMAND_TABLE_COMPLETION_LIMIT, tableLookupTarget.schema);
|
||||
}
|
||||
|
|
@ -3173,23 +3210,25 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
|
||||
// Fetch schemas for schema completion
|
||||
let schemaNames: string[] = [];
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
if (completionContext.suggestTables && !completionContext.insertTable && (schemaLookupDatabase || !completionContext.qualifier)) {
|
||||
const database = schemaLookupDatabase ?? props.database!;
|
||||
if (localOnlyMetadata) {
|
||||
schemaNames = mergeCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames);
|
||||
const schemas = connectionStore.lookupLocalCompletionSchemas(props.connectionId!, database, completionContext.prefix, MAX_COMPLETION_TABLES);
|
||||
schemaNames = schemaLookupDatabase ? schemas : mergeSqlCompletionQualifierNames(schemas, databaseNames);
|
||||
} else {
|
||||
try {
|
||||
const schemas = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
schemaNames = mergeCompletionQualifierNames(schemas, databaseNames);
|
||||
const schemas = await connectionStore.listCompletionSchemas(props.connectionId!, database);
|
||||
schemaNames = schemaLookupDatabase ? schemas : mergeSqlCompletionQualifierNames(schemas, databaseNames);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
schemaNames = databaseNames;
|
||||
schemaNames = schemaLookupDatabase ? [] : databaseNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (completionContext.qualifier && !tableLookupTarget.qualifierDatabase && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
if (completionContext.qualifier && !schemaLookupDatabase && !tableLookupTarget.qualifierDatabase && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
let schemaTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier, props.catalog);
|
||||
if (!localOnlyMetadata) {
|
||||
schemaTables = await listCompletionTablesWithLatencyBudget(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import { connectionNamespaceCreationTarget, databaseNodeNamespaceCreationTarget } from "@/lib/database/databaseNamespaceCreation";
|
||||
import { editableDatabasePropertyGroups, editableSchemaPropertyGroups } from "@/lib/database/databasePropertyEditing";
|
||||
import { buildGetDatabaseCommentSql } from "@/lib/database/dbAdminSql";
|
||||
import { isSchemaAware, supportsDatabaseSchemaQualifier, supportsSqlInListPaste, supportsTransaction } from "@/lib/database/databaseFeatureSupport";
|
||||
import { isSchemaAware, supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsSqlInListPaste, supportsTransaction } from "@/lib/database/databaseFeatureSupport";
|
||||
|
||||
describe("schema awareness", () => {
|
||||
it("keeps SQLite database aliases separate from schema-capable databases", () => {
|
||||
|
|
@ -18,6 +18,14 @@ describe("database and schema qualifiers", () => {
|
|||
it.each(["mysql", "postgres", "oracle", "snowflake"] as const)("does not widen unverified three-part completion for %s", (databaseType) => {
|
||||
expect(supportsDatabaseSchemaQualifier(databaseType)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["mysql", "sqlite", "sqlserver"] as const)("suggests database names for %s", (databaseType) => {
|
||||
expect(supportsDatabaseNameCompletion(databaseType)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["postgres", "oracle", "snowflake", "trino", "prestosql"] as const)("does not add database name completion for %s", (databaseType) => {
|
||||
expect(supportsDatabaseNameCompletion(databaseType)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supportsTransaction", () => {
|
||||
|
|
|
|||
37
apps/desktop/src/lib/__tests__/editor/queryEditorDatabaseNameCompletion.spec.ts
vendored
Normal file
37
apps/desktop/src/lib/__tests__/editor/queryEditorDatabaseNameCompletion.spec.ts
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = queryEditorSource.indexOf(`function ${name}(`);
|
||||
if (start < 0) throw new Error(`Missing QueryEditor function: ${name}`);
|
||||
const bodyStart = queryEditorSource.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let index = bodyStart; index < queryEditorSource.length; index++) {
|
||||
const character = queryEditorSource[index];
|
||||
if (character === "{") depth++;
|
||||
if (character === "}" && --depth === 0) return queryEditorSource.slice(start, index + 1);
|
||||
}
|
||||
throw new Error(`Unterminated QueryEditor function: ${name}`);
|
||||
}
|
||||
|
||||
describe("QueryEditor database name completion wiring", () => {
|
||||
it("uses the shared capability for local lookup, background refresh, and async loading", () => {
|
||||
expect(extractFunction("localCompletionDatabaseNames")).toContain("supportsDatabaseNameCompletion(props.databaseType)");
|
||||
expect(extractFunction("scheduleCompletionMetadataRefresh")).toMatch(/supportsDatabaseNameCompletion\(props\.databaseType\)[\s\S]*?refreshCompletionDatabases\(connectionId\)/);
|
||||
expect(extractFunction("performAsyncCompletionWithResult")).toMatch(/supportsDatabaseNameCompletion\(props\.databaseType\)[\s\S]*?listCompletionDatabases\(props\.connectionId!\)/);
|
||||
});
|
||||
|
||||
it("does not reuse cached tables while completing schemas inside a database", () => {
|
||||
expect(extractFunction("buildLocalSqlCompletionResult")).toMatch(/const tables = schemaLookupDatabase\s*\?\s*\[\]/);
|
||||
expect(extractFunction("performAsyncCompletionWithResult")).toMatch(/let tables = schemaLookupDatabase\s*\?\s*\[\]/);
|
||||
});
|
||||
|
||||
it("limits database and schema disambiguation to engines with both capabilities", () => {
|
||||
const guard = extractFunction("mayCompleteDatabaseSchemaQualifier");
|
||||
|
||||
expect(guard).toContain("supportsDatabaseNameCompletion(props.databaseType)");
|
||||
expect(guard).toContain("supportsDatabaseSchemaQualifierCompletion()");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
|
||||
import { resolveSqlCompletionRoutineLookupTarget, resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { mergeSqlCompletionQualifierNames, resolveSqlCompletionRoutineLookupTarget, resolveSqlCompletionSchemaLookupDatabase, resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
|
||||
describe("sqlCompletionLookupTarget", () => {
|
||||
it("treats qualified table completion as a database lookup for MySQL-compatible engines", () => {
|
||||
|
|
@ -99,6 +99,72 @@ describe("sqlCompletionLookupTarget", () => {
|
|||
expect(target).toEqual({ database: "app", schema: "sales", filter: "ord" });
|
||||
});
|
||||
|
||||
it("routes a SQL Server database qualifier to schema completion", () => {
|
||||
const completionContext = getSqlCompletionContext("select * from Reporting.d", "select * from Reporting.d".length);
|
||||
|
||||
expect(
|
||||
resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
knownDatabases: ["Default_DB", "Reporting"],
|
||||
completionContext,
|
||||
}),
|
||||
).toBe("Reporting");
|
||||
});
|
||||
|
||||
it("does not case-fold database qualifiers while choosing a schema scope", () => {
|
||||
const completionContext = getSqlCompletionContext("select * from reporting.d", "select * from reporting.d".length);
|
||||
|
||||
expect(
|
||||
resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
knownDatabases: ["Reporting"],
|
||||
completionContext,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not let a case-distinct schema hide an exact database qualifier", () => {
|
||||
const completionContext = getSqlCompletionContext("select * from reporting.d", "select * from reporting.d".length);
|
||||
|
||||
expect(
|
||||
resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
knownDatabases: ["reporting"],
|
||||
knownSchemas: ["Reporting"],
|
||||
completionContext,
|
||||
}),
|
||||
).toBe("reporting");
|
||||
});
|
||||
|
||||
it("does not mistake a current-database schema for a database", () => {
|
||||
const completionContext = getSqlCompletionContext("select * from dbo.", "select * from dbo.".length);
|
||||
|
||||
expect(
|
||||
resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
knownDatabases: ["Default_DB", "Reporting"],
|
||||
completionContext,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prefers a current-database schema when a database has the same name", () => {
|
||||
const completionContext = getSqlCompletionContext("select * from dbo.", "select * from dbo.".length);
|
||||
|
||||
expect(
|
||||
resolveSqlCompletionSchemaLookupDatabase({
|
||||
supportsDatabaseSchemaQualifier: true,
|
||||
knownDatabases: ["dbo", "Reporting"],
|
||||
knownSchemas: ["dbo", "sales"],
|
||||
completionContext,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps case-distinct database and schema completion names", () => {
|
||||
expect(mergeSqlCompletionQualifierNames(["Reporting", "dbo"], ["reporting", "dbo"])).toEqual(["Reporting", "dbo", "reporting"]);
|
||||
});
|
||||
|
||||
it("uses the current schema for unqualified table completion", () => {
|
||||
const target = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: "app",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ export function supportsDatabaseSchemaQualifier(dbType?: DatabaseType): boolean
|
|||
return !!dbType && DATABASE_SCHEMA_QUALIFIED_TYPES.has(dbType);
|
||||
}
|
||||
|
||||
export function supportsDatabaseNameCompletion(dbType?: DatabaseType): boolean {
|
||||
return !!dbType && ((!isSchemaAware(dbType) && !isSingleDatabase(dbType)) || dbType === "sqlserver");
|
||||
}
|
||||
|
||||
/**
|
||||
* Doris-family engines that support multi-catalog federation (`SHOW CATALOGS`):
|
||||
* Doris (incl. SelectDB) and StarRocks. Manticore Search shares the MySQL code
|
||||
|
|
|
|||
|
|
@ -16,6 +16,29 @@ function findExactName(names: readonly string[] | undefined, value: string): str
|
|||
return names?.find((name) => name.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
function findCaseSensitiveName(names: readonly string[] | undefined, value: string): string | undefined {
|
||||
return names?.find((name) => name === value);
|
||||
}
|
||||
|
||||
export function mergeSqlCompletionQualifierNames(primary: readonly string[], secondary: readonly string[]): string[] {
|
||||
return [...new Set([...primary, ...secondary])];
|
||||
}
|
||||
|
||||
export function resolveSqlCompletionSchemaLookupDatabase(options: {
|
||||
supportsDatabaseSchemaQualifier?: boolean;
|
||||
completionContext: Pick<SqlCompletionContext, "qualifier" | "qualifierParts" | "suggestTables" | "insertTable">;
|
||||
knownDatabases?: readonly string[];
|
||||
knownSchemas?: readonly string[];
|
||||
}): string | undefined {
|
||||
const { completionContext } = options;
|
||||
if (!options.supportsDatabaseSchemaQualifier || !completionContext.suggestTables || completionContext.insertTable) return undefined;
|
||||
const qualifier = completionContext.qualifier?.trim();
|
||||
const qualifierParts = completionContext.qualifierParts?.filter(Boolean) ?? qualifier?.split(".").filter(Boolean) ?? [];
|
||||
if (qualifierParts.length !== 1) return undefined;
|
||||
if (findCaseSensitiveName(options.knownSchemas, qualifierParts[0]!)) return undefined;
|
||||
return findCaseSensitiveName(options.knownDatabases, qualifierParts[0]!);
|
||||
}
|
||||
|
||||
export function resolveSqlCompletionTableLookupTarget(options: {
|
||||
currentDatabase: string;
|
||||
currentSchema?: string;
|
||||
|
|
|
|||
|
|
@ -725,6 +725,32 @@ describe("connectionStore completion assistant", () => {
|
|||
expect(listDatabases).toHaveBeenCalledTimes(52);
|
||||
});
|
||||
|
||||
it("invalidates cached completion databases for a connection", async () => {
|
||||
const listDatabases = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([{ name: "Archive" }])
|
||||
.mockResolvedValueOnce([{ name: "Reporting" }]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
listDatabases,
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [sqlServerConnection()];
|
||||
store.connectedIds.add("sqlserver-1");
|
||||
|
||||
expect(await store.listCompletionDatabases("sqlserver-1")).toEqual(["Archive"]);
|
||||
expect(await store.listCompletionDatabases("sqlserver-1")).toEqual(["Archive"]);
|
||||
|
||||
store.invalidateCompletionCache("sqlserver-1");
|
||||
|
||||
expect(await store.listCompletionDatabases("sqlserver-1")).toEqual(["Reporting"]);
|
||||
expect(listDatabases).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("evicts old completion schema entries", async () => {
|
||||
const listSchemas = vi.fn(async (_connectionId: string, database: string) => [`schema_${database}`]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2272,6 +2272,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function invalidateCompletionCache(connectionId: string, database?: string) {
|
||||
invalidateMetadataCaches({ connectionId, database });
|
||||
if (database == null) delete completionDatabasesCache.value[connectionId];
|
||||
const cachePrefix = database == null ? `${connectionId}:` : `${connectionId}:${database}:`;
|
||||
const exactCacheKey = database == null ? null : `${connectionId}:${database}`;
|
||||
for (const key of Object.keys(completionTablesCache.value)) {
|
||||
|
|
|
|||
|
|
@ -2371,6 +2371,41 @@ test("schema items include apply value with trailing dot", () => {
|
|||
assert.equal(shouldChainSqlCompletionAfterAccept({ type: "table", apply: "users" }), false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Foo", "FooDB"],
|
||||
["Bar", "BarDB"],
|
||||
])("suggests SQL Server database name %s as a chained qualifier", (prefix, database) => {
|
||||
const sql = `select * from ${prefix}`;
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables: [],
|
||||
columnsByTable: new Map(),
|
||||
schemas: ["FooDB", "BarDB"],
|
||||
databaseType: "sqlserver",
|
||||
dialect: "sqlserver",
|
||||
});
|
||||
const databaseItem = items.find((item) => item.type === "schema" && item.label === database);
|
||||
|
||||
assert.ok(databaseItem);
|
||||
assert.equal(databaseItem.apply, `${database}.`);
|
||||
assert.equal(shouldChainSqlCompletionAfterAccept(databaseItem), true);
|
||||
});
|
||||
|
||||
test("suggests SQL Server schemas after a database qualifier", () => {
|
||||
const sql = "select * from BarDB.d";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables: [],
|
||||
columnsByTable: new Map(),
|
||||
schemas: ["dbo", "data"],
|
||||
databaseType: "sqlserver",
|
||||
dialect: "sqlserver",
|
||||
});
|
||||
const schemaItem = items.find((item) => item.type === "schema" && item.label === "dbo");
|
||||
|
||||
assert.ok(schemaItem);
|
||||
assert.equal(schemaItem.apply, "dbo.");
|
||||
assert.equal(shouldChainSqlCompletionAfterAccept(schemaItem), true);
|
||||
});
|
||||
|
||||
// --- Quoted identifier fix ---
|
||||
|
||||
test("handles quoted identifiers with dots in splitQualifiedName", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue