Library-resident backfill logic for mix sigra.upgrade --backfill-personal-orgs.
The host app's generated data migration
(priv/repo/data_migrations/*_backfill_personal_orgs.exs) is a
10-line shim that calls run_personal_orgs/2. All batching,
telemetry, and SQL logic lives here so fixes ship via
mix deps.update — the host never re-runs the generator.
Idempotency guarantees (Phase 18 D-03)
Two independent layers:
- Selector-level —
where: not exists(...)narrows the work set to users without a personal org on every re-run. A keyset cursor (u.id > ^last_cursor) avoids PostgresOFFSET's O(n) scan and makes crash-resume free. - Insert-level —
Repo.insert_all/3withon_conflict: :nothingandconflict_target: {:unsafe_fragment, "(owner_user_id) WHERE personal = true"}catches any race with concurrent signups, collapsing duplicates silently.
Personal workspace naming (Phase 18 D-04)
- Name —
"{display_name || email_local_part || "Personal"}'s Workspace". - Slug —
"user-#{user.id}". Opaque, immutable, PII-safe.
Telemetry
Emits [:sigra, :upgrade, :backfill, :batch] per batch with
measurements:
:batch_index— zero-based counter:batch_size— rows returned by the selector:inserted— rows actually inserted (may be< batch_sizeon conflict):total_processed— running total across batches
A terminal [:sigra, :upgrade, :backfill, :done] event fires once
when the selector returns an empty batch, with %{total_processed: n, batches: k} measurements.
Summary
Functions
Backfills a personal organization for every user that does not already have one.
Functions
@spec run_personal_orgs( module(), keyword() ) :: {:ok, non_neg_integer()}
Backfills a personal organization for every user that does not already have one.
Idempotent: safe to re-run. See module docs for guarantees.
Returns {:ok, total_inserted}.