module ActsAsScd::ClassMethods

Public Instance Methods

all_of(identity) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 240
def all_of(identity)
  where(identity:identity).reorder('effective_from asc')
end
create_identity(attributes, start=nil) click to toggle source

The first iteration can be defined with a specific start date, but that is in general a bad idea, since it complicates obtaining the first iteration

# File lib/acts_as_scd/class_methods.rb, line 78
def create_identity(attributes, start=nil)
  start ||= START_OF_TIME
  create(attributes.merge(START_COLUMN=>start || START_OF_TIME))
end
create_iteration(identity, attribute_changes, start=nil, options={}) click to toggle source

Create a new iteration options :unterminate - if the identity exists and is terminated, unterminate it (extending the last iteration to the new date) :extend_from - if no prior iteration exists, extend effective_from to the start-of-time (TODO: consider making :extend_from the default, adding an option for the opposite…)

# File lib/acts_as_scd/class_methods.rb, line 88
def create_iteration(identity, attribute_changes, start=nil, options={})
  start = effective_date(start || Date.today)
  transaction do
    current_record = find_by_identity(identity)
    if !current_record && options[:unterminate]
      current_record = latest_of(identity) # terminated.where(IDENTITY_COLUMN=>identity).first
      #   where(IDENTITY_COLUMN=>identity).where("#{effective_to_column_sql} < #{END_OF_TIME}").reorder("#{effective_to_column_sql} desc").limit(1).first
    end
    attributes = {IDENTITY_COLUMN=>identity}.with_indifferent_access
    if current_record
      non_replicated_attrs = %w[id effective_from effective_to updated_at created_at]
      attributes = attributes.merge current_record.attributes.with_indifferent_access.except(*non_replicated_attrs)
    end
    start = START_OF_TIME if options[:extend_from] && !identity_exists?(identity)
    attributes = attributes.merge(START_COLUMN=>start).merge(attribute_changes.with_indifferent_access.except(START_COLUMN, END_COLUMN))
    new_record = create(attributes)
    if new_record.errors.blank? && current_record
      # current_record.update_attributes END_COLUMN=>start
      current_record.send :"#{END_COLUMN}=", start
      current_record.save validate: false
    end
    new_record
  end
end
current_identities() click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 39
def current_identities
  current.identities
end
distinct_identities() click to toggle source

Return objects representing identities; (with a single attribute, :identity) Warning: do not chain this method after other queries; any query should be applied after this method. If identities are required for an association, either latest, earliest or initial can be used (which one is appropriate depends on desired result, data contents, etc.; initial/current are faster)

# File lib/acts_as_scd/class_methods.rb, line 11
def distinct_identities
  # Note that since Rails 2.3.13, when pluck(col) is applied to distinct_identities
  # the "DISTINCT" is lost from the SELECT if added explicitly  as in .select('DISTINCT #{col}'),
  # so we have avoid explicit use of DISTINCT in distinct_identities.
  # This can be used on association queries
  if ActiveRecord::VERSION::MAJOR > 3
    unscope(:select).reorder(identity_column_sql).select(identity_column_sql).uniq
  else
    query = scoped.with_default_scope
    query.select_values.clear
    query.reorder(identity_column_sql).select(identity_column_sql).uniq
  end
end
earliest_of(identity) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 236
def earliest_of(identity)
  where(identity:identity).reorder('effective_to asc').limit(1).first
end
effective_date(d) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 55
def effective_date(d)
  Period.date(d)
end
effective_from_column_sql(table_alias=nil) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 47
def effective_from_column_sql(table_alias=nil)
  %{"#{table_alias || table_name}"."#{START_COLUMN}"}
end
effective_periods(*args) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 210
def effective_periods(*args)
  # periods = unscoped.select("DISTINCT effective_from, effective_to").order('effective_from, effective_to')
  if ActiveRecord::VERSION::MAJOR > 3
    # periods = unscope(where: [:effective_from, :effective_to]).select("DISTINCT effective_from, effective_to").reorder('effective_from, effective_to')
    periods = unscope(where: [:effective_from, :effective_to]).select([:effective_from, :effective_to]).uniq.reorder('effective_from, effective_to')
  else
    query = scoped.with_default_scope
    query.select_values.clear
    periods = query.reorder('effective_from, effective_to').select([:effective_from, :effective_to]).uniq
  end

  # formerly unscoped was used, so any desired condition had to be defined here
  periods = periods.where(*args) if args.present?

  periods.map{|p| Period[p.effective_from, p.effective_to]}
end
effective_to_column_sql(table_alias=nil) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 51
def effective_to_column_sql(table_alias=nil)
  %{"#{table_alias || table_name}"."#{END_COLUMN}"}
end
find_by_identity(identity, at_date=nil) click to toggle source

Note that find_by_identity will return nil if there’s not a current iteration of the identity

# File lib/acts_as_scd/class_methods.rb, line 60
def find_by_identity(identity, at_date=nil)
  # (at_date.nil? ? current : at(at_date)).where(IDENTITY_COLUMN=>identity).first
  if at_date.nil?
    q = current
  else
    q = at(at_date)
  end
  q = q.where(IDENTITY_COLUMN=>identity)
  q.first
end
has_many_iterations_through_identity(assoc, options={}) click to toggle source

