# Job opportunity cutover

The release-readiness build closes the write-side gap:

- normal application creation and Gmail suggested-job creation both create a linked `Job`;
- edits synchronize opportunity fields through `JobOpportunitySync`;
- startup idempotently creates one `Job` for every legacy `JobApplication` whose `JobId` is null.

The startup backfill is additive and runs after the deployment backup. It does not drop or overwrite
legacy application columns.

## Production validation

Run these read-only checks after deployment. Expected result for every count is `0`:

```sql
SELECT COUNT(*) AS missing_job
FROM JobApplications
WHERE JobId IS NULL;

SELECT COUNT(*) AS missing_target
FROM JobApplications a
LEFT JOIN Jobs j ON j.Id = a.JobId
WHERE a.JobId IS NOT NULL AND j.Id IS NULL;

SELECT COUNT(*) AS owner_mismatch
FROM JobApplications a
JOIN Jobs j ON j.Id = a.JobId
WHERE NOT (a.OwnerUserId <=> j.OwnerUserId);

SELECT COUNT(*) AS field_mismatch
FROM JobApplications a
JOIN Jobs j ON j.Id = a.JobId
WHERE NOT (a.CompanyId <=> j.CompanyId)
   OR NOT (a.JobTitle <=> j.JobTitle)
   OR NOT (a.Location <=> j.Location)
   OR NOT (a.JobUrl <=> j.JobUrl)
   OR NOT (a.Description <=> j.Description)
   OR NOT (a.Salary <=> j.Salary);
```

`<=>` is MariaDB/MySQL's null-safe equality operator. Do not use these statements as an update script.

## Expand/contract release order

1. Deploy the additive backfill and synchronized writers.
2. Capture the validation counts above and observe one release.
3. Change reads to use `Job`; keep compatibility columns during that release.
4. Re-run validation against a fresh backup restore.
5. Only then make `JobId` required and drop duplicated opportunity columns in a later migration.

Do not combine the destructive column drop with the first production backfill.
