Skip to content

Commit 17ead59

Browse files
committed
design: decide separate engine-owned reclock; in-band markers rejected
Record the decision: the reclock is a separate, engine-owned collection (the source-remap pattern), not data in the DELTA TABLE. Rationale: clean argumentation (a separable mapping R; v provably = INTEGRATE(DELTA TABLE) driven by R) and no tampering/validation (engine-owned metadata with assumed invariants, not 'just data' in a writable table), plus independent retention. State the DELTA TABLE = user data / reclock = control-plane bookkeeping boundary. The reclock commits in the same multi-shard bundle txn as the recording, so the extra shard is free. In-band mz_progressed markers moved to Alternatives as considered-and-rejected; remove the now-decided open question. https://claude.ai/code/session_015YFH7J7PaEqkBSrAQYaq8H
1 parent a264f9e commit 17ead59

2 files changed

Lines changed: 47 additions & 43 deletions

File tree

doc/developer/design/20260604_recorders.md

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -241,32 +241,38 @@ it writes the `DELTA TABLE` (so `mz_now()` there is A); `INTEGRATE`'s query read
241241
the `DELTA TABLE` in B and reclocks B→A as it writes `v` (placing by
242242
`mz_timestamp`, driving the frontier via the reclock; `mz_now()` there is B).
243243

244-
**Representing the reclock (a choice that does not change the model).** The A→B
245-
mapping can be a **separate collection**, or live **in-band** as progress markers
246-
in the `DELTA TABLE` (`SUBSCRIBE`'s `mz_progressed` rows — frontier advances with
247-
no data), making the `DELTA TABLE` a persisted `SUBSCRIBE` stream. Either way the
248-
semantics are identical; only the representation differs:
249-
250-
- *In-band* gives a single-shard, single-CAS `RECORD` (data and reclock cannot
251-
diverge), but burdens every consumer of the `DELTA TABLE` with progress-marker
252-
noise — the same awkwardness as consuming `SUBSCRIBE` directly.
253-
- *Separate collection* keeps the `DELTA TABLE` clean to query, at the cost of
254-
committing the reclock atomically with the data — but that is a **combined
255-
CAS** the design already needs for the multi-output bundle
256-
(`RECORD`/`INTEGRATE`/`DELETE` at one `T`), so it is nearly free.
257-
258-
It is an open implementation choice (see Open Questions); the clean-data argument
259-
leans toward a separate collection. (The conceptual symmetry is worth noting
260-
regardless: `CHANGES` turns *changes* into data via `mz_diff`, the frontier can be
261-
turned into data via `mz_progressed`, and `INTEGRATE` reads them back.)
244+
**The reclock is a separate, engine-owned collection (decision).** The A→B
245+
mapping is kept as its own durable collection — a monotone mapping between the two
246+
domains, exactly the **source-remap pattern** (`20210714_reclocking.md`) — *not*
247+
as data in the `DELTA TABLE`. Two grounds:
248+
249+
- **Clean reasoning.** The reclock is a separable object `R`, so `v` is provably
250+
`INTEGRATE(DELTA TABLE)` with its frontier driven by `R` — a function we can
251+
state and verify in isolation, rather than one entangled with parsing frontier
252+
information back out of the data stream.
253+
- **No tampering, no validation.** It is **engine-owned metadata, not user
254+
data**, so its invariants (monotone, well-formed) are *maintained* by the
255+
engine and *assumed* by every consumer — there is nothing for a user to corrupt
256+
(the frontier is not "just data" in a writable table) and nothing to
257+
defensively validate on read. It can also be retained independently of the
258+
data, long enough to interpret history.
259+
260+
This respects a boundary worth stating plainly: **the `DELTA TABLE` is
261+
user-queryable data; the reclock is control-plane bookkeeping.** Folding the
262+
latter into the former is the root of both the tampering risk and the
263+
consumption noise that the in-band alternative incurs (see Alternatives).
264+
265+
The reclock is committed atomically with the recording — in the same multi-shard
266+
transaction the multi-output bundle (`RECORD`/`INTEGRATE`/`DELETE` at one `T`)
267+
already requires — so the extra shard costs nothing in atomicity machinery.
262268

263269
Replica races are an *exactly-once* concern — a delta must not be recorded twice
264270
— not a correctness one (the data and frontiers each function sees are
265-
deterministic); whichever representation, the guard is the CAS on the recording
266-
commit. Non-determinism is confined to the recorded **values** (frozen at
267-
processing time), so `v` is a definite function of the recorded data. The
268-
`mz_timestamp` remains queryable as data for "as of input-time" questions, within
269-
`RETAIN HISTORY`.
271+
deterministic); the guard is the CAS on the recording commit. Non-determinism is
272+
confined to the recorded **values** (frozen at processing time), so `v` is a
273+
definite function of the recorded `DELTA TABLE` + reclock. The `mz_timestamp`
274+
remains queryable as data for "as of input-time" questions, within `RETAIN
275+
HISTORY`.
270276

