chrono_model 0.4.0 → 0.5.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,12 +12,25 @@ module ChronoModel
12
12
  end
13
13
 
14
14
  if table_exists? && !chrono?
15
- raise Error, "#{table_name} is not a temporal table. " \
15
+ puts "WARNING: #{table_name} is not a temporal table. " \
16
16
  "Please use change_table :#{table_name}, :temporal => true"
17
17
  end
18
18
 
19
19
  history = TimeMachine.define_history_model_for(self)
20
20
  TimeMachine.chrono_models[table_name] = history
21
+
22
+ # STI support. TODO: more thorough testing
23
+ #
24
+ def self.inherited(subclass)
25
+ super
26
+
27
+ # Do not smash stack: as the below method is defining a
28
+ # new anonymous class, without this check this leads to
29
+ # infinite recursion.
30
+ unless subclass.name.nil?
31
+ TimeMachine.define_inherited_history_model_for(subclass)
32
+ end
33
+ end
21
34
  end
22
35
 
23
36
  # Returns an Hash keyed by table name of ChronoModels
@@ -55,63 +68,84 @@ module ChronoModel
55
68
  self.primary_key = old
56
69
  end
57
70
 
58
- # SCD Type 2 validity from timestamp
71
+ # Returns the previous history entry, or nil if this
72
+ # is the first one.
59
73
  #
60
- def valid_from
61
- utc_timestamp_from('valid_from')
74
+ def pred
75
+ return nil if self.valid_from.year.zero?
76
+
77
+ if self.class.timeline_associations.empty?
78
+ self.class.where(:id => rid, :valid_to => valid_from_before_type_cast).first
79
+ else
80
+ super(:id => rid, :before => valid_from)
81
+ end
62
82
  end
63
83
 
64
- # SCD Type 2 validity to timestamp
84
+ # Returns the next history entry, or nil if this is the
85
+ # last one.
65
86
  #
66
- def valid_to
67
- utc_timestamp_from('valid_to')
87
+ def succ
88
+ return nil if self.valid_to.year == 9999
89
+
90
+ if self.class.timeline_associations.empty?
91
+ self.class.where(:id => rid, :valid_from => valid_to_before_type_cast).first
92
+ else
93
+ super(:id => rid, :after => valid_to)
94
+ end
68
95
  end
96
+ alias :next :succ
69
97
 
70
- # History recording timestamp
98
+ # Returns the first history entry
71
99
  #
72
- def recorded_at
73
- utc_timestamp_from('recorded_at')
100
+ def first
101
+ self.class.where(:id => rid).order(:valid_from).first
74
102
  end
75
103
 
76
- # Virtual attribute used to pass around the
77
- # current timestamp in association queries
104
+ # Returns the last history entry
78
105
  #
79
- def as_of_time
80
- Conversions.string_to_utc_time attributes['as_of_time']
106
+ def last
107
+ self.class.where(:id => rid).order(:valid_from).last
81
108
  end
82
109
 
83
- # Inhibit destroy of historical records
110
+ # Returns this history entry's current record
84
111
  #
85
- def destroy
86
- raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records'
112
+ def record
113
+ self.class.superclass.find(rid)
87
114
  end
88
-
89
- private
90
- # Hack around AR timezone support. These timestamps are recorded
91
- # by the chrono rewrite rules in UTC, but AR reads them as they
92
- # were stored in the local timezone - thus here we reset its
93
- # assumption. TODO: OPTIMIZE.
94
- #
95
- if ActiveRecord::Base.default_timezone != :utc
96
- def utc_timestamp_from(attr)
97
- attributes[attr].utc + Time.now.utc_offset
98
- end
99
- else
100
- def utc_timestamp_from(attr)
101
- attributes[attr]
102
- end
103
- end
104
115
  end
105
116
 
106
117
  model.singleton_class.instance_eval do
107
118
  define_method(:history) { history }
108
119
  end
109
120
 
121
+ history.singleton_class.instance_eval do
122
+ define_method(:sti_name) { model.sti_name }
123
+ end
124
+
110
125
  model.const_set :History, history
111
126
 
112
127
  return history
113
128
  end
114
129
 
