Time Travel for Data

FakeDataDSL's Time Travel feature lets you generate data as it would have existed at a specific point in time, or create realistic progressions of data over time. Perfect for testing time-sensitive features, historical reports, and temporal queries.

Quick Start

# Generate data from 30 days ago
FakeDataDSL::TimeTravel.at(30.days.ago) do
  user = FakeDataDSL.generate("User")
  # user[:created_at] is 30 days ago
  # user[:updated_at] is 30 days ago
end

# Generate data progression
orders = FakeDataDSL::TimeTravel.progression("Order", 
  from: 1.year.ago, 
  to: Time.current, 
  count: 12
)
# 12 orders spread across the past year

Basic Usage

Generate at Specific Time

# All timestamps generated within the block are relative to the travel time
FakeDataDSL::TimeTravel.at(Date.new(2025, 1, 1)) do
  user = FakeDataDSL.generate("User")
  # user[:created_at] => 2025-01-01 (random time on that day)
end

# With specific time
FakeDataDSL::TimeTravel.at(Time.new(2025, 6, 15, 14, 30, 0)) do
  event = FakeDataDSL.generate("Event")
  # event[:scheduled_at] => around 2025-06-15 14:30
end

Generate Between Dates

# Timestamps fall randomly within the range
FakeDataDSL::TimeTravel.between(1.month.ago, Time.current) do
  log = FakeDataDSL.generate("AuditLog")
  # log[:created_at] is somewhere in the last month
end

# Generate multiple records spread across range
records = FakeDataDSL::TimeTravel.between(1.year.ago, Time.current) do
  10.times.map { FakeDataDSL.generate("Transaction") }
end
# Transactions spread randomly across the year

Time Progressions

Linear Progression

Generate records evenly distributed over time:

# 12 monthly reports
reports = FakeDataDSL::TimeTravel.progression("MonthlyReport",
  from: 1.year.ago,
  to: Time.current,
  count: 12
)

reports.each_with_index do |report, i|
  puts "#{report[:created_at].strftime('%B %Y')}: $#{report[:revenue]}"
end
# January 2025: $12,345
# February 2025: $13,456
# ...

Custom Intervals

# Daily records
daily = FakeDataDSL::TimeTravel.progression("DailyMetric",
  from: 7.days.ago,
  to: Time.current,
  interval: 1.day
)

# Hourly records
hourly = FakeDataDSL::TimeTravel.progression("HourlyLog",
  from: 24.hours.ago,
  to: Time.current,
  interval: 1.hour
)

# Weekly records
weekly = FakeDataDSL::TimeTravel.progression("WeeklyDigest",
  from: 3.months.ago,
  to: Time.current,
  interval: 1.week
)

With Variations

# Add realistic variation to timestamps
orders = FakeDataDSL::TimeTravel.progression("Order",
  from: 1.month.ago,
  to: Time.current,
  count: 30,
  jitter: 6.hours  # ±6 hours variation
)

Time Series Data

Generate Time Series

# Stock prices over time
prices = FakeDataDSL::TimeTravel.time_series("StockPrice",
  from: 1.year.ago,
  to: Time.current,
  interval: 1.day,
  fields: {
    open: { type: :trending, start: 100, volatility: 0.02 },
    close: { type: :trending, start: 100, volatility: 0.02 },
    volume: { type: :random, range: 1000..10000 }
  }
)
# Metrics that grow over time
FakeDataDSL::TimeTravel.time_series("UserMetrics",
  from: 1.year.ago,
  to: Time.current,
  interval: 1.week,
  trend: :growth,      # :growth, :decline, :stable, :seasonal
  growth_rate: 0.05    # 5% weekly growth
)

Seasonal Patterns

# E-commerce orders with seasonal variation
orders = FakeDataDSL::TimeTravel.time_series("Order",
  from: 1.year.ago,
  to: Time.current,
  interval: 1.day,
  seasonality: {
    pattern: :weekly,
    peaks: [5, 6],        # Saturday, Sunday
    peak_multiplier: 2.5
  }
)

Schema Annotations

In DSL Files

