The project is in a healthy, maintained state
separate_history is a Ruby on Rails gem that adds automatic versioning to ActiveRecord models. It stores versioned records in dedicated _histories tables for each model.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

 Project Readme

SeparateHistory

CI Gem Version License: MIT

SeparateHistory provides a simple and flexible way to keep a complete history of your ActiveRecord model changes in a separate, dedicated history table. It automatically records every create, update, and destroy event, ensuring you have a full audit trail of your data.

Features

  • Automatic History Tracking: Automatically creates a history record for every create, update, and destroy action on your models.
  • Dedicated History Tables: Keeps your history data separate from your primary tables, ensuring your main application's performance is not impacted.
  • Point-in-Time Recovery: Easily retrieve the state of a record at any point in the past.
  • Easy Setup: Get started with a single line in your model and a simple migration generator.
  • Flexible Configuration: Select which attributes to track, customize history table names, and more.
  • Data Integrity: Includes a manipulated? method to easily check if a history record has been altered after its creation.

Installation

Add this line to your application's Gemfile:

gem 'separate_history'

And then execute:

$ bundle install

Quick Start

Getting started with SeparateHistory is a three-step process:

1. Generate the History Table Migration

Use the provided generator to create a migration for the history table. For a model named User, run:

$ rails g separate_history:sync User

This creates a migration file that defines the schema for your history table.

2. Generate the History Model

Next, generate the history model file. This model will include the necessary SeparateHistory::History module.

$ rails g separate_history:model User

This creates the app/models/user_history.rb file.

3. Run the Migration and Add to Your Model

Run the migration to create the table in your database:

$ rails db:migrate

Finally, add the has_separate_history macro to your original model:

# app/models/user.rb
class User < ApplicationRecord
  has_separate_history
end

That's it! Now, every change to a User instance will be recorded in the user_histories table.

Usage

Basic Setup

  1. Generate and run the migration for your model:
rails generate separate_history:migration User
rails db:migrate
  1. Add to your model:
class User < ApplicationRecord
  has_separate_history
end

Tracking Options

Track Specific Attributes

class Article < ApplicationRecord
  has_separate_history only: [:title, :content]
end

Exclude Specific Attributes

class User < ApplicationRecord
  has_separate_history except: [:last_sign_in_ip, :encrypted_password]
end

Track Only Changed Attributes

class User < ApplicationRecord
  has_separate_history track_changes: true
end

Advanced Usage

Custom History Class Name

class AdminUser < ApplicationRecord
  has_separate_history history_class_name: 'AdminActionLog'
end

Track Specific Events

Track only certain events (create/update/destroy):

class Document < ApplicationRecord
  has_separate_history events: [:create, :update]  # Only track creation and updates
end

Accessing History

# Get all history records for a user
user = User.find(1)
user.user_histories.each do |history|
  puts "Event: #{history.event} at #{history.history_created_at}"
end

# Or use the alias
user.separate_histories.each { |h| puts h.inspect }

Class-Level History Queries

# Get historical state of a record at a specific time
old_user = User.history_as_of(user_id, 1.month.ago)

# Check if history exists for a record
if User.history_exists?(user_id)
  # Do something with history
end

Point-in-Time History

You can retrieve the state of a record at any given point in time using the history_for class method. It returns the last history record created before or at the specified timestamp, giving you a precise snapshot of the record's state.

This query uses the history_updated_at timestamp to ensure accuracy, even if records were created out of order or their timestamps were manually altered.

# Get the user record as it was 2 days ago
user_snapshot = User.history_for(user.id, 2.days.ago)
puts user_snapshot.name # => "Old Name"

# Get what a user looked like 1 week ago
user_week_ago = user.history_as_of(1.week.ago)

# Get the state of a record that might have been deleted
old_user = User.history_as_of(deleted_user_id, 1.month.ago)

Error Handling

When the history table is missing, you'll get a helpful error message:

History table `user_histories` is missing. 
Run `rails g separate_history:model User` to create it.

Validation and Options

SeparateHistory includes built-in validation for options:

# These will raise ArgumentError:
has_separate_history only: [:name], except: [:email]  # Can't use both only and except
has_separate_history invalid_option: true             # Invalid option
has_separate_history events: [:invalid_event]         # Invalid event type
has_separate_history track_changes: 'yes'             # Must be boolean

Instance Methods

When you include has_separate_history in your model, the following instance methods become available:

  • #snapshot_history
    Manually create a snapshot history record for the current state.

  • #history?
    Returns true if any history exists for this record, otherwise false.

  • #history_as_of(timestamp)
    Returns the state of the record at or before the given timestamp.

  • #all_history
    Returns all history records for this instance.

  • #latest_history
    Returns the most recent history record for this instance.

  • #clear_history(force: true)
    Deletes all history records for this instance.
    Warning: You must pass force: true to confirm deletion.

Example:

user = User.create!(name: "Alice")
user.update!(name: "Bob")
user.snapshot_history
user.history? # => true
user.all_history # => [<UserHistory ...>, ...]
user.latest_history # => <UserHistory ...>
user.history_as_of(1.day.ago) # => <UserHistory ...>
user.clear_history(force: true)

Advanced Usage

Tracking Only Changes

By default, SeparateHistory saves a complete snapshot of the record on every change. For high-traffic tables, this can lead to a lot of data storage. You can optimize this by enabling the track_changes option. When set to true, only the attributes that actually changed during an update event will be saved.

# in app/models/user.rb
class User < ApplicationRecord
  has_separate_history track_changes: true
end

With this enabled, if you only update a user's name, the history record will store the new name, but all other attributes will be nil.

Excluding Attributes

You can prevent certain attributes from being saved to the history table by using the except option. This is useful for ignoring fields that change frequently but aren't important for auditing, like sign_in_count or last_login_at.

# Only track changes to name and email
class User < ApplicationRecord
  has_separate_history only: [:name, :email]
end

# Track all attributes except for sign_in_count
class User < ApplicationRecord
  has_separate_history except: [:sign_in_count]
end

Custom History Class Name

If you want to use a different name for your history model, you can specify it with the history_class_name option.

# in app/models/user.rb
class User < ApplicationRecord
  has_separate_history history_class_name: 'UserAuditTrail'
end

# in app/models/user_audit_trail.rb
class UserAuditTrail < ApplicationRecord
  # ...
end

Checking for Manipulation

To verify that a history record has not been altered since it was first created, you can use the manipulated? method.

last_history = user.histories.last
last_history.manipulated? # => false

# If someone changes the record later...
last_history.update(name: "A new name")
last_history.manipulated? # => true

Creating Snapshots

If you add SeparateHistory to a model with existing records, you may want to create an initial history entry for them. You can do this by creating a snapshot event. This is also useful for creating periodic backups of your records.

Here is an example of a Rake task to create an initial snapshot for all records in your User model:

# lib/tasks/history.rake
namespace :history do
  desc "Create initial history records for existing users"
  task sync_users: :environment do
    User.find_each do |user|
      history_class = User.history_class
      unless history_class.exists?(original_id: user.id)
        history_class.create!(user.attributes.merge(original_id: user.id, event: 'snapshot'))
        puts "Created snapshot for User ##{user.id}"
      end
    end
  end
end

Run it with bundle exec rake history:sync_users.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake to run the tests (RSpec, Minitest, and RuboCop).

This project uses appraisal to test against multiple versions of Rails. The test suites can be run with:

$ bundle exec appraisal rake

Before running test suites install dependencies.

$ bundle exec appraisal install

You can also run bin/console for an interactive prompt that will allow you to experiment.

Development

For a detailed log of the debugging and development process for the Rails 7 compatibility fixes, please see DEV.md.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sarvesh4396/separate_history.

License

The gem is available as open source under the terms of the MIT License.