mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Add the calendar data layer: JSCalendar events with materialised occurrences
Storage is JSCalendar (RFC 8984) in jsonb, with everything a query touches
projected into real columns. Both halves earn their place. JSCalendar is
canonical because everything this will ever talk to speaks it or converts
cleanly — iCalendar both ways, CalDAV, JMAP calendars when that draft
lands — and a bespoke schema gets re-derived badly the first time an .ics
round-trips, silently dropping participants, alerts and overrides. But
Postgres cannot do range logic on "PT1H", so the columns exist too.
Recurrence is the part that is easy to get almost right, and there are
only three options. Expanding RRULEs at query time means loading every
recurring event ever created to answer "what is in July" — a standup from
2019 still generates instances — and nothing is indexable. Materialising
everything does not terminate for FREQ=DAILY with no UNTIL. So:
materialise to a bounded horizon, roll it forward nightly, and fall back
to live expansion outside it.
calendar_event_occurrence carries a GENERATED tsrange, and views read it
with `span && tsrange(from, to)` against a GiST index. The naive
`starts_at < :to AND ends_at > :from` is not just slower — it silently
misses the conference that started last week and is still running, because
starts_at ordering stops approximating ends_at ordering once multi-day
events exist. That index lives only in the migration: this DBAL has no
flag for USING gist, so every schema diff asks to drop it, and nothing
here may run doctrine:schema:update.
Two findings from writing the tests:
* sabre accepts FREQ=SECONDLY and FREQ=MINUTELY at validation but its
iterator has no branch to advance on either, so it yields one instant
forever. The occurrence cap turned that into a thousand identical rows
— a worse failure than a hang, because it looks like it worked. The
converter now refuses both frequencies, and the materialiser stops on
any rule that fails to move forward.
* Expansion has to happen in the event's own zone. A 09:00 Berlin
standup is 08:00 UTC in winter and 07:00 UTC in summer; expanding in
UTC passes every test written in UTC and moves everyone's meeting
twice a year. Pinned by testWeeklyExpansionHoldsLocalTimeAcrossADst-
Transition.
Provenance tables (event_source_link, event_suppression) ship now though
nothing writes them yet: they are what will let extraction re-run over a
whole mailbox as a backfill rather than a resync, and a suppression the
user has already expressed must survive that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
379f6771dd
commit
d3fb1692b3
25 files changed
+2880
-1
No files matched your search
@@ -20,6 +20,7 @@
|
||||
"minishlink/web-push": "^11.0",
|
||||
"phpdocumentor/reflection-docblock": "^6.0",
|
||||
"phpstan/phpdoc-parser": "^2.3.3",
|
||||
"sabre/vobject": "^4.6",
|
||||
"scheb/2fa-backup-code": "^8.6",
|
||||
"scheb/2fa-bundle": "^8.6",
|
||||
"scheb/2fa-totp": "^8.6",
|
||||
|
||||
Reference in new issue
Block a user