ActionCount was the one action that could be declared without declaring what it costs.
The limit was asked for on a scan, on a rollup read and on a composed batch; a count slipped between
them. That made "every declaration in this store says what it costs" a sentence with one exception in
it, and an exception reachable by writing "action": "count" and leaving one field out.
validateOperation's case ActionScan, ActionCount branch
(internal/store/ops.go:683) now refuses Limit <= 0 for both, with a
different sentence for each: a scan declares how many rows it may return, a count declares how far it
walks. The walk is the whole cost of a count — a count over forty million documents returns the
number 40000000, and the caller finds out how big the collection was by waiting.
A Limit < 0 refusal used to sit further down, after scanAcross, and it
was the only thing catching a negative limit on a count. The new rule covers every non-positive limit
for both actions, so that branch became unreachable and was removed — an unreachable refusal is
a rule a reader believes is doing work.
It is a breaking change to a declaration that was previously accepted: an existing
count declaration with no limit stops being declarable. After a tag that is a migration
somebody has to run; before it, it is a rule nobody has written against yet. It is listed under
Breaking in the CHANGELOG for that reason.
The second reason is SAPE-20. compose.go's
ceiling() has asked a count for a limit since composed operations landed, so a count
without one was already unusable as a leg — the two rules had to agree before either shipped.
TestACountDeclaredOverTheWireMustSayHowFarItWalks
(internal/server/count_limit_test.go:27), which also pins that this fires through the
Declare frame and not only through sapedb apply.apply: the shared table in
TestDeclaringOverTheWireRefusesExactlyWhatApplyRefuses
(internal/cli/declare_apply_test.go:44) — one table, both paths, so the two
cannot drift apart.count_limit_test.go:73 records what the old Limit < 0 branch used to
catch, so removing it is a measured removal rather than a deletion nobody checked.89db51a in sape-desktop ("a count declares its walk here too").sapedb/store: the declaration does not make sense: a count must declare how far it
walks…ceiling()'s own count check was kept rather than folded into this one. It reads
declarations already on disk — including ones written before this line existed — so the
two rules are deliberately not one.Explore. asOperation forces Limit to
MostRows when it is missing or too large, before validateOperation
runs, so neither limit rule can fire through the shell. That is the right answer for an operator who
did not say and the wrong answer for a declaration; the gap is written down at
internal/server/declare.go:33-42, not closed.