- Drop first/second-person voice; reference-style prose throughout - Remove stale information; align with current code (argon2id primary, hybrid cookie/Bearer auth, sliding 24h idle, AES-256-GCM PHI at rest, backup codes, node-pg-migrate, collation-drift guard, multi-arch Docker, auto-version pipeline) - Preserve all technical accuracy and code examples - Remove any remaining references to separate PedsHub Quiz app - Keep consistent tone across files (tables + code blocks, imperatives where needed) - api-reference.md and developer-guide.md route tables expanded to reflect current routes (billing, sessions)
93 lines
2.9 KiB
Markdown
93 lines
2.9 KiB
Markdown
# 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.
|