Skip to content

feat(connectors)!: return per-statement result sets, wire up frontend tabs - #389

Merged
tianzhou merged 5 commits into
mainfrom
fix/sqlserver-multi-statement-results
Jul 31, 2026
Merged

feat(connectors)!: return per-statement result sets, wire up frontend tabs#389
tianzhou merged 5 commits into
mainfrom
fix/sqlserver-multi-statement-results

Conversation

@tianzhou

@tianzhou tianzhou commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Started as the SQL Server fix for #380 (dropped result sets in a multi-statement batch), but landed on a broader redesign: instead of flattening every statement's rows into one array, SQLResult now returns a list of per-statement results, and the local web frontend gets a real multi-tab UI for it.

Why the redesign, not just a SQL Server fix: making SQL Server flatten multi-statement rows the same way MySQL/MariaDB already did was itself the wrong shape. SELECT 1 AS a; SELECT 2 AS b returning {rows: [{a:1},{b:2}], rowCount: 2} is indistinguishable from a single query that returned two differently-shaped rows glued together - a caller has no way to tell where one statement's output ends and the next begins.

Backend (src/connectors/interface.ts):

  • SQLResult is now { resultSets: SQLResultSet[], messages? }, one entry per statement in execution order. Each SQLResultSet also carries an optional sql - the statement that produced it, when a connector can attribute it unambiguously.
  • postgres/sqlite/mysql/mariadb populate sql for every statement (they process statements in a way that preserves reliable order/count alignment). SQL Server only populates it for an unambiguous single-statement batch: node-mssql's recordsets/rowsAffected arrays aren't index-aligned per statement (documented in buildResultSets), so a batch mixing writes with selects gets one result set per SELECT plus a trailing result set summarizing the write-only statements - not fully per-statement, but no read statement's rows are ever merged with another's, which is the actual SQL Server returns only the first result set of a multi-statement query, while MySQL/MariaDB return all #380 bug.
  • execute_sql/custom-tool JSON responses now key their statement list as statements: [{sql, rows, count}, ...] instead of flat rows/count. explain_sql keeps its flat shape since it only ever runs exactly one statement.

Frontend (frontend/): the shipped local web UI reads execute_sql's response directly and broke under this change, so it's rebuilt around the same idea: executeTool() now returns one QueryResult per statement instead of a single merged result, and ToolDetailView creates one result tab per statement (labeled (i/N) for batches of more than one) instead of forcing a whole batch into a single tab. Verified end-to-end in a browser against the --demo SQLite backend.

Also fixed, as a byproduct of that browser verification: frontend/src/api/tools.ts called response.json() directly, but the backend's stateless HTTP transport answers SSE-framed responses for this request shape - every query through the web UI over HTTP transport was silently broken before this PR too, unrelated to the resultSets change itself.

BREAKING CHANGE: execute_sql/custom tool JSON responses no longer have top-level rows/count - they're under statements, each entry additionally carrying sql.

Test plan

  • New integration tests per connector for multi-statement batches
  • pnpm test - full suite, 1272/1272 passing
  • pnpm exec tsc --noEmit (backend) and tsc -b (frontend) - no new errors
  • Manual verification in a browser: multi-statement batch → two correctly-labeled tabs, each with its own SQL/rows; single-statement query → one untabbed tab, unchanged

Closes #380

🤖 Generated with Claude Code

executeSQL and executeReadOnly read result.recordset (singular), which
node-mssql defines as only the first statement's result set. A batch
like "SELECT 1 AS a; SELECT 2 AS b" silently dropped the second SELECT,
unlike MySQL/MariaDB which already concatenate all statements' rows.

Read result.recordsets (plural) instead, flattening every SELECT's
rows in order, and sum rowsAffected across all statements so rowCount
stays consistent with rows rather than reporting only the first
statement's count.

Closes #380
Copilot AI review requested due to automatic review settings July 31, 2026 08:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes SQL Server connector behavior for multi-statement batches so that all result sets are returned (matching MySQL/MariaDB behavior) and rowCount stays consistent with the aggregated results.

Changes:

  • Switch SQL Server execution paths from result.recordset (first result set only) to result.recordsets (all result sets), flattening rows in emission order.
  • Update rowCount calculation to sum rowsAffected across all statements in the batch (including mixed write/read batches).
  • Add SQL Server integration tests covering multi-statement SELECT, mixed write/read batches, and readonly mode behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/connectors/sqlserver/index.ts Reads and flattens all SQL Server recordsets and sums rowsAffected across batch statements for consistent rows/rowCount.
src/connectors/tests/sqlserver.integration.test.ts Adds integration coverage for multi-result-set batches and summed rowCount, including readonly mode.

Comment thread src/connectors/sqlserver/index.ts Outdated
Comment thread src/connectors/sqlserver/index.ts Outdated
…pers

Addresses Copilot review feedback on #389: || masked a genuine 0
identically to a missing value, which was accidental rather than
intentional. Deliberately not adding Array.isArray guards — mssql's
IResult<T> type guarantees recordsets: Array<IRecordSet> and
rowsAffected: number[], the same contract extractPlanXml already
trusts without validation.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

