Schema Migration Generator

FakeDataDSL can generate ActiveRecord migrations from your DSL schemas, keeping your database in sync with your data definitions. When schemas evolve, generate diff-based migrations automatically.

Quick Start

# Generate migration from schema
fake_data_dsl migrate schemas/user.dsl -o db/migrate/

# Generate diff migration between versions
fake_data_dsl migrate --diff schemas/v1/user.dsl schemas/v2/user.dsl -o db/migrate/
# Ruby API
schema = FakeDataDSL.load("schemas/user.dsl")
migration = FakeDataDSL::MigrationGenerator.create_table(schema)
puts migration

Basic Usage

Generate Create Table Migration

# Input schema (user.dsl)
# User:
#   id: uuid
#   name: name
#   email: email @unique
#   role: enum(user, admin, moderator)
#   active: boolean
#   profile: Profile?
#   created_at: timestamp
#   updated_at: timestamp

schema = FakeDataDSL.load("schemas/user.dsl")
migration = FakeDataDSL::MigrationGenerator.create_table(schema)

Output:

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users, id: :uuid do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :role, null: false, default: 'user'
      t.boolean :active, null: false, default: true
      t.references :profile, type: :uuid, foreign_key: true

      t.timestamps
    end

    add_index :users, :email, unique: true
    add_index :users, :role
  end
end

Generate from Multiple Schemas

schemas = FakeDataDSL.load_all("schemas/")
migrations = FakeDataDSL::MigrationGenerator.from_files(schemas)

migrations.each do |name, content|
  File.write("db/migrate/#{timestamp}_create_#{name}.rb", content)
end

Type Mapping

DSL to ActiveRecord Types

DSL Type ActiveRecord Type Column Options
uuid :uuid primary key or reference
text :string
paragraph :text
number :integer
number(min..max) :integer limit based on range
float :float
money :decimal precision: 10, scale: 2
boolean :boolean
date :date
timestamp :datetime
time :time
email :string with email index
phone :string
url :string
ip_address :inet (PostgreSQL)
json :jsonb (PostgreSQL)
enum(...) :string with check constraint
array(...) :string[] (PostgreSQL) or :text

Custom Type Mappings

FakeDataDSL::MigrationGenerator.configure do |config|
  config.type_mappings = {
    "money" => { type: :decimal, precision: 15, scale: 4 },
    "slug" => { type: :string, limit: 255 },
    "ip_address" => { type: :inet }  # PostgreSQL only
  }
end

Constraints and Indexes

From Annotations

# In DSL
User:
  id: uuid
  email: email @unique
  username: text @unique @indexed
  role: enum(user, admin) @indexed
  tenant_id: uuid @indexed

Output includes:

add_index :users, :email, unique: true
add_index :users, :username, unique: true
add_index :users, :role
add_index :users, :tenant_id

Composite Indexes

# In DSL
Order:
  user_id: Ref(User.id) @indexed
  created_at: timestamp

  @index [:user_id, :created_at]

Output:

add_index :orders, [:user_id, :created_at]

Foreign Keys

# In DSL
Order:
  user_id: Ref(User.id)
  product_id: Ref(Product.id)

Output:

t.references :user, type: :uuid, foreign_key: true, null: false
t.references :product, type: :uuid, foreign_key: true, null: false

Schema Evolution

Diff-Based Migrations

When your schema changes, generate a migration for just the differences:

old_schema = FakeDataDSL.load("schemas/v1/user.dsl")
new_schema = FakeDataDSL.load("schemas/v2/user.dsl")

migration = FakeDataDSL::MigrationGenerator.diff(old_schema, new_schema)

Example diff:

# v1
User:
  id: uuid
  name: name
  email: email

# v2 (added avatar_url, removed legacy_field, changed age type)
User:
  id: uuid
  name: name
  email: email
  avatar_url: url
  age: number  # was: text

Generated migration:

class MigrateUsersV1ToV2 < ActiveRecord::Migration[7.1]
  def change
    # Added columns
    add_column :users, :avatar_url, :string

    # Changed columns
    change_column :users, :age, :integer

    # Removed columns
    remove_column :users, :legacy_field, :string
  end
end

Safe Migrations

For production-safe migrations:

migration = FakeDataDSL::MigrationGenerator.diff(old_schema, new_schema,
  safe_mode: true
)

Output with safety:

class MigrateUsersV1ToV2 < ActiveRecord::Migration[7.1]
  # Disable DDL transactions for safety
  disable_ddl_transaction!

  def change
    # Added columns (safe - no lock)
    add_column :users, :avatar_url, :string

    # Changed columns (may lock - add concurrently)
    safety_assured do
      change_column :users, :age, :integer
    end

    # Removed columns (safe - just marks as ignored)
    safety_assured do
      remove_column :users, :legacy_field, :string
    end
  end
end

CLI Commands

Create Table

# Single schema
fake_data_dsl migrate schemas/user.dsl -o db/migrate/

# All schemas in directory
fake_data_dsl migrate schemas/ --all -o db/migrate/

# With custom timestamp
fake_data_dsl migrate schemas/user.dsl -o db/migrate/ --timestamp 20260124120000

Diff Migration

# From two files
fake_data_dsl migrate --diff schemas/v1/user.dsl schemas/v2/user.dsl

# From git history
fake_data_dsl migrate --diff HEAD~1:schemas/user.dsl schemas/user.dsl

# Safe mode for production
fake_data_dsl migrate --diff old.dsl new.dsl --safe

