mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 10:28:44 +00:00
fix(oracle): improve alias column completion
This commit is contained in:
@@ -414,25 +414,27 @@ function usesOracleSessionCompletionColumns(schema?: string | null): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function completionColumnRequestContext() {
|
||||
function completionColumnRequestContext(reference?: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted">) {
|
||||
return {
|
||||
clientSessionId: props.clientSessionId,
|
||||
version: props.completionContextVersion,
|
||||
tableQuoted: reference?.nameQuoted,
|
||||
schemaQuoted: reference?.schemaQuoted,
|
||||
};
|
||||
}
|
||||
|
||||
async function listCompletionColumnsForEditor(connectionId: string, database: string, table: string, schema?: string, catalog = props.catalog) {
|
||||
async function listCompletionColumnsForEditor(connectionId: string, database: string, table: string, schema?: string, catalog = props.catalog, reference?: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted">) {
|
||||
const requestedVersion = props.completionContextVersion;
|
||||
const sessionScoped = usesOracleSessionCompletionColumns(schema);
|
||||
const columns = await connectionStore.listCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext(), catalog);
|
||||
const columns = await connectionStore.listCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext(reference), catalog);
|
||||
if (sessionScoped && requestedVersion !== props.completionContextVersion) throw new Error("Stale Oracle completion context");
|
||||
return columns;
|
||||
}
|
||||
|
||||
async function refreshCompletionColumnsForEditor(connectionId: string, database: string, table: string, schema?: string, catalog = props.catalog) {
|
||||
async function refreshCompletionColumnsForEditor(connectionId: string, database: string, table: string, schema?: string, catalog = props.catalog, reference?: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted">) {
|
||||
const requestedVersion = props.completionContextVersion;
|
||||
const sessionScoped = usesOracleSessionCompletionColumns(schema);
|
||||
const columns = await connectionStore.refreshCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext(), catalog);
|
||||
const columns = await connectionStore.refreshCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext(reference), catalog);
|
||||
if (sessionScoped && requestedVersion !== props.completionContextVersion) throw new Error("Stale Oracle completion context");
|
||||
return columns;
|
||||
}
|
||||
@@ -2688,7 +2690,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
continue;
|
||||
}
|
||||
const target = completionMetadataTarget(refTable);
|
||||
const localColumns = target && !usesOracleSessionCompletionColumns(target.schema) ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, refTable.name, target.schema, target.catalog) : [];
|
||||
const localColumns = target && !usesOracleSessionCompletionColumns(target.schema) ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, refTable.name, target.schema, target.catalog, refTable) : [];
|
||||
if (localColumns.length > 0) {
|
||||
columnsByTable.set(cacheKey, localColumns);
|
||||
}
|
||||
@@ -2795,7 +2797,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
if (cachedColumnsByTable.has(cacheKey)) continue;
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
void refreshCompletionColumnsForEditor(connectionId, target.database, refTable.name, target.schema, target.catalog)
|
||||
void refreshCompletionColumnsForEditor(connectionId, target.database, refTable.name, target.schema, target.catalog, refTable)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(cacheKey, columns);
|
||||
})
|
||||
@@ -3048,7 +3050,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
try {
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) return;
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema, target.catalog);
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema, target.catalog, refTable);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
@@ -3911,7 +3913,7 @@ onMounted(async () => {
|
||||
try {
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
cols = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema, target.catalog);
|
||||
cols = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema, target.catalog, refTable);
|
||||
cachedColumnsByTable.set(cacheKey, cols);
|
||||
} catch {
|
||||
continue;
|
||||
|
||||
@@ -35,6 +35,16 @@ function semanticCompletion(markedSql: string, input: Partial<SqlCompletionProvi
|
||||
}
|
||||
|
||||
describe("semantic SQL completion candidates", () => {
|
||||
it.each([
|
||||
["ordinary lowercase", "SELECT * FROM orders_alias a WHERE a.|", "ORDERS_ALIAS", false],
|
||||
["quoted lowercase", 'SELECT * FROM "orders_alias" a WHERE a.|', "orders_alias", true],
|
||||
["quoted mixed case", 'SELECT * FROM "Orders_Alias" a WHERE a.|', "Orders_Alias", true],
|
||||
] as const)("preserves Oracle identifier semantics for %s aliases", (_label, markedSql, expectedName, expectedQuoted) => {
|
||||
const { context } = semanticCompletion(markedSql, {}, { databaseType: "oracle" });
|
||||
|
||||
expect(context.referencedTables).toEqual([expect.objectContaining({ name: expectedName, nameQuoted: expectedQuoted, alias: "A" })]);
|
||||
});
|
||||
|
||||
it("isolates SQL Server columns for database-qualified tables with the same schema and name", () => {
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
|
||||
["DatabaseA.OUT.orders", [{ name: "source_marker", table: "orders", schema: "OUT" }]],
|
||||
|
||||
@@ -16,14 +16,19 @@ export interface SqlSemanticCompletionScope {
|
||||
export function sqlSemanticReferencedTables(model: SqlSemanticModel): SqlCompletionReferencedTable[] {
|
||||
return model.rowSources
|
||||
.filter((source) => source.kind !== "unknown")
|
||||
.map((source) => ({
|
||||
name: source.name,
|
||||
database: source.metadataTarget?.database,
|
||||
schema: source.qualifierParts[source.qualifierParts.length - 1],
|
||||
alias: source.alias,
|
||||
columns: source.columns,
|
||||
columnAliases: source.columnAliases,
|
||||
}));
|
||||
.map((source) => {
|
||||
const identifierParts = source.qualifiedName?.parts ?? [];
|
||||
return {
|
||||
name: source.name,
|
||||
nameQuoted: !!identifierParts[identifierParts.length - 1]?.quote,
|
||||
database: source.metadataTarget?.database,
|
||||
schema: source.qualifierParts[source.qualifierParts.length - 1],
|
||||
schemaQuoted: source.qualifierParts.length > 0 ? !!identifierParts[identifierParts.length - 2]?.quote : undefined,
|
||||
alias: source.alias,
|
||||
columns: source.columns,
|
||||
columnAliases: source.columnAliases,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function sqlSemanticLocalColumnsByTable(model: SqlSemanticModel): Map<string, SqlCompletionColumn[]> {
|
||||
|
||||
@@ -1204,8 +1204,10 @@ export type SqlKeywordCase = "preserve" | "upper" | "lower";
|
||||
|
||||
export interface SqlCompletionReferencedTable {
|
||||
name: string;
|
||||
nameQuoted?: boolean;
|
||||
database?: string;
|
||||
schema?: string;
|
||||
schemaQuoted?: boolean;
|
||||
alias?: string;
|
||||
columns?: string[];
|
||||
columnAliases?: string[];
|
||||
@@ -2504,13 +2506,16 @@ function extractReferencedTables(sql: string): SqlCompletionReferencedTable[] {
|
||||
referenced.push({ name: unquoteIdentifier(rawName), alias: cleanAlias });
|
||||
continue;
|
||||
}
|
||||
const parts = splitQualifiedNameParts(rawName);
|
||||
const rawParts = splitQualifiedNameRawParts(rawName);
|
||||
const parts = rawParts.map((part) => unquoteIdentifier(part)).filter(Boolean);
|
||||
const name = parts[parts.length - 1];
|
||||
if (!name) continue;
|
||||
const table: SqlCompletionReferencedTable = {
|
||||
name,
|
||||
nameQuoted: isQuotedIdentifier(rawParts[rawParts.length - 1]),
|
||||
database: parts.length >= 3 ? parts[parts.length - 3] : undefined,
|
||||
schema: parts.length >= 2 ? parts[parts.length - 2] : undefined,
|
||||
schemaQuoted: parts.length >= 2 ? isQuotedIdentifier(rawParts[rawParts.length - 2]) : undefined,
|
||||
alias: cleanAlias,
|
||||
};
|
||||
referenced.push(table);
|
||||
@@ -2771,6 +2776,12 @@ function splitQualifiedName(input: string): [string | undefined, string | undefi
|
||||
}
|
||||
|
||||
function splitQualifiedNameParts(input: string): string[] {
|
||||
return splitQualifiedNameRawParts(input)
|
||||
.map((part) => unquoteIdentifier(part))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function splitQualifiedNameRawParts(input: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current = "";
|
||||
let inDoubleQuote = false;
|
||||
@@ -2800,7 +2811,12 @@ function splitQualifiedNameParts(input: string): string[] {
|
||||
}
|
||||
if (current.trim()) parts.push(current.trim());
|
||||
|
||||
return parts.map((p) => unquoteIdentifier(p)).filter(Boolean);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function isQuotedIdentifier(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
return (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("`") && value.endsWith("`")) || (value.startsWith("[") && value.endsWith("]"));
|
||||
}
|
||||
|
||||
function unquoteIdentifier(value: string): string {
|
||||
|
||||
@@ -272,6 +272,67 @@ describe("connectionStore completion assistant", () => {
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "ORDERS")).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes unquoted Oracle aliases while preserving quoted case before catalog lookup", async () => {
|
||||
const completionAssistantSearch = vi.fn();
|
||||
const getColumns = vi.fn().mockResolvedValue([]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
getColumns,
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [oracleConnection()];
|
||||
store.connectedIds.add("oracle-1");
|
||||
|
||||
await store.listCompletionColumns("oracle-1", "ORCL", "orders_alias", undefined, { clientSessionId: "tab-a", version: 0, tableQuoted: false });
|
||||
await store.listCompletionColumns("oracle-1", "ORCL", "orders_alias", undefined, { clientSessionId: "tab-a", version: 1, tableQuoted: true });
|
||||
await store.listCompletionColumns("oracle-1", "ORCL", "Orders_Alias", undefined, { clientSessionId: "tab-a", version: 2, tableQuoted: true });
|
||||
|
||||
expect(getColumns.mock.calls.map((call) => call[3])).toEqual(["ORDERS_ALIAS", "orders_alias", "Orders_Alias"]);
|
||||
expect(completionAssistantSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps quoted and unquoted Oracle objects separate in the local column index", async () => {
|
||||
const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null }) => ({
|
||||
candidates: [
|
||||
{
|
||||
name: request.parent_name === "ORDERS_ALIAS" ? "UPPER_ID" : "LOWER_ID",
|
||||
kind: "column",
|
||||
schema: "APP",
|
||||
parent_schema: "APP",
|
||||
parent_name: request.parent_name,
|
||||
data_type: "NUMBER",
|
||||
},
|
||||
],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
}));
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
getColumns: vi.fn(),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [oracleConnection()];
|
||||
store.connectedIds.add("oracle-1");
|
||||
|
||||
await store.listCompletionColumns("oracle-1", "ORCL", "orders_alias", "APP", { tableQuoted: false, schemaQuoted: false });
|
||||
await store.listCompletionColumns("oracle-1", "ORCL", "orders_alias", "APP", { tableQuoted: true, schemaQuoted: false });
|
||||
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "ORDERS_ALIAS", "APP").map((column) => column.name)).toEqual(["UPPER_ID"]);
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "orders_alias", "APP").map((column) => column.name)).toEqual(["LOWER_ID"]);
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "orders_alias", "app", undefined, { tableQuoted: false, schemaQuoted: false }).map((column) => column.name)).toEqual(["UPPER_ID"]);
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "orders_alias", "APP", undefined, { tableQuoted: true, schemaQuoted: false }).map((column) => column.name)).toEqual(["LOWER_ID"]);
|
||||
});
|
||||
|
||||
it("isolates Oracle CURRENT_SCHEMA column caches by tab and context version", async () => {
|
||||
const completionAssistantSearch = vi.fn();
|
||||
const getColumns = vi
|
||||
|
||||
@@ -4535,7 +4535,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return `${connectionId}:${database}:${catalog?.toLowerCase() ?? ""}:${schema?.toLowerCase() ?? ""}`;
|
||||
}
|
||||
|
||||
function completionColumnsKey(connectionId: string, database: string, table: string, schema?: string, catalog?: string): string {
|
||||
function completionColumnsKey(connectionId: string, database: string, table: string, schema?: string, catalog?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): string {
|
||||
if (getConfig(connectionId)?.db_type === "oracle") {
|
||||
const normalizedTable = context?.tableQuoted === false ? table.toUpperCase() : table;
|
||||
const normalizedSchema = schema && context?.schemaQuoted === false ? schema.toUpperCase() : (schema ?? "");
|
||||
return `${connectionId}:${database}:${catalog ?? ""}:${normalizedSchema}:${normalizedTable}`;
|
||||
}
|
||||
return `${completionTableScopeKey(connectionId, database, schema, catalog)}:${table.toLowerCase()}`;
|
||||
}
|
||||
|
||||
@@ -4905,8 +4910,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return result;
|
||||
}
|
||||
|
||||
function lookupLocalCompletionColumns(connectionId: string, database: string, table: string, schema?: string, catalog?: string): SqlCompletionColumn[] {
|
||||
return completionColumnIndex.get(completionColumnsKey(connectionId, database, table, schema, catalog))?.columns ?? [];
|
||||
function lookupLocalCompletionColumns(connectionId: string, database: string, table: string, schema?: string, catalog?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): SqlCompletionColumn[] {
|
||||
return completionColumnIndex.get(completionColumnsKey(connectionId, database, table, schema, catalog, context))?.columns ?? [];
|
||||
}
|
||||
|
||||
function lookupLocalCompletionForeignKeys(connectionId: string, database: string, table: string, schema?: string): SqlCompletionForeignKey[] {
|
||||
@@ -5255,15 +5260,18 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return deduped;
|
||||
}
|
||||
|
||||
async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number }, catalog?: string): Promise<SqlCompletionColumn[]> {
|
||||
async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number; tableQuoted?: boolean; schemaQuoted?: boolean }, catalog?: string): Promise<SqlCompletionColumn[]> {
|
||||
const config = getConfig(connectionId);
|
||||
const completionSchema = schema?.trim() || undefined;
|
||||
const oracleIdentifier = config?.db_type === "oracle";
|
||||
const completionTable = oracleIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table;
|
||||
const rawCompletionSchema = schema?.trim() || undefined;
|
||||
const completionSchema = oracleIdentifier && rawCompletionSchema && context?.schemaQuoted === false ? rawCompletionSchema.toUpperCase() : rawCompletionSchema;
|
||||
const usesOracleCurrentSchema = config?.db_type === "oracle" && !completionSchema;
|
||||
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesOracleCurrentSchema) {
|
||||
return [];
|
||||
}
|
||||
const sessionCacheScope = usesOracleCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : "";
|
||||
const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${schema || ""}:${table}${sessionCacheScope}`;
|
||||
const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${completionSchema || ""}:${completionTable}${sessionCacheScope}`;
|
||||
if (!completionColumnsCache.value[cacheKey]) {
|
||||
await withCompletionInFlight(
|
||||
`${cacheKey}:columns`,
|
||||
@@ -5271,7 +5279,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
if (!usesOracleCurrentSchema && !catalog) {
|
||||
try {
|
||||
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, table, completionSchema);
|
||||
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema);
|
||||
if (assistantColumns.length > 0) {
|
||||
completionColumnsCache.value[cacheKey] = assistantColumns.map((column) => ({
|
||||
name: column.name,
|
||||
@@ -5293,7 +5301,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
const querySchema = usesOracleCurrentSchema ? "" : metadataQuerySchema(connectionId, database, completionSchema);
|
||||
completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, table, catalog, usesOracleCurrentSchema ? context?.clientSessionId : undefined);
|
||||
completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, completionTable, catalog, usesOracleCurrentSchema ? context?.clientSessionId : undefined);
|
||||
evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX);
|
||||
},
|
||||
{ scope: completionLimiterScope(connectionId, database), kind: "columns" },
|
||||
@@ -5302,13 +5310,13 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
const columns = completionColumnsCache.value[cacheKey].map((column) => ({
|
||||
name: column.name,
|
||||
table,
|
||||
table: completionTable,
|
||||
schema: completionSchema,
|
||||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable,
|
||||
comment: column.comment,
|
||||
}));
|
||||
if (!usesOracleCurrentSchema) indexCompletionColumns(connectionId, database, table, schema, columns, catalog);
|
||||
if (!usesOracleCurrentSchema) indexCompletionColumns(connectionId, database, completionTable, completionSchema, columns, catalog);
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -5354,7 +5362,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return listCompletionDatabases(connectionId);
|
||||
}
|
||||
|
||||
function refreshCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number }, catalog?: string): Promise<SqlCompletionColumn[]> {
|
||||
function refreshCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number; tableQuoted?: boolean; schemaQuoted?: boolean }, catalog?: string): Promise<SqlCompletionColumn[]> {
|
||||
return listCompletionColumns(connectionId, database, table, schema, context, catalog);
|
||||
}
|
||||
|
||||
|
||||
+193
-20
@@ -1533,29 +1533,84 @@ fn oracle_table_comments_from_query_result(result: db::QueryResult) -> HashMap<S
|
||||
}
|
||||
|
||||
fn oracle_columns_sql(schema: &str, table: &str) -> String {
|
||||
let owner = oracle_columns_owner_filter(schema);
|
||||
format!(
|
||||
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_DEFAULT, \
|
||||
"WITH synonym_chain AS ( \
|
||||
SELECT CONNECT_BY_ROOT s.OWNER AS root_owner, \
|
||||
s.TABLE_OWNER AS resolved_owner, \
|
||||
s.TABLE_NAME AS resolved_table, \
|
||||
LEVEL AS synonym_depth \
|
||||
FROM ALL_SYNONYMS s \
|
||||
START WITH s.SYNONYM_NAME = {table} \
|
||||
AND s.OWNER IN ({owner}, 'PUBLIC') \
|
||||
AND s.DB_LINK IS NULL \
|
||||
CONNECT BY NOCYCLE \
|
||||
PRIOR s.TABLE_OWNER = s.OWNER \
|
||||
AND PRIOR s.TABLE_NAME = s.SYNONYM_NAME \
|
||||
AND s.DB_LINK IS NULL \
|
||||
), \
|
||||
resolved_object AS ( \
|
||||
SELECT resolved_owner, resolved_table \
|
||||
FROM ( \
|
||||
SELECT resolved_owner, resolved_table, resolution_priority, synonym_depth \
|
||||
FROM ( \
|
||||
SELECT o.OWNER AS resolved_owner, o.OBJECT_NAME AS resolved_table, \
|
||||
0 AS resolution_priority, 0 AS synonym_depth \
|
||||
FROM ALL_OBJECTS o \
|
||||
WHERE o.OWNER = {owner} \
|
||||
AND o.OBJECT_NAME = {table} \
|
||||
AND o.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') \
|
||||
UNION ALL \
|
||||
SELECT sc.resolved_owner, sc.resolved_table, \
|
||||
CASE WHEN sc.root_owner = {owner} THEN 1 ELSE 2 END AS resolution_priority, \
|
||||
sc.synonym_depth \
|
||||
FROM synonym_chain sc \
|
||||
JOIN ALL_OBJECTS o \
|
||||
ON o.OWNER = sc.resolved_owner \
|
||||
AND o.OBJECT_NAME = sc.resolved_table \
|
||||
AND o.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') \
|
||||
) \
|
||||
ORDER BY resolution_priority, synonym_depth \
|
||||
) \
|
||||
WHERE ROWNUM = 1 \
|
||||
) \
|
||||
SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_DEFAULT, \
|
||||
c.DATA_LENGTH, c.DATA_PRECISION, c.DATA_SCALE, c.COLUMN_ID, \
|
||||
CASE WHEN cc.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \
|
||||
cm.COMMENTS \
|
||||
FROM ALL_TAB_COLUMNS c \
|
||||
LEFT JOIN ( \
|
||||
SELECT cols.OWNER, cols.TABLE_NAME, cols.COLUMN_NAME \
|
||||
CASE WHEN EXISTS ( \
|
||||
SELECT 1 \
|
||||
FROM ALL_CONS_COLUMNS cols \
|
||||
JOIN ALL_CONSTRAINTS con \
|
||||
ON con.CONSTRAINT_NAME = cols.CONSTRAINT_NAME \
|
||||
AND con.OWNER = cols.OWNER \
|
||||
ON con.OWNER = cols.OWNER \
|
||||
AND con.CONSTRAINT_NAME = cols.CONSTRAINT_NAME \
|
||||
AND con.CONSTRAINT_TYPE = 'P' \
|
||||
) cc ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME \
|
||||
LEFT JOIN ALL_COL_COMMENTS cm \
|
||||
ON cm.OWNER = c.OWNER AND cm.TABLE_NAME = c.TABLE_NAME AND cm.COLUMN_NAME = c.COLUMN_NAME \
|
||||
WHERE c.OWNER = {} AND c.TABLE_NAME = {} \
|
||||
WHERE cols.OWNER = c.OWNER \
|
||||
AND cols.TABLE_NAME = c.TABLE_NAME \
|
||||
AND cols.COLUMN_NAME = c.COLUMN_NAME \
|
||||
) THEN 1 ELSE 0 END AS IS_PK, \
|
||||
( \
|
||||
SELECT cm.COMMENTS \
|
||||
FROM ALL_COL_COMMENTS cm \
|
||||
WHERE cm.OWNER = c.OWNER \
|
||||
AND cm.TABLE_NAME = c.TABLE_NAME \
|
||||
AND cm.COLUMN_NAME = c.COLUMN_NAME \
|
||||
) AS COMMENTS \
|
||||
FROM ALL_TAB_COLUMNS c \
|
||||
JOIN resolved_object ro \
|
||||
ON ro.resolved_owner = c.OWNER AND ro.resolved_table = c.TABLE_NAME \
|
||||
ORDER BY c.COLUMN_ID",
|
||||
oracle_owner_filter(schema),
|
||||
sql_string(table),
|
||||
table = sql_string(table),
|
||||
)
|
||||
}
|
||||
|
||||
fn oracle_columns_owner_filter(schema: &str) -> String {
|
||||
let schema = schema.trim();
|
||||
if schema.is_empty() {
|
||||
"SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')".to_string()
|
||||
} else {
|
||||
sql_string(&schema.to_uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
fn oracle_column_type(data_type: &str, precision: Option<i32>, scale: Option<i32>, length: Option<i32>) -> String {
|
||||
match data_type.to_ascii_uppercase().as_str() {
|
||||
"NUMBER" => match (precision, scale) {
|
||||
@@ -1650,6 +1705,16 @@ async fn external_driver_oracle_columns_via_sql(
|
||||
Ok(deduplicate_column_infos(oracle_columns_from_query_result(result)))
|
||||
}
|
||||
|
||||
fn should_query_oracle_columns_via_sql_first(
|
||||
db_type: &DatabaseType,
|
||||
schema: &str,
|
||||
client_session_id: Option<&str>,
|
||||
) -> bool {
|
||||
*db_type == DatabaseType::Oracle
|
||||
&& schema.trim().is_empty()
|
||||
&& client_session_id.is_some_and(|session_id| !session_id.trim().is_empty())
|
||||
}
|
||||
|
||||
fn oracle_object_statistics_sql(schema: &str) -> String {
|
||||
oracle_object_statistics_owner_segments_sql(schema, "ALL_SEGMENTS")
|
||||
}
|
||||
@@ -2789,8 +2854,8 @@ mod tests {
|
||||
oracle_object_statistics_sql, oracle_object_statistics_user_segments_sql,
|
||||
oracle_table_comment_from_query_result, oracle_table_comment_sql, oracle_table_comments_from_query_result,
|
||||
oracle_table_comments_sql, presto_like_columns_from_query_result, presto_like_information_schema_columns_sql,
|
||||
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result, table_name_filter_matches,
|
||||
visible_schema_filter, TableNameFilter,
|
||||
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result,
|
||||
should_query_oracle_columns_via_sql_first, table_name_filter_matches, visible_schema_filter, TableNameFilter,
|
||||
};
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
use super::{
|
||||
@@ -3676,8 +3741,62 @@ mod tests {
|
||||
|
||||
assert!(sql.contains("ALL_TAB_COLUMNS"));
|
||||
assert!(sql.contains("ALL_COL_COMMENTS"));
|
||||
assert!(sql.contains("c.OWNER = 'DBX_TEST'"));
|
||||
assert!(sql.contains("c.TABLE_NAME = 'test'"));
|
||||
assert!(sql.contains("o.OWNER = 'DBX_TEST'"));
|
||||
assert!(sql.contains("o.OBJECT_NAME = 'test'"));
|
||||
assert!(sql.contains("cols.OWNER = c.OWNER"));
|
||||
assert!(sql.contains("cols.TABLE_NAME = c.TABLE_NAME"));
|
||||
assert!(sql.contains("cm.OWNER = c.OWNER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_sql_resolves_private_and_public_synonyms_in_oracle_precedence_order() {
|
||||
let sql = oracle_columns_sql("", "ORDERS_ALIAS");
|
||||
|
||||
assert!(sql.contains("SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"));
|
||||
assert!(sql.contains("FROM ALL_SYNONYMS s"));
|
||||
assert!(sql.contains("s.OWNER IN (SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA'), 'PUBLIC')"));
|
||||
assert!(sql.contains("CASE WHEN sc.root_owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 1 ELSE 2 END"));
|
||||
assert!(sql.contains("s.DB_LINK IS NULL"));
|
||||
assert!(sql.contains("ORDER BY resolution_priority, synonym_depth"));
|
||||
assert!(sql.contains("WHERE ROWNUM = 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_sql_follows_two_level_synonyms_without_cycles() {
|
||||
let sql = oracle_columns_sql("DBX_TEST", "ORDERS_ALIAS");
|
||||
|
||||
assert!(sql.contains("SELECT CONNECT_BY_ROOT s.OWNER AS root_owner"));
|
||||
assert!(sql.contains("LEVEL AS synonym_depth"));
|
||||
assert!(sql.contains("CONNECT BY NOCYCLE"));
|
||||
assert!(sql.contains("PRIOR s.TABLE_OWNER = s.OWNER"));
|
||||
assert!(sql.contains("PRIOR s.TABLE_NAME = s.SYNONYM_NAME"));
|
||||
assert!(sql.contains("JOIN ALL_OBJECTS o"));
|
||||
assert!(sql.contains("o.OWNER = sc.resolved_owner"));
|
||||
assert!(sql.contains("o.OBJECT_NAME = sc.resolved_table"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_sql_preserves_quoted_case_synonym_names_and_excludes_database_links() {
|
||||
let sql = oracle_columns_sql("DBX_TEST", "Order Alias");
|
||||
|
||||
assert!(sql.contains("s.SYNONYM_NAME = 'Order Alias'"));
|
||||
assert!(sql.contains("o.OBJECT_NAME = 'Order Alias'"));
|
||||
assert!(!sql.contains("ORDER ALIAS"));
|
||||
assert_eq!(sql.matches("s.DB_LINK IS NULL").count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_session_completion_queries_synonym_aware_columns_sql_first() {
|
||||
assert!(should_query_oracle_columns_via_sql_first(&DatabaseType::Oracle, "", Some("tab-1")));
|
||||
assert!(should_query_oracle_columns_via_sql_first(&DatabaseType::Oracle, " ", Some("tab-1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_sql_first_is_limited_to_current_schema_editor_sessions() {
|
||||
assert!(!should_query_oracle_columns_via_sql_first(&DatabaseType::Oracle, "DBX_TEST", Some("tab-1")));
|
||||
assert!(!should_query_oracle_columns_via_sql_first(&DatabaseType::Oracle, "", None));
|
||||
assert!(!should_query_oracle_columns_via_sql_first(&DatabaseType::Oracle, "", Some(" ")));
|
||||
assert!(!should_query_oracle_columns_via_sql_first(&DatabaseType::Postgres, "", Some("tab-1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4784,6 +4903,32 @@ pub async fn get_columns_core_for_session(
|
||||
if uses_presto_like_information_schema_tables(&config.db_type) {
|
||||
return external_driver_presto_like_columns(session, config.as_ref(), database, schema, table).await;
|
||||
}
|
||||
let query_oracle_columns_first =
|
||||
should_query_oracle_columns_via_sql_first(&config.db_type, schema, client_session_id);
|
||||
if query_oracle_columns_first {
|
||||
match external_driver_oracle_columns_via_sql(
|
||||
session.clone(),
|
||||
config.as_ref(),
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(columns) if !columns.is_empty() => return Ok(columns),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][external-driver:get_columns:oracle-primary-sql-failed] connection_id={} database={} schema={} table={} error={}",
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let columns = session
|
||||
.invoke_with_timeout::<Vec<db::ColumnInfo>>(
|
||||
"getColumns",
|
||||
@@ -4796,7 +4941,7 @@ pub async fn get_columns_core_for_session(
|
||||
agent_metadata_timeout(Some(config.as_ref())),
|
||||
)
|
||||
.await?;
|
||||
if columns.is_empty() && config.db_type == DatabaseType::Oracle {
|
||||
if columns.is_empty() && config.db_type == DatabaseType::Oracle && !query_oracle_columns_first {
|
||||
match external_driver_oracle_columns_via_sql(
|
||||
session.clone(),
|
||||
config.as_ref(),
|
||||
@@ -4872,6 +5017,34 @@ pub async fn get_columns_core_for_session(
|
||||
let fallback_config = db_config.clone();
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
let oracle_sql_config = fallback_config.as_ref().filter(|config| {
|
||||
should_query_oracle_columns_via_sql_first(&config.db_type, schema, client_session_id)
|
||||
});
|
||||
let query_oracle_columns_first = oracle_sql_config.is_some();
|
||||
if let Some(config) = oracle_sql_config {
|
||||
match oracle_columns_via_sql(
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
&mut client,
|
||||
agent_metadata_timeout(Some(config)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(columns) if !columns.is_empty() => return Ok(columns),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][agent:get_columns:oracle-primary-sql-failed] connection_id={} database={} schema={} table={} error={}",
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
match client
|
||||
.get_columns::<Vec<db::ColumnInfo>>(
|
||||
database,
|
||||
@@ -4884,7 +5057,7 @@ pub async fn get_columns_core_for_session(
|
||||
Ok(columns) if !columns.is_empty() => return Ok(deduplicate_column_infos(columns)),
|
||||
Ok(columns) => {
|
||||
if let Some(config) = fallback_config.as_ref() {
|
||||
if config.db_type == DatabaseType::Oracle {
|
||||
if config.db_type == DatabaseType::Oracle && !query_oracle_columns_first {
|
||||
match oracle_columns_via_sql(
|
||||
database,
|
||||
schema,
|
||||
|
||||
Reference in New Issue
Block a user