diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index d3d8cd5ca0..9470f759e4 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -14,6 +14,7 @@ import { TableProperties, ChevronDown, ChevronUp, + RefreshCcw, } from "lucide-vue-next"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -709,6 +710,14 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); {{ t("toolbar.stopQuery") }} +
+ +
{{ t("grid.dataUnavailable") }}
+ +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 9764805156..ae0a554cc7 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -602,6 +602,7 @@ export default { tableInfoSearch: "Search...", tableInfoNoResults: "No results", queryError: "Query Error", + dataUnavailable: "Table data needs to be reloaded.", refresh: "Refresh", commit: "Commit", rollback: "Rollback", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 2a188fc3dc..76e4cb04c3 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -561,6 +561,7 @@ export default { tableInfoSearch: "Buscar...", tableInfoNoResults: "Sin resultados", queryError: "Error de consulta", + dataUnavailable: "Los datos de la tabla deben volver a cargarse.", refresh: "Actualizar", commit: "Confirmar", rollback: "Revertir", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 7c984e8afe..c9505cee50 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -597,6 +597,7 @@ export default { tableInfoSearch: "搜索...", tableInfoNoResults: "无匹配结果", queryError: "查询出错", + dataUnavailable: "表数据需要重新加载。", refresh: "刷新", commit: "提交", rollback: "回滚", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index a102065eb4..782ddd41e1 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1126,13 +1126,22 @@ export const useQueryStore = defineStore("query", () => { async function reloadEvictedTab(id: string) { const tab = tabs.value.find((t) => t.id === id); - if (!tab || !tab.resultEvicted) return; + const shouldReloadMissingDataTab = tab?.mode === "data" && !tab.result && !tab.isExecuting; + if (!tab || (!tab.resultEvicted && !shouldReloadMissingDataTab)) return; tab.resultEvicted = false; const sql = tab.lastExecutedSql ?? tab.sql; if (!sql?.trim()) return; + const settingsStore = useSettingsStore(); await executeTabSql(tab.id, sql, { resultBaseSql: tab.resultBaseSql ?? sql, resultSortedSql: tab.resultSortedSql, + pagination: + tab.mode === "data" + ? { + limit: tab.resultPageLimit ?? settingsStore.editorSettings.pageSize, + offset: tab.resultPageOffset ?? 0, + } + : undefined, }); } diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 0bd15e15e4..46c4c9d457 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -364,6 +364,50 @@ test("data tab execution preserves pagination offset metadata", async () => { } }); +test("reloading an empty data tab fetches table results again", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + let executeBody: any; + + connectionStore.addEphemeralConnection(conn("conn-1")); + const tabId = store.createTab("conn-1", "db", "users", "data", "public"); + const tab = store.tabs.find((item) => item.id === tabId); + assert.ok(tab); + tab.sql = 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;'; + tab.lastExecutedSql = tab.sql; + tab.resultPageLimit = 50; + tab.resultPageOffset = 50; + + globalThis.fetch = (async (input, init) => { + const url = String(input); + if (url === "/api/query/execute-multi") { + executeBody = JSON.parse(String(init?.body ?? "{}")); + return new Response(JSON.stringify([{ columns: ["id"], rows: [[51]], affected_rows: 0, execution_time_ms: 1 }]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }) as typeof fetch; + + try { + await store.reloadEvictedTab(tabId); + + assert.equal(executeBody.sql, 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;'); + assert.equal(executeBody.maxRows, 50); + assert.equal(executeBody.fetchSize, 50); + assert.deepEqual(tab.result?.rows, [[51]]); + assert.equal(tab.resultPageLimit, 50); + assert.equal(tab.resultPageOffset, 50); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("query result export fetches every paginated page", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia());