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.
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/#{}_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, = {})
Parameters:
schema- Schema objectoptions[:table_name]- Override table nameoptions[: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, = {})
Parameters:
old_schema- Previous schema versionnew_schema- New schema versionoptions[:safe_mode]- Generate production-safe migrationoptions[:migration_name]- Custom migration class name
Returns: String containing migration code
MigrationGenerator.from_files
FakeDataDSL::MigrationGenerator.from_files(schemas, = {})
Parameters:
schemas- Array of Schema objectsoptions[:output_dir]- Output directoryoptions[: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. = 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