# db/schemas/historical_order.dsl
HistoricalOrder:
  @time_travel -365.days..-1.day

  id: uuid
  created_at: timestamp
  shipped_at: timestamp(after: created_at, within: 7.days)
  delivered_at: timestamp(after: shipped_at, within: 14.days)

Relative Timestamps

# Timestamps that depend on each other
Order:
  id: uuid
  created_at: timestamp
  confirmed_at: timestamp(after: created_at, within: 1.hour)
  shipped_at: timestamp(after: confirmed_at, within: 3.days)
  delivered_at: timestamp(after: shipped_at, within: 7.days)

When time traveling:

FakeDataDSL::TimeTravel.at(30.days.ago) do
  order = FakeDataDSL.generate("Order")
  # order[:created_at]    => ~30 days ago
  # order[:confirmed_at]  => ~30 days ago (within 1 hour of created_at)
  # order[:shipped_at]    => ~27 days ago (within 3 days of confirmed_at)
  # order[:delivered_at]  => ~20 days ago (within 7 days of shipped_at)
end

Nested Time Contexts

Time Blocks

FakeDataDSL::TimeTravel.at(1.year.ago) do
  old_user = FakeDataDSL.generate("User")

  FakeDataDSL::TimeTravel.at(6.months.ago) do
    mid_user = FakeDataDSL.generate("User")

    FakeDataDSL::TimeTravel.at(Time.current) do
      new_user = FakeDataDSL.generate("User")
    end
  end
end

Progressive Nesting

# User signs up, then creates content over time
FakeDataDSL::TimeTravel.at(1.year.ago) do
  user = FakeDataDSL.generate("User")

  # User writes posts over the following months
  posts = (1..12).map do |month|
    FakeDataDSL::TimeTravel.at(1.year.ago + month.months) do
      FakeDataDSL.generate("Post", user_id: user[:id])
    end
  end
end

Integration with Scenarios

FakeDataDSL::Scenarios.define(:historical_activity) do
  # User from a year ago
  let(:veteran_user) {
    FakeDataDSL::TimeTravel.at(1.year.ago) do
      create(:user, name: "Veteran User")
    end
  }

  # Their posts over time
  let(:posts) {
    FakeDataDSL::TimeTravel.progression("Post",
      from: 11.months.ago,
      to: Time.current,
      count: 24
    ).map { |p| p.merge(user_id: veteran_user[:id]) }
  }

  # Recent new user
  let(:new_user) {
    FakeDataDSL::TimeTravel.at(1.week.ago) do
      create(:user, name: "New User")
    end
  }
end

Time Zone Handling

Explicit Time Zones

# Generate in specific timezone
FakeDataDSL::TimeTravel.at(Time.current, zone: "America/New_York") do
  event = FakeDataDSL.generate("Event")
  # event[:start_time] is in Eastern time
end

# Generate in user's timezone
FakeDataDSL::TimeTravel.at(Time.current, zone: user.time_zone) do
  notification = FakeDataDSL.generate("Notification")
end

Multiple Zones

# Generate events across timezones
offices = ["America/New_York", "Europe/London", "Asia/Tokyo"]
events = offices.map do |zone|
  FakeDataDSL::TimeTravel.at(Time.current, zone: zone) do
    FakeDataDSL.generate("Meeting", timezone: zone)
  end
end

Testing Patterns

Historical Data Tests

RSpec.describe "ReportGenerator" do
  it "generates monthly reports" do
    # Create historical data
    FakeDataDSL::TimeTravel.progression("Order",
      from: 3.months.ago,
      to: Time.current,
      count: 90
    ).each { |order| Order.create!(order) }

    report = ReportGenerator.monthly_summary(2.months.ago)
    expect(report.total_orders).to be > 0
  end
end

Time-Sensitive Features

RSpec.describe "SubscriptionExpiry" do
  it "expires subscriptions correctly" do
    # Create subscription that expires today
    subscription = FakeDataDSL::TimeTravel.at(31.days.ago) do
      Subscription.create!(
        FakeDataDSL.generate("Subscription", duration: 30.days)
      )
    end

    expect(subscription).to be_expired
  end
end

Aging Data

