fix: data-modifying WITH statements misrouted across SQLKit (classifier, adapters, JDBC bridge) - #147
Open
Blankll wants to merge 4 commits into
Open
fix: data-modifying WITH statements misrouted across SQLKit (classifier, adapters, JDBC bridge)#147Blankll wants to merge 4 commits into
Blankll wants to merge 4 commits into
Conversation
sqlparser 0.55 represents `WITH ... INSERT/UPDATE` as a Statement::Query whose body and CTEs carry the DML, so the top-level AST classifier let write statements pass sqlkit__execute_query's read-only gate and rejected them from sqlkit__execute_write. classify_query now walks the query body and every CTE (combining kinds by severity) so data-modifying statements are never classified as Read. The Postgres adapter also treated any WITH-prefixed statement as a row-returning query: a `WITH ... INSERT/UPDATE/DELETE` without RETURNING exposes zero columns, so the writes ran but the result was silently empty. Column-less prepared statements are now routed through execute() and report rows_affected. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…n MySQL/SQLite MySQL and SQLite only allow WITH before read-only SELECT, yet both adapters classified statements by prefix without WITH, so `WITH ... SELECT` fell into the write branch: rusqlite's execute() rejects row-returning statements with ExecuteReturnedResults, making CTE reads on SQLite hard-error. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This was referenced Sep 3, 2026
…ord sniffing QueryExecutor chose executeQuery vs executeUpdate by uppercasing the SQL and checking a prefix list. Any statement starting with WITH was treated as a row-returning query, so data-modifying WITH statements (or plain DML with a leading CTE) hit executeQuery() — drivers that reject statements without a result set (SQL Server, Oracle) threw, and others returned rows_affected=0. Delegate to Statement.execute() and drain results with getResultSet()/ getUpdateCount()/getMoreResults(): row-returning statements (SELECT, WITH ... SELECT, INSERT ... RETURNING) yield columns+rows; everything else yields rows_affected. The heuristic is removed entirely. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
0.62 parses statement shapes 0.55 could not: `WITH ... DELETE`, `WITH ... MERGE` and DELETE/MERGE inside CTE bodies are now valid ASTs (Query body carries SetExpr::Delete/SetExpr::Merge), so the MCP classifier no longer fails closed on them. The recursive classifier gains the Delete/Merge arms (both the statement body and nested CTE positions) and routes them to execute_delete / execute_write respectively. Migration for breaking AST changes: - sql_write.rs: the 0.55 SET-family variants (SetVariable, SetNames, SetNamesDefault, SetRole, SetSessionParam, SetTimeZone, SetTransaction) are consolidated into Statement::Set - sql_service.rs: SelectItem gained ExprWithAliases (Spark) - projected names now use its first alias, falling back to the expression name Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
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.
Problem
WITH … INSERT/UPDATE/DELETE/MERGE(data-modifying CTEs) was misrouted across SQLKit in four places:MCP read-only guard misclassified writes as reads. sqlparser parsed
WITH … INSERT/UPDATEasStatement::Querywith the DML wrapped inside the query body (SetExpr::Insert/Update) and the CTE definitions. The top-level AST classifier only looked at the outer variant, so:sqlkit__execute_querysilently accepted and ran these write statements — the "read-only" promise didn't holdsqlkit__execute_writerejected them with a confusing "does not accept Read statements" errorPostgres adapter swallowed the result. Any SQL starting with
WITHwas treated as a row-returning query. AWITH … INSERTwithoutRETURNINGexposes zero columns, so the writes committed but the UI showed a silently empty grid — norows_affected, no feedback.MySQL/SQLite adapters lacked
WITHin their read classification. Both engines only allowWITHbefore read-onlySELECT, yet such statements fell into the write branch — rusqlite'sexecute()rejects row-returning statements (ExecuteReturnedResults), so CTE reads on SQLite hard-errored.JDBC bridge keyword sniffing.
QueryExecutorchoseexecuteQuery()vsexecuteUpdate()by SQL prefix, so data-modifyingWITHstatements hitexecuteQuery()— throwing on drivers that reject result-less statements (SQL Server, Oracle).sqlparser 0.55 couldn't parse
WITH … DELETE/MERGEat all (or DELETE/MERGE inside CTEs), leavingsqlkit__execute_deleteunusable for them.Fix
capabilities/sql_write.rs— recursive classifier:classify_querywalks the query body and every CTE;combine_kindmerges kinds by severity (Read < Write < Ddl < Delete) so a destructive statement never rides inside a tree classified asRead.database/postgres.rs— prepared statements exposing zero result columns (data-modifying WITH chain withoutRETURNING) run throughclient.execute()and returnrows_affected;WITH … INSERT … RETURNINGstill returns its rows.database/mysql.rs,database/sqlite.rs—WITHadded to the read-keyword list.jdbc-bridge/QueryExecutor.java— prefix sniffing removed;Statement.execute()+getResultSet()/getUpdateCount()/getMoreResults()drain decides routing from the driver's actual protocol response.sqlparser0.55 → 0.62 —WITH … DELETE/MERGE(body and CTE positions) now parse asSetExpr::Delete/Merge, and the classifier routes them to Delete / Write. Migrated breaking AST changes: 0.55 SET-familyStatementvariants consolidated intoStatement::Set;SelectItem::ExprWithAliases(Spark) handled in column extraction (sql_service.rs).Tests
sql_write.rs(16 classifier tests total; full lib suite 367/367):WITH … INSERTchain (the exact org/membership query from the bug report), DML-in-CTE,WITH … UPDATE,WITH … DELETE(top-level + DELETE-in-CTE),WITH … MERGE, read-guard rejection, existingWITH … SELECTread casescargo fmt --checkclean on changed files; no new clippy warningsmvn compileclean for the JDBC bridge (local check against JDK 21; pom targets 25 for release builds)Commits
22f3e34fix(mcp): detect data-modifying WITH statements in read-only guardfb26a78fix(queries): route WITH-prefixed statements through the query path on MySQL/SQLite6a8659efix(jdbc): execute statements via Statement.execute() instead of keyword sniffing2146ecfchore(deps): upgrade sqlparser 0.55 to 0.62Notes
simple_query+ result inspection) were investigated and need no change.