chrono_model 0.4.0 → 0.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -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