Add truncated flag to execute_sql results when max_rows cuts off rows - #406
Merged
Conversation
When a configured max_rows cap fired, the tool response was indistinguishable from a table that genuinely has exactly max_rows rows (count simply equaled the cap), so LLM consumers could silently reason from incomplete data. Detection is exact rather than heuristic: when the cap is the binding constraint, SQLRowLimiter rewrites the query to fetch max_rows + 1 rows (LIMIT/TOP probe). If the probe row comes back, the connector drops it, clamps the count to max_rows, and marks the result set truncated; the statements payload then carries "truncated": true alongside count. The flag is omitted entirely for complete results, and never fires when the query's own smaller LIMIT/TOP is what bounded the result. Applies to all five connectors (PostgreSQL, MySQL, MariaDB, SQLite, SQL Server), including per-statement flags in multi-statement batches. SQL Server now also echoes the original statement text instead of the TOP-rewritten one, matching the other connectors. Closes #404 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SxUnGXE7MRMyezpunumuFH
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses silent max_rows truncation in execute_sql responses by adding an explicit truncated: true signal when the configured cap actually cuts off rows, enabling tool consumers to distinguish capped results from complete results that happen to have exactly max_rows rows.
Changes:
- Add truncation-probe rewrites (
max_rows + 1) and post-processing (flagTruncation) to setSQLResultSet.truncatedonly when truncation is provably occurring. - Propagate the optional
truncatedfield through connector result sets and into the tool response payload (statements[].truncated), while omitting the key for complete results. - Update docs, tool metadata text, and tests (unit, handler, and connector integration) to cover capped vs non-capped scenarios.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/tool-metadata.ts | Updates execute tool description to document the truncated: true signal when max_rows caps results. |
| src/utils/tool-handler-helpers.ts | Extends statements payload shaping to conditionally emit truncated: true only when present on a result set. |
| src/utils/sql-row-limiter.ts | Introduces probe-based max-rows rewrites and a flagTruncation helper to clamp/drop probe rows and mark truncation. |
| src/utils/tests/sql-row-limiter.test.ts | Adds unit coverage for probe rewrites and flagTruncation behavior. |
| src/tools/tests/execute-sql.test.ts | Adds handler-level tests verifying truncated is surfaced when set and omitted otherwise. |
| src/connectors/sqlserver/index.ts | Wires SQL Server execution through probe rewrite + flagTruncation, and ensures sql echoes the original statement (not the rewritten TOP form) when attributable. |
| src/connectors/sqlite/index.ts | Wires SQLite single- and multi-statement read execution through probe rewrite + flagTruncation. |
| src/connectors/postgres/index.ts | Wires Postgres single- and multi-statement execution through probe rewrite + per-statement flagTruncation. |
| src/connectors/mysql/index.ts | Wires MySQL multi-statement execution through per-statement probe rewrites and applies flagTruncation when result-set alignment is exact. |
| src/connectors/mariadb/index.ts | Wires MariaDB multi-statement execution through per-statement probe rewrites and applies flagTruncation when result-set alignment is exact. |
| src/connectors/interface.ts | Extends SQLResultSet with optional truncated?: boolean and documents its exact/probe-based semantics. |
| src/connectors/tests/sqlserver.integration.test.ts | Adds integration assertions for SQL Server truncation flag behavior under cap vs user TOP. |
| src/connectors/tests/sqlite.integration.test.ts | Adds integration coverage for capped vs exact-vs-fewer rows and multi-statement truncation signaling in SQLite. |
| src/connectors/tests/postgres.integration.test.ts | Adds integration assertions for Postgres truncation flag behavior under cap vs user LIMIT. |
| src/connectors/tests/mysql.integration.test.ts | Adds integration assertions for MySQL truncation flag behavior under cap vs user LIMIT. |
| src/connectors/tests/mariadb.integration.test.ts | Adds integration assertions for MariaDB truncation flag behavior under cap vs user LIMIT. |
| docs/tools/execute-sql.mdx | Documents the truncated: true field and how to interpret it (including using COUNT(*) for true totals). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #404 — the
max_rowscap fired silently: a capped result reportedcountequal tomax_rows, which is indistinguishable from a table that genuinely has exactly that many rows, so LLM consumers could silently reason from incomplete data.Now, when the cap actually cuts off rows, the statement's entry in the response carries
"truncated": truealongsidecount. The flag is omitted entirely for complete results (no token cost on the common path), matching howsearch_objectsalready signals truncation.How it works
Detection is exact rather than a
count === max_rowsheuristic. When the cap is the binding constraint,SQLRowLimiterrewrites the query to fetch one probe row past the cap (LIMIT/TOP max_rows + 1). If the probe row comes back, the connector drops it, clampsrowCounttomax_rows, and marks the result set truncated (SQLRowLimiter.flagTruncation). The flag never fires when the query's own smallerLIMIT/TOPis what bounded the result — that's the user's limit, not the cap.Changes
src/utils/sql-row-limiter.ts: newapplyMaxRowsWithTruncationProbe/applyMaxRowsForSQLServerWithTruncationProbe(built on the existing rewrite logic) andflagTruncationpost-processing helpersrc/connectors/interface.ts: optionaltruncatedfield onSQLResultSetexecuteSQL, including per-statement flags in multi-statement batchessrc/utils/tool-handler-helpers.ts:toStatementsPayloademitstruncated: truewhen set (shared byexecute_sqland custom tools)src/utils/tool-metadata.ts: the tool description's row-limit note now tells consumers capped results carry"truncated": truesqlinstead of the TOP-rewritten one, matching the other connectors (and not leaking the probe value)docs/tools/execute-sql.mdxrow-limiting section documents the flagTest plan
flagTruncation(sql-row-limiter.test.ts, 59 tests passing)truncated: truewhen set, omits the key for complete results (execute-sql.test.ts)max_rowsvs fewer rows vs user's own lowerLIMITvs per-statement in multi-statement batches (49 tests passing locally)truncatedassertions added to the existingmaxRowsintegration tests for PostgreSQL, MySQL, MariaDB, SQL Server (Docker-based; will run in CI)pnpm run build:backendclean🤖 Generated with Claude Code
https://claude.ai/code/session_01SxUnGXE7MRMyezpunumuFH
Generated by Claude Code