mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 13:51:31 +00:00
fix(oceanbase): preserve quoted completion identities
This commit is contained in:
+28
-2
@@ -41,7 +41,7 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}, configureMetad
|
||||
const store = {
|
||||
getConfig: vi.fn(() => undefined),
|
||||
lookupLocalCompletionTables: vi.fn((): SqlCompletionTable[] => []),
|
||||
lookupLocalCompletionColumns: vi.fn(() => []),
|
||||
lookupLocalCompletionColumns: vi.fn((): SqlCompletionColumn[] => []),
|
||||
lookupLocalCompletionColumnsByPrefix: vi.fn(() => []),
|
||||
lookupLocalCompletionObjects: vi.fn(() => []),
|
||||
lookupLocalCompletionDatabases: vi.fn((): string[] => []),
|
||||
@@ -53,7 +53,7 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}, configureMetad
|
||||
listCompletionSchemas: vi.fn(async (): Promise<string[]> => []),
|
||||
listCompletionDatabases: vi.fn(async (): Promise<string[]> => []),
|
||||
refreshCompletionTables: vi.fn(async () => []),
|
||||
refreshCompletionColumns: vi.fn(async () => []),
|
||||
refreshCompletionColumns: vi.fn(async (): Promise<SqlCompletionColumn[]> => []),
|
||||
refreshCompletionSchemas: vi.fn(async () => []),
|
||||
refreshCompletionDatabases: vi.fn(async () => []),
|
||||
listRedisCompletionCommandDocs: vi.fn(async () => commands),
|
||||
@@ -104,6 +104,32 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}, configureMetad
|
||||
return { props, currentView, view, store, settings, runtime, completion, provide, startCompletion };
|
||||
}
|
||||
|
||||
describe("OceanBase INSERT completion", () => {
|
||||
beforeEach(() => vi.useRealTimers());
|
||||
|
||||
it.each([
|
||||
["Alias", undefined],
|
||||
["APP.Alias", "APP"],
|
||||
['"Alias"', undefined],
|
||||
['"MixedOwner"."Alias"', "MixedOwner"],
|
||||
])("resolves %s with the selected schema kept as context", async (source, explicitSchema) => {
|
||||
const { provide, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP", modelValue: `INSERT INTO ${source} (` });
|
||||
store.listCompletionColumns.mockImplementation(async (...args: unknown[]) => (args[3] === explicitSchema ? [{ name: "ID", table: "Alias", schema: explicitSchema }] : []));
|
||||
expect((await provide())?.options.map((option) => option.label)).toContain("ID");
|
||||
expect(store.listCompletionColumns).toHaveBeenCalledWith("connection", "demo", "Alias", explicitSchema, expect.objectContaining({ currentSchema: "APP" }), undefined);
|
||||
if (source.startsWith('"')) expect(store.listCompletionColumns).toHaveBeenCalledWith("connection", "demo", "Alias", explicitSchema, expect.objectContaining({ tableQuoted: true }), undefined);
|
||||
});
|
||||
|
||||
it("keeps synchronous INSERT lookup and background refresh unqualified", async () => {
|
||||
const { provide, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP", modelValue: "INSERT INTO Alias (" });
|
||||
store.lookupLocalCompletionColumns.mockImplementation((...args: unknown[]) => (args[3] === undefined ? [{ name: "ID", table: "Alias" }] : []));
|
||||
store.refreshCompletionColumns.mockResolvedValue([{ name: "ID", table: "Alias" }]);
|
||||
expect((await provide(false))?.options.map((option) => option.label)).toContain("ID");
|
||||
expect(store.lookupLocalCompletionColumns).toHaveBeenCalledWith("connection", "demo", "Alias", undefined, undefined, expect.objectContaining({ currentSchema: "APP" }));
|
||||
expect(store.refreshCompletionColumns).toHaveBeenCalledWith("connection", "demo", "Alias", undefined, expect.objectContaining({ currentSchema: "APP" }), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([false, true])("Snowflake namespace completion (semantic=%s)", (semanticCompletionEnabled) => {
|
||||
beforeEach(() => vi.useRealTimers());
|
||||
|
||||
|
||||
+61
-4
@@ -7,6 +7,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { useQueryEditorCompletionMetadata } from "../useQueryEditorCompletionMetadata";
|
||||
import type { QueryEditorProps } from "../queryEditorTypes";
|
||||
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import { buildSelectStarExpansion, buildSqlCompletionItems, getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
|
||||
import { buildSqlSemanticDiagnostics } from "@/lib/sql/semantic/diagnostics";
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({ COMPLETION_METADATA_CONCURRENCY: 4 }));
|
||||
vi.mock("@/lib/backend/api", () => ({}));
|
||||
@@ -42,15 +44,70 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
|
||||
}
|
||||
|
||||
describe("QueryEditor completion metadata ownership", () => {
|
||||
it.each([
|
||||
["Alias", "PUBLIC_ID"],
|
||||
["APP.Alias", "PRIVATE_ID"],
|
||||
['"Alias"', "QUOTED_ID"],
|
||||
])("uses only the cached OceanBase reference %s for star and column completion", async (source, column) => {
|
||||
const { metadata, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP" });
|
||||
store.listCompletionColumns
|
||||
.mockResolvedValueOnce([{ name: "PUBLIC_ID", table: "Alias" }])
|
||||
.mockResolvedValueOnce([{ name: "PRIVATE_ID", table: "Alias", schema: "APP" }])
|
||||
.mockResolvedValueOnce([{ name: "QUOTED_ID", table: "Alias" }]);
|
||||
await metadata.ensureColumnsForTable({ name: "Alias" }, { nameQuoted: false });
|
||||
await metadata.ensureColumnsForTable({ name: "Alias", schema: "APP" });
|
||||
await metadata.ensureColumnsForTable({ name: "Alias" }, { nameQuoted: true });
|
||||
expect(store.listCompletionColumns).toHaveBeenCalledTimes(3);
|
||||
const sql = `SELECT s. FROM ${source} s`;
|
||||
const context = getSqlCompletionContext(sql, 9, { databaseType: "oceanbase-oracle" });
|
||||
expect(buildSelectStarExpansion(context, metadata.cachedColumnsByTable, "oracle", context.qualifier, "oceanbase-oracle")).toBe(`"${column}"`);
|
||||
const items = buildSqlCompletionItems(sql, 9, { tables: [], columnsByTable: metadata.cachedColumnsByTable, databaseType: "oceanbase-oracle", currentSchema: "APP" });
|
||||
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual([column]);
|
||||
expect(items.find((item) => item.label === "s.*")?.apply).toBe(`"${column}"`);
|
||||
});
|
||||
|
||||
it.each(["Alias", "APP.Alias", '"Alias"'])("uses the same OceanBase cache identity for diagnostics on %s", async (source) => {
|
||||
const { metadata, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP" });
|
||||
store.listCompletionColumns.mockResolvedValue([{ name: "ID", table: "Alias" }]);
|
||||
const sql = `SELECT s.MISSING FROM ${source} s`;
|
||||
const reference = getSqlCompletionContext(sql, 9, { databaseType: "oceanbase-oracle" }).referencedTables[0]!;
|
||||
await metadata.ensureColumnsForTable(reference);
|
||||
const tableStart = sql.indexOf(source);
|
||||
const diagnostics = buildSqlSemanticDiagnostics(
|
||||
{
|
||||
tables: [{ name: reference.name, schema: reference.schema, alias: "s", span: { start_line: 1, start_column: tableStart + 1, end_line: 1, end_column: tableStart + source.length } }],
|
||||
columns: [{ name: "MISSING", qualifier: "s", span: { start_line: 1, start_column: 8, end_line: 1, end_column: 16 } }],
|
||||
},
|
||||
{ tables: [], columnsByTable: metadata.cachedColumnsByTable, loadedColumnTables: metadata.loadedColumnsByTable, sql, databaseType: "oceanbase-oracle", currentSchema: "APP" },
|
||||
);
|
||||
expect(diagnostics.map((diagnostic) => diagnostic.message)).toEqual(["Unknown column s.MISSING"]);
|
||||
});
|
||||
|
||||
it("uses scoped synonym columns for JOIN condition completion", async () => {
|
||||
const { metadata, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP" });
|
||||
store.listCompletionColumns.mockResolvedValue([{ name: "TENANT_ID", table: "Alias" }]);
|
||||
await metadata.ensureColumnsForTable({ name: "Alias" });
|
||||
await metadata.ensureColumnsForTable({ name: "Alias", schema: "APP" });
|
||||
const sql = "SELECT * FROM Alias a JOIN APP.Alias b ON ";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, { tables: [], columnsByTable: metadata.cachedColumnsByTable, databaseType: "oceanbase-oracle" });
|
||||
expect(items.map((item) => item.label)).toContain("a.TENANT_ID = b.TENANT_ID");
|
||||
});
|
||||
|
||||
it("keeps foreign-key JOIN suggestions available under the same scoped cache keys", () => {
|
||||
const { metadata } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP" });
|
||||
metadata.cachedForeignKeysByTable.set(metadata.completionCacheKey({ name: "Alias" }), [{ name: "FK_PARENT", column: "PARENT", ref_table: "OtherAlias", ref_column: "ID" }]);
|
||||
const sql = "SELECT * FROM Alias a JOIN OtherAlias b ON ";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, { tables: [], columnsByTable: metadata.cachedColumnsByTable, foreignKeysByTable: metadata.cachedForeignKeysByTable, databaseType: "oceanbase-oracle" });
|
||||
expect(items.map((item) => item.label)).toContain("a.PARENT = b.ID");
|
||||
});
|
||||
|
||||
it("keeps an OceanBase selected schema separate from an explicitly qualified synonym", async () => {
|
||||
const { metadata, store } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "MixedOwner" });
|
||||
store.listCompletionColumns.mockResolvedValue(columns);
|
||||
await metadata.ensureColumnsForTable({ name: "Alias" });
|
||||
expect(store.listCompletionColumns).toHaveBeenLastCalledWith("connection", "demo", "Alias", undefined,
|
||||
expect.objectContaining({ currentSchema: "MixedOwner" }), undefined);
|
||||
expect(store.listCompletionColumns).toHaveBeenLastCalledWith("connection", "demo", "Alias", undefined, expect.objectContaining({ currentSchema: "MixedOwner" }), undefined);
|
||||
await metadata.ensureColumnsForTable({ name: "Alias", schema: "MixedOwner" });
|
||||
expect(store.listCompletionColumns).toHaveBeenLastCalledWith("connection", "demo", "Alias", "MixedOwner",
|
||||
expect.objectContaining({ currentSchema: "MixedOwner" }), undefined);
|
||||
expect(store.listCompletionColumns).toHaveBeenLastCalledWith("connection", "demo", "Alias", "MixedOwner", expect.objectContaining({ currentSchema: "MixedOwner" }), undefined);
|
||||
});
|
||||
|
||||
it.each(["getEditorSqlCompletionContext", "getEditorSemanticModel"] as const)("keys %s by document, editor state, position and dialect", (method) => {
|
||||
|
||||
+24
-11
@@ -42,6 +42,16 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
|
||||
const view = shallowRef<EditorView | null>(currentView);
|
||||
const runtime: Options["runtime"] = { editorIsActive: true, executableStatementRangeCache: null, setSqlDiagnosticsEffect: StateEffect.define<SqlSemanticDiagnostic[]>(), diagnosticComp: null, buildSqlDiagnosticExtension: null, codeMirrorCompletionStatus: () => null };
|
||||
const settingsStore = { editorSettings: { sqlSemanticDiagnosticsEnabled: true } } as Options["settingsStore"];
|
||||
const metadata = {
|
||||
cachedTables: [],
|
||||
cachedColumnsByTable: new Map(),
|
||||
loadedColumnsByTable: new Set<string>(),
|
||||
usesOracleSessionCompletionColumns: () => false,
|
||||
findExactSemanticDiagnosticTable: vi.fn().mockResolvedValue(null),
|
||||
completionCacheKey: (table: { name: string }) => table.name,
|
||||
ensureColumnsForTable: vi.fn().mockResolvedValue(false),
|
||||
isMissingTableMetadataError: () => false,
|
||||
};
|
||||
let diagnostics!: ReturnType<typeof useQueryEditorDiagnostics>;
|
||||
const host = document.createElement("div");
|
||||
const app = createApp({
|
||||
@@ -57,16 +67,7 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
|
||||
sqlBehaviorDialect: () => props.dialect,
|
||||
semanticCompletionEnabled: false,
|
||||
maxCompletionTables: 100,
|
||||
metadata: {
|
||||
cachedTables: [],
|
||||
cachedColumnsByTable: new Map(),
|
||||
loadedColumnsByTable: new Set(),
|
||||
usesOracleSessionCompletionColumns: () => false,
|
||||
findExactSemanticDiagnosticTable: vi.fn().mockResolvedValue(null),
|
||||
completionCacheKey: (table) => table.name,
|
||||
ensureColumnsForTable: vi.fn().mockResolvedValue(false),
|
||||
isMissingTableMetadataError: () => false,
|
||||
},
|
||||
metadata,
|
||||
});
|
||||
return () => h("div");
|
||||
},
|
||||
@@ -83,10 +84,22 @@ function createHarness(overrides: Partial<QueryEditorProps> = {}) {
|
||||
currentView.destroy();
|
||||
parent.remove();
|
||||
});
|
||||
return { diagnostics, props, runtime, settingsStore, currentView, unmount };
|
||||
return { diagnostics, props, runtime, settingsStore, currentView, unmount, metadata };
|
||||
}
|
||||
|
||||
describe("QueryEditor diagnostic scheduling ownership", () => {
|
||||
it("preserves the unqualified quoted OceanBase reference and selected schema through metadata loading", async () => {
|
||||
const sql = 'SELECT MISSING FROM "Alias"';
|
||||
const { diagnostics, metadata } = createHarness({ databaseType: "oceanbase-oracle", dialect: "oracle", schema: "APP", modelValue: sql });
|
||||
analyze.mockResolvedValue({ tables: [{ name: "Alias", span: { start_line: 1, start_column: 21, end_line: 1, end_column: 27 } }], columns: [] });
|
||||
metadata.findExactSemanticDiagnosticTable.mockResolvedValue({ name: "Alias", schema: "PUBLIC" });
|
||||
diagnostics.scheduleSemanticDiagnostics(0);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(metadata.ensureColumnsForTable).toHaveBeenCalledWith(expect.objectContaining({ name: "Alias", nameQuoted: true }), undefined, expect.objectContaining({ schema: "APP" }));
|
||||
expect(metadata.ensureColumnsForTable.mock.calls[0]![0].schema).toBeUndefined();
|
||||
expect(buildDiagnostics).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ currentSchema: "APP" }));
|
||||
});
|
||||
|
||||
it("coalesces timers and shares the latest executable statement cache", async () => {
|
||||
const { diagnostics, currentView, runtime } = createHarness();
|
||||
diagnostics.scheduleSemanticDiagnostics(50);
|
||||
|
||||
@@ -1105,11 +1105,13 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? scope.database;
|
||||
const insertSchema = completionContext.insertSchema ?? scope.schema;
|
||||
const insertColumns = usesOracleSessionCompletionColumns(insertSchema) ? [] : connectionStore.lookupLocalCompletionColumns(props.connectionId, insertDatabase, completionContext.insertTable, insertSchema, props.catalog);
|
||||
const insertTable = insertCompletionTable(completionContext);
|
||||
const target = insertCompletionMetadataTarget(insertTable, scope);
|
||||
const insertKey = completionCacheKey(insertTable, scope);
|
||||
const insertColumns =
|
||||
cachedColumnsByTable.get(insertKey) ?? (usesOracleSessionCompletionColumns(target.schema) ? [] : connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, insertTable.name, target.schema, target.catalog, completionColumnRequestContext(insertTable)));
|
||||
if (insertColumns.length > 0) {
|
||||
columnsByTable.set(completionCacheKey({ name: completionContext.insertTable, database: completionContext.insertDatabase, schema: insertSchema }, scope), insertColumns);
|
||||
columnsByTable.set(insertKey, insertColumns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1287,12 +1289,11 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
}
|
||||
}
|
||||
if (!onDemandOnlyColumns && completionContext.insertTable) {
|
||||
const insertTable = completionContext.insertTable;
|
||||
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? database;
|
||||
void refreshCompletionColumnsForEditor(connectionId, insertDatabase, insertTable, completionContext.insertSchema ?? scope.schema)
|
||||
const insertTable = insertCompletionTable(completionContext);
|
||||
const target = insertCompletionMetadataTarget(insertTable, scope);
|
||||
void refreshCompletionColumnsForEditor(connectionId, target.database, insertTable.name, target.schema, target.catalog, insertTable)
|
||||
.then((columns) => {
|
||||
const insertSchema = completionContext.insertSchema ?? scope.schema;
|
||||
cachedColumnsByTable.set(completionCacheKey({ name: insertTable, database: completionContext.insertDatabase, schema: insertSchema }, scope), columns);
|
||||
cachedColumnsByTable.set(completionCacheKey(insertTable, scope), columns);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
@@ -1412,6 +1413,15 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
return ["routine"];
|
||||
}
|
||||
|
||||
function insertCompletionTable(context: ReturnType<typeof getSqlCompletionContext>) {
|
||||
return { name: context.insertTable!, database: context.insertDatabase, schema: context.insertSchema, nameQuoted: context.insertTableQuoted, schemaQuoted: context.insertSchemaQuoted };
|
||||
}
|
||||
|
||||
function insertCompletionMetadataTarget(table: ReturnType<typeof insertCompletionTable>, scope: CompletionMetadataScope) {
|
||||
if (props.databaseType === "oceanbase-oracle") return completionMetadataTarget(table, scope)!;
|
||||
return { database: (supportsDatabaseSchemaQualifierCompletion() ? table.database : undefined) ?? scope.database, schema: table.schema ?? scope.schema, catalog: props.catalog };
|
||||
}
|
||||
|
||||
async function performAsyncCompletionWithResult(epoch: number, completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number, scope: CompletionMetadataScope) {
|
||||
const localOnlyMetadata = usesLocalOnlyCompletionMetadata();
|
||||
const onDemandOnlyColumns = usesOnDemandOnlyCompletionColumns();
|
||||
@@ -1419,12 +1429,12 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
try {
|
||||
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? scope.database;
|
||||
const insertCols = await listCompletionColumnsForEditor(props.connectionId!, insertDatabase, completionContext.insertTable, completionContext.insertSchema ?? scope.schema);
|
||||
const insertTable = insertCompletionTable(completionContext);
|
||||
const target = insertCompletionMetadataTarget(insertTable, scope);
|
||||
const insertCols = await listCompletionColumnsForEditor(props.connectionId!, target.database, insertTable.name, target.schema, target.catalog, insertTable);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
if (insertCols.length > 0) {
|
||||
const insertSchema = completionContext.insertSchema ?? scope.schema;
|
||||
const insertKey = completionCacheKey({ name: completionContext.insertTable, database: completionContext.insertDatabase, schema: insertSchema }, scope);
|
||||
const insertKey = completionCacheKey(insertTable, scope);
|
||||
insertColumnsByTable.set(insertKey, insertCols);
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { EditorState, Text } from "@codemirror/state";
|
||||
import type { CompletionMetadataScope } from "./queryEditorTypes";
|
||||
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
|
||||
import { COMPLETION_METADATA_CONCURRENCY } from "@/stores/connectionStore";
|
||||
import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
|
||||
import { getSqlCompletionContext, oceanBaseCompletionCacheKey } from "@/lib/sql/sqlCompletion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import type { SqlSemanticModel } from "@/lib/sql/semantic/types";
|
||||
import { analyzeSqlCompletion, type SqlCompletionAnalysisResult } from "@/lib/sql/sqlCompletionAnalysis";
|
||||
@@ -246,11 +246,11 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
|
||||
}
|
||||
|
||||
function completionCacheKey(table: { name: string; catalog?: string | null; database?: string | null; schema?: string | null; nameQuoted?: boolean; schemaQuoted?: boolean }, scope?: CompletionMetadataScope) {
|
||||
if (props.databaseType === "oceanbase-oracle") return oceanBaseCompletionCacheKey(table, scope?.schema ?? props.schema);
|
||||
const schema = table.schema ?? scope?.schema ?? props.schema;
|
||||
const scopedDatabase = scope && scope.database !== props.database ? scope.database : undefined;
|
||||
const database = supportsDatabaseSchemaQualifierCompletion() ? (table.database ?? scopedDatabase) : undefined;
|
||||
const baseKey = schema ? `${database ? `${database}.` : ""}${schema}.${table.name}` : table.name;
|
||||
if (props.databaseType === "oceanbase-oracle") return `${baseKey}:${table.schema ? "qualified" : "unqualified"}`;
|
||||
if (props.databaseType !== "postgres" || (!table.nameQuoted && !table.schemaQuoted)) return baseKey;
|
||||
return `${baseKey}:quoted:s=${table.schemaQuoted ? "1" : "0"}:t=${table.nameQuoted ? "1" : "0"}`;
|
||||
}
|
||||
@@ -363,7 +363,7 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
|
||||
// unqualified table name. The query editor commonly has no schema selected
|
||||
// when the user is working from a database-level tab, so use the same
|
||||
// default as the table/DDL metadata paths instead of returning no columns.
|
||||
const selectedSchema = props.databaseType === "oceanbase-oracle" ? table.schema : table.schema ?? scope?.schema ?? props.schema;
|
||||
const selectedSchema = props.databaseType === "oceanbase-oracle" ? table.schema : (table.schema ?? scope?.schema ?? props.schema);
|
||||
const effectiveSchema = selectedSchema ?? (props.databaseType === "sqlserver" ? metadataSchemaForConnection(connectionStore.getConfig(props.connectionId ?? ""), currentDatabase, undefined) : undefined);
|
||||
if (supportsDatabaseSchemaQualifierCompletion() && table.database) {
|
||||
return { database: table.database, schema: effectiveSchema, catalog: table.catalog ?? props.catalog };
|
||||
@@ -415,9 +415,9 @@ export function useQueryEditorCompletionMetadata(options: QueryEditorCompletionM
|
||||
return remoteMatches.find((item) => completionTablesMatch(item, table)) ?? null;
|
||||
}
|
||||
|
||||
async function ensureColumnsForTable(table: { name: string; database?: string | null; schema?: string | null }, reference?: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted">, scope?: CompletionMetadataScope): Promise<boolean> {
|
||||
async function ensureColumnsForTable(table: { name: string; database?: string | null; schema?: string | null; nameQuoted?: boolean; schemaQuoted?: boolean }, reference: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted"> = table, scope?: CompletionMetadataScope): Promise<boolean> {
|
||||
if (isVirtualCompletionTableReference(table)) return false;
|
||||
const cacheKey = completionCacheKey(table, scope);
|
||||
const cacheKey = completionCacheKey({ ...table, ...reference }, scope);
|
||||
if (cachedColumnsByTable.has(cacheKey)) return true;
|
||||
if (!props.connectionId || props.database == null) return false;
|
||||
const target = completionMetadataTarget(table, scope);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
buildSqlSemanticDiagnostics,
|
||||
isSqlSemanticDiagnosticInputContext,
|
||||
isSqlVirtualTableReference,
|
||||
oceanBaseDiagnosticTableReference,
|
||||
shouldRunSqlSemanticDiagnostics,
|
||||
sqlSemanticDiagnosticRangesForViewport,
|
||||
sqlServerRoutineDefinitionRangesForViewport,
|
||||
@@ -259,7 +260,8 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
};
|
||||
}
|
||||
|
||||
function semanticDiagnosticTablesForScope(tables: SqlTableReference[], scope: CompletionMetadataScope): SqlTableReference[] {
|
||||
function semanticDiagnosticTablesForScope(tables: SqlTableReference[], scope: CompletionMetadataScope, sql: string): SqlTableReference[] {
|
||||
if (props.databaseType === "oceanbase-oracle") return tables.map((table) => oceanBaseDiagnosticTableReference(table, sql));
|
||||
if (props.databaseType !== "sqlserver" || scope.database === props.database) return tables;
|
||||
return tables.map((table) => (table.database ? table : { ...table, database: scope.database, schema: table.schema ?? scope.schema }));
|
||||
}
|
||||
@@ -274,7 +276,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
if (metadata.usesOracleSessionCompletionColumns(table.schema)) {
|
||||
if ((props.databaseType === "oceanbase-oracle" && !table.schema) || metadata.usesOracleSessionCompletionColumns(table.schema)) {
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
@@ -448,7 +450,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
const metadataScope = semanticDiagnosticMetadataScope(sql, range);
|
||||
const scopedAnalysis = {
|
||||
...semanticAnalysis,
|
||||
tables: semanticDiagnosticTablesForScope(semanticAnalysis.tables, metadataScope),
|
||||
tables: semanticDiagnosticTablesForScope(semanticAnalysis.tables, metadataScope, range.sql),
|
||||
};
|
||||
const { tables, missingTables } = await enrichSemanticDiagnosticTables(scopedAnalysis.tables, metadataScope);
|
||||
const columnMetadataMissingTables = await ensureColumnsForSemanticDiagnostics(tables, metadataScope);
|
||||
@@ -468,6 +470,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
loadedColumnTables: metadata.loadedColumnsByTable,
|
||||
sql: range.sql,
|
||||
databaseType: props.databaseType,
|
||||
currentSchema: metadataScope.schema,
|
||||
}),
|
||||
range,
|
||||
sql,
|
||||
@@ -582,7 +585,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
const metadataScope = semanticDiagnosticMetadataScope(sql, range);
|
||||
const scopedAnalysis = {
|
||||
...semanticAnalysis,
|
||||
tables: semanticDiagnosticTablesForScope(semanticAnalysis.tables, metadataScope),
|
||||
tables: semanticDiagnosticTablesForScope(semanticAnalysis.tables, metadataScope, range.sql),
|
||||
};
|
||||
const { tables, missingTables } = await enrichSemanticDiagnosticTables(scopedAnalysis.tables, metadataScope);
|
||||
const loadedColumnsBefore = metadata.loadedColumnsByTable.size;
|
||||
@@ -603,6 +606,7 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
loadedColumnTables: metadata.loadedColumnsByTable,
|
||||
sql: range.sql,
|
||||
databaseType: props.databaseType,
|
||||
currentSchema: metadataScope.schema,
|
||||
},
|
||||
),
|
||||
range,
|
||||
|
||||
@@ -70,9 +70,7 @@ describe("tableMetadataCache columns facet request counts", () => {
|
||||
});
|
||||
|
||||
it("uses a synonym's exact resolved target for indexes while retaining its source name", async () => {
|
||||
mocks.getColumns.mockResolvedValue([
|
||||
{ ...column("ItemId"), is_primary_key: false, resolved_schema: "MixedOwner", resolved_table: "ActualItems", resolved_object_type: "TABLE" },
|
||||
]);
|
||||
mocks.getColumns.mockResolvedValue([{ ...column("ItemId"), is_primary_key: false, resolved_schema: "MixedOwner", resolved_table: "ActualItems", resolved_object_type: "TABLE" }]);
|
||||
mocks.listIndexes.mockResolvedValue([{ name: "ITEMS_PK", columns: ["ItemId"], is_primary: true, is_unique: true }]);
|
||||
const request = { connectionId: "ob-1", database: "service", schema: "AppOwner", tableName: "ItemsAlias", tableType: "SYNONYM", databaseType: "oceanbase-oracle" };
|
||||
|
||||
@@ -106,7 +104,11 @@ describe("tableMetadataCache columns facet request counts", () => {
|
||||
|
||||
it("uses the current schema for parallel index discovery on a known direct table", async () => {
|
||||
let releaseColumns: (value: ColumnInfo[]) => void = () => {};
|
||||
mocks.getColumns.mockReturnValueOnce(new Promise<ColumnInfo[]>((resolve) => { releaseColumns = resolve; }));
|
||||
mocks.getColumns.mockReturnValueOnce(
|
||||
new Promise<ColumnInfo[]>((resolve) => {
|
||||
releaseColumns = resolve;
|
||||
}),
|
||||
);
|
||||
const load = loadTableMetadata({ connectionId: "ob-1", database: "service", schema: "", currentSchema: "SelectedOwner", tableName: "Items", tableType: "TABLE", databaseType: "oceanbase-oracle" });
|
||||
|
||||
await flush();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import { getSqlCompletionContext, isOracleSystemValueName } from "@/lib/sql/sqlCompletion";
|
||||
import { getSqlCompletionContext, isOracleSystemValueName, matchesOceanBaseCompletionCacheKey } from "@/lib/sql/sqlCompletion";
|
||||
import { tokenizeSqlSemantic, tokenIsIdentifier } from "@/lib/sql/semantic/tokens";
|
||||
import { executableStatementRanges, keepsOracleStyleBlockTogether, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { DBX_TDENGINE_TBNAME_COLUMN, isTdengineStableTableType } from "@/lib/table/tableEditing";
|
||||
import type { DatabaseType, SqlColumnReference, SqlGroupByViolation, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
@@ -17,6 +18,7 @@ export interface SqlSemanticDiagnosticSchema {
|
||||
loadedColumnTables?: Set<string>;
|
||||
sql?: string;
|
||||
databaseType?: DatabaseType;
|
||||
currentSchema?: string;
|
||||
}
|
||||
|
||||
export interface SqlSemanticDiagnosticVisibleRange {
|
||||
@@ -265,7 +267,7 @@ function sqlServerStatementNeedsBatchContext(sql: string): boolean {
|
||||
|
||||
export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, schema: SqlSemanticDiagnosticSchema): SqlSemanticDiagnostic[] {
|
||||
const diagnostics: SqlSemanticDiagnostic[] = [];
|
||||
const tables = analysis.tables.filter((table) => table.name.trim());
|
||||
const tables = analysis.tables.filter((table) => table.name.trim()).map((table) => (schema.databaseType === "oceanbase-oracle" && schema.sql ? oceanBaseDiagnosticTableReference(table, schema.sql) : table));
|
||||
const knownTables = new Map<string, SqlTableReference>();
|
||||
const scopesById = scopesByIdMap(analysis.scopes);
|
||||
let tdengineStableTables: Set<string> | undefined;
|
||||
@@ -292,7 +294,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
|
||||
if (!table) continue;
|
||||
if (schema.missingTables?.has(tableReferenceKey(table))) continue;
|
||||
|
||||
const columns = columnsForTable(table, schema.columnsByTable, schema.loadedColumnTables);
|
||||
const columns = columnsForTable(table, schema.columnsByTable, schema.loadedColumnTables, schema.currentSchema);
|
||||
if (!columns) continue;
|
||||
|
||||
const columnNames = new Set(columns.map((item) => normalizeName(item.name)));
|
||||
@@ -592,7 +594,13 @@ function statementIndexAt(sql: string, offset: number): number {
|
||||
return statementIndex;
|
||||
}
|
||||
|
||||
function columnsForTable(table: SqlTableReference, columnsByTable: Map<string, SqlCompletionColumn[]>, loadedColumnTables?: Set<string>): SqlCompletionColumn[] | null {
|
||||
export function oceanBaseDiagnosticTableReference(table: SqlTableReference, sql: string) {
|
||||
const range = sqlTextSpanToOffsetRange(sql, table.span);
|
||||
const identifiers = range ? tokenizeSqlSemantic(sql.slice(range.from, range.to), "oracle").filter(tokenIsIdentifier) : [];
|
||||
return { ...table, nameQuoted: !!identifiers[table.schema ? 1 : 0]?.quote, schemaQuoted: !!table.schema && !!identifiers[0]?.quote };
|
||||
}
|
||||
|
||||
function columnsForTable(table: SqlTableReference, columnsByTable: Map<string, SqlCompletionColumn[]>, loadedColumnTables?: Set<string>, currentSchema?: string): SqlCompletionColumn[] | null {
|
||||
const inlineColumns = (table as SqlTableReference & { columns?: string[] }).columns;
|
||||
if (inlineColumns && inlineColumns.length > 0) {
|
||||
return inlineColumns.map((name) => ({
|
||||
@@ -601,6 +609,8 @@ function columnsForTable(table: SqlTableReference, columnsByTable: Map<string, S
|
||||
schema: table.schema ?? undefined,
|
||||
}));
|
||||
}
|
||||
const scopedColumns = [...columnsByTable].filter(([key]) => matchesOceanBaseCompletionCacheKey(key, table, currentSchema));
|
||||
if (scopedColumns.length > 0) return scopedColumns.length === 1 ? scopedColumns[0]![1] : null;
|
||||
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name, ...keysWithTableName(columnsByTable, table.name)];
|
||||
for (const key of keys) {
|
||||
const normalizedKey = normalizeName(key);
|
||||
|
||||
@@ -1544,6 +1544,8 @@ export interface SqlCompletionContext {
|
||||
insertTable?: string;
|
||||
insertDatabase?: string;
|
||||
insertSchema?: string;
|
||||
insertTableQuoted?: boolean;
|
||||
insertSchemaQuoted?: boolean;
|
||||
statementKind: SqlStatementKind;
|
||||
tableTriggerWord?: string;
|
||||
isGroupBy: boolean;
|
||||
@@ -2668,6 +2670,8 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
|
||||
insertTable: insertInfo?.table,
|
||||
insertDatabase: insertInfo?.database,
|
||||
insertSchema: insertInfo?.schema,
|
||||
insertTableQuoted: insertInfo?.nameQuoted,
|
||||
insertSchemaQuoted: insertInfo?.schemaQuoted,
|
||||
statementKind,
|
||||
tableTriggerWord: lastWord || undefined,
|
||||
isGroupBy: isInGroupByContext(beforeCursor),
|
||||
@@ -3252,7 +3256,7 @@ function detectComparisonLeftColumn(beforeCursor: string): string | undefined {
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function detectInsertColumnListContext(beforeCursor: string): { table: string; database?: string; schema?: string } | null {
|
||||
function detectInsertColumnListContext(beforeCursor: string): { table: string; database?: string; schema?: string; nameQuoted?: boolean; schemaQuoted?: boolean } | null {
|
||||
// Keep quoted identifiers intact so schema/table targets resolve to their
|
||||
// real names instead of placeholder string contents.
|
||||
const cleaned = beforeCursor.replace(/'[^']*'/g, "''");
|
||||
@@ -3263,12 +3267,15 @@ function detectInsertColumnListContext(beforeCursor: string): { table: string; d
|
||||
const fullTable = match[1];
|
||||
if (!fullTable) return null;
|
||||
const parts = splitQualifiedNameParts(fullTable);
|
||||
const rawParts = splitQualifiedNameRawParts(fullTable);
|
||||
const table = parts[parts.length - 1];
|
||||
if (!table) return null;
|
||||
return {
|
||||
table,
|
||||
database: parts.length >= 3 ? parts[parts.length - 3] : undefined,
|
||||
schema: parts.length >= 2 ? parts[parts.length - 2] : undefined,
|
||||
nameQuoted: isQuotedIdentifier(rawParts[rawParts.length - 1]),
|
||||
schemaQuoted: parts.length >= 2 ? isQuotedIdentifier(rawParts[rawParts.length - 2]) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5100,7 +5107,10 @@ function columnsForInsertTarget(context: SqlCompletionContext, columnsByTable: M
|
||||
const schemaKey = context.insertSchema ? normalizeIdentifierPart(context.insertSchema) : undefined;
|
||||
const databaseKey = context.insertDatabase ? normalizeIdentifierPart(context.insertDatabase) : undefined;
|
||||
const qualifiedKey = schemaKey ? normalizeCompletionKey(`${context.insertDatabase ? `${context.insertDatabase}.` : ""}${context.insertSchema}.${context.insertTable}`) : undefined;
|
||||
const reference = { name: context.insertTable, schema: context.insertSchema, nameQuoted: context.insertTableQuoted, schemaQuoted: context.insertSchemaQuoted };
|
||||
return collectCompletionColumns(columnsByTable).filter((column) => {
|
||||
const scopedMatch = matchesOceanBaseCompletionCacheKey(column.key, reference);
|
||||
if (scopedMatch !== undefined) return scopedMatch;
|
||||
if (normalizeIdentifierPart(column.table) !== tableKey) return false;
|
||||
if (!schemaKey) return true;
|
||||
if (!databaseKey && column.schema && normalizeIdentifierPart(column.schema) === schemaKey) return true;
|
||||
@@ -5235,6 +5245,8 @@ function referencedTableMatchesColumnQualifier(table: SqlCompletionReferencedTab
|
||||
}
|
||||
|
||||
function columnMatchesReferencedTable(column: SqlCompletionColumn & { key: string }, table: SqlCompletionReferencedTable): boolean {
|
||||
const scopedMatch = matchesOceanBaseCompletionCacheKey(column.key, table);
|
||||
if (scopedMatch !== undefined) return scopedMatch;
|
||||
if (normalizeIdentifierPart(column.table) !== normalizeIdentifierPart(table.name)) return false;
|
||||
if (!table.schema) return true;
|
||||
return columnMatchesQualifiedTable(column, { database: table.database, schema: table.schema, table: table.name });
|
||||
@@ -5309,7 +5321,26 @@ function buildJoinConditionItems(context: SqlCompletionContext, columnsByTable:
|
||||
return items;
|
||||
}
|
||||
|
||||
type CompletionCacheTableReference = { name: string; schema?: string | null; nameQuoted?: boolean; schemaQuoted?: boolean };
|
||||
|
||||
function oceanBaseCompletionReferenceKey(table: CompletionCacheTableReference): string {
|
||||
const schema = table.schema ? (table.schemaQuoted ? table.schema : table.schema.toUpperCase()) : null;
|
||||
const name = table.nameQuoted ? table.name : table.name.toUpperCase();
|
||||
return `oceanbase-oracle:${JSON.stringify([schema, name, !!table.schemaQuoted, !!table.nameQuoted])}:`;
|
||||
}
|
||||
|
||||
export function oceanBaseCompletionCacheKey(table: CompletionCacheTableReference, currentSchema?: string | null): string {
|
||||
return `${oceanBaseCompletionReferenceKey(table)}${JSON.stringify(currentSchema ?? null)}`;
|
||||
}
|
||||
|
||||
export function matchesOceanBaseCompletionCacheKey(key: string, table: CompletionCacheTableReference, currentSchema?: string): boolean | undefined {
|
||||
if (!key.startsWith("oceanbase-oracle:[")) return undefined;
|
||||
return currentSchema === undefined ? key.startsWith(oceanBaseCompletionReferenceKey(table)) : key === oceanBaseCompletionCacheKey(table, currentSchema);
|
||||
}
|
||||
|
||||
function columnsForReferencedTable(table: SqlCompletionReferencedTable, columnsByTable: Map<string, SqlCompletionColumn[]>): SqlCompletionColumn[] {
|
||||
const scopedColumns = [...columnsByTable].filter(([key]) => matchesOceanBaseCompletionCacheKey(key, table));
|
||||
if (scopedColumns.length > 0) return scopedColumns.length === 1 ? applyReferencedColumnAliases(table, scopedColumns[0]![1]) : [];
|
||||
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name];
|
||||
for (const key of keys) {
|
||||
const columns = columnsByTable.get(key);
|
||||
@@ -5320,6 +5351,8 @@ function columnsForReferencedTable(table: SqlCompletionReferencedTable, columnsB
|
||||
|
||||
function foreignKeysForReferencedTable(table: SqlCompletionReferencedTable, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>): SqlCompletionForeignKey[] {
|
||||
if (!foreignKeysByTable) return [];
|
||||
const scopedKeys = [...foreignKeysByTable].filter(([key]) => matchesOceanBaseCompletionCacheKey(key, table));
|
||||
if (scopedKeys.length > 0) return scopedKeys.length === 1 ? scopedKeys[0]![1] : [];
|
||||
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name];
|
||||
for (const key of keys) {
|
||||
const foreignKeys = foreignKeysByTable.get(key);
|
||||
|
||||
Reference in New Issue
Block a user