Skip to content

Commit 4e26133

Browse files
authored
docs: update migration documentation to reflect registry pattern (#2362)
1 parent 6c9d84a commit 4e26133

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

CLAUDE.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,25 @@ When adding a new user-configurable setting:
9090

9191
## Database
9292

93-
- This project has three database backends: SQLite, PostgreSQL, and MySQL. When modifying migrations or schema, always update ALL three backend baseline migrations consistently. Check column names, table names, and constraints match across all backends.
93+
- This project has three database backends: SQLite, PostgreSQL, and MySQL.
94+
- Schema definitions live in `src/db/schema/` — one file per domain, with separate table definitions per backend (SQLite/PostgreSQL/MySQL).
95+
- When modifying schema, ensure column names and types are consistent across all three backend definitions.
96+
97+
### Migration Registry System
98+
99+
Migrations use a centralized registry in `src/db/migrations.ts`. Each migration is registered with functions for all three backends.
100+
101+
**Adding a new migration:**
102+
1. Create `src/server/migrations/NNN_description.ts` with:
103+
- `export const migration = { up: (db: Database) => {...} }` for SQLite
104+
- `export async function runMigrationNNNPostgres(client)` for PostgreSQL
105+
- `export async function runMigrationNNNMysql(pool)` for MySQL
106+
2. Register it in `src/db/migrations.ts` with `registry.register({ number, name, settingsKey, sqlite, postgres, mysql })`
107+
3. Update `src/db/migrations.test.ts` (count, last migration name)
108+
4. Make migrations **idempotent** — use try/catch for SQLite (`duplicate column`), `IF NOT EXISTS` for PostgreSQL, `information_schema` checks for MySQL
109+
5. **Column naming**: SQLite uses `snake_case`, PostgreSQL/MySQL use `camelCase` (quoted in PG raw SQL)
110+
111+
**Current migration count:** 13 (latest: `013_add_audit_log_missing_columns`)
94112

95113
## Testing
96114

docs/ARCHITECTURE_LESSONS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,79 @@ async updateNodeAsync(nodeNum: number, data: Partial<DbNode>): Promise<void> {
660660
- Validate data integrity with row counts
661661
- Provide verbose logging for troubleshooting
662662

663+
### Migration Registry System
664+
665+
**Problem**: The old migration system required adding migration calls in 3 separate places in `database.ts` (SQLite init, Postgres init, MySQL init), which was error-prone and hard to maintain.
666+
667+
**Solution**: Centralized `MigrationRegistry` in `src/db/migrations.ts`. Each migration is registered once with functions for all three backends.
668+
669+
**Architecture**:
670+
```
671+
src/db/
672+
migrations.ts # Registry barrel - imports and registers all migrations
673+
migrationRegistry.ts # MigrationRegistry class (runner logic)
674+
src/server/migrations/
675+
001_v37_baseline.ts # v3.7 baseline (selfIdempotent)
676+
002_*.ts - 013_*.ts # Incremental migrations
677+
```
678+
679+
**Pattern for new migrations** (e.g., migration 014):
680+
```typescript
681+
// src/server/migrations/014_description.ts
682+
import type { Database } from 'better-sqlite3';
683+
import { logger } from '../../utils/logger.js';
684+
685+
// SQLite
686+
export const migration = {
687+
up: (db: Database): void => {
688+
try {
689+
db.exec('ALTER TABLE foo ADD COLUMN bar TEXT');
690+
} catch (e: any) {
691+
if (e.message?.includes('duplicate column')) {
692+
logger.debug('foo.bar already exists, skipping');
693+
} else { throw e; }
694+
}
695+
},
696+
down: (_db: Database): void => {}
697+
};
698+
699+
// PostgreSQL
700+
export async function runMigration014Postgres(client: import('pg').PoolClient): Promise<void> {
701+
await client.query('ALTER TABLE foo ADD COLUMN IF NOT EXISTS bar TEXT');
702+
}
703+
704+
// MySQL
705+
export async function runMigration014Mysql(pool: import('mysql2/promise').Pool): Promise<void> {
706+
const [rows] = await pool.query(`
707+
SELECT COLUMN_NAME FROM information_schema.COLUMNS
708+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'foo' AND COLUMN_NAME = 'bar'
709+
`);
710+
if (!Array.isArray(rows) || rows.length === 0) {
711+
await pool.query('ALTER TABLE foo ADD COLUMN bar TEXT');
712+
}
713+
}
714+
```
715+
716+
Then register in `src/db/migrations.ts`:
717+
```typescript
718+
import { migration as descriptionMigration, runMigration014Postgres, runMigration014Mysql } from '../server/migrations/014_description.js';
719+
720+
registry.register({
721+
number: 14,
722+
name: 'description',
723+
settingsKey: 'migration_014_description',
724+
sqlite: (db) => descriptionMigration.up(db),
725+
postgres: (client) => runMigration014Postgres(client),
726+
mysql: (pool) => runMigration014Mysql(pool),
727+
});
728+
```
729+
730+
**Key Rules**:
731+
- Migration 001 is `selfIdempotent` (detects existing v3.7+ databases). All others use `settingsKey` for tracking.
732+
- Migrations MUST be idempotent: SQLite uses try/catch (`duplicate column`), PostgreSQL uses `IF NOT EXISTS`, MySQL uses `information_schema` checks.
733+
- Column naming: SQLite uses `snake_case`, PostgreSQL/MySQL use `camelCase` (quoted `"camelCase"` in raw PG SQL).
734+
- Update `src/db/migrations.test.ts` when adding migrations (count, last migration assertions).
735+
663736
### Test Mocking for Multi-Database
664737

665738
**Problem**: Tests that mock DatabaseService fail when auth middleware calls async methods.

0 commit comments

Comments
 (0)