feat(forms): migration 039 backfill existing submissions to members by email

Data-only, idempotent (NULL member_id only): links each prior unlinked
form_submission to the earliest-joined matching space_member by
(space_id, lower(email)=lower(submitter_email)). Not mirrored in
schema.sql (fresh DB has no submissions). Documented in
DATABASE_SCHEMA + DB_SCHEMA_MAP (migration range bumped to 039).
This commit is contained in:
Moheeb Zara
2026-05-16 23:46:56 -07:00
parent dbed9ac1bd
commit 832a0aeba2
3 changed files with 41 additions and 4 deletions
+4 -3
View File
@@ -4,9 +4,9 @@
> **Database**: PostgreSQL via self-hosted Supabase
> **Tables**: ~43
> **Source of Truth**: `scripts/schema.sql` (canonical, idempotent); numbered
> migrations `scripts/0NN_*.sql` (through **038**) upgrade existing
> migrations `scripts/0NN_*.sql` (through **039**) upgrade existing
> deployments. The 13-table reference below is the original baseline; tables
> added by migrations 014-038 are summarized in the migrations section at the
> added by migrations 014-039 are summarized in the migrations section at the
> end. `DB_SCHEMA_MAP.md` has the per-table quick map.
---
@@ -687,7 +687,7 @@ CREATE INDEX idx_activity_space ON activity_log(space_id, created_at);
---
## Migrations 014-038 (additions since the 13-table baseline)
## Migrations 014-039 (additions since the 13-table baseline)
`scripts/schema.sql` is the canonical idempotent schema; each numbered
migration is mirrored as a section in it. Tables/columns added:
@@ -719,6 +719,7 @@ migration is mirrored as a section in it. Tables/columns added:
| 036 | `door_card_slots` (per-connection integer-slot allocation map for controllers that key cards by slot, e.g. HeatSync 0-200). `UNIQUE (connection_id, slot)` lets the DB arbitrate concurrent grants; `UNIQUE (connection_id, card_id)` makes re-grant idempotent. Allocation policy (lowest-free, range bounds) is pure unit-tested logic, not SQL, so it stays adapter-generic. RLS additive + default-deny: SELECT = `door.manage`/`door.operate`, NO write policy (validated service-client executor only, in lockstep with the controller). No new permission codes. No anonymous path |
| 037 | `classes.required_form_id` nullable FK -> `forms(id)` ON DELETE SET NULL. A class can optionally require a form (waiver/intake); signup is hard-gated on the member having a completed `form_submissions` row for it (waiver-on-file), with a `classes.manage` override + sign-up-on-behalf, mirroring the equipment required-cert gate. Gate enforced in the app (pure logic + service-client boolean check); no RLS change (additive nullable column on the already-policied `classes`) |
| 038 | `space_visits` (presence/attendance: check-in/out, `is_host`, in/out notes; partial UNIQUE `(space_id,member_id) WHERE checked_out_at IS NULL` = one open visit). RLS additive/default-deny: SELECT = any space member (presence is social), NO client write policy (validated self-resolved service-client actions; immutable history). No new permission code; org-wide attendance view is all-members by product decision. Also adds `spaces.host_requires_card` (bool, default true) gating host check-in, enforced in the app |
| 039 | DATA backfill only (no structural change): links existing `form_submissions.member_id` (where NULL) to the earliest-joined matching `space_members` by `(space_id, lower(email)=lower(submitter_email))`. Idempotent (NULL-only). Not mirrored in schema.sql (fresh DB has no submissions); ongoing linking is app-side (submitForm + linkSubmissionsByEmail) |
New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`.