Skill: Database Migration

SkillDatabases & data

Changing database schema with Kysely migrations across SQLite, PostgreSQL, and MariaDB. Every migration must ship with a colocated `*.spec.ts` exercising `up`/`down` on every supported dialect, keep patch coverage ≥ 90% on changed lines, update affected query tests, and pass `make fix`, `make test-unit`, `make test-integration`, and `make test-e2e` before submission.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Skill: Database Migration skill

What this skill tells your AI

The instructions your AI receives, as published by exelearning/exelearning in .agents/skills/database-migration/SKILL.md and read by ahel’s review.

Parent: AGENTS.md | Related: backend-service, backend-route

When to Use

Changing database schema — adding tables, columns, indexes, or modifying existing structures.

Key Files

  • src/db/migrations/*.ts — numbered migration files (000_, 001_, 002_, etc.)
  • src/db/migrations/*.spec.ts — colocated migration tests
  • src/db/queries/*.ts — query functions to update after schema changes
  • src/db/client.ts — database client
  • src/db/dialect/ — multi-DB dialect support (SQLite, PostgreSQL, MariaDB)

Pattern

// src/db/migrations/00N_my_migration.ts
import { type Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
    await db.schema.createTable('my_table')
        .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
        .addColumn('name', 'varchar(255)', (col) => col.notNull())
        .addColumn('created_at', 'timestamp', (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
        .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
    await db.schema.dropTable('my_table').execute();
}

Test Pattern

Use DB_PATH=:memory: for fast, isolated tests:

import { describe, it, expect, beforeEach, afterEach } from 'bun:test';

let db: Kysely<any>;
beforeEach(async () => {
    db = createTestDb(); // in-memory SQLite
    await up(db);
});
afterEach(async () => { await db.destroy(); });

it('creates table with expected columns', async () => {
    const result = await db.selectFrom('my_table').selectAll().execute();
    expect(result).toEqual([]);
});

it('down removes the table', async () => {
    await down(db);
    // Verify table no longer exists
});

Commands

bun test src/db/migrations/00N_my_migration.spec.ts     # Run migration test
make fix                                                 # Lint

Gotchas

  • Multi-DB compatibility is non-negotiable — must work on SQLite, PostgreSQL, AND MariaDB. No DB-specific syntax (AUTOINCREMENT vs AUTO_INCREMENT, TEXT vs VARCHAR, etc.). Use Kysely's schema builder which abstracts these differences.
  • Sequential numbering — check existing files and use the next number. Gaps or duplicates break the migration runner.
  • Always provide both up() and down()down() is needed for rollbacks and testing.
  • Update query functions — after adding/modifying columns, update related functions in src/db/queries/. A migration without query updates is incomplete.
  • Never use file-based DBs in tests — always DB_PATH=:memory: for speed and isolation.
  • Soft deletes — some tables use soft deletes (deleted_at column). Queries must include WHERE deleted_at IS NULL or results will include deactivated records.

Done When

  • Migration numbered correctly (next in sequence)
  • up() and down() both implemented
  • Works across all three DB drivers (no DB-specific syntax)
  • .spec.ts colocated, tests both up() and down()
  • Related query files in src/db/queries/ updated
  • make fix passes clean

Signals

GitHub stars
142
Forks
32
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
database-migration-exelearning
Source
github.com/exelearning/exelearning