No release in over 3 years
ActiveRecord::Ghosts allows you to define a sequence column and query with ranges to get real + ghost records. Ghost records behave like AR objects but aren't persisted, perfect for filling gaps in sequences like levels, numbers, etc.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

ActiveRecord::Ghosts 👻

"Virtual rows" for ActiveRecord models. Fill in the gaps in your sequences and work with ghost records that behave like AR objects but aren't persisted.


✨ Features

  • Define a sequence column (like level, number).
  • Query with a range and get real + ghost records.
  • Chain with where / scopes.
  • Works with Enumerator for infinite series.
  • Ghost records respond to .ghost?.

🚀 Installation

Requirements:

  • Ruby 3.4.0+
  • Rails 7.2+

Add to your Gemfile:

gem "activerecord-ghosts"

and run:

bundle install

🏗 Setup

Database Schema

Your model needs an integer column for the sequence field. This column should be indexed for performance.

# Example migration
class CreateProgressLevels < ActiveRecord::Migration[7.2]
  def change
    create_table :progress_levels do |t|
      t.references :user, null: false, foreign_key: true
      t.integer :level, null: false  # ← Ghost sequence column
      t.integer :points, default: 0
      t.timestamps
    end

    # Index for performance (IMPORTANT!)
    add_index :progress_levels, [:user_id, :level], unique: true
    # OR simple index if no associations:
    # add_index :progress_levels, :level
  end
end

Model Configuration

class ProgressLevel < ApplicationRecord
  belongs_to :user

  # Enable ghosts on the integer sequence column
  has_ghosts :level, start: 1  # Optional: custom start value (default: 1)

  validates :level, presence: true, uniqueness: { scope: :user_id }
end

# Alternative start values:
class Invoice < ApplicationRecord
  has_ghosts :number, start: 1000  # Start from 1000 instead of 1
end

# Usage with custom start:
Invoice.ghosts.take(3)  # Will generate ghosts starting from 1000, 1001, 1002...

Requirements:

  • ✅ Ghost column must be integer type
  • ✅ Ghost column should have an index (composite or simple)
  • ✅ Index should have ghost column as leading column for best performance

🛠 Usage

1. Basic ghost series

# Assuming you have levels 1, 2, 5 in database
ProgressLevel.ghosts(1..6).map { |level| [level.level, level.ghost?] }
# => [[1, false], [2, false], [3, true], [4, true], [5, false], [6, true]]

2. With associations and scoping

For a specific user:

user = User.find(1)

# Get levels 1-5 for this user (mix of real + ghost records)
user.progress_levels.ghosts(1..5).each do |level|
  puts "Level #{level.level}: #{level.ghost? ? 'Missing' : 'Completed'} (#{level.points} points)"
end
# Output:
# Level 1: Completed (100 points)
# Level 2: Completed (150 points)
# Level 3: Missing (0 points)      ← Ghost inherits default values
# Level 4: Missing (0 points)      ← Ghost inherits default values
# Level 5: Missing (0 points)      ← Ghost inherits default values

# Alternative syntax:
ProgressLevel.where(user_id: user.id).ghosts(1..5)

Key insight: Ghost records automatically inherit where conditions and model defaults!


3. Infinite series

Without arguments, .ghosts returns an Enumerator that starts from the start value:

# With default start: 1
ProgressLevel.ghosts.take(3).map { |level| [level.level, level.ghost?] }
# => [[1, false], [2, false], [3, true]]

# With custom start: 1000
class Invoice < ApplicationRecord
  has_ghosts :number, start: 1000
end

Invoice.ghosts.take(3).map { |inv| [inv.number, inv.ghost?] }
# => [[1000, true], [1001, true], [1002, true]]  # Starts from 1000!

You can each, each_slice, etc. Records are lazily loaded batch by batch.


📋 Supported Column Types & Limitations

✅ Supported Ghost Columns

  • Integer columns only - t.integer :level, t.bigint :number, etc.
  • Must contain sequential numeric values (1, 2, 3... or 10, 20, 30...)
  • Works with any integer range (1..100, 0..10, -5..5)

❌ Not Supported

  • String columns (t.string :name)
  • Date/DateTime columns (t.date :created_on)
  • UUID columns (t.uuid :external_id)
  • Non-sequential data

🎯 Perfect Use Cases

# ✅ Game levels (1, 2, 3, 4, 5...)
class PlayerLevel
  has_ghosts :level  # integer column
end

# ✅ Invoice numbers (1, 2, 3... or 1000, 1001, 1002...)
class Invoice
  has_ghosts :number  # integer column
end

# ✅ Chapter numbers in a book
class Chapter
  has_ghosts :chapter_number  # integer column
end

# ❌ Don't use for non-sequential data
class User
  has_ghosts :email  # ❌ String - won't work
  has_ghosts :created_at  # ❌ DateTime - not sequential
end

⚠️ Performance note

For best performance, ensure the ghosted column (e.g. :level) has an index. If it doesn't, you'll see a warning:

[activerecord-ghosts] ⚠️ Column :level on progress_levels has no leading index. Ghost queries may be slow.

Composite indexes are fine if your ghost column is the leading column.


❓ FAQ

Q: Can I use string/UUID columns as ghost fields?

A: No, only integer columns are supported. Ghost records fill numeric gaps in sequences.

Q: Do ghost records get saved to the database?

A: No! Ghost records exist only in memory. They behave like ActiveRecord objects but .persisted? returns false.

Q: Can I modify ghost records?

A: Yes! You can call .save! on a ghost record to persist it to the database. After saving, .ghost? will return false.

Q: How do I handle gaps in my sequence?

A: That's exactly what this gem does! It fills gaps with virtual records.

# You have records [1, 2, 5, 8] in database
Model.ghosts(1..10).map(&:id)
# Returns: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Where 3, 4, 6, 7, 9, 10 are ghosts

Q: What about performance with large ranges?

A: Use the enumerator version for infinite sequences:

# ✅ Memory efficient - loads in batches
Model.ghosts.take(1000)

# ❌ Avoid large ranges - loads all at once
Model.ghosts(1..1000000)

📦 Development

Clone and setup:

git clone https://github.com/ilyacoding/activerecord-ghosts
cd activerecord-ghosts
bundle install

🚀 Development & Publishing

Local Development

bundle install
bundle exec rspec

Automated Publishing

This gem uses RubyGems.org Trusted Publishing for secure, automated releases.

Release Process:

  1. Update version in lib/activerecord/ghosts/version.rb
  2. Commit and create tag: git tag v0.1.1 && git push --tags
  3. GitHub Actions automatically publishes to RubyGems.org

Running Tests

bundle exec rspec

📜 License

MIT © Ilya Kovalenko


🤝 Contributing

Pull requests welcome! By participating you agree to follow the Code of Conduct.