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, options = {})

Parameters:

  • model - ActiveRecord model class
  • options - 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!(options = {})

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
)

See Also