RSpec.describe "DataRetention" do
  it "purges old records" do
    # Create records at various ages
    old_record = FakeDataDSL::TimeTravel.at(2.years.ago) do
      AuditLog.create!(FakeDataDSL.generate("AuditLog"))
    end

    recent_record = FakeDataDSL::TimeTravel.at(1.month.ago) do
      AuditLog.create!(FakeDataDSL.generate("AuditLog"))
    end

    DataRetention.purge_old_records!

    expect { old_record.reload }.to raise_error(ActiveRecord::RecordNotFound)
    expect(recent_record.reload).to be_present
  end
end

API Reference

TimeTravel.at

FakeDataDSL::TimeTravel.at(time, options = {}, &block)

Parameters:

  • time - Time/DateTime/Date to travel to
  • options[:zone] - Time zone name (default: system)
  • block - Block where all timestamps are relative to time

Returns: Result of the block

TimeTravel.between

FakeDataDSL::TimeTravel.between(start_time, end_time, options = {}, &block)

Parameters:

  • start_time - Start of the time range
  • end_time - End of the time range
  • options[:distribution] - :uniform (default), :weighted_recent, :weighted_old
  • block - Block where timestamps fall within range

Returns: Result of the block

TimeTravel.progression

FakeDataDSL::TimeTravel.progression(schema_name, options = {})

Parameters:

  • schema_name - Name of the schema to generate
  • options[:from] - Start time (required)
  • options[:to] - End time (required)
  • options[:count] - Number of records
  • options[:interval] - Fixed interval between records
  • options[:jitter] - Random variation for timestamps
  • options[:overrides] - Field overrides

Returns: Array of generated records

TimeTravel.time_series

FakeDataDSL::TimeTravel.time_series(schema_name, options = {})

Parameters:

  • schema_name - Name of the schema
  • options[:from] - Start time
  • options[:to] - End time
  • options[:interval] - Time between data points
  • options[:trend] - :growth, :decline, :stable, :seasonal
  • options[:growth_rate] - Rate of change per interval
  • options[:seasonality] - Seasonal pattern configuration
  • options[:fields] - Field-specific configurations

Returns: Array of time series data points

Configuration

FakeDataDSL::TimeTravel.configure do |config|
  # Default time zone
  config.default_zone = "UTC"

  # Default jitter for progressions
  config.default_jitter = 0

  # Whether to freeze time during generation
  config.freeze_time = true

  # Hook for before/after time travel
  config.before_travel = ->(time) { Rails.logger.debug "Traveling to #{time}" }
  config.after_travel = ->(time) { Rails.logger.debug "Returned from #{time}" }
end

Best Practices

1. Use Relative Times

# Good: Relative times adapt to when tests run
FakeDataDSL::TimeTravel.at(30.days.ago) { ... }

# Avoid: Hardcoded dates become stale
FakeDataDSL::TimeTravel.at(Date.new(2025, 1, 1)) { ... }

2. Be Explicit About Relationships

# Good: Clear temporal relationships
Order:
  created_at: timestamp
  shipped_at: timestamp(after: created_at)  # Explicit dependency

# Avoid: Implicit relationships that may not hold
Order:
  created_at: timestamp
  shipped_at: timestamp  # Could be before created_at!

3. Test Edge Cases

# Test around daylight saving time
FakeDataDSL::TimeTravel.at(Time.new(2025, 3, 9, 2, 30, 0, "-05:00")) do
  # DST transition edge case
end

# Test at year boundaries
FakeDataDSL::TimeTravel.at(Time.new(2025, 12, 31, 23, 59, 59)) do
  # Year-end edge case
end

Troubleshooting

Timestamps Not Affected

# Make sure you're using the block
FakeDataDSL::TimeTravel.at(1.month.ago) do
  FakeDataDSL.generate("User")  # ✓ Affected
end

FakeDataDSL.generate("User")  # ✗ Not affected (outside block)

Time Zone Confusion

# Always be explicit when time zones matter
FakeDataDSL::TimeTravel.at(
  Time.current.in_time_zone("America/New_York"),
  zone: "America/New_York"
) do
  # Clear time zone handling
end

See Also