Fix #480: render Oracle LIMIT as OFFSET ... ROWS FETCH FIRST ... ROWS ONLY - #481
Conversation
…. ROWS ONLY The generator never read GeneratorConfig::limit_fetch_style, and Oracle's LIMIT -> FETCH FIRST rewrite lived only in the transpile normalization pass for the top-level SELECT. ASTs built with the builder, generated without transpile, or containing LIMIT in subqueries, CTEs, INSERT ... SELECT or set operations were emitted with LIMIT, which Oracle rejects (ORA-03049). - Oracle now defaults to LimitFetchStyle::FetchFirst. - The generator honours FetchFirst for SELECT, UNION/INTERSECT/EXCEPT, subquery modifiers and standalone Limit nodes, moving OFFSET first and adding its mandatory ROWS keyword. - LIMIT ALL / LIMIT NULL are dropped instead of becoming FETCH FIRST ALL. - T-SQL TOP n [PERCENT] [WITH TIES] maps to the equivalent FETCH FIRST. - T-SQL's limit_fetch_style is now Top, matching its existing TOP / OFFSET-FETCH output (it has dedicated handling and was never FetchFirst in practice). - The top-level-only normalization rewrite is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Recommendation: request changes before merging. Moving Oracle row-limit rendering into the shared generator is a good architectural direction. It addresses the builder/direct-generator problem and extends support to nested queries. A few remaining cases affect correctness and regression coverage.
Additional observations for scope tracking:
Validation completed: all ten new tests, all 208 dialect-matrix tests, and Rust formatting passed locally. Additional probes reproduced the cases above against both the PR and its base. No Oracle server was used. The implementation adds no obvious expensive traversal or duplicate generation pass; performance assessment here is based on code inspection, not optimized benchmarks. The local SQLGlot implementation shares some of these correctness gaps, so matching its output alone would not resolve them. Addressing the three requested changes above would make the fix substantially safer to merge. |
…edence, CI tests - Parenthesize set-operation operands whose row limit renders as a trailing clause (e.g. T-SQL TOP -> LIMIT / FETCH FIRST), so the limit stays on its branch instead of binding to the whole UNION / INTERSECT / EXCEPT. SQLite gets SELECT * FROM (...) since it rejects parenthesized operands; ClickHouse already binds branch LIMITs locally. - limit_fetch_style = FetchFirst now takes precedence over dialect-specific LIMIT/OFFSET rendering (Presto/Trino OFFSET n LIMIT m), except for T-SQL/Fabric, which keep their TOP / OFFSET ... FETCH NEXT output instead of emitting a duplicate FETCH clause. - Standalone Limit(ALL) / Limit(NULL) nodes emit nothing in FetchFirst style. - Move the regression tests into generator unit tests and tests/dialect_matrix.rs, which CI runs; drop oracle_limit_regression.rs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The update resolves the configuration-precedence issue and moves the regression tests into CI-selected suites. Standalone One variant of the earlier branch-limit finding remains: For example, this T-SQL input: SELECT TOP 5 a FROM t
/* branch note */
UNION ALL SELECT a FROM ugenerates the following for DuckDB and SQLite: SELECT a FROM t LIMIT 5 /* branch note */
UNION ALL SELECT a FROM uI reproduced execution failures in both databases: DuckDB reports a syntax error at The same omission can change results through direct AST generation. When an annotation wraps the limited right-hand operand, the generated SQL becomes: SELECT a FROM t
UNION ALL
SELECT a FROM u LIMIT 5 /* branch note */With ten rows in each table, DuckDB and SQLite return 5 rows instead of the intended 15. Suggested adjustment: inspect through annotation wrappers when deciding whether an operand requires grouping, while preserving its comments during rendering. Add commented SQL cases to the existing Validation on
The local SQLGlot implementation also produces invalid SQL for the commented example, so its output does not provide a correct reference for this case. No Oracle server was used. Code inspection found no additional whole-tree traversal or duplicate generation pass; comparative performance benchmarks were not run. |
generate_set_operand only matched bare Expression::Select, so an operand wrapped in Expression::Annotated (e.g. a comment before UNION ALL) skipped the branch-limit grouping and emitted `... LIMIT 5 /* c */ UNION ALL ...`, which DuckDB/SQLite reject, or let the limit bind to the whole set operation. Recurse through Annotated and render its comments after the parentheses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks. Fixed the
🤖 Generated with Claude Code |
|
Merged, thanks! Would it be possible to instruct your agent not to add himself as a co-committer? This is possible with either global or local instructions via AGENT.md or CLAUDE.md https://code.claude.com/docs/en/settings-reference#git-and-attribution |
let me look into that - would it make sense to enforce this from this project harness? I guess this is relevant for any future contributions of a diverse set of people/agents? |
Fixes #480.
Problem
GeneratorConfig::limit_fetch_stylewas never read by the generator, and Oracle'sLIMIT→FETCH FIRSTconversion existed only in the transpile normalization pass, and only for the top-levelSELECT. As a result Oracle output keptLIMIT(rejected withORA-03049) for:Generator(the repro in the issue)IN (SELECT ...),INSERT ... SELECTUNION/INTERSECT/EXCEPTTOP nsourcesLIMIT ALL/LIMIT NULLwere also turned into the invalidFETCH FIRST ALL ROWS ONLY.Fix
Oracle's generator config now sets
limit_fetch_style: LimitFetchStyle::FetchFirst.The generator honors
FetchFirsteverywhere a limit is rendered: SELECT, set operations, subquery modifiers, and standaloneLimitnodes. It emits[OFFSET n ROWS] FETCH FIRST m [PERCENT] ROWS ONLY, puts OFFSET first, and adds its mandatoryROWSkeyword.No-op limits (
ALL/NULL) are dropped.T-SQL
TOP n [PERCENT] [WITH TIES]maps to the equivalentFETCH FIRST ... ROWS {ONLY | WITH TIES}.The top-level-only rewrite in
normalization/statements.rsis removed, so the generator is the single source of truth.T-SQL's
limit_fetch_stylechanges fromFetchFirsttoTop, which describes what it already does. T-SQL has dedicated TOP / OFFSET-FETCH handling, so its output is unchanged.Set-operation branches keep their own limits. A bare
SELECToperand whose row limit renders as a trailing clause (e.g. T-SQLSELECT TOP 5 ...becomingLIMIT 5/FETCH FIRST 5 ROWS ONLY) is parenthesized, so the limit no longer applies to the wholeUNION/INTERSECT/EXCEPT. This also fixes the same bug for LIMIT targets such as DuckDB, Postgres and Trino. SQLite rejects parenthesized operands, so it getsSELECT * FROM (...). ClickHouse already binds branch LIMITs locally and is left unchanged. Operands wrapped in comments (Expression::Annotated, e.g. a comment beforeUNION ALL) are grouped the same way, with the comment written after the closing parenthesis.Config precedence. Setting
limit_fetch_style: FetchFirstnow overrides dialect-specific LIMIT/OFFSET rendering (for example, Presto/TrinoOFFSET n LIMIT m). T-SQL/Fabric are the exception: they keep their own TOP /OFFSET ... FETCH NEXToutput, since FETCH requires ORDER BY there. This removes the duplicatedFETCH NEXT ... FETCH FIRST ...output.Standalone
Limit(ALL)/Limit(NULL)nodes emit nothing in FetchFirst style.Because the setting now does something, a custom
GeneratorConfig { limit_fetch_style: FetchFirst, .. }also works for any target except T-SQL/Fabric.Before / after (Oracle)
builder::from("t").select_cols(["a"]).limit(5)SELECT a FROM t LIMIT 5SELECT a FROM t FETCH FIRST 5 ROWS ONLYSELECT * FROM (SELECT a FROM t LIMIT 5) AS s... (SELECT a FROM t LIMIT 5) s... (SELECT a FROM t FETCH FIRST 5 ROWS ONLY) sSELECT a FROM t UNION ALL SELECT b FROM u ORDER BY 1 LIMIT 5 OFFSET 2... LIMIT 5 OFFSET 2... ORDER BY 1 OFFSET 2 ROWS FETCH FIRST 5 ROWS ONLYSELECT a FROM t LIMIT ALLSELECT a FROM t FETCH FIRST ALL ROWS ONLYSELECT a FROM tSELECT TOP 10 PERCENT a FROM tSELECT TOP 10 PERCENT a FROM tSELECT a FROM t FETCH FIRST 10 PERCENT ROWS ONLYSELECT a FROM t UNION ALL SELECT TOP 5 a FROM u... UNION ALL SELECT a FROM u LIMIT 5... UNION ALL (SELECT a FROM u FETCH FIRST 5 ROWS ONLY)Tests
src/generator.rs(test_fetch_first_style_*). They cover the builder, pretty output, no-op limits, and precedence for T-SQL, Fabric, Presto, Trino and Postgres.tests/dialect_matrix.rs(oracle_row_limit_regressions), which CI already runs. They cover nested queries; set operations with outer ORDER BY / LIMIT / OFFSET; branch-local TOP on either side of UNION, INTERSECT and EXCEPT for Oracle, DuckDB, T-SQL identity and SQLite; comment-annotated operands (block and line comments) for DuckDB, SQLite and Oracle, plus a generator unit test with annotated AST operands on either side; TOP → FETCH; Oracle FETCH identity; and unchanged output for LIMIT dialects. The separateoracle_limit_regression.rshas been removed.deep_nesting_regression,dialect_matrix, allsqlglot_*fixture suites (identity, dialect identity, transpilation, transpile, parser, pretty),custom_dialect_tests,custom_clickhouse_parser,custom_clickhouse_coverage.cargo fmt --all -- --checkis clean.Not in scope, and unchanged from before this PR:
PERCENTon set-operation limits is still dropped (Union.limitis a bare expression). A FETCH written after a compound query's ORDER BY is still attached to the last branch by the parser.🤖 Generated with Claude Code