Module: ActsAsScd::ClassMethods
- Defined in:
- lib/acts_as_scd/class_methods.rb
Instance Method Summary collapse
- #all_of(identity) ⇒ Object
-
#create_identity(attributes, start = nil) ⇒ Object
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.
-
#create_iteration(identity, attribute_changes, start = nil, options = {}) ⇒ Object
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…).
- #current_identities ⇒ Object
-
#distinct_identities ⇒ Object
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.
- #earliest_of(identity) ⇒ Object
- #effective_date(d) ⇒ Object
- #effective_from_column_sql(table_alias = nil) ⇒ Object
- #effective_periods(*args) ⇒ Object
- #effective_to_column_sql(table_alias = nil) ⇒ Object
-
#find_by_identity(identity, at_date = nil) ⇒ Object
Note that find_by_identity will return nil if there’s not a current iteration of the identity.
-
#has_many_iterations_through_identity(assoc, options = {}) ⇒ Object
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.
-
#has_many_through_identity(assoc, options = {}) ⇒ Object
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.
-
#identities ⇒ Object
This can be applied to an ordered query (but returns an Array, not a query).
- #identities_at(date = nil) ⇒ Object
- #identity_column_definition ⇒ Object
- #identity_column_sql(table_alias = nil) ⇒ Object
- #identity_exists?(identity, at_date = nil) ⇒ Boolean
-
#latest_of(identity) ⇒ Object
Most recent iteration (terminated or not).
- #ordered_identities ⇒ Object
- #slow_changing_migration ⇒ Object
- #terminate_identity(identity, finish = Date.today) ⇒ Object
Instance Method Details
#all_of(identity) ⇒ Object
240 241 242 |
# 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) ⇒ Object
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
78 79 80 81 |
# 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 = {}) ⇒ Object
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…)
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/acts_as_scd/class_methods.rb', line 88 def create_iteration(identity, attribute_changes, start=nil, ={}) start = effective_date(start || Date.today) transaction do current_record = find_by_identity(identity) if !current_record && [: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 [: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 ⇒ Object
39 40 41 |
# File 'lib/acts_as_scd/class_methods.rb', line 39 def current_identities current.identities end |
#distinct_identities ⇒ Object
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)
11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 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) ⇒ Object
236 237 238 |
# 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) ⇒ Object
55 56 57 |
# 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) ⇒ Object
47 48 49 |
# 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) ⇒ Object
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# 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) ⇒ Object
51 52 53 |
# 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) ⇒ Object
Note that find_by_identity will return nil if there’s not a current iteration of the identity
60 61 62 63 64 65 66 67 68 69 |
# 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 = {}) ⇒ Object
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
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/acts_as_scd/class_methods.rb', line 125 def has_many_iterations_through_identity(assoc, ={}) fk = [:foreign_key] || :"#{model_name.to_s.underscore}_identity" assoc_singular = assoc.to_s.singularize other_model_name = [: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}" }, .reverse_merge(foreign_key: fk, primary_key: pk) else has_many assoc, .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 = {}) ⇒ Object
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
179 180 181 182 183 184 |
# File 'lib/acts_as_scd/class_methods.rb', line 179 def has_many_through_identity(assoc, ={}) fk = :"#{model_name.to_s.underscore}_identity" pk = IDENTITY_COLUMN has_many assoc, {:foreign_key=>fk, :primary_key=>pk}.merge() end |
#identities ⇒ Object
This can be applied to an ordered query (but returns an Array, not a query)
30 31 32 33 |
# 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) ⇒ Object
35 36 37 |
# File 'lib/acts_as_scd/class_methods.rb', line 35 def identities_at(date=nil) at(date).identities end |
#identity_column_definition ⇒ Object
186 187 188 |
# 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) ⇒ Object
43 44 45 |
# 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) ⇒ Boolean
71 72 73 |
# 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) ⇒ Object
Most recent iteration (terminated or not)
232 233 234 |
# 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 ⇒ Object
25 26 27 |
# File 'lib/acts_as_scd/class_methods.rb', line 25 def ordered_identities distinct_identities.pluck(identity_column_sql) end |
#slow_changing_migration ⇒ Object
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# 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) ⇒ Object
113 114 115 116 117 118 119 |
# 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 |