diff --git a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts index 35d1c539b..eb8e571e5 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.hiddenPrimaryKey.spec.ts @@ -569,6 +569,22 @@ describe("queryStore hidden primary key editing", () => { expect(tab.resultSortedSql).toBe("SELECT name, `id` AS `__DBX_PK_0` FROM users ORDER BY name ASC"); await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"])); expect(tab.queryAnalysis).toBeDefined(); + + await store.executeTabSql(tabId, "SELECT name FROM users", { + resultBaseSql: "SELECT name FROM users", + resultSortedSql: tab.resultSortedSql, + querySort: { + resultColumns: ["name"], + columnIndex: 0, + column: "name", + direction: "asc", + }, + pagination: { offset: 100, limit: 100 }, + }); + + await vi.waitFor(() => expect(tab.querySourceColumns).toEqual(["name", "id"])); + expect(tab.queryEditabilityReason).toBeUndefined(); + expect(tab.result?.hidden_column_indexes).toEqual([1]); }); it("clears result sorting when the editor SQL is executed again", async () => { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index e559ca6b4..4a49bd860 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -3817,7 +3817,10 @@ export const useQueryStore = defineStore("query", () => { if (tab.mode === "query") { const prepared = await prepareEditableQueryExecution(tab, sqlToExecute, conn, effectiveDbType, executionDatabase, traceId, elapsed); sqlToExecute = prepared.sql; - queryMetadataSql = prepared.metadataSql; + // Database sorting executes a generated wrapper around the user's query. + // Keep editability metadata anchored to the original query so the wrapper + // does not turn an otherwise editable result into a complex read-only one. + queryMetadataSql = options?.resultSortedSql && !options?.querySort ? queryBaseSql : prepared.metadataSql; hiddenPrimaryKeys = prepared.hiddenPrimaryKeys; if (options?.querySort) { const sorted = await api.buildSortedQuerySql({ diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 837b80743..8c6e4a8d7 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -1934,12 +1934,14 @@ test("keeps joined query read-only when multiple source tables are writable cand } }); -test("uses dbo as SQL Server metadata schema when query omits schema", async () => { +test("uses dbo as SQL Server metadata schema and keeps sorted query results editable", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); const store = useQueryStore(); const originalFetch = globalThis.fetch; + const baseSql = "select id, name from users"; + const analyzedSql: string[] = []; const columnRequests: Array<{ schema: string | null; table: string | null }> = []; connectionStore.addEphemeralConnection(sqlServerConn("sqlserver-1")); @@ -1961,7 +1963,13 @@ test("uses dbo as SQL Server metadata schema when query omits schema", async () } if (url === "/api/query/analyze-editability") { const body = JSON.parse(String(init?.body ?? "{}")); - assert.equal(body.sql, "select id, name from users"); + analyzedSql.push(body.sql); + if (body.sql !== baseSql) { + return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } return new Response( JSON.stringify({ editable: true, @@ -2020,14 +2028,27 @@ test("uses dbo as SQL Server metadata schema when query omits schema", async () try { const tabId = store.createTab("sqlserver-1", "app", "Query 1", "query"); - await store.executeTabSql(tabId, "select id, name from users"); + await store.executeTabSql(tabId, baseSql); const tab = store.tabs.find((item) => item.id === tabId); await waitFor(() => columnRequests.length > 0 && tab?.tableMeta?.tableName === "users"); + assert.deepEqual(analyzedSql, [baseSql]); assert.deepEqual(columnRequests, [{ schema: "dbo", table: "users" }]); assert.equal(tab?.tableMeta?.schema, "dbo"); assert.equal(tab?.tableMeta?.columns[0]?.comment, "编号"); assert.equal(tab?.tableMeta?.columns[1]?.comment, "姓名"); + + const sortedSql = "SELECT * FROM (select id, name from users) t([ID], [NAME]) ORDER BY [NAME] ASC;"; + await store.executeTabSql(tabId, sortedSql, { + resultBaseSql: baseSql, + resultSortedSql: sortedSql, + preserveResultDuringExecution: true, + }); + + await waitFor(() => analyzedSql.length === 2); + assert.deepEqual(analyzedSql, [baseSql, baseSql]); + assert.equal(tab?.queryEditabilityReason, undefined); + assert.equal(tab?.queryAnalysis?.tableName, "users"); } finally { globalThis.fetch = originalFetch; restoreStorage();