Database Migrations
Database migrations provide version control for your database schema, allowing you to track and manage changes to your database structure over time. They enable teams to synchronize database changes and provide a way to roll back changes when needed.
Creating new migration files
To create new migration file run this command:
npm run pdev generate migration new_migration_filename
This will create a new migraiton file src/database/migrations/DATE_MICROTIME_new_migration_filename.ts
What are Migrations?
Migrations are classes that define changes to be made to your database schema. Each migration has:
- An
up()method that applies the changes - A
down()method that reverts the changes
Basic Migration Structure
import { Migration, Schema } from "@devbro/neko-sql";
export class CreateUsersTable extends Migration {
async up(schema: Schema): Promise<void> {
await schema.createTable("users", (table) => {
table.id();
table.timestamps();
table.string("name");
table.string("email").unique();
table.boolean("active").default(true);
});
}
async down(schema: Schema): Promise<void> {
await schema.dropTable("users");
}
}
Migration with Foreign Keys
export class CreatePostsTable extends Migration {
async up(schema: Schema): Promise<void> {
await schema.createTable("posts", (table) => {
table.id();
table.timestamps();
table.string("title");
table.text("content");
table.integer("user_id");
table.boolean("published").default(false);
table.foreign("user_id").references("id").on("users").onDelete("cascade");
table.index("user_id");
table.index("published");
});
}
async down(schema: Schema): Promise<void> {
await schema.dropTable("posts");
}
}
Alter Table Migration
export class AddPhoneToUsers extends Migration {
async up(schema: Schema): Promise<void> {
await schema.alterTable("users", (table) => {
table.string("phone_number", 20).nullable();
table.index("phone_number");
});
}
async down(schema: Schema): Promise<void> {
await schema.alterTable("users", (table) => {
table.dropColumn("phone_number");
});
}
}
Creating Tables
Basic Table
await schema.createTable("users", (table) => {
table.id(); // Auto-increment primary key
table.timestamps(); // created_at and updated_at
table.string("name");
table.string("email").unique();
table.boolean("active").default(true);
});
Column Types
await schema.createTable("products", (table) => {
// Auto-increment ID
table.id();
// String types
table.string("name", 255); // VARCHAR(255)
table.text("description"); // TEXT
table.char("code"); // CHAR
// Numeric types
table.integer("quantity"); // INTEGER
table.float("price"); // FLOAT
table.double("weight"); // DOUBLE PRECISION
// Boolean
table.boolean("in_stock"); // BOOLEAN
// Date/Time
table.date("manufactured_date"); // DATE
table.timestamp("sold_at"); // TIMESTAMP
table.timestampTz("delivered_at"); // TIMESTAMP WITH TIME ZONE
table.datetime("checked_at"); // Alias for timestamp
table.datetimeTz("verified_at"); // Alias for timestampTz
// JSON
table.json("metadata"); // JSON
table.jsonb("settings"); // JSONB (PostgreSQL)
// Timestamps
table.timestamps(); // created_at, updated_at
// raw, whatever custom column you want to create
table.raw("area GEOGRAPHY(POLYGON, 4326)");
});
Column Modifiers
await schema.createTable("posts", (table) => {
table.id();
// Nullable column
table.string("subtitle").nullable();
// NOT NULL (default)
table.string("title"); // NOT NULL by default
// Default value
table.integer("views").default(0);
table.boolean("published").default(false);
table.string("status").default("draft");
// Unique constraint
table.string("slug").unique();
// Multiple modifiers
table.string("email").length(200).unique().nullable(false);
});
Primary Keys
await schema.createTable("composite_keys", (table) => {
table.integer("user_id");
table.integer("role_id");
// Composite primary key
table.primary(["user_id", "role_id"]);
});
Foreign Keys
await schema.createTable("posts", (table) => {
table.id();
table.integer("user_id");
table.integer("category_id");
// Foreign key with cascade
table
.foreign("user_id")
.references("id")
.on("users")
.onDelete("cascade")
.onUpdate("cascade");
// Foreign key with restrict
table
.foreign("category_id")
.references("id")
.on("categories")
.onDelete("restrict")
.onUpdate("restrict");
});
// onDelete/onUpdate options:
// - 'cascade': Delete/update child records
// - 'set null': Set child column to NULL
// - 'restrict': Prevent deletion/update
// - 'no action': Similar to restrict
Indexes
You can add indexes to your tables for better query performance:
await schema.createTable("articles", (table) => {
table.id();
table.string("title");
table.string("slug");
table.text("content");
table.string("author");
table.string("category");
// Basic index
table.index("title");
// Named index
table.index("slug", "idx_article_slug");
// Unique index
table.unique("slug");
// Composite index
table.index(["author", "category"]);
// Index with custom type
table.index("content").type("gin"); // For PostgreSQL full-text search
// Multiple indexes
table.index("author");
table.index("category");
table.unique(["author", "slug"]);
});
// Index types (PostgreSQL):
// - 'btree': Default, good for equality and range queries
// - 'hash': For equality comparisons
// - 'gin': For full-text search, JSONB
// - 'gist': For geometric data
// - 'spgist': Space-partitioned GiST
// - 'brin': Block Range INdexes
Modifying Tables
Add Columns
await schema.alterTable("users", (table) => {
table.string("phone_number").nullable();
table.date("birth_date");
table.text("bio");
});
Drop Columns
await schema.alterTable("users", (table) => {
table.dropColumn("temporary_field");
table.dropColumn("old_column");
});
Add Indexes to Existing Table
await schema.alterTable("users", (table) => {
table.index("email");
table.index(["first_name", "last_name"], "idx_full_name");
});
Add Column with Index
await schema.alterTable("posts", (table) => {
table.string("slug").unique();
table.index("slug"); // Separate index on the same column
});
Dropping Tables
// Drop table
await schema.dropTable("old_table");
// Drop table if exists
await schema.dropTableIfExists("temp_table");
Table Information
// Get all tables
const tables = await schema.tables();
console.log(tables);
// Check if table exists
const exists = await schema.tableExists("users");
console.log(exists); // true or false
Refresh Migrations
Clean up the database by undoing all migrations, then reapply all available migrations:
npm run pdev migrate --refresh
Fresh Migrations
Drop and recreate the database, then run all migrations (use with caution as this is not recoverable!):
npm run pdev migrate --fresh
Migration Best Practices
- Always Write Down Methods: Every migration should have a corresponding down method that can undo the changes:
- Use Descriptive Migration Names: it will help when you want to find relevant migration.
- Handle Foreign Key Dependencies: When creating tables with foreign keys, ensure the referenced tables exist.
- Use Transactions for Multiple Operations and carefully: If you have multiple operations in the same migration file, it can be helpful to use transactions in case of failure. Otherwise, in cases of partial migration, you will need to manually undo the changes.
- Test Your Migrations: Always test both up and down methods.