fix(plugin-mysql): report the real error when a query fails while reading rows (#1884) - #1891
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4e7e52580
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| do { | ||
| let fetchResult = try await queryExecutor.executeQuery( | ||
| sql: plan.executedSQL, | ||
| sql: sql, |
There was a problem hiding this comment.
Enforce rowCap before executing default plugin queries
When a capped SELECT reaches a plugin that relies on PluginDatabaseDriver’s default executeUserQuery (for example PostgreSQL/ClickHouse/DuckDB-style .limit plugins), this now sends the original SQL and only passes rowCap; the default implementation calls execute(query:) first and trims raw.rows after the full result has already been materialized. Before this change, plan.executedSQL contained the injected LIMIT cap+1 for uncapped .limit dialect queries, so the row-cap setting bounded server/client work. Large uncapped result sets can now still be fully fetched and held in memory despite the row cap, so the affected drivers or default path need to stream/stop at rowCap before dropping the server-side limit.
Useful? React with 👍 / 👎.
Fixes #1884.
A MySQL
SELECTending inORDER BYcame back as an empty grid: no rows, no error, ~0.1s. The same query without theORDER BYreturned rows, and the reporter's other MySQL clients ran it fine.Root cause
Two layers, both ours.
1. The silent failure (this is the bug).
mysql_fetch_row()returnsNULLfor two different things: a clean end of rows, and an error part way through reading. The C API requires checkingmysql_errno()to tell them apart.executeQuerySynconly checked errno inside itsif truncatedbranch, so when the loop ended naturally it returned whatever rows it had,[]if the failure hit before row one, as a successful query. Column metadata arrives before row data, so the result still had real column headers. That combination is exactly the report: correct-looking grid, zero rows, no error. The prepared-statement path had the same defect, treatingMYSQL_NO_DATA(real end of rows) and a genuine error the same way.ORDER BYexposes it because a filesort makes the server read, sort, and materialize the whole result before it sends row one, so the client's first fetch blocks through that entire window. An unsorted scan streams immediately and has almost no equivalent window. Parse-time errors were already caught bymysql_real_query's return code, which is why only this class slipped through silently.2. The trigger. Since 0.55.0 the row cap was applied by appending
LIMIT cap+1to the SQL. That is not neutral: adding aLIMITto anORDER BYquery moves MySQL 8.0 onto an ordered index scan (prefer_ordering_index) instead of a filesort. It is the one thing that differed between TablePro and the clients that worked, since they send the query as written.What changed
mysql_errno()on every exit path and throws the error the server reported, so a real failure shows up as an error instead of an empty table.SQLLimitInjectorand theinjectRowLimitdriver hook are gone.KILL QUERYthrough the existing cancel path instead of draining every remaining row over the wire.0) resolved to0rather thannil, so the over-fetch becameLIMIT 1and unlimited silently returned a single row.Note for review
The injector did two jobs, and only one was wrong. It also detected an existing top-level
LIMIT/FETCH/TOPand skipped the cap for that run. Deleting it outright would have capped an explicitLIMIT 50000down to 10,000 and regressed #956 ("your own LIMIT wins"). The lexer is kept as a read onlySQLLimitDetector; the string rewriting is what was unsound, not the scanning.ABI
scripts/check-pluginkit-abi.sh mainreports one change:injectRowLimitremoved fromPluginDatabaseDriverand its default extension. It defaulted tonil, which CLAUDE.md lists as additive, so nocurrentPluginKitVersionbump and no plugin re-release. Needs theabi-additivelabel.Testing
swiftlint lint --strict: 0 violations.SQLLimitDetectorTestscovering both reported repros, plus theORDER BYcoverage the old injector tests never had.resolveRowCaptests, including the unlimited-returns-one-row regression.TableProPluginKitbuilds via SwiftPM after the protocol removal.Not covered by the suite: the driver fix needs a live MySQL to exercise, since it is C-bridge code. Worth confirming on a real server that
KILL QUERYon a truncated result surfacesER_QUERY_INTERRUPTED(1317) and not a connection-lost code, otherwise every truncated query would report an error.