[Employer Sync 2.2] Convert org_memberships.is_default and is_owner to boolean
TL;DR: Both flags are varchar storing 't'/'f'. Convert them to real boolean columns. Safe multi-step deploy so the running app never reads a value it cannot decode and never writes a value the DB cannot accept.
Context
org_memberships.is_default and org_memberships.is_owner are stored as varchar with values 't' and 'f' (default 'f', NOT NULL). The intent is boolean; the storage is string. Every read in the codebase that needs a boolean has to compare strings, and every write has to encode strings. The employer-sync mapper (issue 4.3) would inherit the same wart on every membership it writes — so the right time to fix the type is before 4.3 lands.
Problem
A one-line change_column :is_default, :boolean, using: "is_default = 't'" converts the data correctly. It does not coordinate with the running app. The moment the column type changes, every code path that compares is_owner == 't' or writes is_owner: 'f' returns the wrong answer or raises. A single-migration approach is unsafe.
Direction
Use the standard online-conversion pattern. Four deploys total — none of them risky on their own.
Step 1 — Audit every call site.
Grep jodapp-api and jodapp-web for every read and write of is_default and is_owner on Org::Membership. Attach the list to this issue. Common shapes to look for:
membership.is_owner == 't'or'true'update(is_owner: 't')/update!(is_default: 'f')where(is_owner: 't')- Serializer field outputs, factory defaults, fixtures, seed data.
Step 2 — Make the reads boolean-typed (DB still varchar).
In the Org::Membership model class (grep for the file — the path follows the project's DDD layout), add Rails attribute coercion so the model returns booleans regardless of the underlying column type:
attribute :is_default, :boolean
attribute :is_owner, :boolean
This is a code-only change. The DB column is still varchar, but every read via the model returns true / false. Update every read call site found in step 1 to consume booleans. Deploy.
Step 3 — Switch the writes to booleans (DB still varchar).
Change every write call site to pass true / false. Rails serialises booleans back to 't' / 'f' while the column type is still varchar, so the DB keeps accepting them. Deploy. Observe production for one day with no errors.
Step 4 — Convert the column type.
change_column :org_memberships, :is_default, :boolean,
using: "(is_default = 't')", null: false, default: false
change_column :org_memberships, :is_owner, :boolean,
using: "(is_owner = 't')", null: false, default: false
Drop the attribute :is_default, :boolean / attribute :is_owner, :boolean overrides from step 2 — once the column is boolean, they are redundant.
Acceptance
- Step 1 audit attached to this issue.
- Step 2 deployed; reads return booleans via the model; no behavioural change.
- Step 3 deployed; all writes use booleans; production observed for one day with no errors.
- Step 4 migration runs cleanly on QA, then production. The two attribute overrides are removed.
-
db/structure.sqlregenerated; the two columns areboolean NOT NULL default false.
Why this is its own sub-issue
The naive change_column ... using: is one line and looks safe in isolation. It is unsafe in production because reads and writes are not in lockstep with the DB type. Splitting the conversion into four sequenced deploys (audit → tolerant-read → write-switch → type-flip) is the only shape that does not require downtime or a coordinated dual deploy. Embedding it inside 2.1 would conflate index migrations (one safe deploy) with a type conversion (four).
Why it sits in Phase A
This is independent of all other sub-issues — it touches the DB layer and one model file, nothing else. It blocks 4.3 (which writes booleans on every migrated membership). Starting it now means it is done by the time 4.3 is ready to land.