…g them

SQLResult.rows/rowCount flattened every statement in a batch into one
array/count, so a caller had no way to tell "SELECT 1 AS a; SELECT 2 AS
b" apart from a single query that happened to return two rows - the
flattening the SQL Server fix (#389) added for MySQL/MariaDB-parity
was itself the wrong shape to standardize on.

SQLResult is now `{ resultSets: SQLResultSet[], messages? }`, one
entry per statement in execution order. execute_sql, explain_sql, and
custom tools already read connector-provided rows through this shape;
their JSON output changes to `resultSets: [{rows, count}, ...]`
instead of flat `rows`/`count` (except explain_sql, which only ever
executes one statement and keeps returning the flat shape internally).

Per connector:
- postgres/sqlite: already looped per-statement for multi-statement
  batches: now push one result set per statement instead of merging.
- mysql/mariadb: multi-statement-result-parser.ts now builds a
  SQLResultSet per statement (parseQueryResultSets) instead of
  concatenating rows across statements.
- sqlserver: recordsets/rowsAffected aren't index-aligned per
  statement in node-mssql's API (see buildResultSets' comment), so a
  batch mixing writes with selects gets one result set per SELECT plus
  a trailing result set summarizing the write-only statements - not
  fully per-statement, but no read statement's rows are ever merged
  with another's.

BREAKING CHANGE: execute_sql/custom tool JSON responses now nest rows
under `resultSets` instead of returning them at the top level.
@tianzhou tianzhou changed the title fix(sqlserver): return every result set from multi-statement batches feat(connectors)!: return per-statement result sets instead of merging them Jul 31, 2026
@tianzhou
tianzhou requested a review from Copilot July 31, 2026 09:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Comment thread src/utils/multi-statement-result-parser.ts
… frontend tabs

The prior commit introduced SQLResult.resultSets but named the tool-facing
JSON key the same, and never attributed a statement's source text to its
result - both of which broke the local web frontend's query editor
(frontend/src/api/tools.ts), a real, shipped consumer of execute_sql that
reads the tool response directly. It was silently going to return empty
results for every query.

- SQLResultSet gains an optional `sql` field: the statement that produced
  it, when a connector can attribute it unambiguously. postgres/sqlite/
  mysql/mariadb populate it for every statement (they process statements
  in a way that preserves reliable order/count alignment); SQL Server only
  populates it for an unambiguous single-statement batch, since recordsets/
  rowsAffected aren't index-aligned per statement there (same limitation
  buildResultSets already documented).
- execute_sql/custom-tool JSON responses now key their statement array as
  `statements` (previously `resultSets`), each entry `{sql, rows, count}`.
- Rebuilt the frontend's tab model around this: executeTool now returns one
  QueryResult per statement instead of a single merged result, and
  ToolDetailView creates one ResultTab per statement (labeled "(i/N)" for
  batches of more than one) instead of forcing a whole batch into one tab.
- Also fixed an unrelated pre-existing bug this surfaced while verifying in
  a browser: frontend/src/api/tools.ts called response.json() directly, but
  the backend's stateless HTTP transport answers SSE-framed responses
  (event:/data: lines) for this request shape - every query through the web
  UI over HTTP transport was silently broken before this fix too.

Verified end-to-end in a browser against the --demo SQLite backend: a
two-statement batch produces two correctly-labeled, independently
scrollable tabs, each showing its own statement's SQL and rows; a
single-statement query still shows one untabbed result as before.
@tianzhou tianzhou changed the title feat(connectors)!: return per-statement result sets instead of merging them feat(connectors)!: return per-statement result sets, wire up frontend tabs Jul 31, 2026
… in buildResultSets

- Extract toStatementsPayload() to tool-handler-helpers.ts, used by both
  execute-sql.ts and custom-tool-handler.ts instead of each inlining the
  same resultSets.map(...) - keeps their output contracts from silently
  diverging if the shape changes later.
- SQLServerConnector.buildResultSets no longer re-parses the source SQL
  internally just to gate the `sql` attribution. executeSQL now computes
  isSingleStatement once and threads it through (directly, or via
  executeReadOnly's new parameter) instead of calling splitSQLStatements
  a second time on every query.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/connectors/tests/sqlserver.integration.test.ts:833

  • This test name is misleading: the connector no longer returns a single summed rowCount for a mixed batch; it returns one result set for the SELECT plus a trailing write-summary result set. Renaming the test will make the asserted behavior easier to understand and prevent future confusion.
    it('should sum rowCount across a mixed write/read multi-statement batch', async () => {

@tianzhou
tianzhou merged commit 233125b into main Jul 31, 2026
3 checks passed
@tianzhou
tianzhou deleted the fix/sqlserver-multi-statement-results branch July 31, 2026 10:41
@tianzhou tianzhou mentioned this pull request Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL Server returns only the first result set of a multi-statement query, while MySQL/MariaDB return all

2 participants