# Database migrations

The project uses [node-pg-migrate](https://github.com/salsita/node-pg-migrate)
for versioned, reversible schema changes layered on top of the idempotent
baseline init in `src/db/database.js`.

## Boot sequence

1. `initDatabase()` in `src/db/database.js` — the baseline.
   - `CREATE TABLE IF NOT EXISTS` for every legacy table.
   - `ALTER TABLE ADD COLUMN IF NOT EXISTS` for every column added before the
     migration tool existed.
   - Collation-drift check + auto `REINDEX DATABASE` on mismatch.
   - `COLLATE "C"` conversion for lookup-critical indexes (gated by
     `migration.text_indexes_c` flag in `app_settings`).
2. `src/db/migrate.js` — runs every file in `/app/migrations/` that isn't
   already recorded in the `pgmigrations` table, in filename order. Each
   applied file is inserted into `pgmigrations` so it runs exactly once.

All new schema changes go in versioned migration files, not in the inline
baseline.

## Creating a migration

```bash
docker exec -w /app pediatric-ai-scribe npm run migrate:new -- add_avatar_url
```

Produces `migrations/<utc-ms>_add_avatar_url.js` with empty `up()` and
`down()`. Edit:

```js
exports.up = (pgm) => {
  pgm.addColumn('users', {
    avatar_url: { type: 'text', notNull: false }
  });
  pgm.createIndex('users', 'avatar_url');
};

exports.down = (pgm) => {
  pgm.dropIndex('users', 'avatar_url');
  pgm.dropColumn('users', 'avatar_url');
};
```

Full API: https://salsita.github.io/node-pg-migrate/

## Commands

| Command | Effect |
|---|---|
| `npm run migrate:up` | Apply all pending |
| `npm run migrate:down` | Roll back the most recent one |
| `npm run migrate:new -- <name>` | Scaffold a new file |
| `npm run migrate:status` | Dump `pgmigrations` as a table |

Or SQL directly:

```bash
docker exec pedscribe-db psql -U pedscribe -d pedscribe \
  -c "SELECT id, name, run_on FROM pgmigrations ORDER BY id;"
```

## Raw SQL inside a migration

```js
exports.up = (pgm) => {
  pgm.sql(`
    CREATE INDEX CONCURRENTLY idx_audit_action
    ON audit_log (action)
    WHERE action IN ('login', 'login_failed', 'session_idle_timeout');
  `);
};
```

`CREATE INDEX CONCURRENTLY` cannot run in a transaction. Add
`exports.disableTransaction = true;` to the file when using it.

## Conventions

- One logical change per file. No bundling unrelated alters.
- Always write `down()` unless rollback is impossible (e.g., dropping a column
  that had unique data).
- Name files by what they do (`add_foo`, `backfill_bar`), not ticket numbers.
- Filename UTC-ms prefix drives ordering across forks / PRs.
- Never edit an already-applied migration. Fix forward with a new file.

## Rollback semantics

`migrate:down` runs the file's `down()` and removes its row from
`pgmigrations`. An empty / missing `down()` still clears the row — the next
`up` reapplies the migration. Treat missing `down` as "no-op rollback" and
document it in the file header.
