+
-
+
+
+
+
+ {{ t("grid.applyPageSize") }}
+
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue
index a45d67db0..cd10f174c 100644
--- a/apps/desktop/src/components/layout/ContentArea.vue
+++ b/apps/desktop/src/components/layout/ContentArea.vue
@@ -534,6 +534,8 @@ defineExpose({ focusSearch, refreshData });
:connection-id="activeTab.connectionId"
:database="activeTab.database"
:table-meta="activeTab.tableMeta"
+ :page-offset="activeTab.resultPageOffset"
+ :page-limit="activeTab.resultPageLimit"
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
@update:where-input="(v: string) => (activeTab.whereInput = v)"
@reload="
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 5615851ec..f5647040f 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -403,7 +403,7 @@ export default {
"metadata-unavailable": "DBX could not load table metadata, so result editing is disabled.",
},
sortUnsupported: "This SQL does not support full-result sorting. Try again with a single SELECT query.",
- truncatedHint: "Results truncated to 10,000 rows. Use LIMIT/OFFSET in your query to paginate.",
+ truncatedHint: "Results truncated to {count} rows. Use the footer pagination or adjust rows per page.",
},
welcome: {
title: "Database Workspace",
@@ -1139,10 +1139,6 @@ export default {
executeMode: "Execute Mode (Cmd+Enter)",
executeModeAll: "Execute all SQL",
executeModeCurrent: "Execute statement at cursor",
- resultPageSize: "Query result rows per page",
- resultPageSizeDescription:
- "Used for new queries, table browsing, and result pagination. Very large values may slow queries and rendering.",
- resultPageSizeOption: "{count} rows/page",
wordWrap: "Word wrap",
wordWrapDescription: "Wrap long SQL lines within the editor width",
redisScanPageSize: "Redis scan count",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 00ccf5640..4705eaea1 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -395,7 +395,7 @@ export default {
"metadata-unavailable": "无法读取目标表元数据,暂不能启用结果编辑。",
},
sortUnsupported: "当前 SQL 不支持全量排序,请改为单条 SELECT 查询后再尝试。",
- truncatedHint: "结果已截断,仅显示前 10,000 行。如需更多数据,请使用 LIMIT/OFFSET 分页查询。",
+ truncatedHint: "结果已截断,仅显示前 {count} 行。可通过底部分页继续加载,或调整每页行数。",
},
welcome: {
title: "数据库工作台",
@@ -1116,9 +1116,6 @@ export default {
executeMode: "执行模式 (Cmd+Enter)",
executeModeAll: "执行全部 SQL",
executeModeCurrent: "执行光标所在语句",
- resultPageSize: "查询结果每页行数",
- resultPageSizeDescription: "用于新查询、表数据浏览和分页跳转。设置过大可能会降低查询和渲染速度。",
- resultPageSizeOption: "{count} 行/页",
wordWrap: "自动换行",
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
redisScanPageSize: "Redis 扫描数量",
diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts
index fb8a2bc55..7f01d4370 100644
--- a/apps/desktop/src/stores/queryStore.ts
+++ b/apps/desktop/src/stores/queryStore.ts
@@ -466,9 +466,9 @@ export const useQueryStore = defineStore("query", () => {
const connStore = useConnectionStore();
const conn = connStore.getConfig(tab.connectionId);
const useAgentCursor = !!conn?.db_type && AGENT_DRIVER_TYPES.has(conn.db_type);
+ const settingsStore = useSettingsStore();
await closeResultSession(tab, options?.pagination?.sessionId);
if (tab.mode === "query") {
- const settingsStore = useSettingsStore();
const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 };
const plan = buildQueryPaginationExecutionPlan({
sql,
@@ -483,6 +483,8 @@ export const useQueryStore = defineStore("query", () => {
pageOffset = plan.pageOffset;
countSql = plan.countSql;
useAgentResultSession = plan.useAgentResultSession;
+ } else if (tab.mode === "data") {
+ pageLimit = settingsStore.editorSettings.pageSize;
}
const mongoFind = conn?.db_type === "mongodb" ? parseMongoFindCommand(sql) : null;
if (mongoFind) {
@@ -522,7 +524,7 @@ export const useQueryStore = defineStore("query", () => {
typeof pageLimit === "number"
? useAgentResultSession
? {
- maxRows: 10000,
+ maxRows: pageLimit,
fetchSize: pageLimit,
pageSize: pageLimit,
resultSessionId: options?.pagination?.sessionId,
diff --git a/crates/dbx-core/src/db/clickhouse_driver.rs b/crates/dbx-core/src/db/clickhouse_driver.rs
index b8ee736fb..7e2bcb989 100644
--- a/crates/dbx-core/src/db/clickhouse_driver.rs
+++ b/crates/dbx-core/src/db/clickhouse_driver.rs
@@ -96,12 +96,17 @@ async fn ch_query_with_limit(
resp.json::
().await.map_err(|e| format!("ClickHouse parse error: {e}"))
}
-fn limited_query_result(result: ChJsonResult, execution_time_ms: u128) -> QueryResult {
+fn query_result_row_limit(max_rows: Option) -> usize {
+ max_rows.unwrap_or(MAX_ROWS).max(1)
+}
+
+fn limited_query_result(result: ChJsonResult, execution_time_ms: u128, max_rows: Option) -> QueryResult {
let columns: Vec = result.meta.iter().map(|c| c.name.clone()).collect();
let mut rows = result.data;
- let truncated = rows.len() > MAX_ROWS;
+ let row_limit = query_result_row_limit(max_rows);
+ let truncated = rows.len() > row_limit;
if truncated {
- rows.truncate(MAX_ROWS);
+ rows.truncate(row_limit);
}
QueryResult { columns, rows, affected_rows: 0, execution_time_ms, truncated, session_id: None, has_more: false }
}
@@ -188,11 +193,21 @@ pub async fn get_columns(client: &ChClient, database: &str, table: &str) -> Resu
}
pub async fn execute_query(client: &ChClient, database: &str, sql: &str) -> Result {
+ execute_query_with_max_rows(client, database, sql, None).await
+}
+
+pub async fn execute_query_with_max_rows(
+ client: &ChClient,
+ database: &str,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let start = Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if starts_with_executable_sql_keyword(sql, &["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"]) {
- let result = ch_query_with_limit(client, sql, Some(database), QueryResultLimit::Limited(MAX_ROWS + 1)).await?;
- Ok(limited_query_result(result, start.elapsed().as_millis()))
+ let result = ch_query_with_limit(client, sql, Some(database), QueryResultLimit::Limited(row_limit + 1)).await?;
+ Ok(limited_query_result(result, start.elapsed().as_millis(), Some(row_limit)))
} else {
let url = build_query_url(&client.base_url, Some(database), QueryResultLimit::Unlimited);
let req = build_request(client, client.http.post(&url).body(sql.to_string()));
@@ -239,7 +254,7 @@ mod tests {
rows: crate::query::MAX_ROWS + 1,
};
- let result = limited_query_result(result, 12);
+ let result = limited_query_result(result, 12, None);
assert_eq!(result.columns, vec!["id"]);
assert_eq!(result.rows.len(), crate::query::MAX_ROWS);
diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs
index e2e6808e7..5e662895b 100644
--- a/crates/dbx-core/src/db/mysql.rs
+++ b/crates/dbx-core/src/db/mysql.rs
@@ -314,8 +314,22 @@ pub async fn get_columns(pool: &MySqlPool, database: &str, table: &str) -> Resul
.collect())
}
+fn query_result_row_limit(max_rows: Option) -> usize {
+ max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
+}
+
pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result {
+ execute_query_with_max_rows(pool, sql, bare, None).await
+}
+
+pub async fn execute_query_with_max_rows(
+ pool: &MySqlPool,
+ sql: &str,
+ bare: bool,
+ max_rows: Option,
+) -> Result {
let start = Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if is_result_set_query(sql) {
if bare || requires_text_protocol_query(sql) {
@@ -335,14 +349,14 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result crate::query::MAX_ROWS {
+ if result_rows.len() > row_limit {
break;
}
}
- let truncated = result_rows.len() > crate::query::MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
if truncated {
- result_rows.truncate(crate::query::MAX_ROWS);
+ result_rows.truncate(row_limit);
}
Ok(QueryResult {
@@ -369,14 +383,14 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result crate::query::MAX_ROWS {
+ if result_rows.len() > row_limit {
break;
}
}
- let truncated = result_rows.len() > crate::query::MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
if truncated {
- result_rows.truncate(crate::query::MAX_ROWS);
+ result_rows.truncate(row_limit);
}
Ok(QueryResult {
diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs
index cdac59854..46c3a3c5b 100644
--- a/crates/dbx-core/src/db/postgres.rs
+++ b/crates/dbx-core/src/db/postgres.rs
@@ -333,8 +333,21 @@ pub async fn get_columns(pool: &PgPool, schema: &str, table: &str) -> Result) -> usize {
+ max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
+}
+
pub async fn execute_query(pool: &PgPool, sql: &str) -> Result {
+ execute_query_with_max_rows(pool, sql, None).await
+}
+
+pub async fn execute_query_with_max_rows(
+ pool: &PgPool,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let start = Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if starts_with_executable_sql_keyword(sql, &["SELECT", "SHOW", "EXPLAIN", "WITH", "TABLE"]) {
let mut stream = sqlx::query(sql).persistent(false).fetch(pool);
@@ -354,7 +367,7 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result crate::query::MAX_ROWS {
+ if result_rows.len() > row_limit {
break;
}
}
@@ -364,9 +377,9 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result crate::query::MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
if truncated {
- result_rows.truncate(crate::query::MAX_ROWS);
+ result_rows.truncate(row_limit);
}
Ok(QueryResult {
@@ -394,11 +407,21 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result Result {
+ execute_query_with_schema_and_max_rows(pool, schema, sql, None).await
+}
+
+pub async fn execute_query_with_schema_and_max_rows(
+ pool: &PgPool,
+ schema: &str,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let mut conn = pool.acquire().await.map_err(|e| e.to_string())?;
let set_path = format!("SET search_path TO \"{}\", public", schema);
sqlx::query(&set_path).execute(&mut *conn).await.map_err(|e| e.to_string())?;
let start = Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if starts_with_executable_sql_keyword(sql, &["SELECT", "SHOW", "EXPLAIN", "WITH", "TABLE"]) {
let mut stream = sqlx::query(sql).persistent(false).fetch(&mut *conn);
@@ -418,7 +441,7 @@ pub async fn execute_query_with_schema(pool: &PgPool, schema: &str, sql: &str) -
.map(|i| pg_value_to_json(&row, i, column_types.get(i).map(String::as_str).unwrap_or("")))
.collect(),
);
- if result_rows.len() > crate::query::MAX_ROWS {
+ if result_rows.len() > row_limit {
break;
}
}
@@ -429,9 +452,9 @@ pub async fn execute_query_with_schema(pool: &PgPool, schema: &str, sql: &str) -
columns = desc.columns().iter().map(|c| c.name().to_string()).collect();
}
- let truncated = result_rows.len() > crate::query::MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
if truncated {
- result_rows.truncate(crate::query::MAX_ROWS);
+ result_rows.truncate(row_limit);
}
Ok(QueryResult {
diff --git a/crates/dbx-core/src/db/sqlite.rs b/crates/dbx-core/src/db/sqlite.rs
index 4d2f0e7bb..e77341458 100644
--- a/crates/dbx-core/src/db/sqlite.rs
+++ b/crates/dbx-core/src/db/sqlite.rs
@@ -162,7 +162,20 @@ pub async fn list_triggers(pool: &SqlitePool, _schema: &str, table: &str) -> Res
}
pub async fn execute_query(pool: &SqlitePool, sql: &str) -> Result {
+ execute_query_with_max_rows(pool, sql, None).await
+}
+
+fn query_result_row_limit(max_rows: Option) -> usize {
+ max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
+}
+
+pub async fn execute_query_with_max_rows(
+ pool: &SqlitePool,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let start = Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if starts_with_executable_sql_keyword(sql, &["SELECT", "PRAGMA", "EXPLAIN", "WITH"]) {
let desc = pool.describe(sql).await.map_err(|e| e.to_string())?;
@@ -191,14 +204,14 @@ pub async fn execute_query(pool: &SqlitePool, sql: &str) -> Result crate::query::MAX_ROWS {
+ if result_rows.len() > row_limit {
break;
}
}
- let truncated = result_rows.len() > crate::query::MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
if truncated {
- result_rows.truncate(crate::query::MAX_ROWS);
+ result_rows.truncate(row_limit);
}
Ok(QueryResult {
diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs
index 0fbb20c75..15dd43e33 100644
--- a/crates/dbx-core/src/db/sqlserver.rs
+++ b/crates/dbx-core/src/db/sqlserver.rs
@@ -13,6 +13,10 @@ use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryRes
pub type SqlServerClient = Client>;
const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"];
+fn query_result_row_limit(max_rows: Option) -> usize {
+ max_rows.unwrap_or(MAX_ROWS).max(1)
+}
+
pub async fn connect(
host: &str,
port: u16,
@@ -64,7 +68,12 @@ fn columns_from_metadata(metadata: &tiberius::ResultMetadata) -> Vec {
metadata.columns().iter().map(|c| c.name().to_string()).collect()
}
-async fn collect_first_result_limited(mut stream: QueryStream<'_>, start: Instant) -> Result {
+async fn collect_first_result_limited(
+ mut stream: QueryStream<'_>,
+ start: Instant,
+ max_rows: Option,
+) -> Result {
+ let row_limit = query_result_row_limit(max_rows);
let mut columns: Vec = vec![];
let mut rows: Vec> = Vec::new();
let mut truncated = false;
@@ -76,7 +85,7 @@ async fn collect_first_result_limited(mut stream: QueryStream<'_>, start: Instan
}
QueryItem::Metadata(_) => {}
QueryItem::Row(row) if row.result_index() == 0 => {
- if rows.len() < MAX_ROWS {
+ if rows.len() < row_limit {
rows.push(row_to_json(&row));
} else {
truncated = true;
@@ -120,7 +129,12 @@ fn push_sqlserver_result_set(results: &mut Vec, result: Option, start: Instant) -> Result, String> {
+async fn collect_result_sets_limited(
+ mut stream: QueryStream<'_>,
+ start: Instant,
+ max_rows: Option,
+) -> Result, String> {
+ let row_limit = query_result_row_limit(max_rows);
let mut results = Vec::new();
let mut current: Option = None;
@@ -140,7 +154,7 @@ async fn collect_result_sets_limited(mut stream: QueryStream<'_>, start: Instant
rows: Vec::new(),
truncated: false,
});
- if result.rows.len() < MAX_ROWS {
+ if result.rows.len() < row_limit {
result.rows.push(row_to_json(&row));
} else {
result.truncated = true;
@@ -527,14 +541,22 @@ pub async fn list_triggers(
}
pub async fn execute_query(client: &mut SqlServerClient, sql: &str) -> Result {
+ execute_query_with_max_rows(client, sql, None).await
+}
+
+pub async fn execute_query_with_max_rows(
+ client: &mut SqlServerClient,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let start = Instant::now();
if starts_with_executable_sql_keyword(sql, &["SELECT", "EXEC", "WITH", "TABLE"]) {
let stream = client.query(sql, &[]).await.map_err(|e| e.to_string())?;
- collect_first_result_limited(stream, start).await
+ collect_first_result_limited(stream, start, max_rows).await
} else if requires_simple_query_batch(sql) {
let stream = client.simple_query(sql).await.map_err(|e| e.to_string())?;
- let _ = collect_result_sets_limited(stream, start).await?;
+ let _ = collect_result_sets_limited(stream, start, max_rows).await?;
Ok(QueryResult {
columns: vec![],
rows: vec![],
@@ -559,9 +581,17 @@ pub async fn execute_query(client: &mut SqlServerClient, sql: &str) -> Result Result, String> {
+ execute_batch_with_max_rows(client, sql, None).await
+}
+
+pub async fn execute_batch_with_max_rows(
+ client: &mut SqlServerClient,
+ sql: &str,
+ max_rows: Option,
+) -> Result, String> {
let start = Instant::now();
let stream = client.simple_query(sql).await.map_err(|e| e.to_string())?;
- let mut results = collect_result_sets_limited(stream, start).await?;
+ let mut results = collect_result_sets_limited(stream, start, max_rows).await?;
if results.is_empty() {
results.push(QueryResult {
diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs
index 3ec29a46b..94c15015d 100644
--- a/crates/dbx-core/src/query.rs
+++ b/crates/dbx-core/src/query.rs
@@ -19,8 +19,21 @@ pub struct QueryExecutionOptions {
pub result_session_id: Option,
}
+fn query_result_row_limit(max_rows: Option) -> usize {
+ max_rows.unwrap_or(MAX_ROWS).max(1)
+}
+
pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result {
+ duckdb_execute_with_max_rows(con, sql, None)
+}
+
+pub fn duckdb_execute_with_max_rows(
+ con: &duckdb::Connection,
+ sql: &str,
+ max_rows: Option,
+) -> Result {
let start = std::time::Instant::now();
+ let row_limit = query_result_row_limit(max_rows);
if starts_with_executable_sql_keyword(sql, &["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH", "PRAGMA"]) {
let mut stmt = con.prepare(sql).map_err(|e| e.to_string())?;
@@ -33,9 +46,6 @@ pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result= MAX_ROWS {
- break;
- }
let vals: Vec = (0..col_count)
.map(|i| {
row.get::<_, String>(i)
@@ -53,9 +63,15 @@ pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result row_limit {
+ break;
+ }
}
- let truncated = result_rows.len() >= MAX_ROWS;
+ let truncated = result_rows.len() > row_limit;
+ if truncated {
+ result_rows.truncate(row_limit);
+ }
Ok(db::QueryResult {
columns,
rows: result_rows,
@@ -84,6 +100,7 @@ fn duckdb_execute_for_database(
attached_names: &[String],
database: Option<&str>,
sql: &str,
+ max_rows: Option,
) -> Result {
if let Some(database) = database.map(str::trim).filter(|database| !database.is_empty()) {
let catalog = if database == "main" {
@@ -93,16 +110,21 @@ fn duckdb_execute_for_database(
};
con.execute_batch(&format!("USE {}", duckdb_quote_ident(&catalog))).map_err(|e| e.to_string())?;
}
- duckdb_execute(con, sql)
+ duckdb_execute_with_max_rows(con, sql, max_rows)
}
fn duckdb_quote_ident(value: &str) -> String {
format!("\"{}\"", value.replace('"', "\"\""))
}
-pub fn truncate_result(mut result: db::QueryResult) -> db::QueryResult {
- if result.rows.len() > MAX_ROWS {
- result.rows.truncate(MAX_ROWS);
+pub fn truncate_result(result: db::QueryResult) -> db::QueryResult {
+ truncate_result_with_max_rows(result, None)
+}
+
+pub fn truncate_result_with_max_rows(mut result: db::QueryResult, max_rows: Option) -> db::QueryResult {
+ let row_limit = query_result_row_limit(max_rows);
+ if result.rows.len() > row_limit {
+ result.rows.truncate(row_limit);
result.truncated = true;
}
result
@@ -243,11 +265,12 @@ pub async fn do_execute(
let sql = sql.to_string();
let database = database.map(str::to_string);
let attached_names = duckdb_attached_names;
+ let max_rows = options.max_rows;
drop(connections);
wait_for_query(cancel_token, async move {
let task = tokio::task::spawn_blocking(move || {
let con = con.lock().map_err(|e| e.to_string())?;
- duckdb_execute_for_database(&con, &attached_names, database.as_deref(), &sql)
+ duckdb_execute_for_database(&con, &attached_names, database.as_deref(), &sql, max_rows)
});
task.await.map_err(|e| e.to_string())?
})
@@ -256,34 +279,46 @@ pub async fn do_execute(
PoolKind::Mysql(p, mode) => {
let p = p.clone();
let bare = *mode == crate::connection::MysqlMode::Bare;
+ let max_rows = options.max_rows;
drop(connections);
- wait_for_query(cancel_token, db::mysql::execute_query(&p, sql, bare)).await
+ wait_for_query(cancel_token, db::mysql::execute_query_with_max_rows(&p, sql, bare, max_rows)).await
}
PoolKind::Postgres(p) => {
let p = p.clone();
let schema = schema.map(|s| s.to_string());
+ let max_rows = options.max_rows;
drop(connections);
if let Some(schema) = schema {
- wait_for_query(cancel_token, db::postgres::execute_query_with_schema(&p, &schema, sql)).await
+ wait_for_query(
+ cancel_token,
+ db::postgres::execute_query_with_schema_and_max_rows(&p, &schema, sql, max_rows),
+ )
+ .await
} else {
- wait_for_query(cancel_token, db::postgres::execute_query(&p, sql)).await
+ wait_for_query(cancel_token, db::postgres::execute_query_with_max_rows(&p, sql, max_rows)).await
}
}
PoolKind::Sqlite(p) => {
let p = p.clone();
+ let max_rows = options.max_rows;
drop(connections);
- wait_for_query(cancel_token, db::sqlite::execute_query(&p, sql)).await
+ wait_for_query(cancel_token, db::sqlite::execute_query_with_max_rows(&p, sql, max_rows)).await
}
PoolKind::ClickHouse(client) => {
let client = client.clone();
let database = pool_key.split(':').nth(1).unwrap_or("default").to_string();
+ let max_rows = options.max_rows;
drop(connections);
- wait_for_query(cancel_token, db::clickhouse_driver::execute_query(&client, &database, sql))
- .await
- .map(truncate_result)
+ wait_for_query(
+ cancel_token,
+ db::clickhouse_driver::execute_query_with_max_rows(&client, &database, sql, max_rows),
+ )
+ .await
+ .map(|result| truncate_result_with_max_rows(result, max_rows))
}
PoolKind::SqlServer(client) => {
let client = client.clone();
+ let max_rows = options.max_rows;
drop(connections);
let mut client = match cancel_token.as_ref() {
Some(token) => tokio::select! {
@@ -293,15 +328,18 @@ pub async fn do_execute(
},
None => client.lock().await,
};
- wait_for_query(cancel_token, db::sqlserver::execute_query(&mut client, sql)).await.map(truncate_result)
+ wait_for_query(cancel_token, db::sqlserver::execute_query_with_max_rows(&mut client, sql, max_rows))
+ .await
+ .map(|result| truncate_result_with_max_rows(result, max_rows))
}
PoolKind::Elasticsearch(client) => {
let client = client.clone();
let sql = sql.to_string();
+ let max_rows = options.max_rows;
drop(connections);
wait_for_query(cancel_token, db::elasticsearch_driver::execute_rest_query(&client, &sql))
.await
- .map(truncate_result)
+ .map(|result| truncate_result_with_max_rows(result, max_rows))
}
PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()),
PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()),
@@ -309,6 +347,7 @@ pub async fn do_execute(
let client = client.clone();
let sql = sql.to_string();
let schema = schema.map(|s| s.to_string());
+ let max_rows = options.max_rows;
drop(connections);
wait_for_query(cancel_token, async move {
let mut client = client.lock().await;
@@ -324,7 +363,7 @@ pub async fn do_execute(
}
})
.await
- .map(truncate_result)
+ .map(|result| truncate_result_with_max_rows(result, max_rows))
}
PoolKind::ExternalTabular(ext_pool) => {
if !starts_with_executable_sql_keyword(sql, &["SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN", "PRAGMA"]) {
@@ -332,11 +371,12 @@ pub async fn do_execute(
}
let con = ext_pool.cache.clone();
let sql = sql.to_string();
+ let max_rows = options.max_rows;
drop(connections);
wait_for_query(cancel_token, async move {
let task = tokio::task::spawn_blocking(move || {
let con = con.lock().map_err(|e| e.to_string())?;
- duckdb_execute(&con, &sql)
+ duckdb_execute_with_max_rows(&con, &sql, max_rows)
});
task.await.map_err(|e| e.to_string())?
})
@@ -348,13 +388,14 @@ pub async fn do_execute(
let sql = sql.to_string();
let schema = schema.map(str::to_string);
let database = config.effective_database().unwrap_or("").to_string();
+ let max_rows = options.max_rows;
drop(connections);
wait_for_query(cancel_token, async move {
let params = external_driver_query_params(&config, &sql, &database, schema.as_deref());
session.invoke::("executeQuery", params).await
})
.await
- .map(truncate_result)
+ .map(|result| truncate_result_with_max_rows(result, max_rows))
}
}
}
@@ -490,7 +531,7 @@ pub async fn execute_multi_core_with_options(
};
if is_sqlserver {
- return execute_multi_sqlserver(state, &pool_key, sql, cancel_token).await;
+ return execute_multi_sqlserver(state, &pool_key, sql, cancel_token, options).await;
}
let statements = split_sql_statements(sql);
@@ -547,9 +588,11 @@ async fn execute_multi_sqlserver(
pool_key: &str,
sql: &str,
cancel_token: Option,
+ options: QueryExecutionOptions,
) -> Result, String> {
let batches = split_sql_batches(sql);
let mut all_results = Vec::new();
+ let max_rows = options.max_rows;
for batch in &batches {
if is_canceled(&cancel_token) {
@@ -582,7 +625,7 @@ async fn execute_multi_sqlserver(
None => client.lock().await,
};
- match db::sqlserver::execute_batch(&mut client, batch).await {
+ match db::sqlserver::execute_batch_with_max_rows(&mut client, batch, max_rows).await {
Ok(results) => all_results.extend(results),
Err(e) => {
all_results.push(db::QueryResult {
diff --git a/packages/app-tests/paginationPageSize.test.ts b/packages/app-tests/paginationPageSize.test.ts
index 4064cd672..d5ab8aec2 100644
--- a/packages/app-tests/paginationPageSize.test.ts
+++ b/packages/app-tests/paginationPageSize.test.ts
@@ -28,3 +28,79 @@ test("data grid page size menu exposes a custom input", () => {
assert.match(source, /settingsStore\.updateEditorSettings\(\{ pageSize: normalizedSize \}\)/);
assert.match(source, /t\("grid\.customRowsPerPage"\)/);
});
+
+test("data grid page size follows the global editor setting", () => {
+ const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
+
+ assert.match(source, /\(\) => settingsStore\.editorSettings\.pageSize/);
+ assert.match(source, /pageSize\.value = normalizeResultPageSize\(value, pageSize\.value\)/);
+});
+
+test("truncated result copy uses the active page size", () => {
+ const gridSource = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
+ const zhSource = readFileSync("apps/desktop/src/i18n/locales/zh-CN.ts", "utf8");
+ const enSource = readFileSync("apps/desktop/src/i18n/locales/en.ts", "utf8");
+
+ assert.match(gridSource, /const showTruncationWarning = computed/);
+ assert.match(gridSource, /v-if="showTruncationWarning"/);
+ assert.match(gridSource, /t\("grid\.truncatedHint", \{ count: pageSize \}\)/);
+ assert.match(zhSource, /结果已截断,仅显示前 \{count\} 行/);
+ assert.match(enSource, /Results truncated to \{count\} rows/);
+ assert.doesNotMatch(zhSource, /仅显示前 10,000 行/);
+ assert.doesNotMatch(enSource, /truncated to 10,000 rows/);
+});
+
+test("query execution sends the selected page size to agent drivers", () => {
+ const source = readFileSync("apps/desktop/src/stores/queryStore.ts", "utf8");
+
+ assert.match(source, /if \(tab\.mode === "data"\) \{/);
+ assert.match(source, /pageLimit = settingsStore\.editorSettings\.pageSize/);
+ assert.match(source, /maxRows: pageLimit,\s*fetchSize: pageLimit,\s*pageSize: pageLimit/s);
+ assert.doesNotMatch(source, /maxRows: 10000,\s*fetchSize: pageLimit,\s*pageSize: pageLimit/s);
+});
+
+test("native sql drivers receive the selected row limit", () => {
+ const querySource = readFileSync("crates/dbx-core/src/query.rs", "utf8");
+ const postgresSource = readFileSync("crates/dbx-core/src/db/postgres.rs", "utf8");
+ const mysqlSource = readFileSync("crates/dbx-core/src/db/mysql.rs", "utf8");
+ const sqliteSource = readFileSync("crates/dbx-core/src/db/sqlite.rs", "utf8");
+ const sqlserverSource = readFileSync("crates/dbx-core/src/db/sqlserver.rs", "utf8");
+ const clickhouseSource = readFileSync("crates/dbx-core/src/db/clickhouse_driver.rs", "utf8");
+
+ assert.match(querySource, /let max_rows = options\.max_rows/);
+ assert.match(querySource, /db::postgres::execute_query_with_max_rows\(&p, sql, max_rows\)/);
+ assert.match(querySource, /db::mysql::execute_query_with_max_rows\(&p, sql, bare, max_rows\)/);
+ assert.match(querySource, /db::sqlite::execute_query_with_max_rows\(&p, sql, max_rows\)/);
+ assert.match(querySource, /db::clickhouse_driver::execute_query_with_max_rows\(&client, &database, sql, max_rows\)/);
+ assert.match(querySource, /db::sqlserver::execute_query_with_max_rows\(&mut client, sql, max_rows\)/);
+ assert.match(querySource, /truncate_result_with_max_rows\(result, max_rows\)/);
+ assert.match(postgresSource, /let row_limit = query_result_row_limit\(max_rows\)/);
+ assert.match(mysqlSource, /let row_limit = query_result_row_limit\(max_rows\)/);
+ assert.match(sqliteSource, /let row_limit = query_result_row_limit\(max_rows\)/);
+ assert.match(sqlserverSource, /let row_limit = query_result_row_limit\(max_rows\)/);
+ assert.match(clickhouseSource, /let row_limit = query_result_row_limit\(max_rows\)/);
+});
+
+test("table data grid receives pagination context", () => {
+ const source = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8");
+
+ assert.match(source, /:page-offset="activeTab\.resultPageOffset"/);
+ assert.match(source, /:page-limit="activeTab\.resultPageLimit"/);
+});
+
+test("data grid page size menu keeps the custom control compact", () => {
+ const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
+
+ assert.match(source, /DropdownMenuContent align="end" class="w-36"/);
+ assert.match(source, /class="h-7 w-24 text-xs tabular-nums/);
+ assert.match(source, /:aria-label="t\('grid\.applyPageSize'\)"/);
+ assert.doesNotMatch(source, /\s*\{\{ t\("grid\.applyPageSize"\) \}\}/);
+});
+
+test("editor settings dialog does not duplicate result page size controls", () => {
+ const source = readFileSync("apps/desktop/src/components/editor/EditorSettingsDialog.vue", "utf8");
+
+ assert.doesNotMatch(source, /editPageSize/);
+ assert.doesNotMatch(source, /settings\.resultPageSize/);
+ assert.doesNotMatch(source, /pageSize: normalizeResultPageSize/);
+});