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.
This commit is contained in:
t8y2 2026-05-28 22:44:13 +08:00
parent e15e99aeb5
commit 539faef23d
8 changed files with 43 additions and 1 deletions

View File

@ -188,6 +188,7 @@ const props = defineProps<{
pageOffset?: number;
pageLimit?: number;
countSql?: string;
totalRowCount?: number;
loading?: boolean;
cacheKey?: string;
onExecuteSql?: (sql: string) => Promise<void>;
@ -5605,7 +5606,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<!-- Bottom status bar -->
<div class="flex items-center gap-2 px-3 py-1 border-t text-xs text-muted-foreground bg-muted/30 shrink-0">
<span v-if="hasData">{{ t("grid.totalRows", { count: result.rows.length }) }}</span>
<span v-if="hasData">
{{ t("grid.totalRows", { count: result.rows.length }) }}
<span v-if="typeof totalRowCount === 'number' && totalRowCount > 0" class="text-muted-foreground/70">{{
t("grid.totalRowCount", { count: totalRowCount })
}}</span>
</span>
<span v-if="showTruncationWarning" class="text-amber-500 text-xs ml-1">(truncated)</span>
<span v-if="!hasData">{{ t("grid.rowsAffected", { count: result.affected_rows }) }}</span>
<span>{{ result.execution_time_ms }}ms</span>

View File

@ -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="
(

View File

@ -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",

View File

@ -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",

View File

@ -357,6 +357,7 @@ export default {
grid: {
rows: "{count} 行",
totalRows: "共 {count} 行",
totalRowCount: "(总计 {count} 行)",
rowsAffected: "影响 {count} 行",
querySuccess: "查询执行成功",
noRows: "暂无数据",

View File

@ -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() });

View File

@ -326,6 +326,7 @@ export interface QueryTab {
resultPageLimit?: number;
resultPageOffset?: number;
resultCountSql?: string;
resultTotalRowCount?: number;
resultSessionId?: string;
pinned?: boolean;
result?: QueryResult;

View File

@ -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<Regex> = 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)