[BUG] Multi-query and transactional script execution surfaces stale results or strips column headers due to backwards search for non-empty row sets (lastResultWithRows).
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 58/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- clickhouse, mysql, postgresql, sqlite, typescript
Research direction
Read src/app/api/db/multi-query/route.ts and src/app/api/db/transaction/route.ts, where the main result is selected, then trace how src/hooks/use-query-execution.ts passes it to src/components/ResultsGrid.tsx. Check existing tests for multi-query and transaction responses; add coverage for a final successful statement with zero rows, preserved fields and row counts, and pagination. Done means the final statement's result is shown without stale rows.
Written by the indexing model from the issue text.
Description
Description
When running multiple statements in the Query Editor ("Run Selection" or "Run Script"), requests are dispatched to POST /api/db/multi-query or POST /api/db/transaction.
To determine the primary result object returned to the frontend's ResultsGrid, both handlers use a reverse array search looking for the last statement whose result set contains rows.length > 0:
In src/app/api/db/multi-query/route.ts#L362-L375:
// Return the last successful result with rows as the main result (for ResultsGrid)
const lastResultWithRows = [...results]
.reverse()
.find((r) => r.status === "success" && r.rows && r.rows.length > 0);
const hasError = results.some((r) => r.status === "error");
return NextResponse.json({
// Main result (for backward compatibility with ResultsGrid)
rows: lastResultWithRows?.rows || [],
fields: lastResultWithRows?.fields || [],
rowCount: lastResultWithRows?.rowCount || 0,
executionTime: totalExecutionTime,
...carriedChannels(lastResultWithRows),
multiStatement: true,
...
And identically in src/app/api/db/transaction/route.ts#L124:
const shown = [...statements].reverse().find((r) => r.status === "success" && r.rows && r.rows.length > 0);
Because find() requires rows.length > 0, any final statement that legitimately returns 0 rows (an empty table or query with no matching rows) is skipped. The handler walks backwards and surfaces the output of a prior statement instead.
This causes three severe correctness and UX bugs:
- Misleading Data & Stale Ghost Rows: If a developer runs a script to clean up or verify records:
Statement 3 succeeds and returnsSELECT count(*) FROM expired_tokens; -- Statement 1: returns 100 rows DELETE FROM expired_tokens; -- Statement 2: deletes records SELECT count(*) FROM expired_tokens; -- Statement 3: returns 0 rows (empty table)rows: [],fields: ['count']. Becauserows.length === 0,lastResultWithRowsdiscards Statement 3 and renders the rows from Statement 1! The user sees the old 100 rows in ResultsGrid, leading them to falsely conclude that theDELETEfailed. - Missing Schema Headers on Empty Result Sets: If a query returns 0 rows (e.g.
SELECT id, name, created_at FROM users WHERE id = -1;), single-query mode (POST /api/db/query) returnsfields: ["id", "name", "created_at"]androws: [], allowingResultsGridto display the column schema. Inmulti-query, becauselastResultWithRowsisundefined,fieldsfalls back to[], completely stripping the table column headers from the UI. - Loss of DML Row Counts & Pagination: For scripts ending in
UPDATEorINSERT, anyrowCountreturned by the final statement is overwritten by an earlier statement's rows or set to 0. Additionally,POST /api/db/multi-queryomits thepaginationobject entirely from the response, permanently disabling "Load More" pagination even when statements are truncated by Studio's default bounds.
Steps to Reproduce
- Open LibreDB Studio Query Editor.
- Connect to any database (PostgreSQL, MySQL, SQLite, DuckDB, etc.).
- Paste and run the following multi-statement script:
SELECT 'Initial Check' AS stage, 10 AS value; SELECT 'Post Check' AS stage, 0 AS value WHERE 1 = 0; - Observe the output in
ResultsGrid. - Statement 2 executed cleanly, but the ResultsGrid displays
stage: 'Initial Check', value: 10. - Now run a script with a single query that returns 0 rows:
SELECT id, username, email FROM (SELECT 1 AS id, 'a' AS username, 'b' AS email) t WHERE 1 = 0; -- comment to trigger multi-query splitter SELECT 1 WHERE 1 = 0; - Notice that column headers (
id,username,email) completely disappear becausefieldsis returned as[].
Expected Behavior
- The main result should reflect the last executed statement in the script (or the last query statement), retaining its
fieldsmetadata even ifrows.length === 0. - If Statement 2 returns an empty result set,
ResultsGridmust display Statement 2's column schema with a "0 rows returned" badge, rather than silently resurrecting Statement 1's stale data. - The UI should ideally expose a tab or dropdown to inspect each individual statement's result from the
statementsarray.
Actual Behavior
lastResultWithRowsskips valid statements that return 0 rows.- Stale data from prior statements is presented as the final query outcome.
- Column headers disappear (
fields: []) when no statement produces rows.
Environment
- LibreDB Studio Version: 0.5.4+ (all versions with multi-statement support)
- Browser: All
- OS: macOS / Linux / Windows
- Database Type: PostgreSQL, MySQL, SQLite, DuckDB, MSSQL, ClickHouse, etc.
Additional Context
- Route handler:
src/app/api/db/multi-query/route.ts#L362-L375 - Transaction route handler:
src/app/api/db/transaction/route.ts#L124 - Frontend consumer:
src/hooks/use-query-execution.ts#L858-L885andsrc/components/ResultsGrid.tsx
Possible Solution
- In
src/app/api/db/multi-query/route.tsandsrc/app/api/db/transaction/route.ts, pick the last successful query statement rather than requiringrows.length > 0:// Pick the last successful statement that produced a query result structure (even with 0 rows) const lastResultWithFields = [...results] .reverse() .find((r) => r.status === "success" && Array.isArray(r.fields) && r.fields.length > 0); const lastSuccessful = [...results].reverse().find((r) => r.status === "success"); const activeResult = lastResultWithFields ?? lastSuccessful ?? results[results.length - 1]; return NextResponse.json({ rows: activeResult?.rows || [], fields: activeResult?.fields || [], rowCount: activeResult?.rowCount ?? activeResult?.rows?.length ?? 0, executionTime: totalExecutionTime, ...carriedChannels(activeResult), multiStatement: true, statementCount: statements.length, executedCount: results.length, hasError, statements: results, }); - Enhance
ResultsGrid/QueryEditorto include a statement switcher bar whenresult.multiStatementis true, enabling users to click between Statement 1, Statement 2, etc., and inspect each statement's output individually.
- Dominant language
- TypeScript
- Stars
- 810
- Forks
- 158
- Avg merge
- 9h 45m
- Merged PRs (30d)
- 521
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from libredb/libredb-studio
-
[BUG] Column profiling fails with SQL syntax errors on columns named after reserved keywords due to unforced identifier quoting in /api/db/profilePossibly taken @saad-works claimed this 2 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
libredb/libredb-studio#2245 · 2 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
[BUG] MCP isLoopbackBind() defaults to false when HOSTNAME is unset or bracketed, disabling Host validation and DNS rebinding protectionPossibly taken @saad-works claimed this 3 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
libredb/libredb-studio#2244 · 1 comment ·
Maintainers usually reply within 1 day
-
[BUG] inspect_schema matches named container case-insensitively without preferring exact matchPossibly taken @Maaz2212 claimed this 7 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
libredb/libredb-studio#2128 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 58/100
libredb/libredb-studio#2270 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 42/100
libredb/libredb-studio#2267 · 3 comments ·
Maintainers usually reply within 1 day
All issues in libredb/libredb-studio
Similar issues
-
Add: Shop On TV SDOpencheck:passed streams:add
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
Maintainers usually reply within 1 day
-
bug implicit-state
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
book-layout.spec fails when the similar-books rail holds other projects' booksPossibly taken @praetorianer777 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
praetorianer777/GoTome#373 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Tencent/tdesign-react#4465 · 1 comment ·
Maintainers usually reply within 1 day
-
area:cards enhancement ready-for-agent
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
fil-donadoni/tolaria#8883 ·
Maintainers usually reply within 1 day