chrono_model 0.13.0 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 883f6c37f76cc8b57b2d7f6b59e1a5627592907e
4
- data.tar.gz: 8b35df17eb98dbddd3d0b5c5c046b7b9d7d00501
2
+ SHA256:
3
+ metadata.gz: 940172f2bad591657a9db2e6124c5b36ccab57cae41f8a0a85c2d473345b8f2c
4
+ data.tar.gz: 0060e673c40a25cd8d44f925483c85cb08dc794b15291705fa69e64c2b3d9eca
5
5
  SHA512:
6
- metadata.gz: 99fdb72cb9604d4566049199192b3982f5f44a088ad0f94335e677d8d847fd0a993390d6c17bb2381ec9c32c37c9fd378ba356ce7f9628b0e23aabe82b6702eb
7
- data.tar.gz: 7e0be5293bcea607ee1fc1300ad28b491bc9620a2e4ee965f3763e7dadb509f702019fbbc27b254ba41243bd0e35ac4cf9131ab79beb561f87a814de2483b604
6
+ metadata.gz: b850d300fe412425c472f04e022ff89ca9ab9d299f65c52ac65a4ccfd401828301d524868b57f5490e84d32d7baf4dd2c446ba9cf9ea6ab780910ee9ced80ad5
7
+ data.tar.gz: 26f7751a0ff8dc234f32d391a6cb84a853edd2213547613db3d413cceaba42475f12537a7f5e647d6c307f9016b445c16f956a5f912542c2c83a19b3de4f31e0
data/README.md CHANGED
@@ -257,7 +257,23 @@ more information.
257
257
 
258
258
  ## History manipulation
259
259
 
260
- History objects can be changed and `.save`d just like any other record.
260
+ History objects can be changed and `.save`d just like any other record. They
261
+ cannot be deleted.
262
+
263
+ ## Upgrading
264
+
265
+ ChronoModel currently performs upgrades by dropping and re-creating the views
266
+ that give access to current data. If you have built other database objects on
267
+ these views, the upgrade cannot be performed automatically as the dependant
268
+ objects must be dropped first.
269
+
270
+ When booting, ChronoModel will issue a warning in your logs about the need of
271
+ a structure upgrade. Structure usually changes across versions. In this case,
272
+ you need to set up a rake task that drops your dependant objects, runs
273
+ ChronoModel.upgrade! and then re-creates them.
274
+
275
+ A migration system should be introduced, but it is seen as overkill for now,
276
+ given that usually database objects have creation and dropping scripts.
261
277
 
262
278
 
263
279
  ## Running tests
data/lib/chrono_model.rb CHANGED
@@ -8,6 +8,16 @@ require 'chrono_model/utils'
8
8
  module ChronoModel
9
9
  class Error < ActiveRecord::ActiveRecordError #:nodoc:
10
10
  end
11
+
12
+ def self.upgrade!
13
+ connection = ActiveRecord::Base.connection
14
+
15
+ unless connection.is_a?(ChronoModel::Adapter)
16
+ raise ChronoModel::Error, "This database connection is not a ChronoModel::Adapter"
17
+ end
18
+
19
+ connection.chrono_upgrade!
20
+ end
11
21
  end
12
22
 
13
23
  if defined?(Rails)
@@ -462,7 +462,13 @@ module ChronoModel
462
462
  end
463
463
 
464
464
  def chrono_setup!
465
- chrono_create_schemas
465
+ chrono_ensure_schemas
466
+
467
+ chrono_upgrade_warning
468
+ end
469
+
470
+ def chrono_upgrade!
471
+ chrono_ensure_schemas
466
472
 
467
473
  chrono_upgrade_structure!
468
474
  end
@@ -541,37 +547,69 @@ module ChronoModel
541
547
  private
542
548
  # Create the temporal and history schemas, unless they already exist
543
549
  #
544
- def chrono_create_schemas
550
+ def chrono_ensure_schemas
545
551
  [TEMPORAL_SCHEMA, HISTORY_SCHEMA].each do |schema|
546
552
  execute "CREATE SCHEMA #{schema}" unless schema_exists?(schema)
547
553
  end
548
554
  end
549
555
 
