Version / build tested against
v0.5.0 (live repro against the released image). Code re-verified at origin/main @ 461c3adb.
Deployment mode
Origin — single node (local)
Engine(s) involved
Not engine-specific / unsure
Summary
Follow-up to #216 and #226/#228: scalar division by zero now correctly raises 22012, but two contexts still keep the error away from the client, and both report success rather than failing. A constant-folded expression with no FROM (SELECT 1/0) still returns one NULL row — the literal example in #216's title. Separately, an expression that errors in any non-scalar position — aggregate argument, WHERE, GROUP BY key, HAVING, window argument, ORDER BY — makes the statement return an empty result set with CommandComplete: SELECT 0 and no ErrorResponse. That second shape is a change from what #228 described (a silently wrong total) but not a fix: SELECT count(*) FROM t WHERE 10/d = 10 returns zero rows, when count(*) must return exactly one row for any predicate.
Steps to reproduce
CREATE COLLECTION agg_probe (id TEXT PRIMARY KEY, d INT);
INSERT INTO agg_probe (id, d) VALUES ('a', 1);
INSERT INTO agg_probe (id, d) VALUES ('b', 0);
INSERT INTO agg_probe (id, d) VALUES ('c', 2);
-- 1. Scalar context — correct since #226.
SELECT 10 / d FROM agg_probe; -- ERROR 22012 division by zero
-- 2. Every non-scalar context returns 0 rows, no error:
SELECT sum(10 / d) FROM agg_probe; -- 0 rows
SELECT avg(10 / d) FROM agg_probe; -- 0 rows
SELECT min(10 / d) FROM agg_probe; -- 0 rows
SELECT count(*) FROM agg_probe WHERE 10 / d = 10; -- 0 rows <- must be exactly 1 row
SELECT 10 / d AS k FROM agg_probe GROUP BY k; -- 0 rows
SELECT count(*) FROM agg_probe HAVING sum(10/d) > 0; -- 0 rows
SELECT sum(10 / d) OVER () FROM agg_probe; -- 0 rows
SELECT id FROM agg_probe ORDER BY 10 / d; -- 0 rows
-- Control on the same table and connection — the path itself is fine.
SELECT count(*) FROM agg_probe; -- 1 row: 3
-- 3. Constant-folded division, no FROM clause — still a NULL row, not 22012.
SELECT 1/0; -- 1 row: NULL
SELECT 1%0; -- 1 row: NULL
SELECT 1.0/0; -- 1 row: NULL
Expected behavior
An expression that raises 22012 in a scalar context raises it in every context — aggregate argument, WHERE, GROUP BY, HAVING, window argument and ORDER BY — and constant-only arithmetic is not exempt from the error path. Nothing in this set should be reported to the client as a successful, empty, or NULL result.
Actual behavior
1. Non-scalar contexts report success with an empty result set. Captured at the wire level, same session, d = 0:
| statement |
RowDescription |
DataRows |
CommandComplete |
SELECT sum(10 / d) FROM t |
1 field |
0 |
SELECT 0 |
SELECT count(*) FROM t (control) |
1 field |
1 |
SELECT 1 |
SELECT 1/0 |
1 field |
1 (NULL) |
SELECT 1 |
No ErrorResponse is sent, and nothing is written to the server log — checked with docker logs --since around the query, which is empty for the failing case. A client cannot distinguish "the predicate errored on every row" from "nothing matched".
2. SELECT 1/0 is still folded. Column-based division reaches the runtime error path (SELECT 10/d FROM t raises 22012), but the constant-only forms with no FROM do not, which suggests they are folded before that path is reached. 1%0 and 1.0/0 behave the same way. Modulo has an explicit runtime guard at
nodedb-query/src/functions/math.rs#L61
("Zero modulus raises SQLSTATE 22012 instead of folding to..."), which the constant path evidently does not reach either.
One code-level observation, offered as a lead rather than a diagnosis. The aggregate half of #228 is carried by compute_aggregate_binary, which does return Err(EvalError::DivisionByZero) and has a unit test asserting exactly that —
aggregate.rs#L721-L735:
#[test]
fn division_by_zero_in_expr_propagates() {
// `SUM(a / b)` over a document with `b = 0` must surface
// `EvalError::DivisionByZero` rather than folding the offending row
// to NULL and silently continuing the aggregate.
...
assert_eq!(err, EvalError::DivisionByZero);
}
At 461c3adb that function has no call sites in the workspace outside its own test module — the only other mention is the re-export at
msgpack_scan/mod.rs#L20:
$ grep -rn "compute_aggregate_binary" --include=*.rs .
nodedb-query/src/msgpack_scan/mod.rs:20:pub use aggregate::compute_aggregate_binary;
nodedb-query/src/msgpack_scan/aggregate.rs:26:pub fn compute_aggregate_binary(
(remaining matches are all inside aggregate.rs's own #[cfg(test)] module)
Because it is pub in a library crate, no dead-code warning fires. If the executing aggregate path is a different one, that would explain why the unit test passes while the end-to-end behaviour does not — but which path actually runs is the maintainer's call, not something to conclude from a grep.
What actually happened? (check all that are true)
Proposed severity
SEV-2 — High: major functionality broken or silently-wrong results; stored data intact
Matching the level #228 carried. Stored data is intact, but a query that must return a row returns none, with a success tag and no log line, so a caller has no signal that anything went wrong.
Reproducibility
Always — every attempt
Last known-good version / commit (if a regression)
Not a regression — this is the residue of two partly-completed fixes rather than a behaviour that previously worked. Symptom 2 has changed shape since #228 (silently-wrong total → empty result set) but is still silent.
Environment & logs
Linux x86_64, Docker, released image farhansyah/nodedb:0.5.0, single node, default configuration apart from a loopback NODEDB_HOST. Observed over pgwire from a Crystal client; wire-level rows/tags read directly from the RowDescription / DataRow / CommandComplete sequence. Server log across the failing queries is empty — no warning, no error.
Before submitting
Version / build tested against
v0.5.0(live repro against the released image). Code re-verified atorigin/main @ 461c3adb.Deployment mode
Origin — single node (local)
Engine(s) involved
Not engine-specific / unsure
Summary
Follow-up to #216 and #226/#228: scalar division by zero now correctly raises
22012, but two contexts still keep the error away from the client, and both report success rather than failing. A constant-folded expression with noFROM(SELECT 1/0) still returns oneNULLrow — the literal example in #216's title. Separately, an expression that errors in any non-scalar position — aggregate argument,WHERE,GROUP BYkey,HAVING, window argument,ORDER BY— makes the statement return an empty result set withCommandComplete: SELECT 0and noErrorResponse. That second shape is a change from what #228 described (a silently wrong total) but not a fix:SELECT count(*) FROM t WHERE 10/d = 10returns zero rows, whencount(*)must return exactly one row for any predicate.Steps to reproduce
Expected behavior
An expression that raises
22012in a scalar context raises it in every context — aggregate argument,WHERE,GROUP BY,HAVING, window argument andORDER BY— and constant-only arithmetic is not exempt from the error path. Nothing in this set should be reported to the client as a successful, empty, orNULLresult.Actual behavior
1. Non-scalar contexts report success with an empty result set. Captured at the wire level, same session,
d = 0:SELECT sum(10 / d) FROM tSELECT 0SELECT count(*) FROM t(control)SELECT 1SELECT 1/0NULL)SELECT 1No
ErrorResponseis sent, and nothing is written to the server log — checked withdocker logs --sincearound the query, which is empty for the failing case. A client cannot distinguish "the predicate errored on every row" from "nothing matched".2.
SELECT 1/0is still folded. Column-based division reaches the runtime error path (SELECT 10/d FROM traises22012), but the constant-only forms with noFROMdo not, which suggests they are folded before that path is reached.1%0and1.0/0behave the same way. Modulo has an explicit runtime guard atnodedb-query/src/functions/math.rs#L61("Zero modulus raises SQLSTATE 22012 instead of folding to..."), which the constant path evidently does not reach either.
One code-level observation, offered as a lead rather than a diagnosis. The aggregate half of #228 is carried by
compute_aggregate_binary, which does returnErr(EvalError::DivisionByZero)and has a unit test asserting exactly that —aggregate.rs#L721-L735:At
461c3adbthat function has no call sites in the workspace outside its own test module — the only other mention is the re-export atmsgpack_scan/mod.rs#L20:Because it is
pubin a library crate, no dead-code warning fires. If the executing aggregate path is a different one, that would explain why the unit test passes while the end-to-end behaviour does not — but which path actually runs is the maintainer's call, not something to conclude from a grep.What actually happened? (check all that are true)
Proposed severity
SEV-2 — High: major functionality broken or silently-wrong results; stored data intact
Matching the level #228 carried. Stored data is intact, but a query that must return a row returns none, with a success tag and no log line, so a caller has no signal that anything went wrong.
Reproducibility
Always — every attempt
Last known-good version / commit (if a regression)
Not a regression — this is the residue of two partly-completed fixes rather than a behaviour that previously worked. Symptom 2 has changed shape since #228 (silently-wrong total → empty result set) but is still silent.
Environment & logs
Linux x86_64, Docker, released image
farhansyah/nodedb:0.5.0, single node, default configuration apart from a loopbackNODEDB_HOST. Observed over pgwire from a Crystal client; wire-level rows/tags read directly from theRowDescription/DataRow/CommandCompletesequence. Server log across the failing queries is empty — no warning, no error.Before submitting
mainbuild (not a stale local branch).