Create Database Migration
SkillDatabases & dataCreate Knex database migrations for the Benefriches API. Use when adding, modifying, or removing database columns/tables. Handles schema changes (create table, add/drop/rename columns), data migrations, and updates to tableTypes.d.ts.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Create Database Migration skill
What this skill tells your AI
The instructions your AI receives, as published by incubateur-ademe/benefriches in .claude/skills/create-database-migration/SKILL.md and read by ahel’s review.
Generate timestamped Knex migrations following project conventions.
Quick Start
- Create migration:
pnpm --filter api knex:new-migration {description}- Example:
pnpm --filter api knex:new-migration add-column-email-to-users-table - Creates timestamped file in
apps/api/src/shared-kernel/adapters/sql-knex/migrations/
- Example:
- Implement
up()anddown()functions in the generated file - Update
apps/api/src/shared-kernel/adapters/sql-knex/tableTypes.d.tsif schema changes - If new table created: add table name to
tablesToCleanUpinapps/api/test/tablesToCleanUp.ts(child tables before parent tables) - Run:
pnpm --filter api knex:migrate-latest
Transaction Handling
Knex automatically wraps each migration in a transaction — the knex parameter in up()/down() is already a transaction object. Do NOT call knex.transaction() inside migrations.
// WRONG — redundant nested transaction
export async function up(knex: Knex): Promise<void> {
await knex.transaction(async (trx) => {
await trx.schema.createTable("example", (table) => { /* ... */ });
});
}
// CORRECT — knex is already a transaction
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("example", (table) => { /* ... */ });
}
To opt out of auto-transaction for a specific migration (e.g., DDL that can't run in a transaction):
export const config = { transaction: false };
File Naming
Format: {YYYYMMDDHHmmss}_{verb}-{description}.ts
| Operation | Pattern | Example |
|---|---|---|
| Create table | create-table-{name} | 20250211105813_create-table-users-features-alerts.ts |
| Add column | add-column-{name}-to-{table} or add-{name}-to-{table} | 20250915091313_add_newsletter_subscription_to_users_table.ts |
| Drop column | drop-{column}-from-{table} | 20250613111514_drop-is_friche-column-from-sites-table.ts |
| Rename column | rename-{old}-to-{new}-in-{table} | 20250729160857_rename-insee-to-city_code-in-cities-table.ts |
| Update data | update-{description} | 20250225095318_update-friche-activity-values-in-sites-table.ts |
Use kebab-case OR snake_case consistently within a single filename.
Migration Templates
Create Table
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("table_name", (table) => {
table.uuid("id").primary();
table.string("name").notNullable();
table.uuid("related_id").references("id").inTable("other_table");
table.timestamp("created_at").notNullable();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists("table_name");
}
Add Column
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.string("column_name"); // nullable by default
// OR: table.boolean("flag").defaultTo(false);
// OR: table.string("required_col").notNullable();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.dropColumn("column_name");
});
}
Drop Column
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.dropColumn("column_name");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.string("column_name").nullable();
});
// Optionally restore data if recoverable
}
Rename Column
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.renameColumn("old_name", "new_name");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.table("table_name", (table) => {
table.renameColumn("new_name", "old_name");
});
}
Data Migration
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex("table_name")
.whereIn("column", ["old_value1", "old_value2"])
.update({ column: "new_value" });
}
export async function down(knex: Knex): Promise<void> {
await knex("table_name")
.where("column", "new_value")
.update({ column: "old_value1" }); // Best effort
}
Complex Data Migration (JSON columns)
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
const rows = await knex("table_name")
.select("id", "json_column")
.whereRaw(`json_column::json->>'field' IS NOT NULL`);
for (const row of rows) {
const data = row.json_column as Record<string, unknown>;
const updated = { ...data, newField: transformValue(data.oldField) };
delete (updated as Record<string, unknown>).oldField;
await knex("table_name").update({ json_column: updated }).where({ id: row.id });
}
}
export async function down(): void {
return; // Data migration not reversible
}
Column Types Reference
| Knex Method | PostgreSQL | TypeScript |
|---|---|---|
table.uuid("id") | UUID | string |
table.string("name") | VARCHAR(255) | string |
table.text("desc") | TEXT | string |
table.boolean("flag") | BOOLEAN | boolean |
table.integer("count") | INTEGER | number |
table.float("amount") | REAL | number |
table.timestamp("at") | TIMESTAMP | Date |
table.json("data") | JSON | Record<string, unknown> |
tableTypes.d.ts Updates
After schema changes, update apps/api/src/shared-kernel/adapters/sql-knex/tableTypes.d.ts:
// Add new type for new table
type SqlNewTable = {
id: string;
name: string;
created_at: Date;
optional_col: string | null; // nullable columns use | null
};
// Register in Tables interface
declare module "knex/types/tables" {
interface Tables {
new_table: SqlNewTable; // table_name: SqlType
}
}
Rules:
- Use
snake_casefor column names (matches DB) - Use
| nullfor nullable columns (not?:) - Use
Datefor timestamps (Knex converts) - Register table in
Tablesinterface
Checklist
- Migration created with
pnpm --filter api knex:new-migration {description} -
up()implements forward migration -
down()reverses migration (or returns void if not possible) -
tableTypes.d.tsupdated for schema changes (new table → addSqlXxxtype + register inTablesinterface) - If new table created: add table name to
tablesToCleanUparray inapps/api/test/tablesToCleanUp.ts(respecting deletion order: child tables before parent tables) - Migration tested:
pnpm --filter api knex:migrate-latest - Rollback tested:
pnpm --filter api knex:migrate-rollback
Signals
- GitHub stars
- 45
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
create-database-migration- Source
- github.com/incubateur-ademe/benefriches