ActiveRecord Schema Inference
FakeDataDSL can automatically infer DSL schemas from your ActiveRecord models, eliminating the need to manually define schemas for existing database tables.
Quick Start
# Infer schema from a single model
schema = FakeDataDSL::ActiveRecordInference.infer(User)
# Infer schemas from all models
FakeDataDSL::ActiveRecordInference.discover_all!
# Generate fake data directly from model
user_data = FakeDataDSL.from_model(User).generate
Basic Usage
Single Model Inference
# Given an ActiveRecord model:
class User < ApplicationRecord
# Table: users
# Columns: id (uuid), name (string), email (string),
# age (integer), active (boolean), created_at (datetime)
end
# Infer the schema
schema = FakeDataDSL::ActiveRecordInference.infer(User)
# The inferred schema is equivalent to:
# User:
# id: uuid
# name: name
# email: email
# age: number
# active: boolean
# created_at: timestamp
Generate from Model
# Direct generation (no schema file needed)
user = FakeDataDSL.from_model(User).generate
# => { id: "550e8400-...", name: "John Doe", email: "john@example.com", ... }
# Generate multiple
users = FakeDataDSL.from_model(User).generate_many(10)
# With overrides
admin = FakeDataDSL.from_model(User).generate(role: "admin")
Smart Type Inference
Column Name Patterns
The inference engine uses column names to determine appropriate types:
| Column Pattern | Inferred Type |
|---|---|
*email* |
email |
*phone*, *mobile* |
phone |
*url*, *website*, *link* |
url |
*password* |
password |
*name* |
name |
*first_name* |
first_name |
*last_name* |
last_name |
*address* |
address |
*city* |
city |
*country* |
country |
*zip*, *postal* |
zip_code |
*latitude*, *lat* |
latitude |
*longitude*, *lng* |
longitude |
*avatar*, *image*, *photo* |
image_url |
*description*, *bio*, *about* |
paragraph |
*title*, *subject* |
sentence |
*company*, *organization* |
company |
*ip* |
ip_address |
*token*, *api_key* |
secure_token |
*uuid* |
uuid |
*slug* |
slug |
*color* |
hex_color |
Column Type Mapping
| ActiveRecord Type | DSL Type |
|---|---|
string |
text |
text |
paragraph |
integer |
number |
bigint |
number |
float |
float |
decimal |
money |
boolean |
boolean |
date |
date |
datetime |
timestamp |
time |
time |
uuid |
uuid |
json, jsonb |
json |
binary |
binary |
inet |
ip_address |
Association Inference
Belongs To
class Order < ApplicationRecord
belongs_to :user
belongs_to :product
end
schema = FakeDataDSL::ActiveRecordInference.infer(Order)
# Generates:
# Order:
# user_id: Ref(User.id)
# product_id: Ref(Product.id)
Has Many / Has One
class User < ApplicationRecord
has_many :orders
has_one :profile
end
# By default, has_many/has_one are not included in the schema
# Use options to include them:
schema = FakeDataDSL::ActiveRecordInference.infer(User, include_associations: true)
# Generates:
# User:
# orders: array(Order, 0..5)
# profile: Profile?
Polymorphic Associations
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
schema = FakeDataDSL::ActiveRecordInference.infer(Comment)
# Generates:
# Comment:
# commentable_type: enum(Post, Article, Video)
# commentable_id: uuid
Validation Inference
Presence Validations
class User < ApplicationRecord
validates :name, presence: true
validates :bio, presence: false # Optional
end
schema = FakeDataDSL::ActiveRecordInference.infer(User)
# Generates:
# User:
# name: name # Required (no ?)
# bio: paragraph? # Optional (has ?)
Numericality Validations
class Product < ApplicationRecord
validates :price, numericality: { greater_than: 0, less_than: 10000 }
validates :quantity, numericality: { only_integer: true, in: 0..1000 }
end
schema = FakeDataDSL::ActiveRecordInference.infer(Product)
# Generates:
# Product:
# price: money(range: 0.01..9999.99)
# quantity: number(0..1000)
Inclusion Validations
class User < ApplicationRecord
validates :role, inclusion: { in: %w[user admin moderator] }
validates :status, inclusion: { in: %w[active inactive pending] }
end
schema = FakeDataDSL::ActiveRecordInference.infer(User)
# Generates:
# User:
# role: enum(user, admin, moderator)
# status: enum(active, inactive, pending)
Length Validations
class Post < ApplicationRecord
validates :title, length: { minimum: 5, maximum: 100 }
validates :body, length: { minimum: 50 }
end
schema = FakeDataDSL::ActiveRecordInference.infer(Post)
# Generates:
# Post:
# title: text(length: 5..100)
# body: paragraph(min_length: 50)
Format Validations
class User < ApplicationRecord
validates :username, format: { with: /\A[a-z0-9_]+\z/ }
end
schema = FakeDataDSL::ActiveRecordInference.infer(User)
# Generates:
# User:
# username: regex("[a-z0-9_]+")
Enum Inference
class Order < ApplicationRecord
enum status: { pending: 0, processing: 1, shipped: 2, delivered: 3 }
enum priority: [:low, :medium, :high]
end
schema = FakeDataDSL::ActiveRecordInference.infer(Order)
# Generates:
# Order:
# status: enum(pending, processing, shipped, delivered)
# priority: enum(low, medium, high)
Discovery Options
Full Discovery
# Discover all models in app/models
FakeDataDSL::ActiveRecordInference.discover_all!
# With options
FakeDataDSL::ActiveRecordInference.discover_all!(
include_sti: true, # Include STI subclasses
include_concerns: false, # Skip models with only concerns
exclude: [AdminUser, Audit], # Skip specific models
namespace: "App::Models" # Only discover in namespace
)
Single Model Options
schema = FakeDataDSL::ActiveRecordInference.infer(User,
include_associations: true, # Include has_many, has_one
include_validations: true, # Infer from validations
include_timestamps: true, # Include created_at, updated_at
skip_columns: [:password_digest, :encrypted_password],
custom_mappings: {
"legacy_id" => "number",
"metadata" => "json"
}
)
Custom Type Mappings
Global Mappings
# config/initializers/fake_data_dsl.rb
FakeDataDSL::ActiveRecordInference.configure do |config|
# Add custom column name patterns
config.column_patterns = {
/ssn|social_security/i => "ssn",
/iban/i => "iban",
/credit_card/i => "credit_card"
}
# Add custom type mappings
config.type_mappings = {
"citext" => "text",
"hstore" => "json"
}
end
Per-Model Mappings
schema = FakeDataDSL::ActiveRecordInference.infer(User,
custom_mappings: {
"legacy_status" => "enum(active, inactive)",
"settings" => "json"
}
)
Saving Inferred Schemas
To File
# Save single schema
schema = FakeDataDSL::ActiveRecordInference.infer(User)
schema.save_to("db/schemas/user.dsl")
# Save all discovered schemas
FakeDataDSL::ActiveRecordInference.discover_all!
FakeDataDSL.save_all_schemas("db/schemas/")
Generated Schema Format
# db/schemas/user.dsl (auto-generated)
# Generated from User model on 2026-01-24
# Do not edit manually - regenerate with:
# rails generate fake_data_dsl:schema User
User:
@inferred_from ActiveRecord::User
id: uuid
name: name
email: email @unique
role: enum(user, admin, moderator)
active: boolean(true:90%)
profile_id: Ref(Profile.id)?
created_at: timestamp
updated_at: timestamp
Rails Generator
Generate Schema from Model
# Generate schema for User model
rails generate fake_data_dsl:schema User
# Generate for multiple models
rails generate fake_data_dsl:schema User Order Product
# Generate for all models
rails generate fake_data_dsl:schema --all
# With options
rails generate fake_data_dsl:schema User --include-associations --skip-timestamps
Generator Options
| Option | Description |
|---|---|
--include-associations |
Include has_many/has_one |
--skip-timestamps |
Exclude created_at/updated_at |
--skip-validations |
Don't infer from validations |
--output-dir DIR |
Output directory (default: db/schemas) |
--force |
Overwrite existing files |
Bidirectional Sync
Schema → Model Comparison
# Compare inferred schema with existing schema file
diff = FakeDataDSL::ActiveRecordInference.compare(User, "db/schemas/user.dsl")
diff.added_fields # Fields in model but not schema
diff.removed_fields # Fields in schema but not model
diff.changed_fields # Fields with different types
Auto-Update Schemas
# Update schema file to match model
FakeDataDSL::ActiveRecordInference.sync!(User, "db/schemas/user.dsl")
# Sync all schemas
FakeDataDSL::ActiveRecordInference.sync_all!("db/schemas/")
Rake Tasks
# Infer schema for a model
rake fake_data_dsl:infer[User]
# Infer all models
rake fake_data_dsl:infer:all
# Compare schemas with models
rake fake_data_dsl:compare
# Sync schemas with models
rake fake_data_dsl:sync
API Reference
ActiveRecordInference.infer
FakeDataDSL::ActiveRecordInference.infer(model, = {})
Parameters:
model- ActiveRecord model classoptions- Hash of options
Options:
:include_associations- Include has_many/has_one (default: false):include_validations- Infer from validations (default: true):include_timestamps- Include created_at/updated_at (default: true):skip_columns- Array of columns to skip:custom_mappings- Hash of column => type mappings
Returns: FakeDataDSL::Schema
ActiveRecordInference.discover_all!
FakeDataDSL::ActiveRecordInference.discover_all!( = {})
Parameters:
options- Hash of options
Options:
:include_sti- Include STI subclasses (default: false):exclude- Array of models to skip:namespace- Only discover in namespace
Returns: Array of registered schema names
Best Practices
1. Version Control Inferred Schemas
# Keep generated schemas in version control
# db/schemas/user.dsl
2. Override Smart Inference
# When inference isn't perfect, add manual overrides
schema = FakeDataDSL::ActiveRecordInference.infer(User,
custom_mappings: {
"legacy_code" => "regex('[A-Z]{3}[0-9]{4}')"
}
)
3. Use Comments for Clarity
Generated schemas include comments:
# db/schemas/user.dsl
# Generated from User model
# Last updated: 2026-01-24
User:
# Primary key
id: uuid
# User identity
name: name
email: email @unique
Troubleshooting
Model Not Found
# Ensure model is loaded
Rails.application.eager_load!
# Then infer
FakeDataDSL::ActiveRecordInference.infer(User)
Wrong Type Inferred
# Use custom mapping
schema = FakeDataDSL::ActiveRecordInference.infer(User,
custom_mappings: {
"status_code" => "number(100..599)" # Not inferred as enum
}
)
Missing Associations
# Enable association inference
schema = FakeDataDSL::ActiveRecordInference.infer(User,
include_associations: true
)