mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(oracle): restore alias completion and DIP visibility
This commit is contained in:
@@ -49,7 +49,7 @@ WHERE username IS NOT NULL
|
||||
AND username NOT IN (
|
||||
'SYS','SYSTEM','SYSMAN','DBSNMP','SYSBACKUP','SYSDG','SYSKM','SYSRAC','OUTLN',
|
||||
'AUDSYS','LBACSYS','DVF','DVSYS','APPQOSSYS','CTXSYS','MDSYS','MDDATA',
|
||||
'ORDSYS','ORDDATA','ORDPLUGINS','XDB','ANONYMOUS','DIP','EXFSYS',
|
||||
'ORDSYS','ORDDATA','ORDPLUGINS','XDB','ANONYMOUS','EXFSYS',
|
||||
'GSMADMIN_INTERNAL','GSMCATUSER','GSMROOTUSER','GSMUSER','OJVMSYS','OLAPSYS',
|
||||
'ORACLE_OCM','SI_INFORMTN_SCHEMA','WMSYS','XS$NULL','DBSFWUSER',
|
||||
'REMOTE_SCHEDULER_AGENT','PDBADMIN','DGPDB_INT','OPS$ORACLE',
|
||||
@@ -1135,11 +1135,17 @@ func (s *server) currentSchema() (string, error) {
|
||||
}
|
||||
|
||||
func (s *server) normalizeSchema(schema string) (string, error) {
|
||||
schema = strings.TrimSpace(schema)
|
||||
if schema == "" {
|
||||
return s.currentSchema()
|
||||
return resolveOracleSchema(schema, s.currentSchema, s.sessionUser)
|
||||
}
|
||||
|
||||
func resolveOracleSchema(schema string, currentSchema, sessionUser func() (string, error)) (string, error) {
|
||||
if schema = strings.TrimSpace(schema); schema != "" {
|
||||
return strings.ToUpper(schema), nil
|
||||
}
|
||||
return strings.ToUpper(schema), nil
|
||||
if current, err := currentSchema(); err == nil && strings.TrimSpace(current) != "" {
|
||||
return strings.ToUpper(strings.TrimSpace(current)), nil
|
||||
}
|
||||
return sessionUser()
|
||||
}
|
||||
|
||||
func (s *server) sessionUser() (string, error) {
|
||||
|
||||
@@ -652,6 +652,9 @@ func TestListDatabasesSQLUsesUserDictionaryInsteadOfObjectDictionary(t *testing.
|
||||
if strings.Contains(sqlText, "ALL_TABLES") || strings.Contains(sqlText, "ALL_VIEWS") {
|
||||
t.Fatalf("schema listing should not scan object dictionaries, got: %s", oracleListDatabasesSQL)
|
||||
}
|
||||
if strings.Contains(sqlText, "'DIP'") {
|
||||
t.Fatalf("schema listing should not hide an existing user named DIP, got: %s", oracleListDatabasesSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDatabasesSQLCanApplyVisibleSchemaFilter(t *testing.T) {
|
||||
@@ -672,6 +675,41 @@ func TestListDatabasesSQLCanApplyVisibleSchemaFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOracleSchemaPrefersCurrentSchemaOverSessionUser(t *testing.T) {
|
||||
currentCalls := 0
|
||||
sessionUserCalls := 0
|
||||
schema, err := resolveOracleSchema(
|
||||
"",
|
||||
func() (string, error) {
|
||||
currentCalls++
|
||||
return "REPORTING", nil
|
||||
},
|
||||
func() (string, error) {
|
||||
sessionUserCalls++
|
||||
return "APP", nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil || schema != "REPORTING" {
|
||||
t.Fatalf("resolved schema = %q, err = %v; want REPORTING", schema, err)
|
||||
}
|
||||
if currentCalls != 1 || sessionUserCalls != 0 {
|
||||
t.Fatalf("unexpected resolver calls: current=%d session_user=%d", currentCalls, sessionUserCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOracleSchemaFallsBackToSessionUser(t *testing.T) {
|
||||
schema, err := resolveOracleSchema(
|
||||
"",
|
||||
func() (string, error) { return "", errors.New("CURRENT_SCHEMA unavailable") },
|
||||
func() (string, error) { return "APP", nil },
|
||||
)
|
||||
|
||||
if err != nil || schema != "APP" {
|
||||
t.Fatalf("resolved schema = %q, err = %v; want APP", schema, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTablesSQLUsesSplitDictionaryQuery(t *testing.T) {
|
||||
sqlText := strings.ToUpper(oracleListTablesSQL)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget }
|
||||
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 { usesOracleSessionCompletionColumns as shouldUseOracleSessionCompletionColumns } from "@/lib/sql/oracleCompletionSession";
|
||||
import { extractIdentifierDetailsAt, isSqlKeyword, matchTable, mergeSqlObjectNavigationType, splitQualifiedIdentifier, sqlObjectHoverDetail, sqlObjectNavigationTarget, type SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
|
||||
import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sql/sqlDiagnostics";
|
||||
import {
|
||||
@@ -95,6 +96,8 @@ const props = defineProps<{
|
||||
connectionId?: string;
|
||||
database?: string;
|
||||
schema?: string;
|
||||
clientSessionId?: string;
|
||||
completionContextVersion?: number;
|
||||
databaseType?: DatabaseType;
|
||||
dialect?: "mysql" | "postgres" | "sqlserver";
|
||||
syntaxDialect?: "mysql" | "postgres" | "sqlserver";
|
||||
@@ -350,6 +353,38 @@ const cachedInsertValueHintColumnsByTable = new Map<string, string[]>();
|
||||
const cachedForeignKeysByTable = new Map<string, SqlCompletionForeignKey[]>();
|
||||
const loadedColumnsByTable = new Set<string>();
|
||||
|
||||
function usesOracleSessionCompletionColumns(schema?: string | null): boolean {
|
||||
return shouldUseOracleSessionCompletionColumns({
|
||||
databaseType: props.databaseType,
|
||||
selectedSchema: props.schema,
|
||||
referenceSchema: schema,
|
||||
clientSessionId: props.clientSessionId,
|
||||
});
|
||||
}
|
||||
|
||||
function completionColumnRequestContext() {
|
||||
return {
|
||||
clientSessionId: props.clientSessionId,
|
||||
version: props.completionContextVersion,
|
||||
};
|
||||
}
|
||||
|
||||
async function listCompletionColumnsForEditor(connectionId: string, database: string, table: string, schema?: string) {
|
||||
const requestedVersion = props.completionContextVersion;
|
||||
const sessionScoped = usesOracleSessionCompletionColumns(schema);
|
||||
const columns = await connectionStore.listCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext());
|
||||
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) {
|
||||
const requestedVersion = props.completionContextVersion;
|
||||
const sessionScoped = usesOracleSessionCompletionColumns(schema);
|
||||
const columns = await connectionStore.refreshCompletionColumns(connectionId, database, table, schema, completionColumnRequestContext());
|
||||
if (sessionScoped && requestedVersion !== props.completionContextVersion) throw new Error("Stale Oracle completion context");
|
||||
return columns;
|
||||
}
|
||||
|
||||
const zoomCommitScheduler = createEditorZoomCommitScheduler((fontSize) => {
|
||||
if (settingsStore.editorSettings.fontSize === fontSize) return;
|
||||
settingsStore.updateEditorSettings({ fontSize });
|
||||
@@ -1378,7 +1413,7 @@ function requestInsertValueHintTableColumns(table: string, schema?: string, data
|
||||
cachedInsertValueHintColumnsByTable.set(cacheKey, insertValueHintColumnNames(databaseType, columns));
|
||||
return;
|
||||
}
|
||||
const columns = await connectionStore.listCompletionColumns(connectionId, target.database, table, target.schema);
|
||||
const columns = await listCompletionColumnsForEditor(connectionId, target.database, table, target.schema);
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
};
|
||||
void loadColumns()
|
||||
@@ -1458,7 +1493,7 @@ async function ensureColumnsForTable(table: { name: string; schema?: string | nu
|
||||
if (!props.connectionId || props.database == null) return false;
|
||||
const target = completionMetadataTarget(table);
|
||||
if (!target) return false;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId, target.database, table.name, target.schema);
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId, target.database, table.name, target.schema);
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
loadedColumnsByTable.add(cacheKey.toLowerCase());
|
||||
return true;
|
||||
@@ -1782,6 +1817,10 @@ async function enrichSemanticDiagnosticTables(tables: SqlTableReference[]): Prom
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
if (usesOracleSessionCompletionColumns(table.schema)) {
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const match = await findExactSemanticDiagnosticTable(table);
|
||||
if (!match) missingTables.add(tableReferenceKey(table));
|
||||
@@ -2439,7 +2478,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
const insertSchema = completionContext.insertSchema ?? props.schema;
|
||||
const insertColumns = connectionStore.lookupLocalCompletionColumns(props.connectionId, props.database, completionContext.insertTable, insertSchema);
|
||||
const insertColumns = usesOracleSessionCompletionColumns(insertSchema) ? [] : connectionStore.lookupLocalCompletionColumns(props.connectionId, props.database, completionContext.insertTable, insertSchema);
|
||||
if (insertColumns.length > 0) {
|
||||
columnsByTable.set(insertSchema ? `${insertSchema}.${completionContext.insertTable}` : completionContext.insertTable, insertColumns);
|
||||
}
|
||||
@@ -2453,7 +2492,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
columnsByTable.set(cacheKey, cached);
|
||||
} else {
|
||||
const target = completionMetadataTarget(qualifiedColumnTarget);
|
||||
const localColumns = target ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, qualifiedColumnTarget.name, target.schema) : [];
|
||||
const localColumns = target && !usesOracleSessionCompletionColumns(target.schema) ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, qualifiedColumnTarget.name, target.schema) : [];
|
||||
if (localColumns.length > 0) {
|
||||
columnsByTable.set(cacheKey, localColumns);
|
||||
}
|
||||
@@ -2482,7 +2521,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
continue;
|
||||
}
|
||||
const target = completionMetadataTarget(refTable);
|
||||
const localColumns = target ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, refTable.name, target.schema) : [];
|
||||
const localColumns = target && !usesOracleSessionCompletionColumns(target.schema) ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, refTable.name, target.schema) : [];
|
||||
if (localColumns.length > 0) {
|
||||
columnsByTable.set(cacheKey, localColumns);
|
||||
}
|
||||
@@ -2558,8 +2597,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
}
|
||||
if (!onDemandOnlyColumns && completionContext.insertTable) {
|
||||
const insertTable = completionContext.insertTable;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, database, insertTable, completionContext.insertSchema ?? props.schema)
|
||||
void refreshCompletionColumnsForEditor(connectionId, database, insertTable, completionContext.insertSchema ?? props.schema)
|
||||
.then((columns) => {
|
||||
const insertSchema = completionContext.insertSchema ?? props.schema;
|
||||
cachedColumnsByTable.set(insertSchema ? `${insertSchema}.${insertTable}` : insertTable, columns);
|
||||
@@ -2571,8 +2609,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
if (!onDemandOnlyColumns && qualifiedColumnTarget && qualifiedColumnCacheKey && !cachedColumnsByTable.has(qualifiedColumnCacheKey)) {
|
||||
const target = completionMetadataTarget(qualifiedColumnTarget);
|
||||
if (target) {
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, target.database, qualifiedColumnTarget.name, target.schema)
|
||||
void refreshCompletionColumnsForEditor(connectionId, target.database, qualifiedColumnTarget.name, target.schema)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(qualifiedColumnCacheKey, columns);
|
||||
})
|
||||
@@ -2588,8 +2625,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
if (cachedColumnsByTable.has(cacheKey)) continue;
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, target.database, refTable.name, target.schema)
|
||||
void refreshCompletionColumnsForEditor(connectionId, target.database, refTable.name, target.schema)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(cacheKey, columns);
|
||||
})
|
||||
@@ -2692,7 +2728,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
try {
|
||||
const insertCols = await connectionStore.listCompletionColumns(props.connectionId!, props.database!, completionContext.insertTable, completionContext.insertSchema ?? props.schema);
|
||||
const insertCols = await listCompletionColumnsForEditor(props.connectionId!, props.database!, completionContext.insertTable, completionContext.insertSchema ?? props.schema);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
if (insertCols.length > 0) {
|
||||
const insertSchema = completionContext.insertSchema ?? props.schema;
|
||||
@@ -2776,6 +2812,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
|
||||
// Collect referenced tables — enrich with schema from filtered table lookup
|
||||
let refs = completionContext.referencedTables.map((rt) => {
|
||||
if (usesOracleSessionCompletionColumns(rt.schema)) return rt;
|
||||
if (!rt.schema) {
|
||||
const cached = tables.find((t) => t.name.toLowerCase() === rt.name.toLowerCase());
|
||||
if (cached && cached.schema) {
|
||||
@@ -2784,12 +2821,13 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
}
|
||||
return rt;
|
||||
});
|
||||
const unresolvedRefs = refs.filter((rt) => !rt.schema && !rt.columns && !isVirtualCompletionTableReference(rt));
|
||||
const unresolvedRefs = refs.filter((rt) => !usesOracleSessionCompletionColumns(rt.schema) && !rt.schema && !rt.columns && !isVirtualCompletionTableReference(rt));
|
||||
if (!localOnlyMetadata && unresolvedRefs.length > 0) {
|
||||
const lookupGroups = await Promise.all(unresolvedRefs.map((rt) => connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20, props.schema, false, props.schema)));
|
||||
if (epoch !== completionEpoch) return null;
|
||||
const lookupTables = lookupGroups.flat();
|
||||
refs = refs.map((rt) => {
|
||||
if (usesOracleSessionCompletionColumns(rt.schema)) return rt;
|
||||
if (rt.schema || rt.columns) return rt;
|
||||
const matched = lookupTables.find((table) => table.name.toLowerCase() === rt.name.toLowerCase());
|
||||
return matched?.schema ? { ...rt, schema: matched.schema } : rt;
|
||||
@@ -2830,7 +2868,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
try {
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId!, target.database, refTable.name, target.schema);
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
@@ -3639,6 +3677,7 @@ onMounted(async () => {
|
||||
let referencedTables: Array<SqlCompletionReferencedTable & Pick<SqlCompletionTable, "type">> = context.referencedTables;
|
||||
// Enrich referenced tables with schema from cachedTables
|
||||
referencedTables = referencedTables.map((rt) => {
|
||||
if (usesOracleSessionCompletionColumns(rt.schema)) return rt;
|
||||
const cached = cachedTables.find((ct) => ct.name.toLowerCase() === rt.name.toLowerCase() && (!rt.schema || !ct.schema || ct.schema.toLowerCase() === rt.schema.toLowerCase()));
|
||||
if (!cached) return rt;
|
||||
return {
|
||||
@@ -3682,7 +3721,8 @@ onMounted(async () => {
|
||||
let cols = cachedColumnsByTable.get(cacheKey);
|
||||
if (!cols) {
|
||||
try {
|
||||
cols = await connectionStore.listCompletionColumns(props.connectionId!, props.database!, refTable.name, refTable.schema ?? props.schema);
|
||||
const columnSchema = refTable.schema ?? props.schema;
|
||||
cols = await listCompletionColumnsForEditor(props.connectionId!, props.database!, refTable.name, columnSchema);
|
||||
cachedColumnsByTable.set(cacheKey, cols);
|
||||
} catch {
|
||||
continue;
|
||||
@@ -3821,6 +3861,13 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch([() => props.clientSessionId, () => props.completionContextVersion], () => {
|
||||
completionEpoch++;
|
||||
refreshCompletionCache();
|
||||
setSemanticDiagnostics([]);
|
||||
scheduleSemanticDiagnostics();
|
||||
});
|
||||
|
||||
watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect], () => {
|
||||
executableStatementRangeCache = null;
|
||||
if (!view.value || !sqlLanguageComp || !buildSqlLanguageExtension || !sqlSemanticHighlightComp || !buildSqlSemanticHighlightExtension || !sqlSignatureComp || !buildSqlSignatureExtension) return;
|
||||
|
||||
@@ -814,6 +814,8 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
:connection-id="activeTab.connectionId"
|
||||
:database="activeTab.database"
|
||||
:schema="activeTab.schema"
|
||||
:client-session-id="activeTab.id"
|
||||
:completion-context-version="activeTab.completionContextVersion"
|
||||
:database-type="activeEffectiveDatabaseType"
|
||||
:dialect="editorDialect"
|
||||
:syntax-dialect="editorSyntaxDialect"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { usesOracleSessionCompletionColumns } from "@/lib/sql/oracleCompletionSession";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("Oracle session-scoped completion", () => {
|
||||
it("uses the tab session only for unqualified references without a selected schema", () => {
|
||||
expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a" })).toBe(true);
|
||||
expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a", referenceSchema: "REPORTING" })).toBe(false);
|
||||
expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a", selectedSchema: "REPORTING" })).toBe(false);
|
||||
expect(usesOracleSessionCompletionColumns({ databaseType: "postgres", clientSessionId: "tab-a" })).toBe(false);
|
||||
expect(usesOracleSessionCompletionColumns({ databaseType: "oracle" })).toBe(false);
|
||||
});
|
||||
|
||||
it("passes tab context into QueryEditor and clears local caches when the tab or context changes", () => {
|
||||
expect(contentAreaSource).toContain(':client-session-id="activeTab.id"');
|
||||
expect(contentAreaSource).toContain(':completion-context-version="activeTab.completionContextVersion"');
|
||||
expect(queryEditorSource).toMatch(/watch\(\s*\[\(\) => props\.clientSessionId, \(\) => props\.completionContextVersion\][\s\S]*?refreshCompletionCache\(\)/);
|
||||
});
|
||||
});
|
||||
@@ -640,8 +640,8 @@ export async function getObjectSource(connectionId: string, database: string, sc
|
||||
return get(`/api/schema/object-source?${qs({ connection_id: connectionId, database, schema, table: name, object_type: objectType, signature })}`);
|
||||
}
|
||||
|
||||
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ColumnInfo[]> {
|
||||
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
|
||||
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string, clientSessionId?: string): Promise<ColumnInfo[]> {
|
||||
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table, catalog, client_session_id: clientSessionId })}`);
|
||||
}
|
||||
|
||||
export async function getSqlServerColumnMetadata(connectionId: string, database: string, schema: string, table: string): Promise<SqlServerColumnMetadata[]> {
|
||||
|
||||
@@ -801,8 +801,8 @@ export async function listSchemaInfos(connectionId: string, database: string): P
|
||||
return invoke("list_schema_infos", { connectionId, database });
|
||||
}
|
||||
|
||||
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ColumnInfo[]> {
|
||||
return invoke("get_columns", { connectionId, database, schema, table, catalog });
|
||||
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string, clientSessionId?: string): Promise<ColumnInfo[]> {
|
||||
return invoke("get_columns", { connectionId, database, schema, table, catalog, clientSessionId });
|
||||
}
|
||||
|
||||
export async function getSqlServerColumnMetadata(connectionId: string, database: string, schema: string, table: string): Promise<SqlServerColumnMetadata[]> {
|
||||
|
||||
@@ -26,7 +26,6 @@ const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>>
|
||||
"audsys",
|
||||
"ctxsys",
|
||||
"dbsnmp",
|
||||
"dip",
|
||||
"dvf",
|
||||
"dvsys",
|
||||
"exfsys",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export function usesOracleSessionCompletionColumns(options: { databaseType?: DatabaseType; selectedSchema?: string; referenceSchema?: string | null; clientSessionId?: string }): boolean {
|
||||
return options.databaseType === "oracle" && !options.selectedSchema && !options.referenceSchema && !!options.clientSessionId;
|
||||
}
|
||||
@@ -232,6 +232,95 @@ describe("connectionStore completion assistant", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("lets Oracle resolve CURRENT_SCHEMA for unqualified column completion", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
});
|
||||
const getColumns = vi.fn().mockResolvedValue([{ name: "REPORT_ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null, comment: null }]);
|
||||
|
||||
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");
|
||||
|
||||
const columns = await store.listCompletionColumns("oracle-1", "ORCL", "ORDERS", undefined, { clientSessionId: "tab-a", version: 0 });
|
||||
|
||||
expect(completionAssistantSearch).not.toHaveBeenCalled();
|
||||
expect(getColumns).toHaveBeenCalledWith("oracle-1", "ORCL", "", "ORDERS", undefined, "tab-a");
|
||||
expect(columns).toEqual([expect.objectContaining({ name: "REPORT_ID", table: "ORDERS", schema: undefined, dataType: "NUMBER" })]);
|
||||
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "ORDERS")).toEqual([]);
|
||||
});
|
||||
|
||||
it("isolates Oracle CURRENT_SCHEMA column caches by tab and context version", async () => {
|
||||
const completionAssistantSearch = vi.fn();
|
||||
const getColumns = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([{ name: "APP_ID", data_type: "NUMBER", is_nullable: false }])
|
||||
.mockResolvedValueOnce([{ name: "REPORT_ID", data_type: "NUMBER", is_nullable: false }])
|
||||
.mockResolvedValueOnce([{ name: "SYSTEM_ID", data_type: "NUMBER", is_nullable: false }]);
|
||||
|
||||
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");
|
||||
|
||||
const app = await store.listCompletionColumns("oracle-1", "ORCL", "SHARED_TABLE", undefined, { clientSessionId: "tab-a", version: 0 });
|
||||
const cachedApp = await store.listCompletionColumns("oracle-1", "ORCL", "SHARED_TABLE", undefined, { clientSessionId: "tab-a", version: 0 });
|
||||
const reporting = await store.listCompletionColumns("oracle-1", "ORCL", "SHARED_TABLE", undefined, { clientSessionId: "tab-a", version: 1 });
|
||||
const independentTab = await store.listCompletionColumns("oracle-1", "ORCL", "SHARED_TABLE", undefined, { clientSessionId: "tab-b", version: 0 });
|
||||
|
||||
expect(app.map((column) => column.name)).toEqual(["APP_ID"]);
|
||||
expect(cachedApp.map((column) => column.name)).toEqual(["APP_ID"]);
|
||||
expect(reporting.map((column) => column.name)).toEqual(["REPORT_ID"]);
|
||||
expect(independentTab.map((column) => column.name)).toEqual(["SYSTEM_ID"]);
|
||||
expect(getColumns).toHaveBeenCalledTimes(3);
|
||||
expect(getColumns.mock.calls.map((call) => call[5])).toEqual(["tab-a", "tab-a", "tab-b"]);
|
||||
expect(completionAssistantSearch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps explicit Oracle schema completion on the shared assistant path", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [{ name: "EXPLICIT_ID", kind: "column", schema: "REPORTING", parent_schema: "REPORTING", parent_name: "SHARED_TABLE", data_type: "NUMBER" }],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
});
|
||||
const getColumns = vi.fn();
|
||||
|
||||
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");
|
||||
|
||||
const columns = await store.listCompletionColumns("oracle-1", "ORCL", "SHARED_TABLE", "REPORTING", { clientSessionId: "tab-a", version: 2 });
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "REPORTING", parent_schema: "REPORTING", parent_name: "SHARED_TABLE" }));
|
||||
expect(getColumns).not.toHaveBeenCalled();
|
||||
expect(columns).toEqual([expect.objectContaining({ name: "EXPLICIT_ID", schema: "REPORTING" })]);
|
||||
});
|
||||
|
||||
it("maps Oracle package members without scanning every schema", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [{ name: "CALCULATE_BONUS", kind: "function", schema: "HR", parent_schema: "HR", parent_name: "PAYROLL", data_type: "FUNCTION" }],
|
||||
|
||||
@@ -83,6 +83,51 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(tab.result?.columns).toEqual(["Error"]);
|
||||
});
|
||||
|
||||
it("invalidates only the executing Oracle tab after successful CURRENT_SCHEMA changes", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oracle-1",
|
||||
name: "Oracle",
|
||||
db_type: "oracle",
|
||||
database: "ORCL",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.executeMulti
|
||||
.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }])
|
||||
.mockResolvedValueOnce([{ columns: ["Error"], rows: [["schema missing"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }])
|
||||
.mockResolvedValueOnce([{ columns: [], rows: [], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabA = store.createTab("oracle-1", "ORCL", "Tab A");
|
||||
const tabB = store.createTab("oracle-1", "ORCL", "Tab B");
|
||||
|
||||
await store.executeTabSql(tabA, "ALTER SESSION SET CURRENT_SCHEMA = REPORTING");
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)?.completionContextVersion).toBe(1);
|
||||
expect(store.tabs.find((tab) => tab.id === tabB)?.completionContextVersion).toBeUndefined();
|
||||
|
||||
await store.executeTabSql(tabA, "/* retry */ ALTER SESSION SET CURRENT_SCHEMA = MISSING");
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)?.completionContextVersion).toBe(1);
|
||||
|
||||
await store.executeTabSql(tabA, "-- switch back\nALTER SESSION SET CURRENT_SCHEMA = APP");
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)?.completionContextVersion).toBe(2);
|
||||
expect(mocks.executeMulti.mock.calls.map((call) => call[5]?.clientSessionId)).toEqual([tabA, tabA, tabA]);
|
||||
});
|
||||
|
||||
it("invalidates Oracle completion metadata when clearing a tab schema resets its session", async () => {
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "oracle-1",
|
||||
name: "Oracle",
|
||||
db_type: "oracle",
|
||||
database: "ORCL",
|
||||
});
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Oracle", "query", "REPORTING");
|
||||
|
||||
store.updateSchema(tabId, undefined);
|
||||
|
||||
expect(store.tabs.find((tab) => tab.id === tabId)?.completionContextVersion).toBe(1);
|
||||
});
|
||||
|
||||
it("preserves the selected statement's absolute editor range", async () => {
|
||||
mocks.executeMulti.mockResolvedValue([{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
|
||||
@@ -2281,7 +2281,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
||||
const schemaFilterConfig = config;
|
||||
const effectiveDb = schemaFilterConfig.database || "";
|
||||
const cacheKey = schemaCacheKey(connectionId, effectiveDb, "schemas");
|
||||
const cacheKey = schemaCacheKey(connectionId, effectiveDb, config.db_type === "oracle" ? "schemas-v2" : "schemas");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey);
|
||||
if (cached.hit) {
|
||||
@@ -4781,39 +4781,45 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return deduped;
|
||||
}
|
||||
|
||||
async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionColumn[]> {
|
||||
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(getConfig(connectionId)) && !schema) {
|
||||
async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number }): Promise<SqlCompletionColumn[]> {
|
||||
const config = getConfig(connectionId);
|
||||
const completionSchema = schema?.trim() || undefined;
|
||||
const usesOracleCurrentSchema = config?.db_type === "oracle" && !completionSchema;
|
||||
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesOracleCurrentSchema) {
|
||||
return [];
|
||||
}
|
||||
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`;
|
||||
const sessionCacheScope = usesOracleCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : "";
|
||||
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}${sessionCacheScope}`;
|
||||
if (!completionColumnsCache.value[cacheKey]) {
|
||||
await withCompletionInFlight(
|
||||
`${cacheKey}:columns`,
|
||||
async () => {
|
||||
await ensureConnected(connectionId);
|
||||
try {
|
||||
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, table, schema);
|
||||
if (assistantColumns.length > 0) {
|
||||
completionColumnsCache.value[cacheKey] = assistantColumns.map((column) => ({
|
||||
name: column.name,
|
||||
data_type: column.dataType ?? "",
|
||||
is_nullable: column.isNullable ?? true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: column.comment ?? null,
|
||||
numeric_precision: null,
|
||||
numeric_scale: null,
|
||||
character_maximum_length: null,
|
||||
}));
|
||||
evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX);
|
||||
return;
|
||||
if (!usesOracleCurrentSchema) {
|
||||
try {
|
||||
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, table, completionSchema);
|
||||
if (assistantColumns.length > 0) {
|
||||
completionColumnsCache.value[cacheKey] = assistantColumns.map((column) => ({
|
||||
name: column.name,
|
||||
data_type: column.dataType ?? "",
|
||||
is_nullable: column.isNullable ?? true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: column.comment ?? null,
|
||||
numeric_precision: null,
|
||||
numeric_scale: null,
|
||||
character_maximum_length: null,
|
||||
}));
|
||||
evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the existing metadata path below.
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the existing metadata path below.
|
||||
}
|
||||
const querySchema = metadataQuerySchema(connectionId, database, schema);
|
||||
completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, table);
|
||||
const querySchema = usesOracleCurrentSchema ? "" : metadataQuerySchema(connectionId, database, completionSchema);
|
||||
completionColumnsCache.value[cacheKey] = await api.getColumns(connectionId, database, querySchema, table, undefined, usesOracleCurrentSchema ? context?.clientSessionId : undefined);
|
||||
evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX);
|
||||
},
|
||||
{ scope: completionLimiterScope(connectionId, database), kind: "columns" },
|
||||
@@ -4823,12 +4829,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const columns = completionColumnsCache.value[cacheKey].map((column) => ({
|
||||
name: column.name,
|
||||
table,
|
||||
schema,
|
||||
schema: completionSchema,
|
||||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable,
|
||||
comment: column.comment,
|
||||
}));
|
||||
indexCompletionColumns(connectionId, database, table, schema, columns);
|
||||
if (!usesOracleCurrentSchema) indexCompletionColumns(connectionId, database, table, schema, columns);
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -4874,8 +4880,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return listCompletionDatabases(connectionId);
|
||||
}
|
||||
|
||||
function refreshCompletionColumns(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionColumn[]> {
|
||||
return listCompletionColumns(connectionId, database, table, schema);
|
||||
function refreshCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number }): Promise<SqlCompletionColumn[]> {
|
||||
return listCompletionColumns(connectionId, database, table, schema, context);
|
||||
}
|
||||
|
||||
function refreshCompletionForeignKeys(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionForeignKey[]> {
|
||||
|
||||
@@ -186,6 +186,25 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas
|
||||
return results;
|
||||
}
|
||||
|
||||
function isOracleCurrentSchemaStatement(statement: string | undefined): boolean {
|
||||
let remaining = statement?.trimStart() ?? "";
|
||||
while (remaining) {
|
||||
if (remaining.startsWith("--")) {
|
||||
const newline = remaining.indexOf("\n");
|
||||
remaining = newline < 0 ? "" : remaining.slice(newline + 1).trimStart();
|
||||
continue;
|
||||
}
|
||||
if (remaining.startsWith("/*")) {
|
||||
const end = remaining.indexOf("*/", 2);
|
||||
if (end < 0) return false;
|
||||
remaining = remaining.slice(end + 2).trimStart();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return /^ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\s*=/i.test(remaining);
|
||||
}
|
||||
|
||||
function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string, databaseType?: DatabaseType, sourceRange?: { from: number; to: number }): QueryResult {
|
||||
result.sourceStatement = sourceStatement;
|
||||
if (sourceRange) {
|
||||
@@ -514,6 +533,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
function queueTabSessionReset(tab: QueryTab) {
|
||||
tab.completionContextVersion = (tab.completionContextVersion ?? 0) + 1;
|
||||
const previousReset = pendingTabSessionResets.get(tab.id);
|
||||
const reset = (async () => {
|
||||
if (previousReset) await previousReset;
|
||||
@@ -3412,6 +3432,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
executionPromise = api.executeMulti(tab.connectionId, executionDatabase, sqlToExecute, executionSchema, executionId, executionOptions);
|
||||
}
|
||||
const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset);
|
||||
const successfulOracleSchemaChanges = effectiveDbType === "oracle" ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
|
||||
if (hiddenPrimaryKeys.length > 0 && results.length === 1) {
|
||||
const hiddenIndexes = hiddenResultColumnIndexes(results[0]!.columns, hiddenPrimaryKeys);
|
||||
if (hiddenIndexes.length > 0) results[0]!.hidden_column_indexes = hiddenIndexes;
|
||||
@@ -3428,6 +3449,9 @@ export const useQueryStore = defineStore("query", () => {
|
||||
});
|
||||
const current = tabs.value.find((t) => t.id === id);
|
||||
if (current?.executionId === executionId) {
|
||||
if (successfulOracleSchemaChanges > 0) {
|
||||
current.completionContextVersion = (current.completionContextVersion ?? 0) + successfulOracleSchemaChanges;
|
||||
}
|
||||
const activeGroupIndex = current.activeResultIndex;
|
||||
const activeGroupResults = current.results;
|
||||
const shouldAppendResult = !!options?.appendResult && !!current.result;
|
||||
|
||||
@@ -831,6 +831,8 @@ export interface QueryTab {
|
||||
explainExecutionId?: string;
|
||||
/** Per-run connection session for explain flows that require session state. */
|
||||
explainClientSessionId?: string;
|
||||
/** Invalidates tab-scoped completion metadata after session context changes. */
|
||||
completionContextVersion?: number;
|
||||
mode: "data" | "query" | "redis" | "redis-dashboard" | "mongo" | "mongo-gridfs" | "mongo-bucket" | "vector" | "etcd" | "zookeeper" | "mq" | "nacos" | "objects" | "structure" | "users" | "dameng-jobs" | "processlist" | "mysql-dashboard" | "postgres-dashboard";
|
||||
mqTenant?: string;
|
||||
mqInitialTab?: "topics";
|
||||
|
||||
@@ -4268,6 +4268,20 @@ async fn retry_metadata_connection<T, F, Fut>(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: Option<&str>,
|
||||
operation: F,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<T, String>>,
|
||||
{
|
||||
retry_metadata_connection_for_session(state, connection_id, database, None, operation).await
|
||||
}
|
||||
|
||||
async fn retry_metadata_connection_for_session<T, F, Fut>(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: Option<&str>,
|
||||
client_session_id: Option<&str>,
|
||||
mut operation: F,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
@@ -4277,7 +4291,7 @@ where
|
||||
let result = operation().await;
|
||||
match result {
|
||||
Err(error) if is_retryable_metadata_error(&error) => {
|
||||
state.reconnect_pool(connection_id, database).await?;
|
||||
state.reconnect_pool_for_session(connection_id, database, client_session_id).await?;
|
||||
operation().await
|
||||
}
|
||||
_ => result,
|
||||
@@ -4295,8 +4309,21 @@ pub async fn get_columns_core(
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
retry_metadata_connection(state, connection_id, Some(database), || async {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
get_columns_core_for_session(state, connection_id, database, schema, table, None).await
|
||||
}
|
||||
|
||||
pub async fn get_columns_core_for_session(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
client_session_id: Option<&str>,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
retry_metadata_connection_for_session(state, connection_id, Some(database), client_session_id, || async {
|
||||
let pool_key = state
|
||||
.get_or_create_pool_for_session(connection_id, Some(database), client_session_id)
|
||||
.await?;
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
let duckdb_attached_names = duckdb_attached_database_names(state, connection_id).await;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct SchemaQuery {
|
||||
pub signature: Option<String>,
|
||||
pub object_types: Option<String>,
|
||||
pub apply_visible_filter: Option<bool>,
|
||||
pub client_session_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_databases(
|
||||
@@ -300,9 +301,16 @@ pub async fn list_columns(
|
||||
.await
|
||||
.map_err(AppError)?
|
||||
} else {
|
||||
dbx_core::schema::get_columns_core(&state.app, &q.connection_id, database, schema, table)
|
||||
.await
|
||||
.map_err(AppError)?
|
||||
dbx_core::schema::get_columns_core_for_session(
|
||||
&state.app,
|
||||
&q.connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
q.client_session_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(AppError)?
|
||||
};
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ test("Dameng default SYSDBA user remains selectable", () => {
|
||||
assert.deepEqual(filterDatabaseNamesForConnection(["SYS", "SYSDBA", "SYSAUDITOR"], config({ db_type: "dameng" })), ["SYSDBA"]);
|
||||
});
|
||||
|
||||
test("Oracle keeps an existing DIP user visible", () => {
|
||||
assert.deepEqual(filterSchemaNamesForConnection(["DBX_TEST", "DIP", "SYSTEM"], config({ db_type: "oracle", database: "XE" }), "XE"), ["DBX_TEST", "DIP"]);
|
||||
});
|
||||
|
||||
test("visible database selection is stale when connection target changes", () => {
|
||||
const previous = config({ host: "db.internal", visible_databases: ["app"] });
|
||||
assert.equal(visibleDatabaseSelectionIsStale(previous, config({ host: "db.internal" })), false);
|
||||
|
||||
@@ -292,12 +292,21 @@ pub async fn get_columns(
|
||||
schema: String,
|
||||
table: String,
|
||||
catalog: Option<String>,
|
||||
client_session_id: Option<String>,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
if let Some(catalog) = external_doris_catalog(&state, &connection_id, catalog.as_deref()).await {
|
||||
return dbx_core::schema::get_doris_catalog_columns_core(&state, &connection_id, &catalog, &database, &table)
|
||||
.await;
|
||||
}
|
||||
dbx_core::schema::get_columns_core(&state, &connection_id, &database, &schema, &table).await
|
||||
dbx_core::schema::get_columns_core_for_session(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&schema,
|
||||
&table,
|
||||
client_session_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user