Read a series' rows by its id, and say how far they reach - #96
Merged
Merged
Conversation
Splitting a series ("this and all following") needs every occurrence the
provider keeps as a row of its own: a changed one stands in for its slot,
a cancelled one is an occurrence the user deleted. readSeriesRows read them
through get_events over a month around the series' start and the cutoff, so
a row further out was never seen, and on Google a long series set off a full
resync the following write threw away.
The hosts now read the rows by id (decision 135): every cached
`{series}::rid::` row of the calendar, whatever its date, cancelled ones
included, on the table's primary key. The read never warms, never repairs
bindings and never hides anything. With the rows the host says how far its
cache reaches (decision 139): complete (Exchange, CalDAV, local and birthday
calendars), a window (Google, about a year ahead), or unknown (never read,
or written to since the last refresh). Rows and window are read in one
transaction.
- host-core: `cache::series_rows` and `CacheStore::read_series_rows`, with
`SeriesReach` (generated for TypeScript) and `SeriesRows`. An occurrence id
or an empty id is refused; an external calendar without an adapter is an
error, as for get_events.
- Desktop: a `get_series_rows` command.
- Phone: `get_events_json` takes an optional `series_id` instead of a method
of its own, and its doc comment is left alone, so a native library built
before this still loads (UniFFI's checksum covers the doc comment) and
answers with the stretch's events. `seriesRowsFromHost` reads that list as
rows whose reach is unknown.
- shared: `readSeriesRows` asks by series and returns `{ rows, reach }`; a
master without a rule is not read (the carry used to read for every single
copy). The six callers take `.rows`; the reach is for the question before a
split, which comes with the rule that puts the rows into the new series.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A windowed provider (Google) fills its cache by the time a row is shown, not by the slot it names, on the full read and on every delta. So a cancelled row, which stands at its slot, is missing only when the slot lies outside the window, but a changed occurrence moved out of it is missing whatever its slot. The `Window` doc and the module doc said the slot decided; the question before a split has to know the difference. - A test for the path the read exists for: an external calendar with an adapter, routed to its account and read from that account's cache, without the adapter being asked. - The phone bridge's `getEventsJson` declaration names the second answer shape. It is not covered by UniFFI's checksum. - TODO.md: Google rows reach only as far as the cache; the phone half waits for a new `.so` (↻). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First PR of the arc "exceptions when splitting a series" (decision 134), decisions 135 and 139.
Why
Splitting a series ("this and all following") needs every occurrence the provider keeps as a row of its own,
{series}::rid::{slot}: a changed one stands in for its slot, and a cancelled one is an occurrence the user deleted, which the new series must not bring back.readSeriesRowsread them throughget_eventsover a month around the series' start and the cutoff.The change
The read goes by id (135). The hosts return every cached
{series}::rid::row of the calendar, whatever its date, cancelled ones included. It is a range scan on the table's primary key (id >= 'S::rid::' AND id < 'S::rid:;'). It never warms, never repairs bindings (their repair judges "not in this batch", which a filtered batch would break) and never hides anything.The host says how far its rows reach (139).
SeriesReachis one of:complete: Exchange and CalDAV (the cache holds the whole calendar), local and birthday calendars (no such rows at all);window: the stretch the cache was filled for (Google, about now − 92 days to now + 366 days; the CalDAV fallback without a listing);unknown: never read, or written to since the last refresh, because every write clears the window.Rows and window are read in one transaction, so a refresh cannot land between them.
cache::series_rowsroutes likeget_events(an external calendar without an adapter is an error, not an empty answer) and refuses an empty id or an occurrence's id.CacheStore::read_series_rowsdoes the read.SeriesReachis generated for TypeScript.get_series_rowscommand.get_events_jsontakes an optionalseries_idrather than a method of its own, and its doc comment stays unchanged. UniFFI's checksum covers the doc comment, and a new method or a changed checksum would fail an older.soat load. An older.soignores the field and answers with the stretch's events;seriesRowsFromHostreads that list as rows whose reach isunknown. The bindings are unchanged (check:bindingspasses).readSeriesRowsasks by series and returns{ rows, reach }. A master without a rule is not read; the carry used to read for every single copy. The six callers (both editors, both carries, both delete helpers) take.rows.Not yet used: the reach. The question before a split comes with the PR that puts the rows into the new series. Until then the tail's exceptions come from the master alone, and a question about the rows' reach would promise what the split does not yet do. A
windowvouches for rows by where they stand now, not by the slot they name (see the review round), and that question has to account for it.Checks
cargo test --workspace --all-features: 2841 passed; fmt, clippy-D warningsclean;host-corealone clean;cargo xtask ts-types --checkcurrent (newSeriesReach.ts).tsc,eslintclean; vitest 2171 passed, locally and underTZ=UTC;npm run check:bindingspasses.ev-10andev-1xnot taken forev-1; another calendar's rows left alone; the reach for a window, a whole calendar, a calendar never read and one invalidated after a write (its rows still read); local and birthday calendars answer with none andcomplete; no adapter isNotFound; an occurrence id or an empty id is refused.get_events_jsonwith aseries_idanswers with the object; unroutable isNotFound; an occurrence isInvalidField.unknown; a master without a rule is not read.series_id; a master without a rule read; an older host's events not narrowed; an older host's answer taken as complete; the desktop askingget_events..sopath end to end (no runner).Review round
One adversarial round (2 lenses, every finding verified): 4 confirmed, all low after verification; 1 refuted (an unreadable stored window failing the read: only builds from 1 to 2 June 2026 wrote one, and those rows were reset since).
list_events_fullwithtimeMin/timeMax;list_events_incrementalkeeps a change only if cancelled or in the window). A cancelled row stands at its slot, so it is missing only when that slot lies outside the window. A changed occurrence moved out of the window is missing whatever its slot. TheWindowdoc said the slot decided; it and the module doc are corrected, since the later question must not key on the series' end alone.getEventsJsondeclaration now names the second answer shape; it is not covered by UniFFI's checksum..so(↻).Checks for the round: host-core series tests 10 passed, clippy and fmt clean,
ts-types --checkcurrent.Phone
Nothing to regenerate. A new
.sois needed for the series read itself; an older one keeps today's read.🤖 Generated with Claude Code