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:
parent
e15e99aeb5
commit
539faef23d
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ export default {
|
|||
grid: {
|
||||
rows: "{count} 行",
|
||||
totalRows: "共 {count} 行",
|
||||
totalRowCount: "(总计 {count} 行)",
|
||||
rowsAffected: "影响 {count} 行",
|
||||
querySuccess: "查询执行成功",
|
||||
noRows: "暂无数据",
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ export interface QueryTab {
|
|||
resultPageLimit?: number;
|
||||
resultPageOffset?: number;
|
||||
resultCountSql?: string;
|
||||
resultTotalRowCount?: number;
|
||||
resultSessionId?: string;
|
||||
pinned?: boolean;
|
||||
result?: QueryResult;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue