diff --git a/e2e/support/start-mock-trino.ts b/e2e/support/start-mock-trino.ts index 2610438e..e11b22b7 100644 --- a/e2e/support/start-mock-trino.ts +++ b/e2e/support/start-mock-trino.ts @@ -109,6 +109,8 @@ const DEFAULT_RESPONSE = { // // FROM paged_table QUEUED, then 3 pages of 10 rows via GET // FROM finished_paged_table first response already FINISHED, 1 more page via GET +// FROM huge_table one page of MAX_CLIENT_ROWS + 1 rows (row limit exceeded) +// FROM exact_limit_table exactly MAX_CLIENT_ROWS rows, then an empty trailing page // FAILS_SILENTLY first response FAILED without an error object // FAILS_LATER one page of rows, then FAILED without an error object // HOLD: RUNNING until released via POST /__mock/release/ @@ -188,6 +190,52 @@ function startPagedQuery(id: string, sql: string): Page | null { }); return { id, nextUri: nextUri(id, 1), stats: { state: 'QUEUED' } }; } + if (sql.includes('FROM huge_table')) { + // First page is empty; the follow-up page exceeds MAX_CLIENT_ROWS. + queries.set(id, { + sql, + pages: [ + { + id, + columns: [{ name: 'id', type: 'integer' }], + data: Array.from({ length: 10_001 }, (_, i) => [i + 1]), + nextUri: nextUri(id, 2), + stats: { state: 'RUNNING' } + } + ] + }); + return { + id, + columns: [{ name: 'id', type: 'integer' }], + data: [], + nextUri: nextUri(id, 1), + stats: { state: 'RUNNING' } + }; + } + if (sql.includes('FROM exact_limit_table')) { + // Like Trino for `LIMIT 10000`: the page completing the result still carries a nextUri, + // which only leads to an empty FINISHED page. + queries.set(id, { + sql, + pages: [ + { + id, + columns: [{ name: 'id', type: 'integer' }], + data: Array.from({ length: 10_000 }, (_, i) => [i + 1]), + nextUri: nextUri(id, 2), + stats: { state: 'FINISHED' } + }, + { id, stats: { state: 'FINISHED' } } + ] + }); + return { + id, + columns: [{ name: 'id', type: 'integer' }], + data: [], + nextUri: nextUri(id, 1), + stats: { state: 'RUNNING' } + }; + } if (sql.includes('FROM finished_paged_table')) { queries.set(id, { sql, pages: [{ id, data: rows(11, 10), stats: { state: 'FINISHED' } }] }); return { diff --git a/e2e/trino/trino.spec.ts b/e2e/trino/trino.spec.ts index b305cef4..2349e393 100644 --- a/e2e/trino/trino.spec.ts +++ b/e2e/trino/trino.spec.ts @@ -36,6 +36,65 @@ test.describe('Trino query editor', () => { await expect(table.getByRole('cell', { name: '1' })).toBeVisible(); await expect(table.getByRole('cell', { name: 'Alice' })).toBeVisible(); await expect(page.getByText('Rows 1–3 of 3')).toBeVisible(); + await expect(page.getByText(/^3 rows returned \|/)).toBeVisible(); + }); + + test('hitting the row limit is reported in the status bar', async ({ page }) => { + await setTabSql(page, 'SELECT * FROM huge_table'); + await page.goto('/trino'); + await waitForHydration(page); + await page.locator('.monaco-editor').first().click(); + + await page.getByRole('button', { name: 'Run', exact: true }).click(); + await waitForQueryComplete(page); + + const status = page.locator('[data-query-state="FINISHED"]'); + await expect(status.getByText('Row limit reached', { exact: true })).toBeVisible(); + await expect(status.getByText(/^10,001 rows returned \|/)).toBeVisible(); + await expect(status).not.toContainText('rows processed'); + await expect(page.getByRole('table', { name: 'Query results' })).toBeVisible(); + // A single statement's row limit is only reported in the status bar. + await expect(page.getByText(/remaining rows not fetched/)).toHaveCount(0); + }); + + test('a result of exactly the row limit is not reported as truncated', async ({ page }) => { + await setTabSql(page, 'SELECT * FROM exact_limit_table'); + await page.goto('/trino'); + await waitForHydration(page); + await page.locator('.monaco-editor').first().click(); + + await page.getByRole('button', { name: 'Run', exact: true }).click(); + await waitForQueryComplete(page); + + const status = page.locator('[data-query-state="FINISHED"]'); + await expect(status.getByText('Finished', { exact: true })).toBeVisible(); + await expect(status.getByText(/^10,000 rows returned \|/)).toBeVisible(); + await expect(page.getByText('Row limit reached')).toHaveCount(0); + }); + + test('row limit in a multi-statement script is reported on the affected statement', async ({ + page + }) => { + await setTabSql(page, 'SELECT * FROM huge_table; SELECT id, name FROM users'); + await page.goto('/trino'); + await waitForHydration(page); + await page.locator('.monaco-editor').first().click(); + + await page.keyboard.press('Control+Shift+Enter'); + await expect(page.getByRole('table', { name: 'Query results — Statement 2' })).toBeVisible(); + + const panelFor = async (index: number) => { + const toggle = page.getByRole('button', { name: `Toggle statement ${index} results` }); + return page.locator(`[id="${await toggle.getAttribute('aria-controls')}"]`); + }; + const warning = 'Row limit of 10,000 exceeded — remaining rows not fetched.'; + await expect((await panelFor(1)).getByText(warning)).toBeVisible(); + await expect((await panelFor(2)).getByText(warning)).toHaveCount(0); + + // The status bar describes the last statement, which completed normally. + const status = page.locator('[data-query-state="FINISHED"]'); + await expect(status.getByText('Finished', { exact: true })).toBeVisible(); + await expect(status.getByText(/^3 rows returned \|/)).toBeVisible(); }); test('Ctrl+Enter triggers query execution', async ({ page }) => { diff --git a/messages/de.json b/messages/de.json index b7ce2634..01152b28 100644 --- a/messages/de.json +++ b/messages/de.json @@ -87,8 +87,11 @@ "trino_state_finished": "Abgeschlossen", "trino_state_failed": "Fehlgeschlagen", "trino_state_cancelled": "Abgebrochen", - "trino_progress_info": "{rows} Zeilen verarbeitet | {elapsed} s", - "trino_row_limit_reached": "Zeilenlimit von {limit} erreicht — Abfrage abgebrochen.", + "trino_state_row_limit": "Zeilenlimit erreicht", + "trino_progress_info": "{rows} Zeilen abgerufen | {elapsed}", + "trino_elapsed_ms": "{value} ms", + "trino_elapsed_s": "{value} s", + "trino_row_limit_reached": "Zeilenlimit von {limit} überschritten — weitere Zeilen wurden nicht abgerufen.", "trino_catalog_browser": "Katalogbrowser", "trino_catalog_browser_toggle": "Katalogbrowser ein-/ausblenden", "trino_catalog_label": "Katalog", diff --git a/messages/en.json b/messages/en.json index d64250a1..02ae08b6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -87,8 +87,11 @@ "trino_state_finished": "Finished", "trino_state_failed": "Failed", "trino_state_cancelled": "Cancelled", - "trino_progress_info": "{rows} rows processed | {elapsed}s", - "trino_row_limit_reached": "Row limit of {limit} reached — query cancelled.", + "trino_state_row_limit": "Row limit reached", + "trino_progress_info": "{rows} rows returned | {elapsed}", + "trino_elapsed_ms": "{value} ms", + "trino_elapsed_s": "{value} s", + "trino_row_limit_reached": "Row limit of {limit} exceeded — remaining rows not fetched.", "trino_catalog_browser": "Catalog browser", "trino_catalog_browser_toggle": "Toggle catalog browser", "trino_catalog_label": "Catalog", diff --git a/src/lib/components/trino/StatementResult.svelte b/src/lib/components/trino/StatementResult.svelte index a24b804f..fe76213d 100644 --- a/src/lib/components/trino/StatementResult.svelte +++ b/src/lib/components/trino/StatementResult.svelte @@ -1,10 +1,11 @@