module ActsAsScd

Constants

END_COLUMN

Column that represents end of an iteration’s life

END_OF_TIME

Internal value to represent the end of time

IDENTITY_COLUMN

Column that represents the identity of an entity

START_COLUMN

Column that represents start of an iteration’s life

START_OF_TIME

Internal value to represent the start of time

VERSION

Public Class Methods

included(model) click to toggle source
# File lib/acts_as_scd.rb, line 27
def self.included(model)
  initialize_scd model
end
initialize_scd(model) click to toggle source
# File lib/acts_as_scd/initialize.rb, line 17
def self.initialize_scd(model)
  model.extend ClassMethods

  # Current iterations
  model.scope :current, ->{model.where("#{model.effective_to_column_sql} = :date", :date=>END_OF_TIME)}
  model.scope :initial, ->{model.where("#{model.effective_from_column_sql} = :date", :date=>START_OF_TIME)}
  # Iterations effective at given date
  # Note that since Array has an 'at' method, this cannot be applied directly to
  # associations (the Array method would be used after generating an Array from the query).
  # It is necessary to use .scoped.at(...) for associations.
  model.scope :at, ->(date=nil){
    # TODO: consider renaming this to current_at or active_at to avoid having to use
    # scoped with associations
    if date.present?
      model.where(%{#{model.effective_from_column_sql}<=:date AND #{model.effective_to_column_sql}>:date}, :date=>model.effective_date(date))
    else
      model.current
    end
  }
  # Iterations superseded/terminated
  model.scope :ended, ->{model.where("#{model.effective_to_column_sql} < :date", :date=>END_OF_TIME)}
  model.scope :earliest, ->(identity=nil){
    if identity
      identity_column = model.identity_column_sql('earliest_tmp')
      if Array==identity
        identity_list = identity.map{|i| model.connection.quote(i)}*','
        where_condition = "WHERE #{identity_column} IN (#{identity_list})"
      else
        where_condition = "WHERE #{identity_column}=#{model.connection.quote(identity)}"
      end
    end
    model.where(
      %{(#{model.identity_column_sql}, #{model.effective_from_column_sql}) IN
          (SELECT #{model.identity_column_sql('earliest_tmp')},
                  MIN(#{model.effective_from_column_sql('earliest_tmp')}) AS earliest_from
           FROM #{model.table_name} AS "earliest_tmp"
           #{where_condition}
           GROUP BY #{model.identity_column_sql('earliest_tmp')})
       }
    )
  }
  # Latest iteration (terminated or current) of each identity
  model.scope :latest, ->(identity=nil){
    if identity
      identity_column = model.identity_column_sql('latest_tmp')
      if Array===identity
        identity_list = identity.map{|i| model.connection.quote(i)}*','
        where_condition = "WHERE #{identity_column} IN (#{identity_list})"
      else
        where_condition = "WHERE #{identity_column}=#{model.connection.quote(identity)}"
      end
    end
    model.where(
      %{(#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
        (SELECT #{model.identity_column_sql('latest_tmp')},
                MAX(#{model.effective_to_column_sql('latest_tmp')}) AS latest_to
         FROM #{model.table_name} AS "latest_tmp"
         #{where_condition}
         GROUP BY #{model.identity_column_sql('latest_tmp')})
       }
    )
  }
  # Last superseded/terminated iterations
  # model.scope :last_ended, ->{model.where(%{#{model.effective_to_column_sql} = (SELECT max(#{model.effective_to_column_sql('max_to_tmp')}) FROM "#{model.table_name}" AS "max_to_tmp" WHERE #{model.effective_to_column_sql('max_to_tmp')}<#{END_OF_TIME})})}
  # last iterations of terminated identities
  # model.scope :terminated, ->{model.where(%{#{model.effective_to_column_sql}<#{END_OF_TIME} AND #{model.effective_to_column_sql}=(SELECT max(#{model.effective_to_column_sql('max_to_tmp')}) FROM "#{model.table_name}" AS "max_to_tmp")})}
  model.scope :terminated, ->(identity=nil){
    where_condition = identity && " WHERE #{model.identity_column_sql('max_to_tmp')}=#{model.connection.quote(identity)} "
    model.where(
      %{#{model.effective_to_column_sql}<#{END_OF_TIME}
        AND (#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
          (SELECT #{model.identity_column_sql('max_to_tmp')},
                  max(#{model.effective_to_column_sql('max_to_tmp')})
           FROM "#{model.table_name}" AS "max_to_tmp" #{where_condition})
       }
    )
  }
  # iterations superseded
  model.scope :superseded, ->(identity=nil){
    where_condition = identity && " AND #{model.identity_column_sql('max_to_tmp')}=#{model.connection.quote(identity)} "
    model.where(
      %{(#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
        (SELECT #{model.identity_column_sql('max_to_tmp')},
                max(#{model.effective_to_column_sql('max_to_tmp')})
         FROM "#{model.table_name}" AS "max_to_tmp"
         WHERE #{model.effective_to_column_sql('max_to_tmp')}<#{END_OF_TIME})
               #{where_condition}
               AND EXISTS (SELECT * FROM "#{model.table_name}" AS "ex_from_tmp"
                           WHERE #{model.effective_from_column_sql('ex_from_tmp')}==#{model.effective_to_column_sql})
      }
    )
  }
  model.before_validation :compute_identity
  model.validates_uniqueness_of IDENTITY_COLUMN, :scope=>[START_COLUMN, END_COLUMN], :message=>"Invalid effective period"
  model.before_destroy :remove_this_iteration
end

Public Instance Methods

antecessor() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 26
def antecessor
  return nil if effective_from==START_OF_TIME
  self.class.where(identity:identity, effective_to:effective_from).first
end
antecessors() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 36
def antecessors
  return self.class.where('1=0') if effective_from==START_OF_TIME
  self.class.where(identity:identity).where('effective_to<=:date', date: effective_from).reorder('effective_to')
end
at(date=nil) click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 13
def at(date=nil)
  if date.present?
    self.class.find_by_identity(identity, date)
  else
    current
  end
end
current() click to toggle source

TODO: replace identity by send(IDENTITY_COLUMN)…

# File lib/acts_as_scd/instance_methods.rb, line 5
def current
  self.class.find_by_identity(identity)
end
current?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 92
def current?
  effective_period.current?
end
earliest() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 49
def earliest
  self.class.where(identity:identity).reorder('effective_from asc').limit(1).first
end
effective_from_date() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 70
def effective_from_date
  case effective_from
  when END_OF_TIME
    raise "Invalid effective_from value: #{END_OF_TIME}"
  else
    Period::DateValue[effective_from].to_date
  end
end
effective_period() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 66
def effective_period
  Period[effective_from, effective_to]
end
effective_to_date() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 79
def effective_to_date
  case effective_to
  when START_OF_TIME
    raise "Invalid effective_to value #{START_OF_TIME}"
  else
    Period::DateValue[effective_to].to_date
  end
end
ended?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 58
def ended?
  effective_to < END_OF_TIME
end
ended_at?(date) click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 62
def ended_at?(date)
  effective_to <= self.class.effective_date(date)
end
future_limited?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 100
def future_limited?
  effective_period.future_limited?
end
history() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 41
def history
  self.class.all_of(identity)
end
initial() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 9
def initial
  self.class.initial.where(IDENTITY_COLUMN=>identity).first
end
initial?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 88
def initial?
  effective_period.initial?
end
latest() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 45
def latest
  self.class.where(identity:identity).reorder('effective_to desc').limit(1).first
end
limited?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 104
def limited?
  effective_period.limited?
end
past_limited?() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 96
def past_limited?
  effective_period.past_limited?
end
remove_this_iteration() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 108
def remove_this_iteration
  s = successor
  s.update_attributes effective_from: self.effective_from if s
  a = antecessor
  a.update_attributes effective_to: self.effective_to if a
end
successor() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 21
def successor
  return nil if effective_to==END_OF_TIME
  self.class.where(identity:identity, effective_from:effective_to).first
end
successors() click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 31
def successors
  return self.class.where('1=0') if effective_to==END_OF_TIME
  self.class.where(identity:identity).where('effective_from>=:date', date: effective_to).reorder('effective_from')
end
terminate_identity(finish=Date.today) click to toggle source
# File lib/acts_as_scd/instance_methods.rb, line 53
def terminate_identity(finish=Date.today)
   finish = self.class.effective_date(finish)
   update_attributes END_COLUMN=>finish
end