fix(sql,vector): reject negative LIMIT, bound ANALYZE name, unify oversample - #270
fix(sql,vector): reject negative LIMIT, bound ANALYZE name, unify oversample#270EnRaiha wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Several edge cases remain incorrect (negative UNKNOWN-param/string limits still degrade to unbounded, whitespace-sensitive ANALYZE/COMPACT matching, and InvalidLimitValue is not actually mapped to SQLSTATE 2201W on the pgwire path).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR addresses three planner/parsing correctness issues across nodedb-sql and nodedb-vector: it prevents negative LIMIT/OFFSET values from silently becoming “unbounded”, fixes ANALYZE <name>(...) collection name parsing, and removes oversample constant drift between the vector cost model and the BBQ codec default.
Changes:
- Make LIMIT/OFFSET extraction fallible and introduce
SqlError::InvalidLimitValuefor negative bounds. - Tighten
ANALYZEparsing to be keyword-boundary matched and strip trailing parenthesized payload from the target name. - Use
rerank::codecs::bbq::DEFAULT_OVERSAMPLEas the single oversample default in vector cost estimation (with a regression test).
File summaries
| File | Description |
|---|---|
| nodedb-vector/src/planner/cost.rs | Cost model uses BBQ codec oversample default and adds a regression test to prevent drift. |
| nodedb-sql/src/planner/select/select_stmt.rs | Propagates LIMIT/OFFSET extraction errors during select planning. |
| nodedb-sql/src/planner/select/query_tail.rs | Makes LIMIT/OFFSET extraction fallible and rejects negative literals. |
| nodedb-sql/src/planner/select/limit.rs | Propagates LIMIT/OFFSET extraction errors during LIMIT application. |
| nodedb-sql/src/error.rs | Adds SqlError::InvalidLimitValue variant and documentation. |
| nodedb-sql/src/ddl_ast/parse/maintenance.rs | Fixes ANALYZE/COMPACT collection parsing and adds regression tests for ANALYZE users(id). |
| nodedb-sql/src/coerce.rs | Introduces expr_as_nonnegative_usize and a regression test for negative LIMIT. |
Review details
Suppressed comments (1)
nodedb-sql/src/ddl_ast/parse/maintenance.rs:29
upper.starts_with("COMPACT ")has the same whitespace-sensitivity as the ANALYZE path (only a literal space matches). This can cause validCOMPACTstatements with non-space whitespace to fall through to the SQL planner.
if upper.starts_with("COMPACT ") {
- Files reviewed: 7/7 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| pub fn expr_as_nonnegative_usize( | ||
| expr: &ast::Expr, | ||
| clause: &'static str, | ||
| ) -> Result<Option<usize>, SqlError> { | ||
| if matches!( | ||
| expr, | ||
| ast::Expr::UnaryOp { | ||
| op: ast::UnaryOperator::Minus, | ||
| .. | ||
| } | ||
| ) { | ||
| return Err(SqlError::InvalidLimitValue { | ||
| detail: format!("{clause} must not be negative"), | ||
| }); | ||
| } | ||
| Ok(expr_as_usize_literal(expr)) | ||
| } |
| // name. PostgreSQL has no `ANALYZE ... (cols)` form; any trailing | ||
| // `(...)` after the collection name is stripped so the name stays | ||
| // clean. | ||
| if upper == "ANALYZE" || upper.starts_with("ANALYZE ") { |
| /// A LIMIT or OFFSET clause carried a negative literal, which Postgres | ||
| /// rejects as SQLSTATE `2201W` (invalid_limit_value) rather than treating | ||
| /// as unbounded. Without the rejection the planner silently drops the | ||
| /// bound and runs the full collection scan. | ||
| #[error("invalid limit value: {detail}")] |
There was a problem hiding this comment.
🟡 Changes recommended
SqlError::InvalidLimitValue is not actually mapped to SQLSTATE 2201W in the control-plane/pgwire error mapping, so clients will likely receive the wrong SQLSTATE classification.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Lite
| /// A LIMIT or OFFSET clause carried a negative literal. | ||
| /// | ||
| /// Postgres rejects this as SQLSTATE `2201W` (invalid_limit_value). | ||
| /// NodeDB surfaces this as a planner error that the pgwire layer maps | ||
| /// to `2201W` via an explicit `Error::InvalidLimitValue` conversion | ||
| /// (see `control/planner`); unmapped variants fall back to `PlanError` | ||
| /// (`42601`). The rejection prevents the planner from silently dropping | ||
| /// the bound and running a full collection scan. | ||
| #[error("invalid limit value: {detail}")] |
| /// `(limit, offset)` for the LIMIT clause. A missing clause is | ||
| /// `(None, 0)`; a non-literal bound is `None` (unbounded). | ||
| pub(in crate::planner::select) fn limit_offset(&self) -> (Option<usize>, usize) { | ||
| match self.limit_clause { | ||
| /// | ||
| /// Negative literals are rejected with [`SqlError::InvalidLimitValue`] | ||
| /// (SQLSTATE 2201W class) instead of silently degrading to unbounded. |
… clippy) - cargo update: h2 0.4.15 -> 0.4.19 (RUSTSEC-2026-0258 empty DATA frames), chacha20 0.10.1 -> 0.10.2 (yanked), wide 1.6.0 -> 1.7.0 (yanked), wasmtime 47.0.3 -> 47.0.4 (RUSTSEC-2026-0268/-0269) - deny.toml: ignore RUSTSEC-2026-0247/-0248/-0251 (lorо-internal's im/sized-chunks/bitmaps, archived upstream, no fixed version, review-by 2027-05-01); allow-git now matches Cargo.toml's nuskey8/zerompk (was farhan-syah — source-not-allowed for zerompk + zerompk_derive) - clippy: codebook_bytes.is_none_or (pq.rs); allow(dead_code) on wire harness start_with_failpoints - cargo deny check: advisories ok, bans ok, licenses ok, sources ok - clippy --workspace --all-targets --all-features -D warnings: 0
check_calvin_determinism.sh flagged sub_plan.rs:122 (Instant::now in build_dummy_task_at). The existing no-determinism comment sat in the doc block six lines above; the gate requires same-line or directly preceding. Test-only builder; deadline never reaches Calvin state.
…rsample - S1: LIMIT/OFFSET negative literals now error with InvalidLimitValue (SQLSTATE 2201W class) instead of silently degrading to unbounded; new fallible expr_as_nonnegative_usize in coerce.rs, limit_offset returns Result, validated downstream in select_stmt/limit apply. - S2: ANALYZE/COMPACT keyword-boundary matched; trailing parenthesised payload (ANALYZE users(id)) no longer absorbed into the collection name; both now strip ( ... ) suffixes. - V3: cost planner rerank oversample now reads rerank::codecs::bbq::DEFAULT_OVERSAMPLE (single source of truth) instead of a local divergent const (3 vs 4). Regression: 861 nodedb-sql lib tests + nodedb-vector cost tests green; repro tests s1/s2/v3 flip red->green.
… boundary, 2201W doc - coerce: reject SingleQuotedString/Number '-...' in addition to UnaryOp Minus - maintenance: use parts[0] eq_ignore_ascii_case for ANALYZE/COMPACT boundary - error: clarify InvalidLimitValue pgwire mapping (PlanError 42601 fallback, explicit 2201W needs control-plane map)
…LSTATE 2201W Copilot review follow-up (drill): - coerce: literal-only negative rejection across all forms — LIMIT -5, LIMIT '-1', LIMIT - '-1' error; LIMIT -$1 (non-literal) keeps documented unbounded semantics - maintenance: ANALYZE/COMPACT boundary via parts[0] (tab-safe) - error: InvalidLimitValue now carried to pgwire as SQLSTATE 2201W via Error::InvalidLimitValue + error_map arm + classify arm; sqlstate const INVALID_LIMIT_VALUE added
dccb8e2 to
5c5f9c4
Compare
PostgreSQL coerces constant LIMIT/OFFSET expressions, so a syntactic UnaryOp::Minus is not itself an error: LIMIT -0 must be accepted (0 rows), LIMIT - '-5' must be accepted (+5), LIMIT -5 and LIMIT '-1' must still be rejected (negative result). expr_as_nonnegative_usize now parses literal operands to i64, negates, and only rejects when the RESULT is negative; non-literal operands keep the documented unbounded semantics. Also drops a stale draft doc block and fixes the stale 'oversample-3' comment in the vector cost planner.
Supply-chain note — zerompk git source (deny.toml allow-git)Reviewer flagged the
|
Summary
Three planner/parsing correctness fixes, each with a regression test:
LIMIT -1silently unbounded (#272)expr_as_nonnegative_usize;limit_offset()returnsResult; statically-negative bounds (all literal forms:-1,'-1',- '-1') rejected asInvalidLimitValue, carried to pgwire as SQLSTATE2201W; non-literal operands (LIMIT -$1) keep the documented unbounded semanticsANALYZE users(id)absorbs(id)(#273)rerank::codecs::bbq::DEFAULT_OVERSAMPLEHow to test
Manual:
SELECT * FROM t LIMIT -1→ errorinvalid limit value(2201W), not unboundedSELECT * FROM t LIMIT 10→ unchangedANALYZE users(id)→ targets collectionusersANALYZE\tusers(tab separator) → targetsusersRegression
cargo test -p nodedb-sql --lib: 863 passedcargo test -p nodedb-vector --lib v3_: passedcargo test -p nodedb --lib error_map: 29 passedFixes #272, #273, #275.