Options

Option Description
-o, --output DIR Output directory
--timestamp TIME Migration timestamp
--safe Generate safe migrations
--dry-run Preview without writing
--rails-version VER Rails/AR version (default: 7.1)
--db-adapter ADAPTER Database adapter (postgresql, mysql, sqlite)

Database-Specific Features

PostgreSQL

FakeDataDSL::MigrationGenerator.configure do |config|
  config.database = :postgresql
end

# Enables:
# - UUID columns without extension (native in PG 13+)
# - JSONB columns
# - Array columns
# - Inet/Cidr columns
# - Enum types (native)

Example with PostgreSQL features:

# In DSL
Product:
  id: uuid
  tags: array(text)
  metadata: json
  status: enum(draft, published, archived) @pg_enum

# Output
class CreateProducts < ActiveRecord::Migration[7.1]
  def change
    create_enum :product_status, %w[draft published archived]

    create_table :products, id: :uuid do |t|
      t.string :tags, array: true, default: []
      t.jsonb :metadata, default: {}
      t.enum :status, enum_type: :product_status, default: 'draft'
    end
  end
end

MySQL

FakeDataDSL::MigrationGenerator.configure do |config|
  config.database = :mysql
end

# Differences:
# - UUID stored as CHAR(36)
# - JSON instead of JSONB
# - No native arrays (serialized)
# - ENUM as native MySQL enum

SQLite

FakeDataDSL::MigrationGenerator.configure do |config|
  config.database = :sqlite
end

# Differences:
# - UUID stored as TEXT
# - JSON stored as TEXT
# - No native arrays
# - ENUM as TEXT with check constraint

Rails Generator

Installation

rails generate fake_data_dsl:install

Generate Migration from Schema

# From existing schema file
rails generate fake_data_dsl:migration User

# Creates: db/migrate/TIMESTAMP_create_users.rb

Generate Migration from Model

# Infer from ActiveRecord model and create diff
rails generate fake_data_dsl:sync_migration User

# Creates migration for any differences between model and schema

API Reference

MigrationGenerator.create_table

FakeDataDSL::MigrationGenerator.create_table(schema, options = {})

Parameters:

  • schema - Schema object
  • options[:table_name] - Override table name
  • options[:id_type] - ID column type (:uuid, :bigint)
  • options[:timestamps] - Include timestamps (default: true)
  • options[:rails_version] - Rails version string

Returns: String containing migration code

MigrationGenerator.diff

FakeDataDSL::MigrationGenerator.diff(old_schema, new_schema, options = {})

Parameters:

  • old_schema - Previous schema version
  • new_schema - New schema version
  • options[:safe_mode] - Generate production-safe migration
  • options[:migration_name] - Custom migration class name

Returns: String containing migration code

MigrationGenerator.from_files

FakeDataDSL::MigrationGenerator.from_files(schemas, options = {})

Parameters:

  • schemas - Array of Schema objects
  • options[:output_dir] - Output directory
  • options[:order] - :alphabetical, :dependency (default: :dependency)

Returns: Hash of { table_name => migration_content }

Configuration

FakeDataDSL::MigrationGenerator.configure do |config|
  # Database adapter
  config.database = :postgresql  # :postgresql, :mysql, :sqlite

  # Rails/ActiveRecord version
  config.rails_version = "7.1"

  # ID column type
  config.default_id_type = :uuid  # :uuid, :bigint, :integer

  # Include timestamps by default
  config.include_timestamps = true

  # Custom type mappings
  config.type_mappings = {
    "money" => { type: :decimal, precision: 15, scale: 4 }
  }

  # Safe mode by default
  config.safe_mode = Rails.env.production?

  # Migration template (ERB)
  config.template_path = "lib/templates/migration.erb"
end

Best Practices

1. Version Your Schemas

schemas/
├── v1/
│   ├── user.dsl
│   └── order.dsl
├── v2/
│   ├── user.dsl
│   └── order.dsl
└── current/
    ├── user.dsl -> ../v2/user.dsl
    └── order.dsl -> ../v2/order.dsl

2. Review Generated Migrations

Always review before running:

# Preview first
fake_data_dsl migrate schemas/user.dsl --dry-run

# Then generate
fake_data_dsl migrate schemas/user.dsl -o db/migrate/

3. Use Safe Mode in Production

# config/initializers/fake_data_dsl.rb
FakeDataDSL::MigrationGenerator.configure do |config|
  config.safe_mode = Rails.env.production?
end

4. Keep Schema and Model in Sync

# In CI
RSpec.describe "Schema Sync" do
  it "schema matches model" do
    User.columns.each do |column|
      expect(user_schema.fields).to include(column.name)
    end
  end
end

Troubleshooting

Type Mismatch

# Error: Unknown type 'custom_type'

# Fix: Add custom mapping
FakeDataDSL::MigrationGenerator.configure do |config|
  config.type_mappings["custom_type"] = { type: :string }
end

Foreign Key Issues

# Error: Table 'profiles' doesn't exist

# Fix: Generate migrations in dependency order
migrations = FakeDataDSL::MigrationGenerator.from_files(schemas,
  order: :dependency  # Creates profiles before users
)

UUID Extension

# For PostgreSQL < 13, enable uuid-ossp extension
class EnableUuidExtension < ActiveRecord::Migration[7.1]
  def change
    enable_extension 'pgcrypto' # or 'uuid-ossp'
  end
end

See Also