# 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/_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 -- ` | 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.