mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(sql): inherit active context for new SQL editors
This commit is contained in:
@@ -490,7 +490,6 @@ const activeOutputView = computed<TabOutputView>({
|
||||
if (tab) queryStore.updateTabUiState(tab.id, { activeOutputView: view });
|
||||
},
|
||||
});
|
||||
const newQueryContextSource = ref<"tab" | "sidebar">("tab");
|
||||
const queryEditorDdlTarget = ref<{ connectionId: string; database: string; catalog?: string; schema?: string; tableName: string; objectType?: ObjectSourceKind } | null>(null);
|
||||
const queryEditorObjectSourceTarget = ref<{
|
||||
connectionId: string;
|
||||
@@ -1111,7 +1110,6 @@ provide(EDITOR_TOOLBAR_ACTIONS, {
|
||||
canNewQuery: canCreateNewQuery,
|
||||
newQuery: (groupId: string) => {
|
||||
queryStore.focusGroup(groupId);
|
||||
newQueryContextSource.value = "tab";
|
||||
void newQuery();
|
||||
},
|
||||
explainMode,
|
||||
@@ -1755,7 +1753,6 @@ watch(
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (id) newQueryContextSource.value = "tab";
|
||||
if (id) activateQuerySurface();
|
||||
else if (previousId) activateOpenSpecialPageFallback();
|
||||
if (id && pluginCenterActive.value) pluginCenterActive.value = false;
|
||||
@@ -1855,13 +1852,6 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => connectionStore.selectedTreeNodeId,
|
||||
(id) => {
|
||||
if (id) newQueryContextSource.value = "sidebar";
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.uiScale,
|
||||
(scale) => {
|
||||
@@ -3098,14 +3088,21 @@ function openConnectionSettings(connectionId: string, initialTab: ConfigTab = "c
|
||||
}
|
||||
|
||||
async function newQuery() {
|
||||
const sqlConnections = connectionStore.connections.filter((connection) => quickConnectionOpenTarget(connection).kind === "query" && supportsGenericNewQuery(connection));
|
||||
const connectedSqlConnectionIds = new Set(sqlConnections.filter((connection) => connectionStore.connectedIds.has(connection.id)).map((connection) => connection.id));
|
||||
let target = resolveNewQueryTarget({
|
||||
activeTab: activeTab.value,
|
||||
selectedTreeNode: findTreeNodeById(connectionStore.treeNodes, connectionStore.selectedTreeNodeId),
|
||||
activeConnectionId: connectionStore.activeConnectionId,
|
||||
connections: connectionStore.connections,
|
||||
preferredSource: newQueryContextSource.value,
|
||||
connectedSqlConnectionIds,
|
||||
});
|
||||
if (!target) return;
|
||||
if (!target) {
|
||||
// No active editor/sidebar choice and no unique online SQL connection:
|
||||
// leave the connection unset so the user can choose in the editor toolbar.
|
||||
if (sqlConnections.length > 0) queryStore.createTab("", "", undefined, "query");
|
||||
return;
|
||||
}
|
||||
let conn = connectionStore.getConfig(target.connectionId);
|
||||
if (!conn) return;
|
||||
|
||||
@@ -3155,7 +3152,6 @@ async function newQuery() {
|
||||
const initialSql = resolveNewQueryInitialSql({
|
||||
activeTab: activeTab.value,
|
||||
selectedTreeNode: findTreeNodeById(connectionStore.treeNodes, connectionStore.selectedTreeNodeId),
|
||||
preferredSource: newQueryContextSource.value,
|
||||
prefillEnabled: settingsStore.editorSettings.prefillNewQueryWithSelect,
|
||||
targetConnectionId: target.connectionId,
|
||||
targetDatabase: target.database,
|
||||
|
||||
@@ -106,6 +106,83 @@ describe("resolveNewQueryTarget", () => {
|
||||
})?.schema,
|
||||
).toBe("reporting");
|
||||
});
|
||||
|
||||
it("prefers the active tab over a selected sidebar node when no explicit source is given", () => {
|
||||
// Every ad-hoc entry (app toolbar, shortcut, welcome screen, editor-group
|
||||
// "+") resolves through this default order, so a sidebar node that merely
|
||||
// happens to be selected must not outrank the editor the user is looking at
|
||||
// (issue #11517).
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
activeTab: { connectionId: "conn-tab", database: "bilibili" },
|
||||
selectedTreeNode: { connectionId: "conn-tree", database: "reporting" },
|
||||
connections: [
|
||||
{ id: "conn-tree", host: "localhost", database: "reporting", db_type: "mysql" },
|
||||
{ id: "conn-tab", host: "localhost", database: "bilibili", db_type: "mysql" },
|
||||
],
|
||||
}),
|
||||
).toMatchObject({ connectionId: "conn-tab", database: "bilibili" });
|
||||
});
|
||||
|
||||
it("falls back to the single online connection instead of the first configured one", () => {
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
connections: [
|
||||
{ id: "conn-unused", host: "localhost", database: "unused_db", db_type: "mysql" },
|
||||
{ id: "conn-online", host: "localhost", database: "online_db", db_type: "mysql" },
|
||||
],
|
||||
connectedSqlConnectionIds: new Set(["conn-online"]),
|
||||
}),
|
||||
).toMatchObject({ connectionId: "conn-online", database: "online_db", shouldRefreshDefaultDatabase: true });
|
||||
});
|
||||
|
||||
it("leaves the connection unselected when several configured connections are offline", () => {
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
connections: [
|
||||
{ id: "conn-first", host: "localhost", database: "first_db", db_type: "mysql" },
|
||||
{ id: "conn-second", host: "localhost", database: "second_db", db_type: "mysql" },
|
||||
],
|
||||
connectedSqlConnectionIds: new Set<string>(),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("leaves the connection unselected when multiple SQL connections are online", () => {
|
||||
// An ambiguous online state must not pick a connection on its own (#11517).
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
connections: [
|
||||
{ id: "conn-first", host: "localhost", database: "first_db", db_type: "mysql" },
|
||||
{ id: "conn-second", host: "localhost", database: "second_db", db_type: "mysql" },
|
||||
{ id: "conn-third", host: "localhost", database: "third_db", db_type: "mysql" },
|
||||
],
|
||||
connectedSqlConnectionIds: new Set(["conn-second", "conn-third"]),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the sole configured connection as the legacy fallback", () => {
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
connections: [{ id: "conn-only", host: "localhost", database: "only_db", db_type: "mysql" }],
|
||||
connectedSqlConnectionIds: new Set<string>(),
|
||||
}),
|
||||
).toMatchObject({ connectionId: "conn-only", database: "only_db" });
|
||||
});
|
||||
|
||||
it("keeps the last active connection ahead of the single online connection", () => {
|
||||
expect(
|
||||
resolveNewQueryTarget({
|
||||
activeConnectionId: "conn-active",
|
||||
connections: [
|
||||
{ id: "conn-active", host: "localhost", database: "active_db", db_type: "mysql" },
|
||||
{ id: "conn-online", host: "localhost", database: "online_db", db_type: "mysql" },
|
||||
],
|
||||
connectedSqlConnectionIds: new Set(["conn-online"]),
|
||||
}),
|
||||
).toMatchObject({ connectionId: "conn-active" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNewQueryTable", () => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// Regression coverage for https://github.com/t8y2/dbx/issues/11517.
|
||||
//
|
||||
// Every ad-hoc "new query" entry (app toolbar, keyboard shortcut, welcome
|
||||
// screen, editor-group "+") funnels into App.vue's newQuery(). That shared entry
|
||||
// used to carry a sticky `newQueryContextSource`, which the sidebar selection
|
||||
// watcher set to "sidebar" while only the active-tab watcher and editor-group
|
||||
// "+" reset it. The global toolbar and shortcut could therefore use a stale
|
||||
// sidebar connection instead of the editor the user was looking at.
|
||||
//
|
||||
// The precedence itself is unit-tested in newQueryContext.spec.ts; this suite
|
||||
// pins the App.vue wiring, because App.vue is a large SFC the suite never mounts.
|
||||
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
|
||||
|
||||
function bodyOf(fnSignature: string): string {
|
||||
const start = appSource.indexOf(fnSignature);
|
||||
expect(start, `expected to find "${fnSignature}" in App.vue`).toBeGreaterThanOrEqual(0);
|
||||
const braceStart = appSource.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let i = braceStart; i < appSource.length; i++) {
|
||||
if (appSource[i] === "{") depth++;
|
||||
else if (appSource[i] === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return appSource.slice(braceStart, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(`unbalanced braces reading body of "${fnSignature}"`);
|
||||
}
|
||||
|
||||
describe("new query entry context wiring", () => {
|
||||
it("does not keep a sticky context source that can outrank the active tab", () => {
|
||||
expect(appSource).not.toContain("newQueryContextSource");
|
||||
});
|
||||
|
||||
it("resolves the shared new-query entry from the active tab and unique online SQL connection", () => {
|
||||
const body = bodyOf("async function newQuery()");
|
||||
// Without an explicit source the resolver defaults to the active tab first
|
||||
// and the selected sidebar node second, so all ad-hoc entries agree.
|
||||
expect(body).not.toContain("preferredSource");
|
||||
expect(body).toContain('quickConnectionOpenTarget(connection).kind === "query"');
|
||||
expect(body).toContain("connectedSqlConnectionIds,");
|
||||
});
|
||||
|
||||
it("creates an unbound editor for manual SQL connection selection when the target is ambiguous", () => {
|
||||
const body = bodyOf("async function newQuery()");
|
||||
expect(body).toContain("if (sqlConnections.length > 0)");
|
||||
expect(body).toContain('queryStore.createTab("", "", undefined, "query")');
|
||||
});
|
||||
|
||||
it("keeps the editor-group entry focused on its own group", () => {
|
||||
expect(bodyOf("newQuery: (groupId: string) =>")).toContain("queryStore.focusGroup(groupId)");
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ interface ResolveNewQueryTargetInput {
|
||||
activeConnectionId?: string | null;
|
||||
connections: Pick<ConnectionConfig, "id" | "host" | "database" | "default_schema" | "db_type">[];
|
||||
preferredSource?: NewQueryContextSource;
|
||||
/** Ids of currently online SQL connections, used only when there is exactly one. */
|
||||
connectedSqlConnectionIds?: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export function findTreeNodeById(nodes: TreeNode[], id: string | null | undefined): TreeNode | null {
|
||||
@@ -40,7 +42,11 @@ export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQue
|
||||
if (secondaryTarget) return secondaryTarget;
|
||||
|
||||
const activeConnection = input.activeConnectionId ? input.connections.find((connection) => connection.id === input.activeConnectionId) : undefined;
|
||||
const fallbackConnection = activeConnection || input.connections[0];
|
||||
// Preserve an explicit active connection, then use the only online SQL
|
||||
// connection. A sole configured connection remains a safe legacy fallback;
|
||||
// when several choices remain, return null so the caller can leave selection
|
||||
// to the user instead of silently taking connections[0].
|
||||
const fallbackConnection = activeConnection ?? singleOnlineSqlConnection(input.connections, input.connectedSqlConnectionIds) ?? (input.connections.length === 1 ? input.connections[0] : undefined);
|
||||
return fallbackConnection
|
||||
? {
|
||||
connectionId: fallbackConnection.id,
|
||||
@@ -51,6 +57,16 @@ export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQue
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one online SQL connection, when there is exactly one. An ambiguous or
|
||||
* empty online set must not be resolved by connection-list order.
|
||||
*/
|
||||
function singleOnlineSqlConnection(connections: ResolveNewQueryTargetInput["connections"], connectedSqlConnectionIds?: ReadonlySet<string>): ResolveNewQueryTargetInput["connections"][number] | undefined {
|
||||
if (!connectedSqlConnectionIds || connectedSqlConnectionIds.size !== 1) return undefined;
|
||||
const [onlyOnlineId] = connectedSqlConnectionIds;
|
||||
return connections.find((connection) => connection.id === onlyOnlineId);
|
||||
}
|
||||
|
||||
function targetFromContext(
|
||||
context: Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog" | "objectBrowser" | "tableMeta"> | Pick<TreeNode, "connectionId" | "database" | "schema" | "catalog"> | undefined,
|
||||
connections: Pick<ConnectionConfig, "id" | "host" | "database" | "default_schema" | "db_type">[],
|
||||
|
||||
+49
@@ -142,3 +142,52 @@ test("new query target repairs stale SQLite file paths without changing attached
|
||||
assert.equal(stalePathTarget?.database, "main");
|
||||
assert.equal(attachedAliasTarget?.database, "analytics");
|
||||
});
|
||||
|
||||
// Issue #11517: without a tab or sidebar context the fallback must not treat the
|
||||
// first *configured* connection as if it were connected, because that silently
|
||||
// binds the new editor to a database the user may never have opened.
|
||||
test("new query target falls back to the single online connection, not the first configured one", () => {
|
||||
const target = resolveNewQueryTarget({
|
||||
connections: [connection("conn-unused", "unused_db"), connection("conn-online", "online_db")],
|
||||
connectedSqlConnectionIds: new Set(["conn-online"]),
|
||||
});
|
||||
|
||||
assert.deepEqual(target, {
|
||||
connectionId: "conn-online",
|
||||
database: "online_db",
|
||||
schema: undefined,
|
||||
shouldRefreshDefaultDatabase: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("new query target remains unselected when several configured connections are offline", () => {
|
||||
const target = resolveNewQueryTarget({
|
||||
connections: [connection("conn-first", "first_db"), connection("conn-second", "second_db")],
|
||||
connectedSqlConnectionIds: new Set<string>(),
|
||||
});
|
||||
|
||||
assert.equal(target, null);
|
||||
});
|
||||
|
||||
test("new query target remains unselected when several SQL connections are online", () => {
|
||||
const target = resolveNewQueryTarget({
|
||||
connections: [connection("conn-first", "first_db"), connection("conn-second", "second_db"), connection("conn-third", "third_db")],
|
||||
connectedSqlConnectionIds: new Set(["conn-second", "conn-third"]),
|
||||
});
|
||||
|
||||
assert.equal(target, null);
|
||||
});
|
||||
|
||||
test("new query target keeps the sole configured connection as the legacy fallback", () => {
|
||||
const target = resolveNewQueryTarget({
|
||||
connections: [connection("conn-only", "only_db")],
|
||||
connectedSqlConnectionIds: new Set<string>(),
|
||||
});
|
||||
|
||||
assert.deepEqual(target, {
|
||||
connectionId: "conn-only",
|
||||
database: "only_db",
|
||||
schema: undefined,
|
||||
shouldRefreshDefaultDatabase: true,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user