fix(grid): respect page size row limits
This commit is contained in:
parent
95d70eede6
commit
261d43c1b7
|
|
@ -24,7 +24,6 @@ import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
|
|||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { aiTestConnection } from "@/lib/api";
|
||||
import { eventToShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize } from "@/lib/paginationPageSize";
|
||||
import {
|
||||
SHORTCUT_DEFINITIONS,
|
||||
findShortcutConflict,
|
||||
|
|
@ -52,7 +51,6 @@ const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
|
|||
const editFontSize = ref(settingsStore.editorSettings.fontSize);
|
||||
const editTheme = ref(settingsStore.editorSettings.theme);
|
||||
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editPageSize = ref(settingsStore.editorSettings.pageSize);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
||||
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
|
||||
|
|
@ -69,7 +67,6 @@ watch(
|
|||
editFontSize.value = settingsStore.editorSettings.fontSize;
|
||||
editTheme.value = settingsStore.editorSettings.theme;
|
||||
editExecuteMode.value = settingsStore.editorSettings.executeMode;
|
||||
editPageSize.value = settingsStore.editorSettings.pageSize;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editAppLayout.value = settingsStore.editorSettings.appLayout;
|
||||
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
|
||||
|
|
@ -93,7 +90,6 @@ function hasChanges(): boolean {
|
|||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
|
||||
editTheme.value !== settingsStore.editorSettings.theme ||
|
||||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
|
||||
editPageSize.value !== settingsStore.editorSettings.pageSize ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
|
||||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
|
||||
|
|
@ -109,7 +105,6 @@ function applySettings() {
|
|||
fontSize: editFontSize.value,
|
||||
theme: editTheme.value,
|
||||
executeMode: editExecuteMode.value,
|
||||
pageSize: normalizeResultPageSize(editPageSize.value),
|
||||
wordWrap: editWordWrap.value,
|
||||
appLayout: editAppLayout.value,
|
||||
redisScanPageSize: editRedisScanPageSize.value,
|
||||
|
|
@ -124,7 +119,6 @@ function resetDefaults() {
|
|||
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
|
||||
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
|
||||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
|
||||
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
|
||||
|
|
@ -136,10 +130,6 @@ function onExecuteModeChange(v: any) {
|
|||
if (v === "all" || v === "current") editExecuteMode.value = v;
|
||||
}
|
||||
|
||||
function onPageSizeInput(event: Event) {
|
||||
editPageSize.value = normalizeResultPageSize((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function onFontFamilyChange(v: any) {
|
||||
if (typeof v === "string") editFontFamily.value = v;
|
||||
}
|
||||
|
|
@ -574,26 +564,6 @@ watch(
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Label for="editor-result-page-size">{{ t("settings.resultPageSize") }}</Label>
|
||||
<span class="text-xs text-muted-foreground tabular-nums">
|
||||
{{ t("settings.resultPageSizeOption", { count: editPageSize }) }}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
id="editor-result-page-size"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:min="MIN_RESULT_PAGE_SIZE"
|
||||
:max="MAX_RESULT_PAGE_SIZE"
|
||||
step="1"
|
||||
:model-value="editPageSize"
|
||||
@input="onPageSizeInput"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.resultPageSizeDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
|
|
|
|||
|
|
@ -1130,6 +1130,12 @@ const customPageSizeInput = ref(String(pageSize.value));
|
|||
watch(pageSize, (value) => {
|
||||
customPageSizeInput.value = String(value);
|
||||
});
|
||||
watch(
|
||||
() => settingsStore.editorSettings.pageSize,
|
||||
(value) => {
|
||||
pageSize.value = normalizeResultPageSize(value, pageSize.value);
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => [props.pageOffset, props.pageLimit],
|
||||
([offset, limit]) => {
|
||||
|
|
@ -1141,6 +1147,9 @@ watch(
|
|||
);
|
||||
const canGoNextPage = computed(() => props.result.has_more === true || props.result.rows.length >= pageSize.value);
|
||||
const canJumpLastPage = computed(() => canGoNextPage.value && (!!props.tableMeta || !!props.countSql));
|
||||
const showTruncationWarning = computed(
|
||||
() => props.result.truncated === true && typeof props.pageLimit !== "number" && props.result.has_more !== true,
|
||||
);
|
||||
const isResultsContext = computed(() => props.context === "results");
|
||||
const resultEditStatus = computed(() => {
|
||||
if (!isResultsContext.value || !hasData.value) return null;
|
||||
|
|
@ -2773,10 +2782,10 @@ defineExpose({
|
|||
</div>
|
||||
<!-- Truncation warning banner -->
|
||||
<div
|
||||
v-if="result.truncated"
|
||||
v-if="showTruncationWarning"
|
||||
class="shrink-0 px-3 py-1 bg-amber-500/10 border-b border-amber-500/20 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5"
|
||||
>
|
||||
<span>{{ t("grid.truncatedHint") }}</span>
|
||||
<span>{{ t("grid.truncatedHint", { count: pageSize }) }}</span>
|
||||
</div>
|
||||
<!-- Content area: table + DDL drawer -->
|
||||
<div class="flex-1 flex min-h-0 overflow-hidden">
|
||||
|
|
@ -3947,7 +3956,7 @@ defineExpose({
|
|||
<!-- 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="result.truncated" class="text-amber-500 text-xs ml-1">(truncated)</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>
|
||||
<span v-if="selectedRowCount > 0 || hasCellSelection" class="text-foreground">{{ selectionSummary }}</span>
|
||||
|
|
@ -3966,26 +3975,36 @@ defineExpose({
|
|||
{{ pageSize }}{{ t("grid.rowsPerPageShort") }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-52">
|
||||
<DropdownMenuContent align="end" class="w-36">
|
||||
<DropdownMenuItem v-for="s in pageSizeOptions" :key="s" @click="changePageSize(s)">
|
||||
{{ s }} {{ t("grid.rowsPerPageShort") }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel class="text-xs">{{ t("grid.customRowsPerPage") }}</DropdownMenuLabel>
|
||||
<div class="flex items-center gap-1.5 px-2 pb-2" @click.stop @keydown.stop>
|
||||
<div class="flex items-center gap-1 px-2 pb-2" @click.stop @keydown.stop>
|
||||
<Input
|
||||
v-model="customPageSizeInput"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:min="MIN_RESULT_PAGE_SIZE"
|
||||
:max="MAX_RESULT_PAGE_SIZE"
|
||||
class="h-7 text-xs"
|
||||
class="h-7 w-24 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
@keydown.enter.prevent.stop="applyCustomPageSize"
|
||||
/>
|
||||
<Button variant="outline" size="sm" class="h-7 px-2 text-xs" @click.stop="applyCustomPageSize">
|
||||
<Check class="h-3 w-3" />
|
||||
{{ t("grid.applyPageSize") }}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
:aria-label="t('grid.applyPageSize')"
|
||||
@click.stop="applyCustomPageSize"
|
||||
>
|
||||
<Check class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ t("grid.applyPageSize") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 扫描数量",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -96,12 +96,17 @@ async fn ch_query_with_limit(
|
|||
resp.json::<ChJsonResult>().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>) -> usize {
|
||||
max_rows.unwrap_or(MAX_ROWS).max(1)
|
||||
}
|
||||
|
||||
fn limited_query_result(result: ChJsonResult, execution_time_ms: u128, max_rows: Option<usize>) -> QueryResult {
|
||||
let columns: Vec<String> = 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<QueryResult, String> {
|
||||
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<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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>) -> usize {
|
||||
max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
|
||||
}
|
||||
|
||||
pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result<QueryResult, String> {
|
||||
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<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
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<Qu
|
|||
.map(|i| mysql_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;
|
||||
}
|
||||
}
|
||||
|
||||
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<Qu
|
|||
.map(|i| mysql_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;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -333,8 +333,21 @@ pub async fn get_columns(pool: &PgPool, schema: &str, table: &str) -> Result<Vec
|
|||
.collect())
|
||||
}
|
||||
|
||||
fn query_result_row_limit(max_rows: Option<usize>) -> usize {
|
||||
max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
|
||||
}
|
||||
|
||||
pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, String> {
|
||||
execute_query_with_max_rows(pool, sql, None).await
|
||||
}
|
||||
|
||||
pub async fn execute_query_with_max_rows(
|
||||
pool: &PgPool,
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<QueryResult, 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(pool);
|
||||
|
|
@ -354,7 +367,7 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, Stri
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -364,9 +377,9 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, Stri
|
|||
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 {
|
||||
|
|
@ -394,11 +407,21 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, Stri
|
|||
}
|
||||
|
||||
pub async fn execute_query_with_schema(pool: &PgPool, schema: &str, sql: &str) -> Result<QueryResult, String> {
|
||||
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<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<QueryResult, String> {
|
||||
execute_query_with_max_rows(pool, sql, None).await
|
||||
}
|
||||
|
||||
fn query_result_row_limit(max_rows: Option<usize>) -> 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<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
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<QueryResult,
|
|||
})
|
||||
.collect(),
|
||||
);
|
||||
if result_rows.len() > 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 {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryRes
|
|||
pub type SqlServerClient = Client<Compat<TcpStream>>;
|
||||
const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"];
|
||||
|
||||
fn query_result_row_limit(max_rows: Option<usize>) -> 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<String> {
|
|||
metadata.columns().iter().map(|c| c.name().to_string()).collect()
|
||||
}
|
||||
|
||||
async fn collect_first_result_limited(mut stream: QueryStream<'_>, start: Instant) -> Result<QueryResult, String> {
|
||||
async fn collect_first_result_limited(
|
||||
mut stream: QueryStream<'_>,
|
||||
start: Instant,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
let row_limit = query_result_row_limit(max_rows);
|
||||
let mut columns: Vec<String> = vec![];
|
||||
let mut rows: Vec<Vec<serde_json::Value>> = 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<QueryResult>, result: Option<SqlS
|
|||
}
|
||||
}
|
||||
|
||||
async fn collect_result_sets_limited(mut stream: QueryStream<'_>, start: Instant) -> Result<Vec<QueryResult>, String> {
|
||||
async fn collect_result_sets_limited(
|
||||
mut stream: QueryStream<'_>,
|
||||
start: Instant,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<Vec<QueryResult>, String> {
|
||||
let row_limit = query_result_row_limit(max_rows);
|
||||
let mut results = Vec::new();
|
||||
let mut current: Option<SqlServerResultSet> = 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<QueryResult, String> {
|
||||
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<usize>,
|
||||
) -> Result<QueryResult, String> {
|
||||
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<Qu
|
|||
}
|
||||
|
||||
pub async fn execute_batch(client: &mut SqlServerClient, sql: &str) -> Result<Vec<QueryResult>, 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<usize>,
|
||||
) -> Result<Vec<QueryResult>, 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 {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,21 @@ pub struct QueryExecutionOptions {
|
|||
pub result_session_id: Option<String>,
|
||||
}
|
||||
|
||||
fn query_result_row_limit(max_rows: Option<usize>) -> usize {
|
||||
max_rows.unwrap_or(MAX_ROWS).max(1)
|
||||
}
|
||||
|
||||
pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result<db::QueryResult, String> {
|
||||
duckdb_execute_with_max_rows(con, sql, None)
|
||||
}
|
||||
|
||||
pub fn duckdb_execute_with_max_rows(
|
||||
con: &duckdb::Connection,
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
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<db::QueryRe
|
|||
|
||||
let mut result_rows = Vec::new();
|
||||
while let Some(row) = rows.next().map_err(|e| e.to_string())? {
|
||||
if result_rows.len() >= MAX_ROWS {
|
||||
break;
|
||||
}
|
||||
let vals: Vec<serde_json::Value> = (0..col_count)
|
||||
.map(|i| {
|
||||
row.get::<_, String>(i)
|
||||
|
|
@ -53,9 +63,15 @@ pub fn duckdb_execute(con: &duckdb::Connection, sql: &str) -> Result<db::QueryRe
|
|||
})
|
||||
.collect();
|
||||
result_rows.push(vals);
|
||||
if result_rows.len() > 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<usize>,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
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<usize>) -> 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::<db::QueryResult>("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<CancellationToken>,
|
||||
options: QueryExecutionOptions,
|
||||
) -> Result<Vec<db::QueryResult>, 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 {
|
||||
|
|
|
|||
|
|
@ -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, /<Check class="h-3 w-3" \/>\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/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue