83 lines
3.6 KiB
Markdown
83 lines
3.6 KiB
Markdown
---
|
|
id: sqlite-engine-streams
|
|
name: SQLite engine — streams (`StreamHandle`, reads, subscribe, trim)
|
|
status: pending
|
|
depends_on: [sqlite-engine-seam-tx]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
tags: [wave-3, sqlite-engine]
|
|
---
|
|
|
|
## Description
|
|
|
|
Implement `Store::stream` and the full `StreamHandle` trait over the
|
|
substrate's stream ops (`stream_publish`, `stream_read_since`,
|
|
`stream_save_offset`, `stream_get_offset` — inherited near-verbatim;
|
|
the `topic` column carries the contract's `stream` field, the one name
|
|
delta, mapped at this layer):
|
|
|
|
- `stream(name)` — validated constructor returning the boxed handle.
|
|
- `publish` / `publish_with_key` — auto-commit publishes through the
|
|
writer slot; `encode_payload` at the seam (`Codec` propagates);
|
|
return the assigned offset.
|
|
- `read_since(offset, limit)` / `read_from_consumer(consumer, limit)`
|
|
— reader-pool reads, offset ASC (global FIFO); map rows into
|
|
`StreamEvent` via the core constructor task's `from_row`.
|
|
**Extent guard (ADR-023 §2)**: `limit <= 0` yields the empty `Vec`
|
|
at the trait-impl entry, before the substrate — the `LIMIT -1`
|
|
dialect artifact dies here (the substrate passes `limit` through
|
|
contract-blind; the guard is the engine's obligation).
|
|
- `save_offset(consumer, offset)` / `get_offset(consumer)` — monotone
|
|
saves (substrate's upsert already refuses regressions silently);
|
|
`get_offset` of an absent consumer = 0.
|
|
- `trim_to(horizon)` — `DELETE FROM __alkstore_stream WHERE topic = ?
|
|
AND offset <= ?` inside the writer-slot lease; boundary argument is
|
|
**total** (ADR-023 §2: a negative horizon deletes nothing,
|
|
idempotently — no guard, pass through); returns the deleted count.
|
|
- `subscribe(consumer)` — the durable receiver: attach, read to tail
|
|
from the stored offset, then wake-driven re-reads off the watcher
|
|
fanout (same bridge shape as `listen`, but yielding
|
|
`Result<StreamEvent>` per the `EventReceiver` trait — `Err` carries
|
|
`Database` only; `None` = closed on watcher death, terminal).
|
|
Receiver-form `save_offset()` saves the last-yielded event's offset
|
|
through the same monotone op (ADR-019 §6); `offset()` reports the
|
|
receiver's position.
|
|
- Consumer identifiers (`consumer`) take the non-empty rule only;
|
|
stream names the shared-namespace rules.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] Full `StreamHandle` impl; publish/read/save/get/trim/subscribe
|
|
all work against a temp-file store
|
|
- [ ] Extent guard: `read_since`/`read_from_consumer` with
|
|
`limit <= 0` return the empty `Vec` (never the whole stream) —
|
|
pinned per ADR-023 §2's backlog row
|
|
- [ ] `trim_to` exact-boundary (`<=`), negative horizon deletes
|
|
nothing, surviving offsets never renumbered
|
|
- [ ] Monotone saves: regression saves are silent no-ops; direct and
|
|
receiver forms compose without regression
|
|
- [ ] `subscribe` replays from the stored offset, then delivers
|
|
post-attach events wake-driven; watcher death closes it
|
|
(`None`, terminal); `recv()`'s `Err` carries `Database` only
|
|
- [ ] Key round-trips exactly (`None` vs `Some`); `stream` field
|
|
carries the contract name (not `topic`)
|
|
- [ ] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt clean
|
|
|
|
## References
|
|
|
|
- docs/architecture/core-contract.md (streams section)
|
|
- docs/architecture/decisions/015-streams-depth.md
|
|
- docs/architecture/decisions/019-mechanism-handle-surfaces.md §6
|
|
- docs/architecture/decisions/023-fourth-review-round.md §2
|
|
- docs/architecture/engine-sqlite.md (Mapping the contract: streams)
|
|
- alkstore-sqlite/src/substrate/ops.rs (stream_* fns)
|
|
|
|
## Notes
|
|
|
|
> To be filled by implementation agent
|
|
|
|
## Summary
|
|
|
|
> To be filled on completion |