Association yo be used in a parent class which has identity and has children which have identities too; the association is implemented through the identity, not the PK. The inverse association should be belongs_to_identity

# File lib/acts_as_scd/class_methods.rb, line 125
def has_many_iterations_through_identity(assoc, options={})
  fk =  options[:foreign_key] || :"#{model_name.to_s.underscore}_identity"
  assoc_singular = assoc.to_s.singularize
  other_model_name = options[:class_name] || assoc_singular.camelize
  other_model = other_model_name.constantize
  pk = IDENTITY_COLUMN

  # all children iterations
  has_many :"#{assoc_singular}_iterations", class_name: other_model_name, foreign_key: fk, primary_key: pk

  # current_children
  if ActiveRecord::VERSION::MAJOR > 3
    has_many assoc, ->{ where "#{other_model.effective_to_column_sql}=#{END_OF_TIME}" },
             options.reverse_merge(foreign_key: fk, primary_key: pk)
  else
    has_many assoc, options.reverse_merge(
                      foreign_key: fk, primary_key: pk,
                      conditions: "#{other_model.effective_to_column_sql}=#{END_OF_TIME}"
                    )
  end

  # children at some date
  define_method :"#{assoc}_at" do |date|
    # other_model.unscoped.at(date).where(fk=>send(pk))
    send(:"#{assoc_singular}_iterations").scoped.at(date)
  end

  # all children identities
  define_method :"#{assoc_singular}_identities" do
    # send(:"#{assoc}_iterations").select("DISTINCT #{other_model.identity_column_sql}").reorder(other_model.identity_column_sql).pluck(:identity)
    # other_model.unscoped.where(fk=>send(pk)).identities
    send(:"#{assoc_singular}_iterations").identities
  end

  # children identities at a date
  define_method :"#{assoc_singular}_identities_at" do |date=nil|
    # send(:"#{assoc}_iterations_at", date).select("DISTINCT #{other_model.identity_column_sql}").reorder(other_model.identity_column_sql).pluck(:identity)
    # other_model.unscoped.where(fk=>send(pk)).identities_at(date)
    send(:"#{assoc_singular}_iterations").identities_at(date)
  end

  # current children identities
  define_method :"#{assoc_singular}_current_identities" do
    # send(assoc).select("DISTINCT #{other_model.identity_column_sql}").reorder(other_model.identity_column_sql).pluck(:identity)
    # other_mode.unscoped.where(fk=>send(pk)).current_identities
    send(:"#{assoc_singular}_iterations").current_identities
  end

end
has_many_through_identity(assoc, options={}) click to toggle source

Association to be used in a parent class which has identity and has children which don’t have identities; the association is implemented through the identity, not the PK. The inverse association should be belongs_to_identity

# File lib/acts_as_scd/class_methods.rb, line 179
def has_many_through_identity(assoc, options={})
  fk = :"#{model_name.to_s.underscore}_identity"
  pk = IDENTITY_COLUMN

  has_many assoc, {:foreign_key=>fk, :primary_key=>pk}.merge(options)
end
identities() click to toggle source

This can be applied to an ordered query (but returns an Array, not a query)

# File lib/acts_as_scd/class_methods.rb, line 30
def identities
  # pluck(identity_column_sql).uniq # does not work if select has been applied
  scoped.map(&IDENTITY_COLUMN).uniq
end
identities_at(date=nil) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 35
def identities_at(date=nil)
  at(date).identities
end
identity_column_definition() click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 186
def identity_column_definition
  @slowly_changing_columns.first
end
identity_column_sql(table_alias=nil) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 43
def identity_column_sql(table_alias=nil)
  %{"#{table_alias || table_name}"."#{IDENTITY_COLUMN}"}
end
identity_exists?(identity, at_date=nil) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 71
def identity_exists?(identity, at_date=nil)
  (at_date.nil? ? self : at(at_date)).where(IDENTITY_COLUMN=>identity).exists?
end
latest_of(identity) click to toggle source

Most recent iteration (terminated or not)

# File lib/acts_as_scd/class_methods.rb, line 232
def latest_of(identity)
  where(identity:identity).reorder('effective_to desc').limit(1).first
end
ordered_identities() click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 25
def ordered_identities
  distinct_identities.pluck(identity_column_sql)
end
slow_changing_migration() click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 190
def slow_changing_migration
  migration = ""

  migration << "def up\n"
  @slowly_changing_columns.each do |col, args|
    migration << "  add_column :#{table_name}, :#{col}, #{args.inspect.unwrap('[]')}\n"
  end
  @slowly_changing_indices.each do |index|
    migration << "  add_index :#{table_name}, #{index.inspect}\n"
  end
  migration << "end\n"

  migration << "def down\n"
  @slowly_changing_columns.each do |col, args|
    migration << "  remove_column :#{table_name}, :#{col}\n"
  end
  migration << "end\n"

end
terminate_identity(identity, finish=Date.today) click to toggle source
# File lib/acts_as_scd/class_methods.rb, line 113
def terminate_identity(identity, finish=Date.today)
   finish = effective_date(finish)
   transaction do
     current_record = find_by_identity(identity)
     current_record.update_attributes END_COLUMN=>finish
   end
end