130
+ def self.define_inherited_history_model_for(subclass)
131
+ # Define history model for the subclass
132
+ history = Class.new(subclass.superclass.history)
133
+ history.table_name = subclass.superclass.history.table_name
134
+
135
+ # Override the STI name on the history subclass
136
+ history.singleton_class.instance_eval do
137
+ define_method(:sti_name) { subclass.sti_name }
138
+ end
139
+
140
+ # Return the subclass history via the .history method
141
+ subclass.singleton_class.instance_eval do
142
+ define_method(:history) { history }
143
+ end
144
+
145
+ # Define the History constant inside the subclass
146
+ subclass.const_set :History, history
147
+ end
148
+
115
149
  # Returns a read-only representation of this record as it was +time+ ago.
116
150
  #
117
151
  def as_of(time)
@@ -127,18 +161,108 @@ module ChronoModel
127
161
  # Returns an Array of timestamps for which this instance has an history
128
162
  # record. Takes temporal associations into account.
129
163
  #
130
- def history_timestamps
131
- self.class.history.timestamps do |query|
132
- query.where(:id => self)
133
- end
164
+ def timeline(options = {})
165
+ self.class.history.timeline(self, options)
134
166
  end
135
167
 
168
+ # Returns a boolean indicating whether this record is an history entry.
169
+ #
136
170
  def historical?
137
- self.kind_of? self.class.history
171
+ self.attributes.key?('as_of_time') || self.kind_of?(self.class.history)
172
+ end
173
+
174
+ # Hack around AR timezone support. These timestamps are recorded
175
+ # by the chrono rewrite rules in UTC, but AR reads them as they
176
+ # were stored in the local timezone - thus here we bypass type
177
+ # casting to force creation of UTC timestamps.
178
+ #
179
+ %w( valid_from valid_to recorded_at as_of_time ).each do |attr|
180
+ define_method(attr) do
181
+ Conversions.string_to_utc_time attributes_before_type_cast[attr]
182
+ end
183
+ end
184
+
185
+ # Inhibit destroy of historical records
186
+ #
187
+ def destroy
188
+ raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
189
+ super
138
190
  end
139
191
 
140
- def attributes(*)
141
- super.tap {|x| x.delete('__xid')}
192
+ # Returns the previous record in the history, or nil if this is the only
193
+ # recorded entry.
194
+ #
195
+ def pred(options = {})
196
+ if self.class.timeline_associations.empty?
197
+ history.order('valid_to DESC').offset(1).first
198
+ else
199
+ return nil unless (ts = pred_timestamp(options))
200
+ self.class.as_of(ts).order('hid desc').find(options[:id] || id)
201
+ end
202
+ end
203
+
204
+ # Returns the previous timestamp in this record's timeline. Includes
205
+ # temporal associations.
206
+ #
207
+ def pred_timestamp(options = {})
208
+ if historical?
209
+ options[:before] ||= as_of_time
210
+ timeline(options.merge(:limit => 1, :reverse => true)).first
211
+ else
212
+ timeline(options.merge(:limit => 2, :reverse => true)).second
213
+ end
214
+ end
215
+
216
+ # Returns the next record in the history timeline.
217
+ #
218
+ def succ(options = {})
219
+ unless self.class.timeline_associations.empty?
220
+ return nil unless (ts = succ_timestamp(options))
221
+ self.class.as_of(ts).order('hid desc').find(options[:id] || id)
222
+ end
223
+ end
224
+
225
+ # Returns the next timestamp in this record's timeline. Includes temporal
226
+ # associations.
227
+ #
228
+ def succ_timestamp(options = {})
229
+ return nil unless historical?
230
+
231
+ options[:after] ||= as_of_time
232
+ timeline(options.merge(:limit => 1, :reverse => false)).first
233
+ end
234
+
235
+ # Returns the differences between this entry and the previous history one.
236
+ # See: +changes_against+.
237
+ #
238
+ def last_changes
239
+ pred = self.pred
240
+ changes_against(pred) if pred
241
+ end
242
+
243
+ # Returns the differences between this record and an arbitrary reference
244
+ # record. The changes representation is an hash keyed by attribute whose
245
+ # values are arrays containing previous and current attributes values -
246
+ # the same format used by ActiveModel::Dirty.
247
+ #
248
+ def changes_against(ref)
249
+ self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
250
+ old, new = ref.public_send(attr), self.public_send(attr)
251
+
252
+ changes.tap do |c|
253
+ changed = old.respond_to?(:history_eql?) ?
254
+ !old.history_eql?(new) : old != new
255
+
256
+ c[attr] = [old, new] if changed
257
+ end
258
+ end
259
+ end
260
+
261
+ # Wraps AR::Base#attributes by removing the __xid internal attribute
262
+ # used to squash together changes made in the same transaction.
263
+ #
264
+ %w( attributes attribute_names ).each do |name|
265
+ define_method(name) { super().tap {|x| x.delete('__xid')} }
142
266
  end
