Skip to main content

Database Migrations

Forja uses SQLx for database migrations. Migrations are plain SQL files that run in order to create and modify the database schema.

Migration Files

Migration files are stored in backend/migrations/ and follow a naming convention:

backend/migrations/
├── 20240101000000_extensions_and_enums.sql
├── 20240101000001_core_infrastructure.sql
├── 20240101000002_media.sql
├── 20240101000003_content.sql
├── 20240101000004_blogs.sql
├── 20240101000005_cv.sql
├── 20240101000006_pages.sql
├── 20240101000007_legal.sql
├── 20240101000008_social_navigation.sql
└── ...

The timestamp prefix determines the order in which migrations run. SQLx tracks which migrations have been applied in a _sqlx_migrations table.

Creating a New Migration

Use the SQLx CLI to create a new migration:

cd backend
sqlx migrate add create_bookmarks

This creates a new file like migrations/20260228120000_create_bookmarks.sql. Edit it with your SQL:

-- Create bookmarks table
CREATE TABLE bookmarks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
url TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Index for site lookups
CREATE INDEX idx_bookmarks_site_id ON bookmarks(site_id);

-- Updated-at trigger
CREATE TRIGGER set_bookmarks_updated_at
BEFORE UPDATE ON bookmarks
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

Running Migrations

Automatic (on Application Start)

Migrations run automatically when the Forja application starts. No manual step is required in production. The application checks the _sqlx_migrations table and applies any pending migrations before accepting requests.

Manual (CLI)

To run migrations manually using the SQLx CLI:

cd backend
sqlx migrate run

This requires the DATABASE_URL environment variable to be set.

Installing the SQLx CLI

If you do not have the SQLx CLI installed:

cargo install sqlx-cli --no-default-features --features postgres

Reversible Migrations

For reversible migrations, create a separate "down" migration by adding --reversible when creating:

sqlx migrate add --reversible create_bookmarks

This creates two files:

  • migrations/<timestamp>_create_bookmarks.up.sql
  • migrations/<timestamp>_create_bookmarks.down.sql

Write the rollback SQL in the .down.sql file:

-- Down migration
DROP TABLE IF EXISTS bookmarks;

To revert the last migration:

sqlx migrate revert
caution

Reversible migrations are useful during development but should be used carefully in production. Dropping tables or columns can cause data loss.

Test Database

Integration tests use a separate test database to avoid polluting the development database. The test database URL is configured via the TEST_DATABASE_URL environment variable.

Ensure the test database has the required PostgreSQL extensions:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "citext";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";

The CI pipeline creates the test database and extensions automatically before running integration tests.

Best Practices

  1. Never modify an existing migration that has been applied to any environment. Always create a new migration for schema changes.

  2. Use IF NOT EXISTS and IF EXISTS guards where appropriate to make migrations idempotent:

    CREATE TABLE IF NOT EXISTS bookmarks (...);
    CREATE INDEX IF NOT EXISTS idx_bookmarks_site_id ON bookmarks(site_id);
  3. Include foreign key constraints to maintain referential integrity:

    site_id UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE
  4. Add indexes for columns used in WHERE clauses, JOINs, or ORDER BY:

    CREATE INDEX idx_bookmarks_site_id ON bookmarks(site_id);
  5. Use the update_updated_at_column() trigger (defined in the base migrations) to auto-update updated_at timestamps.

Required PostgreSQL Extensions

Forja requires three PostgreSQL extensions. These are created by the first migration (20240101000000_extensions_and_enums.sql), but on managed PostgreSQL services they may need to be created manually:

ExtensionPurpose
uuid-osspUUID generation functions (uuid_generate_v4())
citextCase-insensitive text type for slugs and emails
pg_trgmTrigram matching for full-text search