Skip to content

fix(sql): fail a timeseries HAVING instead of dropping it - #402

Open
EnRaiha wants to merge 1 commit into
NodeDB-Lab:mainfrom
EnRaiha:fix/333-having-refused-not-dropped
Open

EnRaiha wants to merge 1 commit into
NodeDB-Lab:mainfrom
EnRaiha:fix/333-having-refused-not-dropped

Conversation

@EnRaiha

@EnRaiha EnRaiha commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

A HAVING over a timeseries aggregate now fails loudly instead of returning rows the
predicate excludes. It also reaches the planner by a second route that previously lost it.

Part of #333. That issue lists four ways the timeseries engine drops what the native scan
cannot express; this closes one of them plus a second route to the same defect.

The defect

A HAVING over a timeseries aggregate was discarded and the statement answered every group
the WHERE clause left — rows the user had explicitly filtered out, with no error.

SELECT grp, COUNT(*) FROM ts GROUP BY grp HAVING COUNT(*) > 1;
-- g_a 3, g_b 2, g_c 1     ... all three returned, including the one the predicate excludes

Two causes:

  1. TimeseriesRules::plan_aggregate (nodedb-sql/src/engine_rules/timeseries.rs:104) never
    read params.having, and SqlPlan::TimeseriesScan (types/plan/variants.rs:285) has no
    having slot. The predicate is built at engine_rules/params.rs:146 and forwarded by the
    other five engine rules (columnar.rs:148, document_schemaless.rs:153,
    document_strict.rs:153, kv.rs:128, spatial.rs:140) — this one discarded it.
  2. has_aggregation (planner/select/select_stmt.rs:382) returned false when GROUP BY
    was empty and the projection named no aggregate, so
    SELECT grp FROM t HAVING COUNT(*) > 1 took the non-aggregate path and no rule saw the
    clause at all.

The fix

File Change
nodedb-sql/src/engine_rules/timeseries.rs plan_aggregate refuses a non-empty having with a typed error naming the collection and the clause
nodedb-sql/src/planner/select/select_stmt.rs has_aggregation treats a present HAVING as an aggregation signal

This refuses; it does not support. Supporting the clause needs a new plan field crossing a
node boundary and executor-side post-group filtering across every consumer of the scan. The
refusal turns a silently wrong answer into an error, which is the honest half of the pair and
what the issue allows. If you would rather the clause worked, say so and I will file the
support as a follow-up and keep the refusal here.

Tests

nodedb/tests/wire/cases/having_matrix_all_engines.rs walks the whole space rather than the
one reported shape: seven collection shapes — six engines plus the default a bare
CREATE COLLECTION selects — crossed with four clause shapes (GROUP BY + HAVING,
bare HAVING, HAVING over a join, and a WHERE control).

Each engine states its whole DDL, because they do not agree on what a collection needs: kv
requires PRIMARY KEY, spatial accepts only COLUMNS (...) with a GEOMETRY column, and
timeseries needs a TIME_KEY.

nodedb/tests/wire/cases/timeseries_having_predicate.rs pins the reported shape and the two
controls that make the refusal meaningful — the same aggregate without HAVING still answers,
and WHERE still filters.

To test locally:

cargo test --test wire timeseries_having_predicate
cargo test --test wire having_matrix_all_engines

Evidence

Arm Result
red — same tests on base 101, the excluded group returned
green — on this commit 0
mutation — both guards neutered 202, so the assertions read the code
fmt + clippy + repo preflight 0

Out of scope

On CI status

As of opening this PR, every check that has run is green: Static gates, Analyze Rust, the six
ASan libFuzzer jobs, the 32-bit wasm build, CodeQL and the secret scan.

The workspace Test Suite / Lint & Check job has not run, because it is opt-in: .github/workflows/ci.yml
dispatches the reusable Test workflow only when the PR carries the run-ci label. It is not on
this PR. If you want it run before reviewing, add the label or tell me and I will.

That job runs cargo clippy --workspace --all-targets --all-features --profile ci -- -D warnings,
and it currently fails on main for two reasons that are not this diff:

1. A clippy lint in a file this branch does not touch.

nodedb/src/control/planner/sql_plan_convert/dml/vector_primary.rs:164:16
error: this boolean expression can be simplified  (clippy::nonminimal_bool)

Reproduced on the base commit, unchanged:

git worktree add --detach /tmp/base <base>
cargo clippy -p nodedb --lib --all-features -- -D warnings   # exit 101, same three sites

PR #385 is the open fix. cargo clippy -p nodedb-sql --lib --tests -- -D warnings — the crate
this branch changes — is clean.

2. A dependency advisory, which no branch can lint its way out of.

error[vulnerability]: `call_ref` and exception `catch` can drop some fuel accounting
  ┌─ Cargo.lock:651
  │ wasmtime 48.0.1

That is cargo-deny reading Cargo.lock, and it blocks PR #385 as well — its only failing check
is this one, not the lint it exists to fix. Bumping wasmtime is its own change.

The pre-push hook runs the workspace preflight and therefore blocks on the same clippy lint. This
push used its documented one-time bypass (PREFLIGHT_SKIP=1), with this section as the stated
reason.

A `HAVING` over a timeseries aggregate was silently discarded: the statement
answered every group the `WHERE` clause left, including the ones the predicate
excludes, with no error.

`TimeseriesRules::plan_aggregate` never read `params.having`, and
`SqlPlan::TimeseriesScan` carries no `having` slot, so the predicate built at
`engine_rules/params.rs:146` was dropped while the other five engine rules
forward it. A second route lost it earlier still: `has_aggregation` returned
false when `GROUP BY` was empty and the projection named no aggregate, so
`SELECT grp FROM t HAVING COUNT(*) > 1` took the non-aggregate path and no
rule saw the clause at all.

The clause now fails loudly. `plan_aggregate` refuses a non-empty `having`
with a typed error naming the collection and the clause, and
`has_aggregation` treats a present `HAVING` as an aggregation signal so the
clause always reaches a rule that can judge it. Supporting it would need a
new plan field crossing a node boundary and executor-side post-group
filtering across every consumer of the scan.

The tests walk the whole space rather than the one reported shape: seven
collection shapes across six engines plus the default, crossed with four
clause shapes, each engine stating its own DDL because they do not agree on
what a collection needs. A join aggregate that answers input rows under an
aggregate heading is a separate defect, recorded rather than asserted away.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 07:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants