fix(sqlserver): suggest database names in query completion
This commit is contained in:
1 parent
3285cb21a4
commit
79873443e4
9 files changed
+284
-45
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user