diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 060d73096e..847fa67104 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -119,7 +119,7 @@ import McpConnectionScopePicker from "@/components/settings/McpConnectionScopePi import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue"; import SqlFormatterSettingsPanel from "./SqlFormatterSettingsPanel.vue"; import { APP_THEME_PALETTES, type AppCornerStyle, type AppThemeAppearance, type AppThemeMode, type AppThemePalette } from "@/lib/app/appTheme"; -import { editorSettingsDraftChanged, editorSettingsDraftFromSettings, editorSettingsPatchFromDraft, normalizeTableOpenPageSizeDraft, type EditorSettingsDraft } from "@/lib/settings/editorSettingsDraft"; +import { editorSettingsDraftChanged, editorSettingsDraftFromSettings, editorSettingsPatchFromDraft, normalizeQueryResultMaxRowsDraft, normalizeTableOpenPageSizeDraft, type EditorSettingsDraft } from "@/lib/settings/editorSettingsDraft"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; import { usePromptTemplateStore } from "@/stores/promptTemplateStore"; @@ -134,6 +134,7 @@ import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSyst import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo"; import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter"; import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize"; +import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit"; import type { PromptTemplate } from "@/types/promptTemplate"; import { GLOBAL_INSTRUCTIONS_MAX, PROMPT_TEMPLATE_CONTENT_MAX, PROMPT_TEMPLATE_NAME_MAX, promptTemplateCharacterCount } from "@/types/promptTemplate"; @@ -314,9 +315,11 @@ const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showInd const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions); const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry); const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow); +const editPageSize = ref(settingsStore.editorSettings.pageSize); const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize); +const editQueryResultMaxRowsEnabled = ref(settingsStore.editorSettings.queryResultMaxRowsEnabled); +const editQueryResultMaxRows = ref(settingsStore.editorSettings.queryResultMaxRows); const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll); -const editInfiniteScrollMaxRows = ref(settingsStore.editorSettings.infiniteScrollMaxRows); const editRegexMaxMatchCount = ref(settingsStore.editorSettings.regexMaxMatchCount); const editAutoCalculateTotalRows = ref(settingsStore.editorSettings.autoCalculateTotalRows); const editTableColumnTemplateRows = ref(tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields)); @@ -328,6 +331,22 @@ function updateTableOpenPageSizeDraft(value: string | number) { editTableOpenPageSize.value = normalizeTableOpenPageSizeDraft(value); } +function updatePageSizeDraft(value: string | number) { + editPageSize.value = normalizeTableOpenPageSizeDraft(value); +} + +function updateQueryResultMaxRowsInput(event: Event) { + const input = event.currentTarget as HTMLInputElement; + const parsed = Number(input.value); + if (!Number.isFinite(parsed) || parsed > MAX_QUERY_RESULT_MAX_ROWS) { + input.value = String(editQueryResultMaxRows.value); + return; + } + const normalized = normalizeQueryResultMaxRowsDraft(input.value); + if (input.value !== String(normalized)) input.value = String(normalized); + editQueryResultMaxRows.value = normalized; +} + function sqlVariableSyntaxToggle(key: keyof SqlVariableSyntaxToggles): boolean { return editSqlVariableSyntaxOverrides.value[editSqlVariableSyntaxDatabaseType.value]?.[key] ?? true; } @@ -471,9 +490,11 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { compactColumnHeaderActions: editCompactColumnHeaderActions.value, dataGridQuickEntry: editDataGridQuickEntry.value, dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value, + pageSize: editPageSize.value, tableOpenPageSize: editTableOpenPageSize.value, + queryResultMaxRowsEnabled: editQueryResultMaxRowsEnabled.value, + queryResultMaxRows: editQueryResultMaxRows.value, infiniteScroll: editInfiniteScroll.value, - infiniteScrollMaxRows: editInfiniteScrollMaxRows.value, regexMaxMatchCount: editRegexMaxMatchCount.value, autoCalculateTotalRows: editAutoCalculateTotalRows.value, tableColumnTemplateFields: normalizedEditTableColumnTemplateFields.value, @@ -743,9 +764,11 @@ function syncEditorSettingsDraftFromStore() { editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions; editDataGridQuickEntry.value = settingsStore.editorSettings.dataGridQuickEntry; editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow; + editPageSize.value = settingsStore.editorSettings.pageSize; editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize; + editQueryResultMaxRowsEnabled.value = settingsStore.editorSettings.queryResultMaxRowsEnabled; + editQueryResultMaxRows.value = settingsStore.editorSettings.queryResultMaxRows; editInfiniteScroll.value = settingsStore.editorSettings.infiniteScroll; - editInfiniteScrollMaxRows.value = settingsStore.editorSettings.infiniteScrollMaxRows; editRegexMaxMatchCount.value = settingsStore.editorSettings.regexMaxMatchCount; editAutoCalculateTotalRows.value = settingsStore.editorSettings.autoCalculateTotalRows; editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(settingsStore.editorSettings.tableColumnTemplateFields); @@ -853,7 +876,8 @@ const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== const duckDbWorkerSettingsRequireRestart = computed(() => editDuckDbWorkerProcessIsolation.value !== startupDuckDbWorkerProcessIsolation.value || normalizeDuckDbWorkerMaxProcesses(editDuckDbWorkerMaxProcesses.value) !== startupDuckDbWorkerMaxProcesses.value); const hasBlockingShortcutConflicts = computed(() => shortcutsChanged.value && hasShortcutConflicts.value); const hasBlockingFormatterConfig = computed(() => activeSettingsTab.value === "formatter" && !sqlFormatterConfigValid.value); -const hasApplyBlocker = computed(() => hasBlockingShortcutConflicts.value || hasBlockingFormatterConfig.value); +const hasBlockingQueryResultRowLimit = computed(() => editQueryResultMaxRowsEnabled.value && editQueryResultMaxRows.value < editPageSize.value); +const hasApplyBlocker = computed(() => hasBlockingShortcutConflicts.value || hasBlockingFormatterConfig.value || hasBlockingQueryResultRowLimit.value); function hasChanges(): boolean { return ( @@ -994,9 +1018,11 @@ function resetDefaultsForTab(tab: SettingsCategory) { editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions; editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry; editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow; + editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize; editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize; + editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled; + editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows; editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll; - editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows; editRegexMaxMatchCount.value = DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount; editAutoCalculateTotalRows.value = DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows; editDuckDbWorkerProcessIsolation.value = DEFAULT_DESKTOP_SETTINGS.duckdb_worker_process_isolation; @@ -1061,9 +1087,11 @@ function resetAllDefaults() { editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions; editDataGridQuickEntry.value = DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry; editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow; + editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize; editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize; + editQueryResultMaxRowsEnabled.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled; + editQueryResultMaxRows.value = DEFAULT_EDITOR_SETTINGS.queryResultMaxRows; editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll; - editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows; editRegexMaxMatchCount.value = DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount; editAutoCalculateTotalRows.value = DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows; editTableColumnTemplateRows.value = tableColumnTemplateRowsFromSettings(DEFAULT_EDITOR_SETTINGS.tableColumnTemplateFields); @@ -4390,119 +4418,6 @@ onUnmounted(() => { -
- -
-
- -

- {{ t("settings.showColumnCommentsInHeaderDescription") }} -

-
- -
-
-
- -

- {{ t("settings.showColumnTypesInHeaderDescription") }} -

-
- -
-
-
- -

- {{ t("settings.showIndexIndicatorsInHeaderDescription") }} -

-
- -
-
-
- -

- {{ t("settings.compactColumnHeaderActionsDescription") }} -

-
- -
-
-
- -

- {{ t("settings.dataGridQuickEntryDescription") }} -

-
- -
-
-
- -

- {{ t("settings.dataGridAutoTransposeSingleRowDescription") }} -

-
- -
-
-
- -

- {{ t("settings.infiniteScrollDescription") }} -

-
- -
-
-
- -

- {{ t("settings.infiniteScrollMaxRowsDescription") }} -

-
- -
-
-
- -

- {{ t("settings.autoCalculateTotalRowsDescription") }} -

-
- -
-
- - -
@@ -4831,7 +4746,141 @@ onUnmounted(() => { {{ t("settings.tableOpenPageSizeDescription") }}

- + +
+
+
+ +

+ {{ t("settings.queryPageSizeDescription", { max: MAX_RESULT_PAGE_SIZE.toLocaleString() }) }} +

+
+ +
+
+
+ +

+ {{ hasBlockingQueryResultRowLimit ? t("settings.queryResultMaxRowsTooSmall", { pageSize: editPageSize }) : editQueryResultMaxRowsEnabled ? t("settings.queryResultMaxRowsDescription") : t("settings.queryResultMaxRowsUnlimitedDescription") }} +

+
+
+ + +
+
+
+
+ +

+ {{ t("settings.infiniteScrollDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.autoCalculateTotalRowsDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.showColumnCommentsInHeaderDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.showColumnTypesInHeaderDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.showIndexIndicatorsInHeaderDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.compactColumnHeaderActionsDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.dataGridQuickEntryDescription") }} +

+
+ +
+
+
+ +

+ {{ t("settings.dataGridAutoTransposeSingleRowDescription") }} +

+
+
diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 406bac024f..37e7179ea1 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -173,6 +173,7 @@ import { replaceColumnValueFilterCondition, } from "@/lib/dataGrid/dataGridColumnFilter"; import { normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/dataGrid/paginationPageSize"; +import { continuousQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { allNullColumnIndexes } from "@/lib/dataGrid/dataGridColumnVisibility"; import { buildDataGridColumnLookupItems, dataGridColumnCommentFor, filterDataGridColumnLookupItems } from "@/lib/dataGrid/dataGridColumnLookup"; import { uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; @@ -528,7 +529,7 @@ const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridR const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); const compactDataGridToolbar = computed(() => isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll); -const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows); +const infiniteScrollMaxRows = computed(() => continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null); function headerColumnComment(column: string): string { diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts b/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts index e32549a674..3ce41146be 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts @@ -27,7 +27,8 @@ const mocks = vi.hoisted(() => ({ updateRedisDbKeyStats: vi.fn(), redisScanPageSize: 100, infiniteScroll: false, - infiniteScrollMaxRows: 5000, + queryResultMaxRowsEnabled: true, + queryResultMaxRows: 5000, })); vi.mock("@/lib/backend/api", () => ({ @@ -66,7 +67,8 @@ vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => ({ editorSettings: { infiniteScroll: mocks.infiniteScroll, - infiniteScrollMaxRows: mocks.infiniteScrollMaxRows, + queryResultMaxRowsEnabled: mocks.queryResultMaxRowsEnabled, + queryResultMaxRows: mocks.queryResultMaxRows, }, }), })); @@ -358,7 +360,8 @@ function resetApiMocks() { vi.clearAllMocks(); mocks.redisScanPageSize = 100; mocks.infiniteScroll = false; - mocks.infiniteScrollMaxRows = 5000; + mocks.queryResultMaxRowsEnabled = true; + mocks.queryResultMaxRows = 5000; mocks.redisScanKeysBatch.mockResolvedValue({ cursor: 0, keys: [], total_keys: 0 }); mocks.redisGetValue.mockImplementation((_connectionId: string, _db: number, keyRaw: string) => Promise.resolve(redisValue(keyRaw))); mocks.redisSetString.mockResolvedValue(undefined); diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue index 24cdbc44ab..a8f8194fba 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue @@ -26,6 +26,7 @@ import type { RedisKeyInfo, RedisScanResult, RedisValue, HistoryEntry } from "@/ import { uuid } from "@/lib/common/utils"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; +import { continuousQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { appendRedisKeysToTreeIndex, canBuildRedisFuzzyTree, @@ -171,7 +172,7 @@ const loadingEmptyText = computed(() => (isValueSearchMode.value && valueQuery.v const redisKeySeparator = computed(() => connectionStore.getConfig(props.connectionId)?.redis_key_separator ?? ":"); const redisScanPageSize = computed(() => connectionStore.getConfig(props.connectionId)?.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT); const redisInfiniteScrollEnabled = computed(() => settingsStore.editorSettings.infiniteScroll); -const redisInfiniteScrollMaxKeys = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows); +const redisInfiniteScrollMaxKeys = computed(() => continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); watch(redisKeySeparator, () => { if (flatKeys.value.length === 0) return; if (useFlatKeySearchRows.value) { diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index 2775e3c72d..ef5560c019 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -16,6 +16,7 @@ import { applyMongoFindSort } from "@/lib/mongo/mongoShellCommand"; import { uuid } from "@/lib/common/utils"; import { simpleDataGridOrderByReferencesMissingColumn, type DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; import type { DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; +import { continuousQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { queryResultBaseSql, queryResultExecutionSql } from "@/lib/tabs/tabPresentation"; import { sqlExecutionTargetCapabilities } from "@/lib/database/sqlExecutionTargetCapabilities"; @@ -265,7 +266,13 @@ export function useDataGridActions(activeTab: ComputedRef) const tab = activeTab.value; if (!tab) return; const appendResult = settingsStore.editorSettings.infiniteScroll && offset > 0 && offset === tab.result?.rows.length; - const appendOptions = appendResult ? { appendResult: { maxRows: settingsStore.editorSettings.infiniteScrollMaxRows } } : {}; + const appendOptions = appendResult + ? { + appendResult: { + maxRows: continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows), + }, + } + : {}; if (tab.mode !== "data") { const sortColumns = visibleQuerySortColumns(tab.result?.columns ?? [], tab.result?.hidden_column_indexes, tab.resultSortColumnIndex ?? -1); const hasDatabaseSort = !!tab.result?.hidden_column_indexes?.length && tab.resultSortMode === "database" && !!tab.resultSortDirection && !!tab.resultSortColumn && !!sortColumns; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 02993f0787..f3be93eedd 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -5397,6 +5397,13 @@ export default { dataGridDisplay: "Data grid display", tableOpenPageSize: "Default rows per page when opening tables", tableOpenPageSizeDescription: "Rows loaded per page when opening a new table. Changing rows per page in a table updates this default for future table tabs.", + queryPageSize: "Default query rows per page", + queryPageSizeDescription: "Rows loaded per query result page. The maximum supported page size is {max}.", + queryResultMaxRows: "Maximum query result rows", + queryResultMaxRowsEnabled: "Limit query result rows", + queryResultMaxRowsDescription: "Caps rows returned by queries, table data, stored procedures, and continuous loading.", + queryResultMaxRowsTooSmall: "Must be at least the query page size ({pageSize}).", + queryResultMaxRowsUnlimitedDescription: "No total row or continuous loading limit. Large results can use substantial memory.", showColumnCommentsInHeader: "Show column comments under names", showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.", showColumnTypesInHeader: "Show column types under names", @@ -5413,8 +5420,6 @@ export default { autoCalculateTotalRows: "Auto-calculate total row count", autoCalculateTotalRowsDescription: "Run COUNT(*) automatically after each query to show the total matching rows. Off by default to keep large queries fast — calculate it on demand from the result footer.", infiniteScrollDescription: "Automatically load the next batch when scrolling to the bottom.", - infiniteScrollMaxRows: "Infinite scroll max items", - infiniteScrollMaxRowsDescription: "Maximum number of items to load automatically in infinite scroll mode (1000–50000).", regexMaxMatchCount: "Maximum select-all matches", regexMaxMatchCountDescription: "Limits the number of selections created by Select All Matches; search counting remains complete (100–10000).", tableColumnTemplateFields: "New Table Preset Fields", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 7c42b006d3..31f5d82495 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -5157,6 +5157,13 @@ export default withEnglishFallback({ dataGridDisplay: "Visualización de la tabla", tableOpenPageSize: "Filas predeterminadas por página al abrir tablas", tableOpenPageSizeDescription: "Filas cargadas por página al abrir una tabla nueva. Cambiar las filas por página actualiza este valor para futuras pestañas de tablas.", + queryPageSize: "Filas predeterminadas por página de consulta", + queryPageSizeDescription: "Filas cargadas por página de resultados. Se admiten hasta {max} filas.", + queryResultMaxRows: "Máximo de filas del resultado", + queryResultMaxRowsEnabled: "Limitar filas del resultado", + queryResultMaxRowsDescription: "Limita las filas conservadas por consultas, datos de tablas, procedimientos almacenados y carga continua.", + queryResultMaxRowsTooSmall: "Debe ser al menos el tamaño de página de consulta ({pageSize}).", + queryResultMaxRowsUnlimitedDescription: "Sin límite total de filas ni de carga continua. Los resultados grandes pueden usar mucha memoria.", showColumnCommentsInHeader: "Mostrar comentarios bajo los nombres", showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.", showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres", @@ -5173,8 +5180,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "Calcular automáticamente el total de filas", autoCalculateTotalRowsDescription: "Ejecuta COUNT(*) automáticamente tras cada consulta para mostrar el total de filas coincidentes. Desactivado por defecto para mantener rápidas las consultas grandes; puedes calcularlo cuando quieras desde el pie de resultados.", infiniteScrollDescription: "Carga automáticamente el siguiente lote al desplazarse hasta el final.", - infiniteScrollMaxRows: "Máximo de elementos en desplazamiento infinito", - infiniteScrollMaxRowsDescription: "Número máximo de elementos que se cargarán automáticamente en modo desplazamiento infinito (1000–50000).", regexMaxMatchCount: "Máximo de coincidencias al seleccionar todas", regexMaxMatchCountDescription: "Limita el número de selecciones creadas por Seleccionar todas las coincidencias; el conteo de búsqueda permanece completo (100–10000).", tableColumnTemplateFields: "Campos predefinidos para tablas nuevas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index af97361b81..0c23528403 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -5157,6 +5157,13 @@ export default withEnglishFallback({ dataGridDisplay: "Visualizzazione griglia dati", tableOpenPageSize: "Righe predefinite per pagina all'apertura delle tabelle", tableOpenPageSizeDescription: "Righe caricate per pagina quando si apre una nuova tabella. La modifica delle righe per pagina aggiorna questo valore per le future schede tabella.", + queryPageSize: "Righe predefinite per pagina di query", + queryPageSizeDescription: "Righe caricate per pagina dei risultati. Sono supportate fino a {max} righe.", + queryResultMaxRows: "Numero massimo di righe del risultato", + queryResultMaxRowsEnabled: "Limita le righe del risultato", + queryResultMaxRowsDescription: "Limita le righe conservate da query, dati tabella, stored procedure e caricamento continuo.", + queryResultMaxRowsTooSmall: "Deve essere almeno pari alle righe per pagina della query ({pageSize}).", + queryResultMaxRowsUnlimitedDescription: "Nessun limite totale di righe o di caricamento continuo. I risultati grandi possono usare molta memoria.", showColumnCommentsInHeader: "Mostra i commenti delle colonne sotto i nomi", showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.", showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi", @@ -5173,8 +5180,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "Calcola automaticamente il totale delle righe", autoCalculateTotalRowsDescription: "Esegue COUNT(*) automaticamente dopo ogni query per mostrare il totale delle righe corrispondenti. Disattivato per impostazione predefinita per mantenere veloci le query grandi; puoi calcolarlo all'occorrenza dal piè di pagina dei risultati.", infiniteScrollDescription: "Carica automaticamente il blocco successivo quando scorri fino in fondo.", - infiniteScrollMaxRows: "Elementi max a scorrimento infinito", - infiniteScrollMaxRowsDescription: "Numero massimo di elementi da caricare automaticamente in modalità scorrimento infinito (1000–50000).", regexMaxMatchCount: "Numero massimo di corrispondenze per Seleziona tutte", regexMaxMatchCountDescription: "Limita il numero di selezioni create da Seleziona tutte le corrispondenze; il conteggio della ricerca rimane completo (100–10000).", tableColumnTemplateFields: "Campi predefiniti per nuove tabelle", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index d66753d624..0da3dcbeff 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -5194,6 +5194,13 @@ export default withEnglishFallback({ dataGridDisplay: "データグリッド表示", tableOpenPageSize: "テーブルを開くときのデフォルト行数", tableOpenPageSizeDescription: "新しいテーブルを開くときに 1 ページあたり読み込む行数です。テーブルで行数を変更すると、今後のテーブルタブのデフォルトも更新されます。", + queryPageSize: "クエリ結果のデフォルト行数", + queryPageSizeDescription: "クエリ結果の 1 ページあたりの行数です。最大 {max} 行まで指定できます。", + queryResultMaxRows: "クエリ結果の最大行数", + queryResultMaxRowsEnabled: "クエリ結果の行数を制限", + queryResultMaxRowsDescription: "クエリ、テーブルデータ、ストアドプロシージャ、および連続読み込みで保持する合計行数を制限します。", + queryResultMaxRowsTooSmall: "クエリのページ行数({pageSize})以上にしてください。", + queryResultMaxRowsUnlimitedDescription: "合計行数または連続読み込み数を制限しません。大きな結果は大量のメモリを使用する可能性があります。", showColumnCommentsInHeader: "列名の下にコメントを表示", showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。", showColumnTypesInHeader: "列名の下にデータ型を表示", @@ -5600,8 +5607,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "総行数を自動計算", autoCalculateTotalRowsDescription: "クエリごとに COUNT(*) を自動実行し、一致する総行数を表示します。大きなクエリを高速に保つため既定はオフです。結果フッターから必要に応じて計算できます。", infiniteScrollDescription: "末尾までスクロールしたときに、次のデータを自動的に読み込みます。", - infiniteScrollMaxRows: "無限スクロールの最大項目数", - infiniteScrollMaxRowsDescription: "無限スクロールモードで自動読み込みする項目の最大数(1000〜50000)。", regexMaxMatchCount: "すべての一致を選択する最大数", regexMaxMatchCountDescription: "「すべての一致を選択」で一度に作成する選択範囲の数を制限します。検索の一致数は完全に集計されます(100〜10000)。", exportRowLimitEnabled: "エクスポート行数を制限", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index f8c25f612f..e6876c94eb 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -4896,6 +4896,13 @@ export default withEnglishFallback({ dataGridDisplay: "데이터 그리드 표시", tableOpenPageSize: "테이블을 열 때 기본 페이지당 행 수", tableOpenPageSizeDescription: "새 테이블을 열 때 페이지당 로드되는 행 수입니다. 테이블에서 페이지당 행을 변경하면 향후 테이블 탭의 기본값이 업데이트됩니다.", + queryPageSize: "쿼리 결과 기본 페이지당 행 수", + queryPageSizeDescription: "쿼리 결과 페이지당 로드할 행 수입니다. 최대 {max}행을 지원합니다.", + queryResultMaxRows: "쿼리 결과 최대 행 수", + queryResultMaxRowsEnabled: "쿼리 결과 행 수 제한", + queryResultMaxRowsDescription: "쿼리, 테이블 데이터, 저장 프로시저 및 연속 로딩에서 유지할 총 행 수를 제한합니다.", + queryResultMaxRowsTooSmall: "쿼리 페이지 크기({pageSize}) 이상이어야 합니다.", + queryResultMaxRowsUnlimitedDescription: "총 행 수 또는 연속 로딩 수를 제한하지 않습니다. 큰 결과는 많은 메모리를 사용할 수 있습니다.", showColumnCommentsInHeader: "이름 아래에 컬럼 주석 표시", showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.", showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시", @@ -4912,8 +4919,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "전체 행 수 자동 집계", autoCalculateTotalRowsDescription: "각 쿼리 후 자동으로 COUNT(*)를 실행하여 일치하는 전체 행 수를 표시합니다. 대규모 쿼리를 빠르게 유지하기 위해 기본적으로 꺼져 있으며, 결과 푸터에서 요청 시 집계할 수 있습니다.", infiniteScrollDescription: "목록 하단으로 스크롤할 때 다음 데이터 묶음을 자동으로 불러옵니다.", - infiniteScrollMaxRows: "무한 스크롤 최대 항목 수", - infiniteScrollMaxRowsDescription: "무한 스크롤 모드에서 자동으로 불러올 항목의 최대 수 (1000–50000).", tableColumnTemplateFields: "새 테이블 미리 정의된 필드", tableColumnTemplateFieldsDescription: "데이터베이스 유형을 선택한 다음 새 테이블을 만들 때 사용할 미리 정의된 필드 타입을 구성하세요.", tableColumnTemplateAdd: "필드 추가", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index d325ce7897..0467b37df9 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -5159,6 +5159,13 @@ export default withEnglishFallback({ dataGridDisplay: "Exibição da grade de dados", tableOpenPageSize: "Linhas padrão por página ao abrir tabelas", tableOpenPageSizeDescription: "Linhas carregadas por página ao abrir uma nova tabela. Alterar as linhas por página atualiza esse padrão para futuras abas de tabelas.", + queryPageSize: "Linhas padrão por página de consulta", + queryPageSizeDescription: "Linhas carregadas por página de resultados. São suportadas até {max} linhas.", + queryResultMaxRows: "Máximo de linhas do resultado", + queryResultMaxRowsEnabled: "Limitar linhas do resultado", + queryResultMaxRowsDescription: "Limita as linhas mantidas por consultas, dados de tabelas, procedimentos armazenados e carregamento contínuo.", + queryResultMaxRowsTooSmall: "Deve ser pelo menos o tamanho da página da consulta ({pageSize}).", + queryResultMaxRowsUnlimitedDescription: "Sem limite total de linhas ou de carregamento contínuo. Resultados grandes podem usar muita memória.", showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes", showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.", showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes", @@ -5175,8 +5182,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "Calcular automaticamente o total de linhas", autoCalculateTotalRowsDescription: "Executa COUNT(*) automaticamente após cada consulta para mostrar o total de linhas correspondentes. Desativado por padrão para manter consultas grandes rápidas; você pode calculá-lo quando quiser no rodapé dos resultados.", infiniteScrollDescription: "Carregar automaticamente o próximo lote ao rolar até o final.", - infiniteScrollMaxRows: "Máximo de itens em rolagem infinita", - infiniteScrollMaxRowsDescription: "Número máximo de itens a carregar automaticamente no modo de rolagem infinita (1000–50000).", regexMaxMatchCount: "Máximo de correspondências para selecionar todas", regexMaxMatchCountDescription: "Limita o número de seleções criadas por Selecionar todas as correspondências; a contagem da pesquisa permanece completa (100–10000).", tableColumnTemplateFields: "Campos predefinidos para novas tabelas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 870aa5e9b2..6db29b0672 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -5319,6 +5319,13 @@ export default withEnglishFallback({ dataGridDisplay: "数据表格显示", tableOpenPageSize: "打开表默认每页行数", tableOpenPageSizeDescription: "新打开数据表时每页加载的行数。在表数据页修改每页行数后,也会更新后续新建表标签页的默认值。", + queryPageSize: "查询结果默认每页行数", + queryPageSizeDescription: "每个查询结果分页加载的行数,最大支持 {max} 行。", + queryResultMaxRows: "查询结果最大行数", + queryResultMaxRowsEnabled: "限制查询结果行数", + queryResultMaxRowsDescription: "限制查询、表数据、存储过程及连续滚动累计保留的总行数。", + queryResultMaxRowsTooSmall: "不能小于查询结果默认每页行数({pageSize})。", + queryResultMaxRowsUnlimitedDescription: "不限制返回总行数或连续滚动累计数量,大结果集可能占用较多内存。", showColumnCommentsInHeader: "在字段名下方显示注释", showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。", showColumnTypesInHeader: "在字段名下方显示数据类型", @@ -5335,8 +5342,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "自动统计总行数", autoCalculateTotalRowsDescription: "每次查询后自动执行 COUNT(*) 显示匹配的总行数。默认关闭以保证大查询速度 —— 可在结果栏按需手动统计。", infiniteScrollDescription: "滚动到底部时自动加载下一批数据,无需手动翻页或加载。", - infiniteScrollMaxRows: "无限滚动最大条目数", - infiniteScrollMaxRowsDescription: "无限滚动模式下自动加载的最大条目数(1000–50000)。", regexMaxMatchCount: "全部选中最大数量", regexMaxMatchCountDescription: "限制“全部选中”一次创建的选区数量;搜索匹配仍完整统计(100–10000)。", tableColumnTemplateFields: "新建表预设字段", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 9e363d759d..ec5c894a3d 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -4603,6 +4603,13 @@ export default withEnglishFallback({ dataGridDisplay: "資料表格顯示", tableOpenPageSize: "開啟資料表預設每頁列數", tableOpenPageSizeDescription: "新開啟資料表時每頁載入的列數。在資料表頁面調整每頁列數後,也會更新後續新建資料表分頁的預設值。", + queryPageSize: "查詢結果預設每頁列數", + queryPageSizeDescription: "每個查詢結果分頁載入的列數,最多支援 {max} 列。", + queryResultMaxRows: "查詢結果最大列數", + queryResultMaxRowsEnabled: "限制查詢結果列數", + queryResultMaxRowsDescription: "限制查詢、資料表資料、預存程序及連續捲動累計保留的總列數。", + queryResultMaxRowsTooSmall: "不能小於查詢結果預設每頁列數({pageSize})。", + queryResultMaxRowsUnlimitedDescription: "不限制傳回總列數或連續捲動累計數量,大型結果集可能占用大量記憶體。", showColumnCommentsInHeader: "在欄位名稱下方顯示註解", showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。", showColumnTypesInHeader: "在欄位名稱下方顯示資料類型", @@ -4619,8 +4626,6 @@ export default withEnglishFallback({ autoCalculateTotalRows: "自動統計總筆數", autoCalculateTotalRowsDescription: "每次查詢後自動執行 COUNT(*) 顯示符合的總筆數。預設關閉以確保大型查詢速度 —— 可在結果列按需手動統計。", infiniteScrollDescription: "滾動到底部時自動載入下一批資料,無需手動翻頁或載入。", - infiniteScrollMaxRows: "無限滾動最大項目數", - infiniteScrollMaxRowsDescription: "無限滾動模式下自動載入的最大項目數(1000–50000)。", regexMaxMatchCount: "全部選取最大數量", regexMaxMatchCountDescription: "限制「全部選取」一次建立的選取範圍數量;搜尋符合結果仍會完整統計(100–10000)。", tableColumnTemplateFields: "新建表預設欄位", diff --git a/apps/desktop/src/lib/dataGrid/__tests__/queryResultRowLimit.spec.ts b/apps/desktop/src/lib/dataGrid/__tests__/queryResultRowLimit.spec.ts new file mode 100644 index 0000000000..be5025a927 --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/__tests__/queryResultRowLimit.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { MAX_QUERY_RESULT_MAX_ROWS, agentProtocolQueryResultMaxRows, capQueryResultTotal, continuousQueryResultMaxRows, effectiveQueryResultMaxRows, limitQueryPagination, normalizeQueryResultMaxRows, queryResultLimitReached } from "../queryResultRowLimit"; + +describe("query result row limits", () => { + it("preserves the legacy 100,000-row default", () => { + expect(effectiveQueryResultMaxRows(undefined, undefined)).toBe(100_000); + expect(effectiveQueryResultMaxRows(true, 250_000)).toBe(250_000); + }); + + it("uses the Java signed-int boundary for unlimited Agent execution", () => { + expect(effectiveQueryResultMaxRows(false, 100_000)).toBeUndefined(); + expect(agentProtocolQueryResultMaxRows(undefined)).toBe(MAX_QUERY_RESULT_MAX_ROWS); + expect(continuousQueryResultMaxRows(false, 100_000)).toBe(MAX_QUERY_RESULT_MAX_ROWS); + expect(continuousQueryResultMaxRows(true, 250_000)).toBe(250_000); + expect(normalizeQueryResultMaxRows(MAX_QUERY_RESULT_MAX_ROWS + 1)).toBe(MAX_QUERY_RESULT_MAX_ROWS); + }); + + it("limits the final page and caps totals", () => { + expect(limitQueryPagination({ limit: 100, offset: 950 }, 1_000)).toEqual({ limit: 50, offset: 950 }); + expect(limitQueryPagination({ limit: 100, offset: 2_000 }, 1_000)).toEqual({ limit: 1, offset: 999 }); + expect(queryResultLimitReached(950, 50, 1_000)).toBe(true); + expect(queryResultLimitReached(900, 50, 1_000)).toBe(false); + expect(capQueryResultTotal(1_500, 1_000)).toBe(1_000); + expect(capQueryResultTotal(1_500, undefined)).toBe(1_500); + }); +}); diff --git a/apps/desktop/src/lib/dataGrid/paginationPageSize.ts b/apps/desktop/src/lib/dataGrid/paginationPageSize.ts index 4228c48e27..e1680d7c22 100644 --- a/apps/desktop/src/lib/dataGrid/paginationPageSize.ts +++ b/apps/desktop/src/lib/dataGrid/paginationPageSize.ts @@ -1,7 +1,7 @@ export const RESULT_PAGE_SIZE_OPTIONS = [50, 100, 500, 1000]; export const DEFAULT_RESULT_PAGE_SIZE = 100; export const MIN_RESULT_PAGE_SIZE = 1; -export const MAX_RESULT_PAGE_SIZE = 100000; +export const MAX_RESULT_PAGE_SIZE = 1_000_000; export function normalizeResultPageSize(value: unknown, fallback = DEFAULT_RESULT_PAGE_SIZE): number { const fallbackValue = Number.isFinite(fallback) && fallback >= MIN_RESULT_PAGE_SIZE ? Math.min(Math.floor(fallback), MAX_RESULT_PAGE_SIZE) : DEFAULT_RESULT_PAGE_SIZE; diff --git a/apps/desktop/src/lib/dataGrid/queryResultRowLimit.ts b/apps/desktop/src/lib/dataGrid/queryResultRowLimit.ts new file mode 100644 index 0000000000..edafd3cfd0 --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/queryResultRowLimit.ts @@ -0,0 +1,36 @@ +export const DEFAULT_QUERY_RESULT_MAX_ROWS = 100_000; +export const MAX_QUERY_RESULT_MAX_ROWS = 2_147_483_647; + +export function normalizeQueryResultMaxRows(value: unknown, fallback = DEFAULT_QUERY_RESULT_MAX_ROWS): number { + const normalizedFallback = Number.isFinite(fallback) ? Math.min(Math.max(1, Math.floor(fallback)), MAX_QUERY_RESULT_MAX_ROWS) : DEFAULT_QUERY_RESULT_MAX_ROWS; + const parsed = Number(value); + if (!Number.isFinite(parsed)) return normalizedFallback; + return Math.min(Math.max(1, Math.floor(parsed)), MAX_QUERY_RESULT_MAX_ROWS); +} + +export function effectiveQueryResultMaxRows(enabled: boolean | undefined, value: unknown): number | undefined { + return enabled !== false ? normalizeQueryResultMaxRows(value) : undefined; +} + +export function continuousQueryResultMaxRows(enabled: boolean | undefined, value: unknown): number { + return effectiveQueryResultMaxRows(enabled, value) ?? MAX_QUERY_RESULT_MAX_ROWS; +} + +export function agentProtocolQueryResultMaxRows(maxRows: number | undefined): number { + return maxRows ?? MAX_QUERY_RESULT_MAX_ROWS; +} + +export function limitQueryPagination(pagination: T, maxRows: number | undefined): T { + if (maxRows === undefined) return pagination; + const offset = Math.min(Math.max(0, pagination.offset), maxRows - 1); + const remaining = maxRows - offset; + return { ...pagination, limit: Math.min(pagination.limit, remaining), offset }; +} + +export function queryResultLimitReached(offset: number | undefined, rowCount: number, maxRows: number | undefined): boolean { + return maxRows !== undefined && Math.max(0, offset ?? 0) + rowCount >= maxRows; +} + +export function capQueryResultTotal(total: number, maxRows: number | undefined): number { + return maxRows === undefined ? total : Math.min(total, maxRows); +} diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index e468528ec1..0f35a77ef4 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { EDITOR_SETTINGS_DRAFT_KEYS, editorSettingsDraftFromSettings, editorSettingsDraftChanged, editorSettingsPatchFromDraft, normalizeTableOpenPageSizeDraft } from "../editorSettingsDraft"; +import { EDITOR_SETTINGS_DRAFT_KEYS, editorSettingsDraftFromSettings, editorSettingsDraftChanged, editorSettingsPatchFromDraft, normalizeQueryResultMaxRowsDraft, normalizeTableOpenPageSizeDraft } from "../editorSettingsDraft"; import type { EditorSettings } from "@/stores/settingsStore"; function makeSettings(overrides: Partial = {}): EditorSettings { @@ -7,6 +7,8 @@ function makeSettings(overrides: Partial = {}): EditorSettings { autoCalculateTotalRows: false, pageSize: 100, tableOpenPageSize: 100, + queryResultMaxRowsEnabled: true, + queryResultMaxRows: 100000, sqlEngine: "desktop", tabSize: 2, keywordCase: "upper", @@ -35,7 +37,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { }); it("includes the table-open page size", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("pageSize"); expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tableOpenPageSize"); + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("queryResultMaxRowsEnabled"); + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("queryResultMaxRows"); }); it("includes the saved SQL open target mode", () => { @@ -89,7 +94,8 @@ describe("editorSettingsDraftFromSettings", () => { describe("normalizeTableOpenPageSizeDraft", () => { it.each([ - [200000, 100000], + [200000, 200000], + [2000000, 1000000], [0, 100], [-1, 100], ["123.9", 123], @@ -102,6 +108,17 @@ describe("normalizeTableOpenPageSizeDraft", () => { }); }); +describe("normalizeQueryResultMaxRowsDraft", () => { + it.each([ + [250000, 250000], + [0, 1], + [2147483648, 2147483647], + [Number.NaN, 100000], + ])("normalizes %s to %s", (value, expected) => { + expect(normalizeQueryResultMaxRowsDraft(value)).toBe(expected); + }); +}); + describe("editorSettingsDraftChanged", () => { it("detects change in continueOnErrorOnBatch", () => { const settings = makeSettings({ continueOnErrorOnBatch: false }); @@ -181,7 +198,7 @@ describe("editorSettingsPatchFromDraft", () => { const draft = editorSettingsDraftFromSettings(settings); const base = editorSettingsDraftFromSettings(settings); draft.tableOpenPageSize = 200000.9; - expect(editorSettingsPatchFromDraft(draft, base).tableOpenPageSize).toBe(100000); + expect(editorSettingsPatchFromDraft(draft, base).tableOpenPageSize).toBe(200000); }); it("includes the saved SQL open target when changed", () => { diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 04a5dc829e..4e61498e01 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -131,7 +131,7 @@ describe("settings search", () => { { titleKey: "settings.savedSqlOpenTarget", category: "editor", targetId: "editor" }, { titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" }, { titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" }, - { titleKey: "settings.dataGridQuickEntry", category: "appearance", targetId: "appearance" }, + { titleKey: "settings.dataGridQuickEntry", category: "data", targetId: "data" }, { titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" }, { titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" }, { titleKey: "toolbar.theme", category: "appearance", targetId: "appearance" }, @@ -155,8 +155,8 @@ describe("settings search", () => { { titleKey: "settings.sqlFormatterParamTypes", category: "formatter", targetId: "formatter" }, { titleKey: "settings.routineSourceOpenMode", category: "navigation", targetId: "navigation" }, { titleKey: "settings.disconnectTabHandlingMode", category: "navigation", targetId: "navigation" }, - { titleKey: "settings.compactColumnHeaderActions", category: "appearance", targetId: "appearance" }, - { titleKey: "settings.infiniteScrollMaxRows", category: "appearance", targetId: "appearance" }, + { titleKey: "settings.compactColumnHeaderActions", category: "data", targetId: "data" }, + { titleKey: "settings.infiniteScroll", category: "data", targetId: "data" }, { titleKey: "settings.globalDateTimeDisplayFormat", category: "data", targetId: "data" }, { titleKey: "settings.globalDateTimeExportFormat", category: "data", targetId: "data" }, { titleKey: "settings.globalDateTimeImportFormat", category: "data", targetId: "data" }, diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 75f17ca194..6a154cd7d6 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -1,5 +1,6 @@ import type { EditorSettings } from "@/stores/settingsStore"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; +import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy"; export const EDITOR_SETTINGS_DRAFT_KEYS = [ @@ -36,9 +37,11 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "compactColumnHeaderActions", "dataGridQuickEntry", "dataGridAutoTransposeSingleRow", + "pageSize", "tableOpenPageSize", + "queryResultMaxRowsEnabled", + "queryResultMaxRows", "infiniteScroll", - "infiniteScrollMaxRows", "regexMaxMatchCount", "autoCalculateTotalRows", "tableColumnTemplateFields", @@ -86,8 +89,13 @@ export function normalizeTableOpenPageSizeDraft(value: unknown): number { return normalizeResultPageSize(value); } +export function normalizeQueryResultMaxRowsDraft(value: unknown): number { + return normalizeQueryResultMaxRows(value); +} + function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown { - if (key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value); + if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value); + if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value); if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value); return value; } diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 789841bb5e..6e9de4e8d6 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -170,16 +170,17 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "navigation-hidden-tables", category: "navigation", titleKey: "settings.sidebarHiddenTablePrefixes", descriptionKey: "settings.sidebarHiddenTablePrefixesDescription", targetId: "navigation" }, { id: "navigation-table-page-size", category: "navigation", titleKey: "settings.sidebarTablePageSize", descriptionKey: "settings.sidebarTablePageSizeDescription", targetId: "navigation" }, { id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" }, + { id: "query-page-size", category: "data", titleKey: "settings.queryPageSize", descriptionKey: "settings.queryPageSizeDescription", targetId: "data" }, { id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" }, - { id: "appearance-header-comments", category: "appearance", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "appearance" }, - { id: "appearance-header-types", category: "appearance", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "appearance" }, - { id: "appearance-index-indicators", category: "appearance", titleKey: "settings.showIndexIndicatorsInHeader", descriptionKey: "settings.showIndexIndicatorsInHeaderDescription", targetId: "appearance" }, - { id: "appearance-compact-header-actions", category: "appearance", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "appearance" }, - { id: "appearance-auto-total", category: "appearance", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "appearance" }, - { id: "appearance-infinite-scroll", category: "appearance", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "appearance" }, - { id: "appearance-infinite-scroll-limit", category: "appearance", titleKey: "settings.infiniteScrollMaxRows", descriptionKey: "settings.infiniteScrollMaxRowsDescription", targetId: "appearance" }, - { id: "appearance-auto-transpose", category: "appearance", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "appearance" }, - { id: "appearance-quick-entry", category: "appearance", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "appearance" }, + { id: "query-result-max-rows", category: "data", titleKey: "settings.queryResultMaxRows", descriptionKey: "settings.queryResultMaxRowsDescription", targetId: "data" }, + { id: "data-grid-header-comments", category: "data", titleKey: "settings.showColumnCommentsInHeader", descriptionKey: "settings.showColumnCommentsInHeaderDescription", targetId: "data" }, + { id: "data-grid-header-types", category: "data", titleKey: "settings.showColumnTypesInHeader", descriptionKey: "settings.showColumnTypesInHeaderDescription", targetId: "data" }, + { id: "data-grid-index-indicators", category: "data", titleKey: "settings.showIndexIndicatorsInHeader", descriptionKey: "settings.showIndexIndicatorsInHeaderDescription", targetId: "data" }, + { id: "data-grid-compact-header-actions", category: "data", titleKey: "settings.compactColumnHeaderActions", descriptionKey: "settings.compactColumnHeaderActionsDescription", targetId: "data" }, + { id: "data-grid-auto-total", category: "data", titleKey: "settings.autoCalculateTotalRows", descriptionKey: "settings.autoCalculateTotalRowsDescription", targetId: "data" }, + { id: "data-grid-infinite-scroll", category: "data", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "data" }, + { id: "data-grid-auto-transpose", category: "data", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "data" }, + { id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" }, { id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" }, { id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" }, ...createToolbarVisibilitySettingsSearchDefinitions(), diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index d270a359ee..800ecba1f9 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -47,7 +47,8 @@ import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeo import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource"; import { beginDataGridNativeSelectionBlock, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection"; import { simpleDataGridOrderByReferencesMissingColumn, sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; -import { MAX_RESULT_PAGE_SIZE, normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; +import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; +import { agentProtocolQueryResultMaxRows, capQueryResultTotal, effectiveQueryResultMaxRows, limitQueryPagination, queryResultLimitReached } from "@/lib/dataGrid/queryResultRowLimit"; import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen"; import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor"; @@ -3610,6 +3611,7 @@ export const useQueryStore = defineStore("query", () => { traceId: string; elapsed: () => string; timeoutSecs: number; + maxRows?: number; }) { const resultRowCount = options.result.rows.length; if (resultRowCount <= 0) { @@ -3652,10 +3654,11 @@ export const useQueryStore = defineStore("query", () => { setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined); return; } - setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, total); + const cappedTotal = capQueryResultTotal(total, options.maxRows); + setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, cappedTotal); queryExecutionLog("info", "count:done", { traceId: options.traceId, - total, + total: cappedTotal, elapsed: options.elapsed(), }); } catch (error) { @@ -4284,6 +4287,8 @@ export const useQueryStore = defineStore("query", () => { return producedResult; } + const queryResultMaxRows = effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows); + if (tab.mode === "query") { const prepared = await prepareEditableQueryExecution(tab, sqlToExecute, conn, effectiveDbType, executionDatabase, traceId, elapsed); sqlToExecute = prepared.sql; @@ -4305,7 +4310,7 @@ export const useQueryStore = defineStore("query", () => { sqlToExecute = sorted.sql; resultSortedSql = sorted.sql; } - const pagination = options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 }; + const pagination = limitQueryPagination(options?.pagination ?? { limit: settingsStore.editorSettings.pageSize, offset: 0 }, queryResultMaxRows); const plan = await api.prepareQueryPaginationExecutionPlan({ sql: sqlToExecute, queryBaseSql, @@ -4336,8 +4341,15 @@ export const useQueryStore = defineStore("query", () => { return false; } } else if (tab.mode === "data") { - pageLimit = options?.pagination?.limit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize); - pageOffset = options?.pagination?.offset ?? 0; + const pagination = limitQueryPagination( + { + limit: options?.pagination?.limit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize), + offset: options?.pagination?.offset ?? 0, + }, + queryResultMaxRows, + ); + pageLimit = pagination.limit; + pageOffset = pagination.offset; } const executionSchema = targetContext?.scope === "connection" ? undefined : connectionQueryExecutionSchema(conn, databaseTargetContext?.database ?? executionTarget?.database ?? tab.database, databaseTargetContext?.schema ?? executionTarget?.schema ?? tab.schema, tab.mode === "data"); @@ -4354,7 +4366,7 @@ export const useQueryStore = defineStore("query", () => { } queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() }); executionDispatched = true; - executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit); + executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit ?? agentProtocolQueryResultMaxRows(queryResultMaxRows)); } else { queryExecutionLog("info", "execute-multi:start", { traceId, elapsed: elapsed() }); // Query and data tabs use a tab-scoped pool so repeated executions keep @@ -4364,14 +4376,13 @@ export const useQueryStore = defineStore("query", () => { ? useAgentResultSession ? { // Agent cursors apply maxRows cumulatively across fetched pages. - // Keep multi-page navigation available without allowing unbounded reads. - maxRows: MAX_RESULT_PAGE_SIZE, + maxRows: agentProtocolQueryResultMaxRows(queryResultMaxRows), fetchSize: pageLimit, pageSize: pageLimit, resultSessionId: options?.pagination?.sessionId, } : { maxRows: pageLimit, fetchSize: pageLimit } - : {}), + : { maxRows: agentProtocolQueryResultMaxRows(queryResultMaxRows) }), ...(executionClientSessionId ? { clientSessionId: executionClientSessionId } : {}), timeoutSecs: queryTimeoutSecs, catalog: executionCatalog, @@ -4497,6 +4508,12 @@ export const useQueryStore = defineStore("query", () => { current.resultTotalRowCount = undefined; } const resultRowCount = current.result?.rows.length ?? 0; + const resultLimitReached = !!current.result && queryResultLimitReached(pageOffset, resultRowCount, queryResultMaxRows); + if (resultLimitReached && current.result) { + current.result.has_more = false; + current.result.truncated = true; + current.resultTotalRowCount = queryResultMaxRows; + } const totalKnownFromIncompletePage = !!current.result && typeof exactTotalFromIncompletePage(current.result, pageLimit, pageOffset, useAgentResultSession) === "number"; const dataCountTarget = current.mode === "data" @@ -4514,7 +4531,8 @@ export const useQueryStore = defineStore("query", () => { }; })() : undefined; - const canAutoCalculateTotalRows = !options?.appendResult && !!current.result && resultRowCount > 0 && !totalKnownFromIncompletePage && settingsStore.editorSettings.autoCalculateTotalRows && ((current.mode === "query" && !!countSql) || (current.mode === "data" && !!dataCountTarget)); + const canAutoCalculateTotalRows = + !options?.appendResult && !!current.result && resultRowCount > 0 && !resultLimitReached && !totalKnownFromIncompletePage && settingsStore.editorSettings.autoCalculateTotalRows && ((current.mode === "query" && !!countSql) || (current.mode === "data" && !!dataCountTarget)); current.resultTotalRowCountLoading = canAutoCalculateTotalRows; // Server-side pagination without a countSql: the backend (currently // the Elasticsearch driver) already reports the true match total via @@ -4528,7 +4546,7 @@ export const useQueryStore = defineStore("query", () => { } touchResult(current); syncDisplayedResultRun(current, queryBaseSql, captureResultRun); - if (!options?.appendResult && !totalRowCountResolved && (current.mode === "query" || current.mode === "data") && current.result) { + if (!options?.appendResult && !resultLimitReached && !totalRowCountResolved && (current.mode === "query" || current.mode === "data") && current.result) { countQueryTotalRowsInBackground({ tabId: id, connectionId: executionConnectionId, @@ -4550,6 +4568,7 @@ export const useQueryStore = defineStore("query", () => { traceId, elapsed, timeoutSecs: queryTimeoutSecs, + maxRows: queryResultMaxRows, }); } queryExecutionLog("info", "result:assigned", { diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index b02cd7603a..e29a6666d8 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -8,6 +8,7 @@ import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeS import { type ColumnFormatterConfig, type CustomColumnFormatterConfig, normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlobalDateTimePattern } from "@/lib/dataGrid/columnFormatter"; import { type DataGridCopyPreference, type DataGridExtractorOptions, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; +import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; @@ -515,7 +516,10 @@ export interface EditorSettings { appLayout: "separated" | "classic"; pageSize: number; tableOpenPageSize: number; + queryResultMaxRowsEnabled: boolean; + queryResultMaxRows: number; infiniteScroll: boolean; + /** Preserved for downgrade compatibility; current clients use queryResultMaxRows. */ infiniteScrollMaxRows: number; regexMaxMatchCount: number; autoCalculateTotalRows: boolean; @@ -702,6 +706,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { appLayout: "classic", pageSize: 100, tableOpenPageSize: 100, + queryResultMaxRowsEnabled: true, + queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS, infiniteScroll: false, infiniteScrollMaxRows: 5000, regexMaxMatchCount: 1000, @@ -1036,6 +1042,8 @@ export function normalizeEditorSettings(settings: Partial, exist appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout, pageSize: normalizeResultPageSize(settings.pageSize), tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize), + queryResultMaxRowsEnabled: settings.queryResultMaxRowsEnabled !== false, + queryResultMaxRows: normalizeQueryResultMaxRows(settings.queryResultMaxRows), infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll, infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows, regexMaxMatchCount: typeof settings.regexMaxMatchCount === "number" && Number.isFinite(settings.regexMaxMatchCount) && settings.regexMaxMatchCount >= 100 && settings.regexMaxMatchCount <= 10000 ? Math.round(settings.regexMaxMatchCount) : DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount, @@ -1572,6 +1580,8 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout; if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize); if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize); + if (partial.queryResultMaxRowsEnabled !== undefined) editorSettings.value.queryResultMaxRowsEnabled = Boolean(partial.queryResultMaxRowsEnabled); + if (partial.queryResultMaxRows !== undefined) editorSettings.value.queryResultMaxRows = normalizeQueryResultMaxRows(partial.queryResultMaxRows, editorSettings.value.queryResultMaxRows); if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll; if (partial.infiniteScrollMaxRows !== undefined) editorSettings.value.infiniteScrollMaxRows = typeof partial.infiniteScrollMaxRows === "number" && partial.infiniteScrollMaxRows >= 1000 && partial.infiniteScrollMaxRows <= 50000 ? Math.round(partial.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows; diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index 3dd16f87a3..dac54feb6d 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -30,6 +30,7 @@ use crate::sql_risk::{classify_sql_risk_for_database, SqlRisk}; pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30); pub const MAX_ROWS: usize = 10000; +pub const AGENT_PROTOCOL_MAX_ROWS: usize = i32::MAX as usize; pub const QUERY_CANCELED: &str = "Query canceled"; /// Fallback when a Mongo connection hits the generic SQL executor instead of the shell path. /// Wording must match packages/mongo-shell `MONGO_SHELL_COMMAND_HINT` @@ -659,7 +660,7 @@ pub fn agent_execute_query_params( ) -> serde_json::Value { let mut params = serde_json::json!({ "sql": sql, - "maxRows": options.max_rows.unwrap_or(MAX_ROWS), + "maxRows": agent_protocol_row_count(options.max_rows.unwrap_or(MAX_ROWS)), }); if let Some(database) = database.map(str::trim).filter(|database| !database.is_empty()) { params["database"] = serde_json::json!(database); @@ -668,7 +669,7 @@ pub fn agent_execute_query_params( params["schema"] = serde_json::json!(schema); } if let Some(fetch_size) = options.fetch_size { - params["fetchSize"] = serde_json::json!(fetch_size); + params["fetchSize"] = serde_json::json!(agent_protocol_row_count(fetch_size)); } if let Some(timeout_secs) = options.timeout_secs { params["timeoutSecs"] = serde_json::json!(timeout_secs); @@ -684,8 +685,8 @@ pub fn agent_execute_query_page_params( ) -> serde_json::Value { let mut params = serde_json::json!({ "sql": sql, - "pageSize": options.page_size.unwrap_or(MAX_ROWS), - "maxRows": options.max_rows.unwrap_or(MAX_ROWS), + "pageSize": agent_protocol_row_count(options.page_size.unwrap_or(MAX_ROWS)), + "maxRows": agent_protocol_row_count(options.max_rows.unwrap_or(MAX_ROWS)), }); if let Some(database) = database.map(str::trim).filter(|database| !database.is_empty()) { params["database"] = serde_json::json!(database); @@ -694,7 +695,7 @@ pub fn agent_execute_query_page_params( params["schema"] = serde_json::json!(schema); } if let Some(fetch_size) = options.fetch_size { - params["fetchSize"] = serde_json::json!(fetch_size); + params["fetchSize"] = serde_json::json!(agent_protocol_row_count(fetch_size)); } if let Some(timeout_secs) = options.timeout_secs { params["timeoutSecs"] = serde_json::json!(timeout_secs); @@ -705,10 +706,14 @@ pub fn agent_execute_query_page_params( pub fn agent_fetch_query_page_params(session_id: &str, page_size: usize) -> serde_json::Value { serde_json::json!({ "sessionId": session_id, - "pageSize": page_size, + "pageSize": agent_protocol_row_count(page_size), }) } +fn agent_protocol_row_count(value: usize) -> usize { + value.clamp(1, AGENT_PROTOCOL_MAX_ROWS) +} + pub fn agent_close_query_session_params(session_id: &str) -> serde_json::Value { serde_json::json!({ "sessionId": session_id, @@ -6484,6 +6489,27 @@ for line in sys.stdin: assert_eq!(params["maxRows"], MAX_ROWS); } + #[test] + fn agent_query_row_counts_are_clamped_to_java_signed_int_range() { + let oversized = AGENT_PROTOCOL_MAX_ROWS.saturating_add(1); + let params = agent_execute_query_page_params( + "SELECT * FROM events", + None, + None, + QueryExecutionOptions { + page_size: Some(oversized), + fetch_size: Some(oversized), + max_rows: Some(oversized), + ..Default::default() + }, + ); + + assert_eq!(params["pageSize"], AGENT_PROTOCOL_MAX_ROWS); + assert_eq!(params["fetchSize"], AGENT_PROTOCOL_MAX_ROWS); + assert_eq!(params["maxRows"], AGENT_PROTOCOL_MAX_ROWS); + assert_eq!(agent_fetch_query_page_params("session-1", oversized)["pageSize"], AGENT_PROTOCOL_MAX_ROWS); + } + #[test] fn agent_fetch_query_page_params_include_session_and_page_size() { let params = agent_fetch_query_page_params("session-1", 500); diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index df39c7ba77..4fc9407b93 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -3950,15 +3950,18 @@ test("MongoDB query result export rejects pagination-plan failures without repla } }); -test("jdbc query pagination uses result sessions without capping max rows to one page", async () => { +test("jdbc query pagination sends the Agent-safe unlimited row boundary when the global limit is disabled", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); + const settingsStore = useSettingsStore(); const store = useQueryStore(); const originalFetch = globalThis.fetch; let prepareBody: any; let executeBody: any; + settingsStore.updateEditorSettings({ queryResultMaxRowsEnabled: false }); + connectionStore.addEphemeralConnection({ ...conn("jdbc-1"), db_type: "jdbc", @@ -4014,7 +4017,7 @@ test("jdbc query pagination uses result sessions without capping max rows to one assert.equal(prepareBody.options.useAgentCursor, true); assert.equal(executeBody.pageSize, 100); assert.equal(executeBody.fetchSize, 100); - assert.equal(executeBody.maxRows, 100000); + assert.equal(executeBody.maxRows, 2147483647); assert.equal(executeBody.clientSessionId, tabId); assert.equal(tab.resultSessionId, "session-1"); assert.equal(tab.result?.has_more, true); @@ -6438,7 +6441,7 @@ for (const resultState of [ { label: "truncated", result: { truncated: true, has_more: false } }, { label: "ambiguous exhaustion", result: {} }, ]) { - test(`oracle agent ${resultState.label} short page uses COUNT instead of inferring 10000`, async () => { + test(`oracle agent ${resultState.label} short page uses COUNT and caps the configured result total`, async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); @@ -6496,11 +6499,11 @@ for (const resultState of [ try { await store.executeTabSql(tabId, "SELECT ID FROM LARGE_TABLE"); - await waitFor(() => tab.resultTotalRowCount === 3_357_833); + await waitFor(() => tab.resultTotalRowCount === 100_000); assert.equal(countRequests, 1); assert.equal(tab.result?.rows.length, 10_000); - assert.notEqual(tab.resultTotalRowCount, 10_000); + assert.equal(tab.resultTotalRowCount, 100_000); } finally { globalThis.fetch = originalFetch; restoreStorage(); diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index efe39c2cb9..9158c46f55 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -63,20 +63,32 @@ async function withMockLocalStorage(initial: Record, run: () => test("normalizes saved query result page size", () => { assert.equal(DEFAULT_EDITOR_SETTINGS.pageSize, 100); assert.equal(normalizeEditorSettings({ pageSize: 5000 }).pageSize, 5000); - assert.equal(normalizeEditorSettings({ pageSize: 200000 }).pageSize, 100000); + assert.equal(normalizeEditorSettings({ pageSize: 200000 }).pageSize, 200000); + assert.equal(normalizeEditorSettings({ pageSize: 2000000 }).pageSize, 1000000); assert.equal(normalizeEditorSettings({ pageSize: 0 }).pageSize, 100); }); test("normalizes the dedicated default row limit for table opens", () => { assert.equal(DEFAULT_EDITOR_SETTINGS.tableOpenPageSize, 100); assert.equal(normalizeEditorSettings({ tableOpenPageSize: 1000 }).tableOpenPageSize, 1000); - assert.equal(normalizeEditorSettings({ tableOpenPageSize: 200000 }).tableOpenPageSize, 100000); + assert.equal(normalizeEditorSettings({ tableOpenPageSize: 200000 }).tableOpenPageSize, 200000); + assert.equal(normalizeEditorSettings({ tableOpenPageSize: 2000000 }).tableOpenPageSize, 1000000); assert.equal(normalizeEditorSettings({ tableOpenPageSize: 0 }).tableOpenPageSize, 100); assert.equal(tableOpenPageLimit(), 100); assert.equal(tableOpenPageLimit(1000), 1000); assert.equal(tableOpenPageLimit(0), 100); }); +test("normalizes the global query result row limit", () => { + assert.equal(DEFAULT_EDITOR_SETTINGS.queryResultMaxRowsEnabled, true); + assert.equal(DEFAULT_EDITOR_SETTINGS.queryResultMaxRows, 100000); + assert.equal(normalizeEditorSettings({}).queryResultMaxRowsEnabled, true); + assert.equal(normalizeEditorSettings({}).queryResultMaxRows, 100000); + assert.equal(normalizeEditorSettings({ queryResultMaxRowsEnabled: false, queryResultMaxRows: 250000 }).queryResultMaxRowsEnabled, false); + assert.equal(normalizeEditorSettings({ queryResultMaxRows: 250000 }).queryResultMaxRows, 250000); + assert.equal(normalizeEditorSettings({ queryResultMaxRows: 2147483648 }).queryResultMaxRows, 2147483647); +}); + test("numericColumnRightAlign defaults to true and round-trips through normalizeEditorSettings", () => { assert.equal(DEFAULT_EDITOR_SETTINGS.numericColumnRightAlign, true); assert.equal(normalizeEditorSettings({}).numericColumnRightAlign, true); diff --git a/packages/app-tests/useDataGridActions.test.ts b/packages/app-tests/useDataGridActions.test.ts index fbdf40d63b..f3fb702686 100644 --- a/packages/app-tests/useDataGridActions.test.ts +++ b/packages/app-tests/useDataGridActions.test.ts @@ -198,7 +198,7 @@ test("infinite pagination fetches and appends only the next table segment", asyn const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const settingsStore = useSettingsStore(); - settingsStore.updateEditorSettings({ infiniteScroll: true, infiniteScrollMaxRows: 5000 }); + settingsStore.updateEditorSettings({ infiniteScroll: true, queryResultMaxRowsEnabled: true, queryResultMaxRows: 5000 }); connectionStore.addEphemeralConnection(conn("mysql-1")); const tabId = queryStore.createTab("mysql-1", "app", "orders", "data"); queryStore.setTableMeta(tabId, { tableName: "orders", tableType: "TABLE", columns: [], primaryKeys: [] });