acts_as_multi_versioned

Hi, I've been playing around with something I call
acts_as_multi_versioned. The difference from acts_as_versioned is that
you can work with different versions simultaneously. For example if
one session sees one version, and another session sees another. It's
designed like this:

All tables need to have two primary-keys, id (autoincrement) and
version (not autoincrement). There is no extra table for older
versions, everything is in the same table.

The variable ActiveRecord::Base.current_version is used to define what
version use currently use. That version will be used for all tables.
When an object, for example with id 3, don't exists for version 4, the
system will search for the closest version available (which means
max(version < current_version). So, a new version only takes extra
space for the modified objects.

Currently it looks like obj.destroy is like doing revert on a revision
system :). Needs to be fixed. Currently it's just a proof of concept,
but it would be nice with comments and suggestions.

Have a nice day!
Tobias Nurmiranta

Minimal example (the function ver sets the current version):

Hi. I've updated the code mentioned below, if anyone is interested or
want to comment. Tables with version-control now have the extra
columns version (primary key) and version_deleted (boolean indicating
if the object is deleted in this version). I needed some complicated
SQL to get it working with find(:all) and find(:first) :). More
details in the old mail.

Have a nice day!
Tobias

-- code --
# acts_as_multi_versioned
class ActiveRecord::Base
  class << self
    attr_accessor :current_version
  end
  self.current_version = 0

  def self.acts_as_multi_versioned
    class_eval %q{
      class << self
        def find_version(obj, version, args = {})
          # Adding a join-statement which selects the rows with
highest
          # version =< current_version for every unique value of id.
          args[:joins] = "INNER JOIN (SELECT
#{primary_key},MAX(version) as version FROM #{table_name} WHERE
version < #{version+1} GROUP BY #{primary_key}) #{table_name}_versions
ON #{table_name}_versions.#{primary_key} =
#{table_name}.#{primary_key} AND #{table_name}_versions.version =
#{table_name}.version AND #{table_name}.version_deleted = 0
#{args[:joins]}"
          args[:readonly] ||= false
          find_without_version(obj, args)
        end
        def find_with_version(obj, args = {})
          find_version(obj, ActiveRecord::Base.current_version, args)
        end
        alias_method_chain :find, :version
      end

      def before_create
        self[:version] = ActiveRecord::Base.current_version
      end

      def update_without_callbacks
        if self.version != ActiveRecord::Base.current_version
          self[:version] = ActiveRecord::Base.current_version
          # Must create a new object when storing a new version-
number. But the
          # old record will work correctly for the next update.
          o = self.clone
          o[self.class.primary_key] = self[self.class.primary_key] #
why is this needed?
          o.save!
        else
          # Modified update_without_callbacks from
composite_primary_keys.
          where_class = "(#{self.class.primary_key} = #{quoted_id})
AND (version = #{self[:version]})"

          connection.update(

            "UPDATE #{self.class.table_name} " +

            "SET #{quoted_comma_pair_list(connection,
attributes_with_quotes_versioned)} " +

            "WHERE #{where_class}",

            "#{self.class.name} Update"

          )

          return true
        end
      end

      def destroy_without_callbacks
        begin
          self.class.find_version(self.id, self.version - 1) # older
version exists?
          self.version_deleted = true
          self.save! # (this will clone and create a deleted record if
only readaccess was made earlier)

        rescue ActiveRecord::RecordNotFound # older record doesn't
exist, remove it from database.
          where_class = "(#{self.class.primary_key} = #{quoted_id})
AND (version = #{self[:version]})"
          unless new_record?

            connection.delete(

              "DELETE FROM #{self.class.table_name} " +

              "WHERE #{where_class}",

              "#{self.class.name} Destroy"

            )

          end
        end
        freeze
      end

      def attributes_with_quotes_versioned # All attributes except
primary-key and 'version'.
        x = attributes_with_quotes(false)
        x.delete "version"
        x
      end
    }
  end
end

ActiveRecord::Base.current_version = 3