Module: ActsAsScd

Defined in:
lib/acts_as_scd.rb,
lib/acts_as_scd/period.rb,
lib/acts_as_scd/version.rb,
lib/acts_as_scd/initialize.rb,
lib/acts_as_scd/block_updater.rb,
lib/acts_as_scd/class_methods.rb,
lib/acts_as_scd/instance_methods.rb,
lib/acts_as_scd/base_class_methods.rb

Defined Under Namespace

Modules: BaseClassMethods, ClassMethods Classes: BlockUpdater, Period

Constant Summary collapse

VERSION =
"0.0.3"
START_OF_TIME =

Internal value to represent the start of time

0
END_OF_TIME =

Internal value to represent the end of time

99999999
IDENTITY_COLUMN =

Column that represents the identity of an entity

:identity
START_COLUMN =

Column that represents start of an iteration’s life

:effective_from
END_COLUMN =

Column that represents end of an iteration’s life

:effective_to

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(model) ⇒ Object



27
28
29
# File 'lib/acts_as_scd.rb', line 27

def self.included(model)
  initialize_scd model
end

.initialize_scd(model) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 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

Instance Method Details

#antecessorObject



26
27
28
29
# 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

#antecessorsObject



36
37
38
39
# 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) ⇒ Object



13
14
15
16
17
18
19
# 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

#currentObject

TODO: replace identity by send(IDENTITY_COLUMN)…



5
6
7
# File 'lib/acts_as_scd/instance_methods.rb', line 5

def current
  self.class.find_by_identity(identity)
end

#current?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/acts_as_scd/instance_methods.rb', line 92

def current?
  effective_period.current?
end

#earliestObject



49
50
51
# 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_dateObject



70
71
72
73
74
75
76
77
# 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_periodObject



66
67
68
# File 'lib/acts_as_scd/instance_methods.rb', line 66

def effective_period
  Period[effective_from, effective_to]
end

#effective_to_dateObject



79
80
81
82
83
84
85
86
# 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?Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/acts_as_scd/instance_methods.rb', line 58

def ended?
  effective_to < END_OF_TIME
end

#ended_at?(date) ⇒ Boolean

Returns:

  • (Boolean)


62
63
64
# File 'lib/acts_as_scd/instance_methods.rb', line 62

def ended_at?(date)
  effective_to <= self.class.effective_date(date)
end

#future_limited?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/acts_as_scd/instance_methods.rb', line 100

def future_limited?
  effective_period.future_limited?
end

#historyObject



41
42
43
# File 'lib/acts_as_scd/instance_methods.rb', line 41

def history
  self.class.all_of(identity)
end

#initialObject



9
10
11
# File 'lib/acts_as_scd/instance_methods.rb', line 9

def initial
  self.class.initial.where(IDENTITY_COLUMN=>identity).first
end

#initial?Boolean

Returns:

  • (Boolean)


88
89
90
# File 'lib/acts_as_scd/instance_methods.rb', line 88

def initial?
  effective_period.initial?
end

#latestObject



45
46
47
# 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?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/acts_as_scd/instance_methods.rb', line 104

def limited?
  effective_period.limited?
end

#past_limited?Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/acts_as_scd/instance_methods.rb', line 96

def past_limited?
  effective_period.past_limited?
end

#remove_this_iterationObject



108
109
110
111
112
113
# 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

#successorObject



21
22
23
24
# 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

#successorsObject



31
32
33
34
# 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) ⇒ Object



53
54
55
56
# 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