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