556
+ # Locate tables needing a structure upgrade
557
+ #
558
+ def chrono_tables_needing_upgrade
559
+ tables = { }
560
+
561
+ _on_temporal_schema { self.tables }.each do |table_name|
562
+ next unless is_chrono?(table_name)
563
+ metadata = chrono_metadata_for(table_name)
564
+ version = metadata['chronomodel']
565
+
566
+ if version.blank?
567
+ tables[table_name] = { version: nil, priority: 'CRITICAL' }
568
+ elsif version != VERSION
569
+ tables[table_name] = { version: version, priority: 'LOW' }
570
+ end
571
+ end
572
+
573
+ return tables
574
+ end
575
+
576
+ # Emit a warning about tables needing an upgrade
577
+ #
578
+ def chrono_upgrade_warning
579
+ upgrade = chrono_tables_needing_upgrade.map do |table, desc|
580
+ "#{table} - priority: #{desc[:priority]}"
581
+ end.join('; ')
582
+
583
+ return if upgrade.empty?
584
+
585
+ logger.warn "ChronoModel: There are tables needing a structure upgrade, and ChronoModel structures need to be recreated."
586
+ logger.warn "ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependant database objects"
587
+ logger.warn "ChronoModel: the upgrade will fail and you have to drop the dependent objects, run .upgrade! and create them"
588
+ logger.warn "ChronoModel: again. Sorry. Some features or the whole library may not work correctly until upgrade is complete."
589
+ logger.warn "ChronoModel: Tables pending upgrade: #{upgrade}"
590
+ end
591
+
550
592
  # Upgrades existing structure for each table, if required.
551
- # TODO: allow upgrades from pre-0.6 structure with box() and stuff.
552
593
  #
553
594
  def chrono_upgrade_structure!
554
595
  transaction do
555
- current = VERSION
556
596
 
557
- _on_temporal_schema { tables }.each do |table_name|
558
- next unless is_chrono?(table_name)
559
- metadata = chrono_metadata_for(table_name)
560
- version = metadata['chronomodel']
597
+ chrono_tables_needing_upgrade.each do |table_name, desc|
561
598
 
562
- if version.blank?
599
+ if desc[:version].blank?
600
+ logger.info "ChronoModel: Upgrading legacy table #{table_name} to #{VERSION}"
563
601
  upgrade_from_legacy(table_name)
602
+ logger.info "ChronoModel: legacy #{table_name} upgrade complete"
603
+ else
604
+ logger.info "ChronoModel: upgrading #{table_name} from #{version} to #{VERSION}"
605
+ chrono_create_view_for(table_name)
606
+ logger.info "ChronoModel: #{table_name} upgrade complete"
564
607
  end
565
608
 
566
- next if version == current
567
-
568
- logger.info "ChronoModel: upgrading #{table_name} from #{version} to #{current}"
569
- chrono_create_view_for(table_name)
570
- logger.info "ChronoModel: upgrade complete"
571
609
  end
572
610
  end
573
611
  rescue => e
574
- message = "ChronoModel structure upgrade failed: #{e.message}. Please drop dependent objects and then run ActiveRecord::Base.connection.chrono_setup!"
612
+ message = "ChronoModel structure upgrade failed: #{e.message}. Please drop dependent objects first and then run ChronoModel.upgrade! again."
575
613
 
576
614
  # Quite important, output it also to stderr.
577
615
  #
@@ -88,7 +88,8 @@ module ChronoModel
88
88
  # Build a preloader at the +as_of_time+ of this relation
89
89
  #
90
90
  def build_preloader
91
- ActiveRecord::Associations::Preloader.new(as_of_time: as_of_time)
91
+ options = as_of_time ? {as_of_time: as_of_time} : {}
92
+ ActiveRecord::Associations::Preloader.new(options)
92
93
  end
93
94
  end
94
95
 
@@ -157,7 +158,7 @@ module ChronoModel
157
158
  def build_scope
158
159
  scope = super
159
160
 
160
- if preload_scope.respond_to?(:as_of_time)
161
+ if preload_scope.respond_to?(:as_of_time) && preload_scope.as_of_time
161
162
  scope = scope.as_of(preload_scope.as_of_time)
162
163
  end
163
164
 
@@ -1,3 +1,3 @@
1
1
  module ChronoModel
2
- VERSION = "0.13.0"
2
+ VERSION = "0.13.1"
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'simplecov'
4
4
  SimpleCov.start
5
5
 
6
+ require 'byebug'
7
+
6
8
  require 'chrono_model'
7
9
 
8
10
  require 'support/connection'
@@ -13,6 +15,8 @@ require 'support/matchers/index'
13
15
  require 'support/matchers/function'
14
16
  require 'support/aruba'
15
17
 
18
+ puts "Testing against Active Record #{ActiveRecord::VERSION::STRING} with Arel #{Arel::VERSION}"
19
+
16
20
  # Rails 5 returns a True/FalseClass