271277
The write verbs differ by *shape*: **`RECORD``DELTA TABLE`** keeps the per-row
272278
change log in domain B (with `mz_timestamp` as data); **`INTEGRATE` → TVC**
@@ -507,22 +513,20 @@ implementation, PR #35967):
507513
operationally heavy; the point is to stay inside Materialize.
508514
- **Distributed locking instead of OCC commit.** Rejected (latency, brittleness,
509515
scalability), per the OCC read-then-write design.
510-
- **Reclock representation: in-band progress markers vs. a separate collection.**
511-
A representation choice, not a model change (see Solution). In-band
512-
(`mz_progressed`, a persisted `SUBSCRIBE`) gives a single-shard CAS but makes
513-
the `DELTA TABLE` awkward to consume; a separate collection keeps the data clean
514-
at the cost of a combined CAS the multi-output bundle already needs. Leaning
515-
toward a separate collection for data cleanliness; left open.
516+
- **In-band progress markers instead of a separate reclock.** Considered and
517+
rejected. Encoding the A→B mapping as `mz_progressed` rows in the `DELTA TABLE`
518+
(a persisted `SUBSCRIBE`) gives a single-shard CAS, but (a) makes the frontier
519+
*user data* — inviting tampering and forcing defensive validation on every read
520+
— and (b) burdens every `DELTA TABLE` consumer with progress-marker noise. Its
521+
single-CAS upside is moot since the multi-output bundle already needs a
522+
multi-shard transaction. (Conceptually neat — `mz_diff` is changes-as-data,
523+
`mz_progressed` is frontier-as-data — but not worth storing that way.)
516524

517525
## Open questions
518526

519527
- **`RECORDER` object model & syntax.** Confirm `CREATE RECORDER WITH <rels> AS
520528
<actions>`. How is the trigger cadence expressed (vs `COMMIT EVERY`)? Are the
521529
bundled actions always one atomic commit at `T`?
522-
- **Reclock representation.** In-band progress markers (`mz_progressed`, a
523-
persisted `SUBSCRIBE`) vs. a separate reclock collection — a representation
524-
choice that does not change the model; trades single-shard CAS against
525-
data-consumption cleanliness (the body leans toward a separate collection).
526530
- **Which domain does `mz_now()` / aging resolve in?** With `INTEGRATE`'s output
527531
reclocked onto the input timeline (domain A), time-based aging and `mz_now()`
528532
in a body could mean **domain A** (event-age — "last 30 days of *events*";

doc/developer/design/20260604_recorders_implementation.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -295,16 +295,16 @@ recorder design tries to sidestep (and, per H2, only partly does).
295295
functions of the `DELTA TABLE` + reclock (the reclock makes the clamped
296296
integration reproducible), so the optimizer *may* treat them as recomputable
297297
over the recorded data.
298-
- The **reclock** (A→B) drives `v`'s domain-A frontier and is recovered on
299-
restart. Representation is an open choice that does not change the model:
300-
*in-band* as `SUBSCRIBE` `mz_progressed` markers in the `DELTA TABLE` (a
301-
persisted subscribe — single-shard CAS, but progress-marker noise for
302-
consumers) vs. a *separate collection* (clean data, but a combined CAS with
303-
the data — nearly free since the multi-output bundle already needs a
304-
multi-shard txn). Leaning separate for data cleanliness. Either way, exactly-
305-
once (no double-recording) is guarded by the CAS on the recording commit; it
306-
is not a determinism problem. Reuses the reclock framework
307-
(`20210714_reclocking.md`).
298+
- The **reclock** (A→B) is a **separate, engine-owned collection** (decided) —
299+
the source-remap pattern (`20210714_reclocking.md`), not data in the `DELTA
300+
TABLE`. It drives `v`'s domain-A frontier and is recovered on restart; its
301+
invariants are engine-maintained and *assumed* (no user tampering, no
302+
read-time validation), and it can be retained independently of the data. It
303+
is committed in the **same multi-shard bundle txn** as the recording, so the
304+
extra shard adds no atomicity machinery. Exactly-once (no double-recording)
305+
is guarded by the CAS on that commit; not a determinism problem. (In-band
306+
`mz_progressed` markers were the considered-and-rejected alternative — user
307+
data → tampering/validation + consumption noise — see the design doc.)
308308
- **Compliance erasure vs. stable history**: true GDPR erasure = advancing
309309
`since` to physically drop history, which forfeits `AS OF`/replay in the
310310
erased range. It is mutually exclusive with stable history there; scope it

0 commit comments

Comments
 (0)