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:
+2
-1
@@ -404,10 +404,11 @@ Default channels created by trigger on space INSERT: `general`, `announcements`,
|
||||
| 036 | `door_card_slots` (Door epic P3). Per-connection integer-slot allocation map (HeatSync keys cards by slot 0-200). UNIQUE(connection_id,slot) + UNIQUE(connection_id,card_id); lowest-free policy in pure logic. SELECT = door.manage/operate, service-client-only writes |
|
||||
| 037 | `classes.required_form_id` nullable FK -> forms(id) ON DELETE SET NULL. Optional per-class form gate (waiver-on-file): signup requires a completed form_submissions row; classes.manage override + on-behalf. App-enforced; no RLS change |
|
||||
| 038 | `space_visits` (presence/attendance) + `spaces.host_requires_card` bool default true. One open visit per member (partial unique); SELECT = any space member; service-client-only writes (self-resolved, immutable). No new permission code |
|
||||
| 039 | DATA backfill only: link `form_submissions.member_id` (NULL) to earliest matching member by `(space_id, lower email = lower submitter_email)`. Idempotent; not in schema.sql |
|
||||
|
||||
---
|
||||
|
||||
## Tables added by migrations 016-038 (quick map)
|
||||
## Tables added by migrations 016-039 (quick map)
|
||||
|
||||
| Table | Key columns | Purpose |
|
||||
|-------|-------------|---------|
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- =============================================================================
|
||||
-- 039: Backfill -- associate existing form submissions with members by email
|
||||
-- =============================================================================
|
||||
-- One-time DATA backfill (no structural change; form_submissions.member_id
|
||||
-- already exists). Links every unlinked submission to the member in the same
|
||||
-- space whose email matches submitter_email (case-insensitive). When more
|
||||
-- than one member shares an email in a space, the earliest-joined one is
|
||||
-- chosen so the result is deterministic and matches the application logic
|
||||
-- (lib/forms-logic.ts pickMemberForEmail).
|
||||
--
|
||||
-- Idempotent: only rows with member_id IS NULL are touched, so re-running is
|
||||
-- a no-op. Not mirrored in scripts/schema.sql (a fresh database has no
|
||||
-- submissions to backfill); the ongoing linking happens in the app
|
||||
-- (submitForm + linkSubmissionsByEmail on member create/email-change).
|
||||
-- =============================================================================
|
||||
|
||||
UPDATE public.form_submissions fs
|
||||
SET member_id = (
|
||||
SELECT m.id
|
||||
FROM public.space_members m
|
||||
WHERE m.space_id = fs.space_id
|
||||
AND m.email IS NOT NULL
|
||||
AND lower(m.email) = lower(fs.submitter_email)
|
||||
ORDER BY m.joined_at ASC NULLS FIRST, m.id ASC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE fs.member_id IS NULL
|
||||
AND fs.submitter_email IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.space_members m
|
||||
WHERE m.space_id = fs.space_id
|
||||
AND m.email IS NOT NULL
|
||||
AND lower(m.email) = lower(fs.submitter_email)
|
||||
);
|
||||
Reference in New Issue
Block a user