17
21
  AR_TRUE, AR_FALSE = ActiveRecord::VERSION::MAJOR == 4 ? ['t', 'f'] : [true, false]
18
22
 
@@ -28,6 +28,10 @@ describe ChronoModel::TimeMachine do
28
28
  ts_eval(subbar) { update_attributes! :name => 'sub-bar sub-bar' }
29
29
  ts_eval(subbar) { update_attributes! :name => 'new sub-bar' }
30
30
 
31
+ #
32
+ foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
33
+ bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } }
34
+
31
35
  #
32
36
  baz = Baz.create :name => 'baz', :bar => bar
33
37
 
@@ -90,6 +94,28 @@ describe ChronoModel::TimeMachine do
90
94
  ) }
91
95
  end
92
96
 
97
+ describe 'does not interfere with AR standard behaviour' do
98
+ all_foos = [ foo ] + foos
99
+ all_bars = [ bar ] + bars
100
+
101
+ it { expect(Foo.count).to eq all_foos.size }
102
+ it { expect(Bar.count).to eq all_bars.size }
103
+
104
+ it { expect(Foo.includes(bars: :sub_bars)).to eq all_foos }
105
+ it { expect(Foo.includes(:bars).preload(bars: :sub_bars)).to eq all_foos }
106
+
107
+ it { expect(Foo.includes(:bars).first.name).to eq 'new foo' }
108
+ it { expect(Foo.includes(:bars).as_of(foo.ts[0]).first.name).to eq 'foo' }
109
+
110
+ it { expect(Foo.joins(:bars).map(&:bars).flatten).to eq all_bars }
111
+ it { expect(Foo.joins(:bars).first.bars.joins(:sub_bars).first.name).to eq 'new bar' }
112
+
113
+ it { expect(Foo.joins(bars: :sub_bars).first.bars.joins(:sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
114
+
115
+ it { expect(Foo.first.bars.includes(:sub_bars)).to eq [ bar ] }
116
+
117
+ end
118
+
93
119
  describe '#as_of' do
94
120
  describe 'accepts a Time instance' do
95
121
  it { expect(foo.as_of(Time.now).name).to eq 'new foo' }
@@ -264,7 +290,7 @@ describe ChronoModel::TimeMachine do
264
290
  end
265
291
 
266
292
  describe 'does not add as_of_time when there are aggregates' do
267
- it { expect(foo.history.select('max(id)').to_sql).to_not match /as_of_time/ }
293
+ it { expect(foo.history.select('max(id)').to_sql).to_not match(/as_of_time/) }
268
294
 
269
295
  it { expect(foo.history.except(:order).select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to eq %w( id foo bar ) }
270
296
  end
@@ -278,7 +304,7 @@ describe ChronoModel::TimeMachine do
278
304
 
279
305
  context '.sorted' do
280
306
  describe 'orders by recorded_at, hid' do
281
- it { expect(foo.history.sorted.to_sql).to match /order by .+"recorded_at", .+"hid"/i }
307
+ it { expect(foo.history.sorted.to_sql).to match(/order by .+"recorded_at", .+"hid"/i) }
282
308
  end
283
309
  end
284
310
  end
@@ -416,7 +442,7 @@ describe ChronoModel::TimeMachine do
416
442
  split = lambda {|ts| ts.map!{|t| [t.to_i, t.usec]} }
417
443
 
418
444
  timestamps_from = lambda {|*records|
419
- ts = records.map(&:history).flatten!.inject([]) {|ret, rec|
445
+ records.map(&:history).flatten!.inject([]) {|ret, rec|
420
446
  ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from)
421
447
  ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to)
422
448
  ret
@@ -570,11 +596,6 @@ describe ChronoModel::TimeMachine do
570
596
 
571
597
  # Class methods
572
598
  context do
573
- foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
574
- bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } }
575
-
576
- after(:all) { foos.each(&:destroy); bars.each(&:destroy) }
577
-
578
599
  describe '.as_of' do
579
600
  it { expect(Foo.as_of(1.month.ago)).to eq [] }
580
601
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcello Barnaba
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-10-29 00:00:00.000000000 Z
12
+ date: 2019-04-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -278,7 +278,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
278
  version: '0'
279
279
  requirements: []
280
280
  rubyforge_project:
281
- rubygems_version: 2.5.2.1
281
+ rubygems_version: 2.7.6.2
282
282
  signing_key:
283
283
  specification_version: 4
284
284
  summary: Temporal extensions (SCD Type II) for Active Record