fix(query): use execution database for result metadata
This commit is contained in:
parent
abac9a7596
commit
d221e0dee7
|
|
@ -123,6 +123,95 @@ describe("queryStore hidden primary key editing", () => {
|
|||
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads metadata from the connection default database when the query tab database is empty", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM users");
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith("mysql-1", "app", "app", "users", undefined);
|
||||
expect(executeMulti).toHaveBeenCalledWith("mysql-1", "", "SELECT name, `id` AS `__DBX_PK_0` FROM users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"]));
|
||||
expect(tab.tableMeta?.database).toBe("app");
|
||||
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses the connection default database for SQL library tabs without a saved database", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.openSavedSql({
|
||||
id: "saved-1",
|
||||
connectionId: "mysql-1",
|
||||
name: "users.sql",
|
||||
database: "",
|
||||
sql: "SELECT name FROM users",
|
||||
createdAt: "2026-07-21T00:00:00.000Z",
|
||||
updatedAt: "2026-07-21T00:00:00.000Z",
|
||||
});
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM users");
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith("mysql-1", "app", "app", "users", undefined);
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"]));
|
||||
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves JDBC catalog metadata lookup when the tab uses the connection default database", async () => {
|
||||
getConnectionConfig.mockReturnValue({ id: "jdbc-1", name: "JDBC MySQL", db_type: "jdbc", connection_string: "jdbc:mysql://localhost:3306/app", database: "app", query_timeout_secs: 30 });
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("jdbc-1", "", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM users");
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith("jdbc-1", "app", "", "users", undefined);
|
||||
expect(executeMulti).toHaveBeenCalledWith("jdbc-1", "", "SELECT name, `id` AS `__DBX_PK_0` FROM users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"]));
|
||||
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps an explicitly selected database instead of falling back to the connection default", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "analytics", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM users");
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith("mysql-1", "analytics", "analytics", "users", undefined);
|
||||
expect(executeMulti).toHaveBeenCalledWith("mysql-1", "analytics", "SELECT name, `id` AS `__DBX_PK_0` FROM users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
|
||||
});
|
||||
|
||||
it("loads metadata from a MySQL cross-database qualified source", async () => {
|
||||
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => {
|
||||
const hidden = sql.includes("__DBX_PK_0");
|
||||
return {
|
||||
editable: true,
|
||||
analysis: {
|
||||
schema: "reporting",
|
||||
tableName: "users",
|
||||
selectStar: false,
|
||||
columns: [{ sourceName: "name", resultName: "name", expression: "name" }, ...(hidden ? [{ sourceName: "id", resultName: "__DBX_PK_0", expression: "`id`" }] : [])],
|
||||
},
|
||||
};
|
||||
});
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT name FROM reporting.users");
|
||||
|
||||
expect(getColumns).toHaveBeenCalledWith("mysql-1", "app", "reporting", "users", undefined);
|
||||
expect(executeMulti).toHaveBeenCalledWith("mysql-1", "app", "SELECT name, `id` AS `__DBX_PK_0` FROM reporting.users", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"]));
|
||||
expect(tab.tableMeta?.database).toBe("app");
|
||||
expect(tab.tableMeta?.schema).toBe("reporting");
|
||||
});
|
||||
|
||||
it("uses a hidden Oracle ROWID to keep keyless base-table query results editable", async () => {
|
||||
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
|
||||
getColumns.mockResolvedValue([
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
|
|||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { loadTableMetadata } from "@/lib/metadata/tableMetadataCache";
|
||||
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { connectionQueryExecutionSchema, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
||||
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
|
||||
import { sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort";
|
||||
|
|
@ -2390,7 +2390,11 @@ export const useQueryStore = defineStore("query", () => {
|
|||
tab.tableMeta = patch.tableMeta;
|
||||
}
|
||||
|
||||
async function loadEditableQuerySource(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, traceId?: string, elapsed?: () => string): Promise<LoadedEditableSource> {
|
||||
async function loadEditableQuerySource(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string, traceId?: string, elapsed?: () => string): Promise<LoadedEditableSource> {
|
||||
// Metadata must resolve in the same namespace as the query execution. An
|
||||
// empty query-tab database still executes in the connection's default DB,
|
||||
// while database-tree dialects may override it with a qualified source.
|
||||
const metadataDatabase = (connectionUsesDatabaseObjectTreeMode(conn) ? source.schema : undefined) || executionDatabase || conn?.database || tab.database;
|
||||
let schema = source.schema || tab.schema;
|
||||
if (!schema) {
|
||||
if (dbType === "postgres" || dbType === "kwdb") schema = "public";
|
||||
|
|
@ -2399,7 +2403,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
// Oracle-family connection databases are service names, not schemas. When
|
||||
// the query does not qualify a schema, let the driver resolve the current
|
||||
// login user's schema instead of looking up metadata under the service name.
|
||||
const resolvedSchema = ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema ? "" : metadataSchemaForConnection(conn, tab.database, schema || undefined);
|
||||
const resolvedSchema = ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined);
|
||||
const metadataSchema = normalizeOracleLikeMetadataIdentifier(dbType, resolvedSchema || undefined, source.schema ? source.schemaQuoted : false) || "";
|
||||
const metadataTableName = normalizeOracleLikeMetadataIdentifier(dbType, source.tableName, source.tableNameQuoted)!;
|
||||
const metadataCatalog = normalizeOracleLikeMetadataIdentifier(dbType, source.catalog, source.catalogQuoted);
|
||||
|
|
@ -2419,7 +2423,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
});
|
||||
const loadedMetadata = await loadTableMetadata({
|
||||
connectionId: tab.connectionId,
|
||||
database: tab.database,
|
||||
database: metadataDatabase,
|
||||
schema: metadataSchema,
|
||||
tableName: metadataTableName,
|
||||
tableType: knownTableType,
|
||||
|
|
@ -2443,6 +2447,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
analysis: normalizeOracleLikeQueryAnalysis(dbType, cloneAnalysisForSource(analysis, metadataSource), metadataSchema || undefined, metadataTableName),
|
||||
tableMeta: {
|
||||
catalog: metadataCatalog,
|
||||
database: metadataDatabase,
|
||||
schema: metadataSchema || undefined,
|
||||
tableName: metadataTableName,
|
||||
tableType: loadedMetadata.metadata.tableType,
|
||||
|
|
@ -2461,14 +2466,14 @@ export const useQueryStore = defineStore("query", () => {
|
|||
async function oracleRowIdIsSafeForQuery(tab: QueryTab, loaded: LoadedEditableSource): Promise<boolean> {
|
||||
const knownType = loaded.tableMeta.tableType?.trim().toUpperCase();
|
||||
if (knownType) return knownType === "TABLE";
|
||||
const objects = await api.listObjects(tab.connectionId!, tab.database, loaded.tableMeta.schema ?? "", ["TABLE", "VIEW", "MATERIALIZED_VIEW"], loaded.tableMeta.tableName, 20, 0, loaded.tableMeta.catalog);
|
||||
const objects = await api.listObjects(tab.connectionId!, loaded.tableMeta.database ?? tab.database, loaded.tableMeta.schema ?? "", ["TABLE", "VIEW", "MATERIALIZED_VIEW"], loaded.tableMeta.tableName, 20, 0, loaded.tableMeta.catalog);
|
||||
const matching = objects.find((object) => object.name.toLowerCase() === loaded.tableMeta.tableName.toLowerCase());
|
||||
return matching?.object_type.trim().toUpperCase() === "TABLE";
|
||||
}
|
||||
|
||||
async function prepareEditableQueryExecution(tab: QueryTab, sql: string, conn: ConnectionConfig | undefined, databaseType: DatabaseType | undefined, traceId: string, elapsed: () => string): Promise<EditableQueryExecutionPreparation> {
|
||||
async function prepareEditableQueryExecution(tab: QueryTab, sql: string, conn: ConnectionConfig | undefined, databaseType: DatabaseType | undefined, executionDatabase: string, traceId: string, elapsed: () => string): Promise<EditableQueryExecutionPreparation> {
|
||||
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [] };
|
||||
if (!databaseType || !HIDDEN_QUERY_KEY_DATABASE_TYPES.has(databaseType) || !tab.connectionId || !tab.database) return unchanged;
|
||||
if (!databaseType || !HIDDEN_QUERY_KEY_DATABASE_TYPES.has(databaseType) || !tab.connectionId) return unchanged;
|
||||
|
||||
try {
|
||||
const editability = analyzeEditableQueryEditability(sql);
|
||||
|
|
@ -2477,7 +2482,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
const sources = editableQuerySources(analysis);
|
||||
if (sources.length !== 1 || analysis.distinct) return unchanged;
|
||||
|
||||
const loaded = await loadEditableQuerySource(tab, analysis, sources[0]!, conn, databaseType, traceId, elapsed);
|
||||
const loaded = await loadEditableQuerySource(tab, analysis, sources[0]!, conn, databaseType, executionDatabase, traceId, elapsed);
|
||||
if (loaded.tableMeta.columns.length === 0) return unchanged;
|
||||
if (loaded.tableMeta.tableType?.toUpperCase().includes("VIEW")) return unchanged;
|
||||
const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(databaseType, loaded.analysis, loaded.source, loaded.tableMeta.columns), loaded.source, loaded.tableMeta.columns);
|
||||
|
|
@ -2517,7 +2522,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function buildQueryMetadataPatch(tab: QueryTab, sql: string, traceId?: string, elapsed?: () => string, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []): Promise<QueryMetadataPatch | undefined> {
|
||||
async function buildQueryMetadataPatch(tab: QueryTab, sql: string, executionDatabase: string, traceId?: string, elapsed?: () => string, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []): Promise<QueryMetadataPatch | undefined> {
|
||||
if (tab.mode !== "query") return;
|
||||
if (!tab.result || !tab.result.columns.length) {
|
||||
return {
|
||||
|
|
@ -2546,7 +2551,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
const analysis = editability.analysis;
|
||||
|
||||
if (!tab.connectionId || !tab.database) {
|
||||
if (!tab.connectionId) {
|
||||
return {
|
||||
queryAnalysis: undefined,
|
||||
querySourceColumns: undefined,
|
||||
|
|
@ -2562,7 +2567,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
const loadedSources: LoadedEditableSource[] = [];
|
||||
try {
|
||||
for (const source of sources) {
|
||||
loadedSources.push(await loadEditableQuerySource(tab, analysis, source, conn, dbType, traceId, elapsed));
|
||||
loadedSources.push(await loadEditableQuerySource(tab, analysis, source, conn, dbType, executionDatabase, traceId, elapsed));
|
||||
}
|
||||
|
||||
const allSourceColumns = loadedSources.map((source) => ({ source: source.source, columns: source.tableMeta.columns }));
|
||||
|
|
@ -2675,12 +2680,12 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
}
|
||||
|
||||
function analyzeQueryMetadataInBackground(tabId: string, sql: string, result: QueryResult, traceId: string, elapsed: () => string, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []) {
|
||||
function analyzeQueryMetadataInBackground(tabId: string, sql: string, result: QueryResult, executionDatabase: string, traceId: string, elapsed: () => string, hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = []) {
|
||||
void (async () => {
|
||||
const tab = tabs.value.find((t) => t.id === tabId);
|
||||
if (!tab || tab.result !== result) return;
|
||||
queryExecutionLog("info", "metadata:start", { traceId, elapsed: elapsed() });
|
||||
const patch = await buildQueryMetadataPatch(tab, sql, traceId, elapsed, hiddenPrimaryKeys);
|
||||
const patch = await buildQueryMetadataPatch(tab, sql, executionDatabase, traceId, elapsed, hiddenPrimaryKeys);
|
||||
if (patch?.queryAnalysis && hiddenPrimaryKeys.length > 0) {
|
||||
patch.queryAnalysis = { ...patch.queryAnalysis, allowInsert: false };
|
||||
}
|
||||
|
|
@ -3303,7 +3308,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
}
|
||||
|
||||
if (tab.mode === "query") {
|
||||
const prepared = await prepareEditableQueryExecution(tab, sqlToExecute, conn, effectiveDbType, traceId, elapsed);
|
||||
const prepared = await prepareEditableQueryExecution(tab, sqlToExecute, conn, effectiveDbType, executionDatabase, traceId, elapsed);
|
||||
sqlToExecute = prepared.sql;
|
||||
queryMetadataSql = prepared.metadataSql;
|
||||
hiddenPrimaryKeys = prepared.hiddenPrimaryKeys;
|
||||
|
|
@ -3491,7 +3496,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
elapsed: elapsed(),
|
||||
});
|
||||
if (current.mode === "query" && current.result) {
|
||||
analyzeQueryMetadataInBackground(id, displayedQueryMetadataSql(current, queryMetadataSql), current.result, traceId, elapsed, hiddenPrimaryKeys);
|
||||
analyzeQueryMetadataInBackground(id, displayedQueryMetadataSql(current, queryMetadataSql), current.result, executionDatabase, traceId, elapsed, hiddenPrimaryKeys);
|
||||
}
|
||||
} else {
|
||||
queryExecutionLog("warn", "stale-result", {
|
||||
|
|
@ -3933,7 +3938,9 @@ export const useQueryStore = defineStore("query", () => {
|
|||
const sourceStatement = tab.result?.sourceStatement;
|
||||
if (tab.mode === "query" && sourceStatement && splitMongoCommandRanges(sourceStatement).length === 0) {
|
||||
const metadataStartedAt = performance.now();
|
||||
analyzeQueryMetadataInBackground(id, sourceStatement, tab.result, uuid().slice(0, 8), () => `${Math.round(performance.now() - metadataStartedAt)}ms`);
|
||||
const connection = useConnectionStore().getConfig(tab.connectionId);
|
||||
const executionDatabase = dataTabExecutionDatabase(connection, tab.database, tab.catalog);
|
||||
analyzeQueryMetadataInBackground(id, sourceStatement, tab.result, executionDatabase, uuid().slice(0, 8), () => `${Math.round(performance.now() - metadataStartedAt)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue