---
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=<pw> 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.
