feat: support parameterized ($n) LIMIT/OFFSET in prepared statements#26233
feat: support parameterized ($n) LIMIT/OFFSET in prepared statements#26233vismaytiwari wants to merge 7 commits into
Conversation
Mirror risingwavelabs#19834 (which added expression support to the LIMIT clause) for OFFSET. The parser now parses OFFSET as a general expression instead of a bare number literal, and the binder evaluates it down to a constant non-negative integer via a shared `bind_limit_or_offset_expr` helper used by both LIMIT and OFFSET. Following PostgreSQL, a NULL OFFSET is treated as zero (a NULL LIMIT remains "no limit"). This lets parameterized queries such as `LIMIT $1 OFFSET $2` (as generated by Prisma ORM) parse and run; previously OFFSET $n failed with "sql parser error: expected a value, found: $2". Closes risingwavelabs#23345
LIMIT/OFFSET are folded to a constant u64 during binding, which runs before bind parameters are substituted, so `LIMIT $1`/`OFFSET $2` (e.g. Prisma) error at bind with "non-const expression". Carry the bound expression on BoundQuery (Option<ExprImpl>) so ParamRewriter substitutes $n, and fold to u64 in the planner after substitution. Constants are still validated at bind time (negatives/eval errors) so errors precede side effects like CREATE TABLE AS; only parameter-dependent expressions are deferred. Part of risingwavelabs#23345.
|
I encountered the very same issue. Looking forward to this PR being merged. |
|
Request changes — the direction is correct, but this needs protocol-level test coverage and a clarified scope before merge. What's right: Scope: this supports a constant / parameter-foldable subset, not full PostgreSQL Required before merge:
Recommended (here or as a tracked follow-up): accept a parameter/constant in Should succeed: Should error (assert the message): |
Addresses review on risingwavelabs#26233: - Parse the FETCH quantity as an expression so `FETCH FIRST $n ROWS ONLY` and `FETCH FIRST 1 + 1 ROWS ONLY` work, mirroring LIMIT/OFFSET. - Clarify the planner error when a value is still non-constant after parameter binding. - Add an extended-protocol test that binds `$n` and re-binds the same prepared statement with different values across executions, covering LIMIT/OFFSET, NULL, a parameter expression, FETCH, and a negative value rejected at Bind rather than Parse. - Add batch slt cases for NULL LIMIT/OFFSET, FETCH expressions, and the rejected non-constant forms (column reference, aggregate, window function, volatile function).
|
Thanks for the detailed review @lmatz — all addressed in 0ba98d1, and I built + ran it locally (sqlparser unit tests, the
The reject matrix (column reference, aggregate, window function, SRF, volatile, subquery) is covered in the batch slt. Let me know if you'd like the volatile/subquery divergence called out anywhere beyond the slt comments. |
| SELECT * FROM generate_series(0,3,1) as t(v) order by v OFFSET generate_series(1, 2); | ||
|
|
||
| # A negative offset is rejected. | ||
| statement error |
There was a problem hiding this comment.
can you put the detailed error message in the test, there is some example above in this file, thanks
There was a problem hiding this comment.
Done — added the detailed error messages to all the rejected cases (5e534b4). Used the inline statement error <message> form since the interspersed query blocks were desyncing the ---- result-block parsing, but the assertions are the real messages: e.g. OFFSET must not be negative, but found: -1, Invalid column: v for a column ref, and ... after OFFSET, but found non-const expression for the aggregate/window/volatile/SRF forms. Verified locally against a running cluster.
Review: Request changes1. Crash:
|
…clause correctly Address review on risingwavelabs#26233: - A NULL LIMIT folded to u64::MAX, so a downstream `offset + limit` overflowed and panicked. Fold it to LIMIT_ALL_COUNT (u64::MAX / 2) instead, matching the no-limit path and keeping `offset + limit` in range. - FETCH quantity errors now report `FETCH FIRST` rather than `LIMIT`, by carrying the clause name from the binder through to the planner. - Correct the `expr_has_parameter` comment: the default ExprVisitor does not descend into subqueries. - Add row-count tests (ordered and unordered, simple and extended protocol) for LIMIT NULL + OFFSET, int8 overflow, and the non-constant-after-parameter planner branch.
|
Thanks @lmatz — great catches. Addressed in e13303f + 5bc8512, all verified locally on a running cluster. 1. 2. FETCH clause + comment. The clause name is now carried from the binder through to the planner, so a bad FETCH quantity reports 3. Tests. Added exact row-count assertions (ordered + unordered, simple + extended) for One thing to flag separately, since it turned out not to be specific to this PR: while testing over a real table I hit a pre-existing panic — |
I hereby agree to the terms of the RisingWave Labs, Inc. Contributor License Agreement.
What this does
#23345: Prisma-style
SELECT ... LIMIT $2 OFFSET $3fails on RisingWave. #26109 fixes the parser (OFFSETnow parses as an expression, likeLIMIT), but$nstill errors at bind time withnon-const expression— becauseLIMIT/OFFSETare folded to a constantu64during binding, which runs before bind parameters are substituted (ParamRewriter/BoundStatement::bind_parameterruns later, at theBindstep).This PR defers that fold for parameter-dependent
LIMIT/OFFSET:BoundQuery.limit/offset:Option<u64>→Option<ExprImpl>(carry the bound expression instead of an eagerly-folded constant).CREATE TABLE AS. A non-constant is rejected at bind unless it contains a$nparameter, in which case it is carried through.BoundQuery::rewrite_exprs_recursivenow rewriteslimit/offset, soParamRewritersubstitutes$ninside them.eval_limit_or_offsetin the planner folds the (now-substituted) expression tou64, preserving the existing semantics:NULL→ no-limit (LIMIT) / zero (OFFSET), negative rejection, non-const error.Plan nodes are unchanged (
LogicalLimit/LogicalTopNstill takeu64); constant limits/offsets produce identical plans.Relationship to #26109
This is stacked on #26109 (parser + constant-expression parity). Until #26109 merges, the diff here includes its commits too; I'll rebase to just the delta afterward. Please review #26109 first.
Open questions / why this is a draft
LIMITduring binding. This PR reverses that for the parameter case (defers to plan time, after substitution). Does this direction look right to you before I polish it?LIMIT(e.g.LIMIT (SELECT $1)) are not detected and are rejected at bind — acceptable?CREATE TABLE ASremains a theoretical edge (table created before the insert is planned).LIMIT $1/OFFSET $2still to add.Part of #23345.
Checklist