--- name: sqlx-migrations description: Never manually pre-apply migrations without tracking rows; use IF NOT EXISTS; let the server apply its own migrations applies-to: gururmm --- # GuruRMM sqlx Migration Discipline ## The core rule Never manually pre-apply migrations via psql without also recording the corresponding row in `_sqlx_migrations`. If the row is missing, the server binary will attempt to re-run the migration at startup and fail when it finds the table or column already exists. ## The correct workflow Let the server binary apply its own migrations on startup: ``` 1. Write the SQL migration file (server/migrations/NNN_description.sql) 2. Use ADD COLUMN IF NOT EXISTS / CREATE TABLE IF NOT EXISTS for idempotence 3. Run cargo sqlx prepare (keeps .sqlx/ offline cache current) 4. Commit the migration file + .sqlx/ changes 5. Build the server binary (push to Gitea triggers build-server.sh) 6. Deploy: stop → copy binary → start 7. sqlx applies the migration on startup and records the checksum row ``` Do not pre-apply the SQL with psql. Do not insert rows into `_sqlx_migrations` manually unless recovering from a specific failure. ## Why: the proc macro excludes pre-applied rows When `DATABASE_URL` is set at compile time, `sqlx::migrate!()` queries `_sqlx_migrations` during compilation and embeds only the migrations not yet present in the DB. If you pre-apply migration 026 via psql and its row is in `_sqlx_migrations` before the build, the compiled binary will not contain migration 026 — then at runtime, finding a row for version 26 with no matching embedded migration causes a fatal startup error. The fix: delete the pre-applied `_sqlx_migrations` row(s), rebuild with `SQLX_OFFLINE=true`, let the server apply them naturally. ## Write idempotent SQL All migrations use `IF NOT EXISTS` or `IF EXISTS` forms: ```sql -- Tables CREATE TABLE IF NOT EXISTS policy_checks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ... ); -- Columns ALTER TABLE agents ADD COLUMN IF NOT EXISTS update_channel TEXT CHECK (update_channel IN ('stable', 'beta')); ``` This protects against the "table already exists" error if a migration is somehow applied twice, and allows the migration to be run safely during development resets. ## SQLX_OFFLINE build environment `SQLX_OFFLINE=true` is set permanently in `/home/guru/.cargo/env` on Saturn. All cargo builds by the `guru` user use the `.sqlx/` offline cache rather than querying the live DB at compile time. This eliminates the proc macro/DB interaction entirely. After any schema change that adds or modifies a `query!()` macro, re-run: ```bash cd /home/guru/gururmm/server && cargo sqlx prepare git add server/.sqlx && git commit -m "build: update sqlx offline query cache" ``` ## Recovery from _sqlx_migrations mismatch If the server fails to start with "migration N was previously applied but is missing in the resolved migrations": ```bash # Option 1: Delete the row (if the migration was manually applied and tables exist) PGPASSWORD= psql -h localhost -U gururmm -d gururmm \ -c "DELETE FROM _sqlx_migrations WHERE version IN (N);" # Then rebuild so the binary embeds the migration # Option 2: If checksum mismatch (binary embedded wrong content) # Fix the SQL file, rerun cargo sqlx prepare, rebuild, deploy ``` Never delete `_sqlx_migrations` rows for migrations that the current binary does NOT embed — those rows protect against re-running already-applied migrations.