← 1.0.0 scope SAPE-20

Composed operations, with a ceiling readable at any depth

Status
Done
Version
1.0.0
Component
server, store
Commits
d730d10, 408257b, 330d2f1, 2d9723c
Opened
ISS-1, ISS-5, ISS-9, ISS-10

Description

A step of a batch may now name an operation that is already declared, instead of a collection. Step gains operation, version and with; a step does one or the other and never both, and writing both is refused when it is declared (internal/store/compose.go:217).

The point is the ceiling. Store.ceiling (compose.go:99) reads the most rows an operation may return straight out of its declaration: 1 for a get or a write, the declared Limit for a scan or a rollup read, 1 for a count, and for a batch the sum of its steps' ceilings. Five rules hold it together, of which the load-bearing two are N4 — a term naming another step may only name one whose ceiling is 1 — and N5 — a batch with a composed step declares a limit, and its steps' ceilings sum at or below it.

N4 is the rule that decides what this product is. Taking a value from a step that may hand back many rows means "run this once for each row that one returned": a for loop written in JSON, and a for loop is the first half of the expression language this store exists not to have. The ceiling would stop being a sum and become a product, and a product of three fifties is 125,000 rows behind three numbers none of which makes a reader look twice.

Why it was in 1.0.0

It is the answer to the standing objection — "no query language means K round trips for K reads" — and answering it after the wire is public means adding fields to Step after clients have shipped decoders for it. The ceiling rule is the same: a limit that is a sum today and a product tomorrow is not a fix, it is a different product.

How it was verified

  1. TestTheCeilingOfAComposedOperationAtAnyDepthIsTheLimitItDeclares (internal/store/compose_test.go:438) builds a tower: level 0 is a scan with limit 50, and every level above has two legs of the level below, declaring exactly the sum — 100, 200, 400. A reading where a leg runs once per row of another leg would put 50×50 = 2500 at level 1 alone and 2500×2500 at level 2. The numbers are what say which reading the code implements.
  2. TestRedeclaringACalleeDoesNotMoveTheCeilingOfWhatAlreadyCallsIt (compose_test.go:590) — a pinned version is what makes the ceiling a fact about a declaration rather than about the store's state at call time.
  3. Self-reference through a pinned version is refused as ErrDamaged (compose.go:124): the records disagree with the rule that wrote them, which is damage, not a user error.
  4. 330d2f1 added a mutation case for the ceiling rule that nothing else caught — the rule was measured against a mutant, not only against an example.
  5. 408257b: a key taken from an earlier step is type-checked like an argument, at declaration time, through keyTypeOf.
  6. Over the wire, end to end: TestAComposedOperationIsDeclaredOnARunningDaemonAndAnsweredInOneCall (declare_live_test.go:438).

What it did not do