From 539faef23df2de174f16c3f80a998f5ab9060e72 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 28 May 2026 22:44:13 +0800 Subject: [PATCH] feat(query): display total row count in result status bar Execute COUNT query asynchronously after main query to show total matching rows. Strip user-supplied LIMIT/OFFSET from count SQL so the total reflects all rows, not just the limited page. --- apps/desktop/src/components/grid/DataGrid.vue | 8 ++++++- .../src/components/layout/ContentArea.vue | 1 + apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/stores/queryStore.ts | 23 +++++++++++++++++++ apps/desktop/src/types/database.ts | 1 + crates/dbx-core/src/query_result_sql.rs | 8 +++++++ 8 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index b2aae9a9a..9eec787b6 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -188,6 +188,7 @@ const props = defineProps<{ pageOffset?: number; pageLimit?: number; countSql?: string; + totalRowCount?: number; loading?: boolean; cacheKey?: string; onExecuteSql?: (sql: string) => Promise; @@ -5605,7 +5606,12 @@ const gridContextMenuItems = computed(() => {
- {{ t("grid.totalRows", { count: result.rows.length }) }} + + {{ t("grid.totalRows", { count: result.rows.length }) }} + {{ + t("grid.totalRowCount", { count: totalRowCount }) + }} + (truncated) {{ t("grid.rowsAffected", { count: result.affected_rows }) }} {{ result.execution_time_ms }}ms diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 97cc884d2..b2427ee19 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -424,6 +424,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); :page-offset="activeTab.resultPageOffset" :page-limit="activeTab.resultPageLimit" :count-sql="activeTab.resultCountSql" + :total-row-count="activeTab.resultTotalRowCount" :on-execute-sql="async (sql: string) => emit('executeSql', sql)" @reload=" ( diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 5b972a032..de14ab0ea 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -361,6 +361,7 @@ export default { grid: { rows: "{count} rows", totalRows: "Total {count} rows", + totalRowCount: "({count} total)", rowsAffected: "{count} rows affected", querySuccess: "Query executed successfully", noRows: "No data", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index d4f191be7..255ce92b8 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -354,6 +354,7 @@ export default { grid: { rows: "{count} filas", totalRows: "Total {count} filas", + totalRowCount: "({count} en total)", rowsAffected: "{count} filas afectadas", querySuccess: "Consulta ejecutada exitosamente", noRows: "Sin datos", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 5b1aa9200..001b71814 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -357,6 +357,7 @@ export default { grid: { rows: "{count} 行", totalRows: "共 {count} 行", + totalRowCount: "(总计 {count} 行)", rowsAffected: "影响 {count} 行", querySuccess: "查询执行成功", noRows: "暂无数据", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 618a49643..10325d734 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -544,6 +544,7 @@ export const useQueryStore = defineStore("query", () => { tab.isCancelling = false; tab.executionId = executionId; tab.lastExecutedSql = sql; + tab.resultTotalRowCount = undefined; console.info("[DBX][executeTabSql:start]", { traceId, tabId: id, @@ -740,6 +741,28 @@ export const useQueryStore = defineStore("query", () => { current.resultPageOffset = pageOffset; current.resultCountSql = countSql; current.resultSessionId = current.result?.session_id ?? undefined; + if (countSql && current.result?.rows.length) { + const capturedExecutionId = executionId; + const capturedTabId = id; + const capturedCountSql = countSql; + const capturedConnectionId = tab.connectionId; + const capturedDatabase = tab.database; + const capturedSchema = tab.schema; + api + .executeQuery(capturedConnectionId, capturedDatabase ?? "", capturedCountSql, capturedSchema) + .then((countResult) => { + const tabAfterCount = tabs.value.find((t) => t.id === capturedTabId); + if (tabAfterCount?.executionId === capturedExecutionId) { + const total = Number(countResult.rows?.[0]?.[0] ?? 0); + if (total > 0) { + tabAfterCount.resultTotalRowCount = total; + } + } + }) + .catch(() => { + // COUNT query failed — silently ignore + }); + } console.info("[DBX][executeTabSql:metadata:start]", { traceId, elapsed: elapsed() }); await analyzeQueryMetadata(current, queryBaseSql); console.info("[DBX][executeTabSql:metadata:done]", { traceId, elapsed: elapsed() }); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 62a0abd44..ec3b005a2 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -326,6 +326,7 @@ export interface QueryTab { resultPageLimit?: number; resultPageOffset?: number; resultCountSql?: string; + resultTotalRowCount?: number; resultSessionId?: string; pinned?: boolean; result?: QueryResult; diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs index 6ec973bd8..01a41c844 100644 --- a/crates/dbx-core/src/query_result_sql.rs +++ b/crates/dbx-core/src/query_result_sql.rs @@ -1,9 +1,15 @@ +use regex::Regex; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; use crate::models::connection::DatabaseType; use crate::sql::find_statement_at_cursor; use crate::sql_dialect::{quote_table_identifier, uses_fetch_first}; +static LIMIT_OFFSET_STRIP_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)(\s+LIMIT\s+\d+(\s+OFFSET\s+\d+)?|\s+OFFSET\s+\d+(\s+LIMIT\s+\d+)?|\s+OFFSET\s+\d+\s+ROWS?\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY|\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY)\s*$").unwrap() +}); + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QuerySqlBuildResult { @@ -183,6 +189,8 @@ pub fn build_count_query_sql(options: CountQuerySqlOptions) -> QuerySqlBuildResu return err("unsupported"); } + let statement = LIMIT_OFFSET_STRIP_RE.replace(&statement, "").to_string(); + let alias = quote_table_identifier(options.database_type, "dbx_count"); let wrapped_sql = if options.database_type == Some(DatabaseType::SqlServer) { sql_server_statement_for_derived_table(&statement)