143
267
 
144
268
  module ClassMethods
@@ -147,30 +271,92 @@ module ChronoModel
147
271
  def as_of(time)
148
272
  history.as_of(time, current_scope)
149
273
  end
274
+
275
+ def attribute_names_for_history_changes
276
+ @attribute_names_for_history_changes ||= attribute_names -
277
+ %w( id hid valid_from valid_to recorded_at as_of_time )
278
+ end
279
+
280
+ def has_timeline(options)
281
+ changes = options.delete(:changes)
282
+ assocs = history.has_timeline(options)
283
+
284
+ attributes = changes.present? ?
285
+ Array.wrap(changes) : assocs.map(&:name)
286
+
287
+ attribute_names_for_history_changes.concat(attributes.map(&:to_s))
288
+ end
289
+
290
+ delegate :timeline_associations, :to => :history
291
+ end
292
+
293
+ module TimeQuery
294
+ OPERATORS = {
295
+ :at => '&&',
296
+ :before => '<<',
297
+ :after => '>>',
298
+ }.freeze
299
+
300
+ def time_query(match, time_or_times, options)
301
+ from_f, to_f = options[:on]
302
+
303
+ from_t, to_t = if time_or_times.kind_of?(Array)
304
+ time_or_times.map! {|t| time_for_time_query(t)}
305
+ else
306
+ [time_for_time_query(time_or_times)]*2
307
+ end
308
+
309
+ if match == :not
310
+ where(%[
311
+ #{build_time_query(:before, from_t, from_f, to_t, to_f)} OR
312
+ #{build_time_query(:after, from_t, from_f, to_t, to_f)}
313
+ ])
314
+ else
315
+ where(build_time_query(match, from_t, from_f, to_t, to_f))
316
+ end
317
+ end
318
+
319
+ private
320
+ def time_for_time_query(t)
321
+ t = Conversions.time_to_utc_string(t.utc) if t.kind_of?(Time)
322
+ t == :now ? 'now()' : "#{connection.quote(t)}::timestamp"
323
+ end
324
+
325
+ def build_time_query(match, from_t, from_f, to_t, to_f)
326
+ %[
327
+ box(
328
+ point( date_part( 'epoch', #{from_f} ), 0 ),
329
+ point( date_part( 'epoch', #{to_f } ), 0 )
330
+ ) #{OPERATORS.fetch(match)}
331
+ box(
332
+ point( date_part( 'epoch', #{from_t} ), 0 ),
333
+ point( date_part( 'epoch', #{to_t } ), 0 )
334
+ )
335
+ ]
336
+ end
150
337
  end
151
338
 
152
339
  # Methods that make up the history interface of the companion History
153
- # model build on each Model that includes TimeMachine
340
+ # model, automatically built for each Model that includes TimeMachine
154
341
  module HistoryMethods
342
+ include TimeQuery
343
+
155
344
  # Fetches as of +time+ records.
156
345
  #
157
346
  def as_of(time, scope = nil)
158
- time = Conversions.time_to_utc_string(time.utc) if time.kind_of? Time
159
-
160
- as_of = superclass.unscoped.readonly.
161
- with(superclass.table_name, at(time))
347
+ as_of = superclass.unscoped.readonly.from(virtual_table_at(time))
162
348
 
163
349
  # Add default scopes back if we're passed nil or a
164
350
  # specific scope, because we're .unscopeing above.
165
351
  #
166
- scopes = scope.present? ? [scope] : (
352
+ scopes = !scope.nil? ? [scope] : (
167
353
  superclass.default_scopes.map do |s|
168
354
  s.respond_to?(:call) ? s.call : s
169
355
  end)
170
356
 
171
- scopes.each do |scope|
172
- scope.order_values.each {|clause| as_of = as_of.order(clause.to_sql)}
173
- scope.where_values.each {|clause| as_of = as_of.where(clause.to_sql)}
357
+ scopes.each do |s|
358
+ s.order_values.each {|clause| as_of = as_of.order(clause)}
359
+ s.where_values.each {|clause| as_of = as_of.where(clause)}
174
360
  end
175
361
 
176
362
  as_of.instance_variable_set(:@temporal, time)
@@ -178,60 +364,133 @@ module ChronoModel
178
364
  return as_of
179
365
  end
180
366
 
367
+ def virtual_table_at(time, name = nil)
368
+ name = name ? connection.quote_table_name(name) :
369
+ superclass.quoted_table_name
370
+
371
+ "(#{at(time).to_sql}) #{name}"
372
+ end
373
+
181
374
  # Fetches history record at the given time
182
375
  #
183
376
  def at(time)
184
- from, to = quoted_history_fields
377
+ time = Conversions.time_to_utc_string(time.utc) if time.kind_of?(Time)
378
+
185
379
  unscoped.
186
- select("#{quoted_table_name}.*, '#{time}' AS as_of_time").
187
- where("'#{time}' >= #{from} AND '#{time}' < #{to}")
380
+ select("#{quoted_table_name}.*, #{connection.quote(time)} AS as_of_time").
381
+ time_query(:at, time, :on => quoted_history_fields)
188
382
  end
189
383
 
190
384
  # Returns the whole history as read only.
191
385
  #
192
386
  def all
193
387
  readonly.
194
- order("#{table_name}.recorded_at, hid").all
388
+ order("#{quoted_table_name}.recorded_at, hid").all
195
389
  end
196
390
 
197
- # Fetches the given +object+ history, sorted by history record time.
391
+ # Fetches the given +object+ history, sorted by history record time
392
+ # by default. Always includes an "as_of_time" column that is either
393
+ # the valid_to timestamp or now() if history validity is maximum.
198
394
  #
199
395
  def of(object)
200
- now = 'LEAST(valid_to, now()::timestamp)'
201
- readonly.
202
- select("#{table_name}.*, #{now} AS as_of_time").
203
- order("#{table_name}.recorded_at, hid").
204
- where(:id => object)
396
+ readonly.where(:id => object).extend(HistorySelect)
205
397
  end
206
398
 
399
+ module HistorySelect #:nodoc:
400
+ Aggregates = %r{(?:(?:bit|bool)_(?:and|or)|(?:array_|string_|xml)agg|count|every|m(?:in|ax)|sum|stddev|var(?:_pop|_samp|iance)|corr|covar_|regr_)\w*\s*\(}i
207
401
 
208
- # Returns an Array of unique UTC timestamps for which at least an
209
- # history record exists. Takes temporal associations into account.
210
- #
211
- def timestamps
212
- assocs = reflect_on_all_associations.select {|a|
213
- [:belongs_to, :has_one, :has_many].include?(a.macro) && a.klass.chrono?
214
- }
402
+ def build_arel
403
+ has_aggregate = select_values.any? do |v|
404
+ v.kind_of?(Arel::Nodes::Function) || # FIXME this is a bit ugly.
405
+ v.to_s =~ Aggregates
406
+ end
407
+
408
+ return super if has_aggregate
409
+
410
+ if order_values.blank?
411
+ self.order_values += ["#{quoted_table_name}.recorded_at, #{quoted_table_name}.hid"]
412
+ end
413
+
414
+ super.tap do |rel|
415
+ rel.project("LEAST(valid_to, now()::timestamp) AS as_of_time")
416
+ end
417
+ end
418
+ end
419
+
420
+ include(Timeline = Module.new do
421
+ # Returns an Array of unique UTC timestamps for which at least an
422
+ # history record exists. Takes temporal associations into account.
423
+ #
424
+ def timeline(record = nil, options = {})
425
+ rid = record.respond_to?(:rid) ? record.rid : record.id if record
426
+
427
+ assocs = options.key?(:with) ?
428
+ timeline_associations_from(options[:with]) : timeline_associations
429
+
430
+ models = []
431
+ models.push self if self.chrono?
432
+ models.concat(assocs.map {|a| a.klass.history})
433
+
434
+ return [] if models.empty?
435
+
436
+ fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
437
+ fields.map! {|f| "#{f} + INTERVAL '2 usec'"}
215
438
 
216
- models = [self].concat(assocs.map {|a| a.klass.history})
217
- fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
439
+ relation = self.
440
+ joins(*assocs.map(&:name)).
441
+ select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts").
442
+ order('ts ' << (options[:reverse] ? 'DESC' : 'ASC'))
218
443
 
219
- relation = self.
220
- joins(*assocs.map(&:name)).
221
- select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts").
222
- order('ts')
444
+ relation = relation.from(%["public".#{quoted_table_name}]) unless self.chrono?
445
+ relation = relation.where(:id => rid) if rid
223
446
 
224
- relation = yield relation if block_given?
447
+ sql = "SELECT ts FROM ( #{relation.to_sql} ) foo WHERE ts IS NOT NULL"
225
448
 
226
- sql = "SELECT ts FROM ( #{relation.to_sql} ) foo WHERE ts IS NOT NULL AND ts < NOW()"
227
- sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
449
+ if options.key?(:before)
450
+ sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
451
+ end
452
+
453
+ if options.key?(:after)
454
+ sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after ])}'"
455
+ end
228
456
 
229
- connection.on_schema(Adapter::HISTORY_SCHEMA) do
230
- connection.select_values(sql, "#{self.name} periods").map! do |ts|
231
- Conversions.string_to_utc_time ts
457
+ if rid
458
+ sql << (self.chrono? ? %[
459
+ AND ts >= ( SELECT MIN(valid_from) FROM #{quoted_table_name} WHERE id = #{rid} )
460
+ AND ts < ( SELECT MAX(valid_to ) FROM #{quoted_table_name} WHERE id = #{rid} )
461
+ ] : %[ AND ts < NOW() ])
462
+ end
463
+
464
+ sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
465
+
466
+ sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
467
+
468
+ connection.on_schema(Adapter::HISTORY_SCHEMA) do
469
+ connection.select_values(sql, "#{self.name} periods").map! do |ts|
470
+ Conversions.string_to_utc_time ts
471
+ end
232
472
  end
233
473
  end
234
- end
474
+
475
+ def has_timeline(options)
476
+ options.assert_valid_keys(:with)
477
+
478
+ timeline_associations_from(options[:with]).tap do |assocs|
479
+ timeline_associations.concat assocs
480
+ end
481
+ end
482
+
483
+ def timeline_associations
484
+ @timeline_associations ||= []
485
+ end
486
+
487
+ def timeline_associations_from(names)
488
+ Array.wrap(names).map do |name|
489
+ reflect_on_association(name) or raise ArgumentError,
490
+ "No association found for name `#{name}'"
491
+ end
492
+ end
493
+ end)
235
494
 
236
495
  def quoted_history_fields
237
496
  @quoted_history_fields ||= [:valid_from, :valid_to].map do |field|
@@ -246,10 +505,13 @@ module ChronoModel
246
505
  def build_arel
247
506
  super.tap do |arel|
248
507
 
249
- # Extract joined tables and add temporal WITH if appropriate
250
- arel.join_sources.map {|j| j.to_sql =~ /JOIN "(\w+)" ON/ && $1}.compact.each do |table|
251
- next unless (model = TimeMachine.chrono_models[table])
252
- with(table, model.history.at(@temporal))
508
+ arel.join_sources.each do |join|
509
+ model = TimeMachine.chrono_models[join.left.table_name]
510
+ next unless model
511
+
512
+ join.left = Arel::Nodes::SqlLiteral.new(
513
+ model.history.virtual_table_at(@temporal, join.left.table_alias || join.left.table_name)
514
+ )
253
515
  end if @temporal
254
516
 
255
517
  end
@@ -257,45 +519,6 @@ module ChronoModel
257
519
  end
258
520
  ActiveRecord::Relation.instance_eval { include QueryMethods }
259
521
 
260
- module Conversions
261
- extend self
262
-
263
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
264
-
265
- def string_to_utc_time(string)
266
- if string =~ ISO_DATETIME
267
- microsec = ($7.to_f * 1_000_000).to_i
268
- Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
269
- end
270
- end
271
-
272
- def time_to_utc_string(time)
273
- [time.to_s(:db), sprintf('%06d', time.usec)].join '.'
274
- end
275
- end
276
-
277
- module Utilities
278
- # Amends the given history item setting a different period.
279
- # Useful when migrating from legacy systems, but it is here
280
- # as this is not a proper API.
281
- #
282
- # Extend your model with the Utilities model if you want to
283
- # use it.
284
- #
285
- def amend_history_period!(hid, from, to)
286
- unless [from, to].all? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
287
- raise 'Can amend history only with UTC timestamps'
288
- end
289
-
290
- connection.execute %[
291
- UPDATE #{history_table_name}
292
- SET valid_from = #{connection.quote(from)},
293
- valid_to = #{connection.quote(to )}
294
- WHERE hid = #{hid.to_i}
295
- ]
296
- end
297
- end
298
-
299
522
  end
300
523
 
301
524
  end
@@ -0,0 +1,89 @@
1
+ module ChronoModel
2
+
3
+ module Conversions
4
+ extend self
5
+
6
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
7
+
8
+ def string_to_utc_time(string)
9
+ if string =~ ISO_DATETIME
10
+ microsec = ($7.to_f * 1_000_000).to_i
11
+ Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
12
+ end
13
+ end
14
+
15
+ def time_to_utc_string(time)
16
+ [time.to_s(:db), sprintf('%06d', time.usec)].join '.'
17
+ end
18
+ end
19
+
20
+ module Utilities
21
+ # Amends the given history item setting a different period.
22
+ # Useful when migrating from legacy systems, but it is here
23
+ # as this is not a proper API.
24
+ #
25
+ # Extend your model with the Utilities model if you want to
26
+ # use it.
27
+ #
28
+ def amend_period!(hid, from, to)
29
+ unless [from, to].all? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
30
+ raise 'Can amend history only with UTC timestamps'
31
+ end
32
+
33
+ connection.execute %[
34
+ UPDATE #{quoted_table_name}
35
+ SET "valid_from" = #{connection.quote(from)},
36
+ "valid_to" = #{connection.quote(to )}
37
+ WHERE "hid" = #{hid.to_i}
38
+ ]
39
+ end
40
+ end
41
+
42
+ module Migrate
43
+ extend self
44
+
45
+ def upgrade_indexes!(base = ActiveRecord::Base)
46
+ use base
47
+
48
+ db.on_schema(Adapter::HISTORY_SCHEMA) do
49
+ db.tables.each do |table|
50
+ if db.is_chrono?(table)
51
+ upgrade_indexes_for(table)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+ attr_reader :db
59
+
60
+ def use(ar)
61
+ @db = ar.connection
62
+ end
63
+
64
+ def upgrade_indexes_for(table_name)
65
+ upgradeable =
66
+ %r{_snapshot$|_valid_(?:from|to)$|_recorded_at$|_instance_(?:update|history)$}
67
+
68
+ indexes_sql = %[
69
+ SELECT DISTINCT i.relname
70
+ FROM pg_class t
71
+ INNER JOIN pg_index d ON t.oid = d.indrelid
72
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
73
+ WHERE i.relkind = 'i'
74
+ AND d.indisprimary = 'f'
75
+ AND t.relname = '#{table_name}'
76
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY(current_schemas(false)) )
77
+ ]
78
+
79
+ db.select_values(indexes_sql).each do |idx|
80
+ if idx =~ upgradeable
81
+ db.execute "DROP INDEX #{idx}"
82
+ end
83
+ end
84
+
85
+ db.send(:chrono_create_history_indexes_for, table_name)
86
+ end
87
+ end
88
+
89
+ end
@@ -1,3 +1,3 @@
1
1
  module ChronoModel
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0.beta"
3
3
  end