chrono_model 1.2.0 → 1.2.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -1
  3. data/lib/chrono_model/time_machine.rb +1 -1
  4. data/lib/chrono_model/time_machine/history_model.rb +4 -0
  5. data/lib/chrono_model/version.rb +1 -1
  6. data/spec/chrono_model/adapter/base_spec.rb +157 -0
  7. data/spec/chrono_model/adapter/ddl_spec.rb +243 -0
  8. data/spec/chrono_model/adapter/indexes_spec.rb +72 -0
  9. data/spec/chrono_model/adapter/migrations_spec.rb +312 -0
  10. data/spec/chrono_model/history_models_spec.rb +32 -0
  11. data/spec/chrono_model/time_machine/as_of_spec.rb +188 -0
  12. data/spec/chrono_model/time_machine/changes_spec.rb +50 -0
  13. data/spec/chrono_model/{adapter → time_machine}/counter_cache_race_spec.rb +2 -2
  14. data/spec/chrono_model/time_machine/default_scope_spec.rb +37 -0
  15. data/spec/chrono_model/time_machine/history_spec.rb +104 -0
  16. data/spec/chrono_model/time_machine/keep_cool_spec.rb +27 -0
  17. data/spec/chrono_model/time_machine/manipulations_spec.rb +84 -0
  18. data/spec/chrono_model/time_machine/model_identification_spec.rb +46 -0
  19. data/spec/chrono_model/time_machine/sequence_spec.rb +74 -0
  20. data/spec/chrono_model/time_machine/sti_spec.rb +100 -0
  21. data/spec/chrono_model/{time_query_spec.rb → time_machine/time_query_spec.rb} +22 -5
  22. data/spec/chrono_model/time_machine/timeline_spec.rb +63 -0
  23. data/spec/chrono_model/time_machine/timestamps_spec.rb +43 -0
  24. data/spec/chrono_model/time_machine/transactions_spec.rb +69 -0
  25. data/spec/support/adapter/helpers.rb +53 -0
  26. data/spec/support/adapter/structure.rb +44 -0
  27. data/spec/support/time_machine/helpers.rb +47 -0
  28. data/spec/support/time_machine/structure.rb +111 -0
  29. metadata +48 -14
  30. data/spec/chrono_model/adapter/sti_bug_spec.rb +0 -49
  31. data/spec/chrono_model/adapter_spec.rb +0 -788
  32. data/spec/chrono_model/time_machine_spec.rb +0 -749
  33. data/spec/support/helpers.rb +0 -198
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe '#last_changes' do
8
+ context 'on plain records' do
9
+ context 'having history' do
10
+ subject { $t.bar.last_changes }
11
+ it { is_expected.to eq('name' => ['bar bar', 'new bar']) }
12
+ end
13
+
14
+ context 'without history' do
15
+ let(:record) { Bar.create!(:name => 'foreveralone') }
16
+ subject { record.last_changes }
17
+ it { is_expected.to be_nil }
18
+ after { record.destroy.history.delete_all } # UGLY
19
+ end
20
+ end
21
+
22
+ context 'on history records' do
23
+ context 'at the beginning of the timeline' do
24
+ subject { $t.bar.history.first.last_changes }
25
+ it { is_expected.to be_nil }
26
+ end
27
+
28
+ context 'in the middle of the timeline' do
29
+ subject { $t.bar.history.second.last_changes }
30
+ it { is_expected.to eq('name' => ['bar', 'foo bar']) }
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#changes_against' do
36
+ context 'can compare records against history' do
37
+ it { expect($t.bar.changes_against($t.bar.history.first)).to eq('name' => ['bar', 'new bar']) }
38
+ it { expect($t.bar.changes_against($t.bar.history.second)).to eq('name' => ['foo bar', 'new bar']) }
39
+ it { expect($t.bar.changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'new bar']) }
40
+ it { expect($t.bar.changes_against($t.bar.history.last)).to eq({}) }
41
+ end
42
+
43
+ context 'can compare history against history' do
44
+ it { expect($t.bar.history.first. changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'bar']) }
45
+ it { expect($t.bar.history.second.changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'foo bar']) }
46
+ it { expect($t.bar.history.third. changes_against($t.bar.history.third)).to eq({}) }
47
+ end
48
+ end
49
+
50
+ end
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
- require 'support/helpers'
2
+ require 'support/time_machine/structure'
3
3
 
4
4
  describe 'models with counter cache' do
5
- include ChronoTest::Helpers::TimeMachine
5
+ include ChronoTest::TimeMachine::Helpers
6
6
 
7
7
  adapter.create_table 'sections', temporal: true, no_journal: %w( articles_count ) do |t|
8
8
  t.string :name
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ # Set up database structure
8
+ #
9
+ adapter.create_table 'defoos', :temporal => true do |t|
10
+ t.string :name
11
+ t.boolean :active
12
+ end
13
+
14
+ class ::Defoo < ActiveRecord::Base
15
+ include ChronoModel::TimeMachine
16
+
17
+ default_scope proc { where(:active => true) }
18
+ end
19
+
20
+ active = ts_eval { Defoo.create! :name => 'active 1', :active => true }
21
+ ts_eval(active) { update_attributes! :name => 'active 2' }
22
+
23
+ hidden = ts_eval { Defoo.create! :name => 'hidden 1', :active => false }
24
+ ts_eval(hidden) { update_attributes! :name => 'hidden 2' }
25
+
26
+ describe 'it honors default_scopes' do
27
+ it { expect(Defoo.as_of(active.ts[0]).map(&:name)).to eq ['active 1'] }
28
+ it { expect(Defoo.as_of(active.ts[1]).map(&:name)).to eq ['active 2'] }
29
+ it { expect(Defoo.as_of(hidden.ts[0]).map(&:name)).to eq ['active 2'] }
30
+ it { expect(Defoo.as_of(hidden.ts[1]).map(&:name)).to eq ['active 2'] }
31
+
32
+ it { expect(Defoo.unscoped.as_of(active.ts[0]).map(&:name)).to eq ['active 1'] }
33
+ it { expect(Defoo.unscoped.as_of(active.ts[1]).map(&:name)).to eq ['active 2'] }
34
+ it { expect(Defoo.unscoped.as_of(hidden.ts[0]).map(&:name)).to eq ['active 2', 'hidden 1'] }
35
+ it { expect(Defoo.unscoped.as_of(hidden.ts[1]).map(&:name)).to eq ['active 2', 'hidden 2'] }
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe '.history' do
8
+ let(:foo_history) {
9
+ ['foo', 'foo bar', 'new foo', 'foo 0', 'foo 1']
10
+ }
11
+
12
+ let(:bar_history) {
13
+ ['bar', 'foo bar', 'bar bar', 'new bar', 'bar 0', 'bar 1']
14
+ }
15
+
16
+ it { expect(Foo.history.all.map(&:name)).to eq foo_history }
17
+ it { expect(Bar.history.all.map(&:name)).to eq bar_history }
18
+
19
+ it { expect(Foo.history.first).to be_a(Foo::History) }
20
+ it { expect(Bar.history.first).to be_a(Bar::History) }
21
+ end
22
+
23
+
24
+ describe '#history' do
25
+ describe 'returns historical instances' do
26
+ it { expect($t.foo.history.size).to eq(3) }
27
+ it { expect($t.foo.history.map(&:name)).to eq ['foo', 'foo bar', 'new foo'] }
28
+
29
+ it { expect($t.bar.history.size).to eq(4) }
30
+ it { expect($t.bar.history.map(&:name)).to eq ['bar', 'foo bar', 'bar bar', 'new bar'] }
31
+ end
32
+
33
+ describe 'does not return read only records' do
34
+ it { expect($t.foo.history.all?(&:readonly?)).to be(false) }
35
+ it { expect($t.bar.history.all?(&:readonly?)).to be(false) }
36
+ end
37
+
38
+ describe 'takes care of associated records' do
39
+ subject { $t.foo.history.map {|f| f.bars.first.try(:name)} }
40
+ it { is_expected.to eq [nil, 'foo bar', 'new bar'] }
41
+ end
42
+
43
+ describe 'does not return read only associated records' do
44
+ it { expect($t.foo.history[2].bars.all?(&:readonly?)).to_not be(true) }
45
+ it { expect($t.bar.history.all? {|b| b.foo.readonly?}).to_not be(true) }
46
+ end
47
+
48
+ describe 'allows a custom select list' do
49
+ it { expect($t.foo.history.select(:id).first.attributes.keys).to eq %w( id ) }
50
+ end
51
+
52
+ describe 'does not add as_of_time when there are aggregates' do
53
+ it { expect($t.foo.history.select('max(id)').to_sql).to_not match(/as_of_time/) }
54
+
55
+ it { expect($t.foo.history.except(:order).select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to eq %w( id foo bar ) }
56
+ end
57
+
58
+ context '.sorted' do
59
+ describe 'orders by recorded_at, hid' do
60
+ it { expect($t.foo.history.sorted.to_sql).to match(/order by .+"recorded_at" ASC, .+"hid" ASC/i) }
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ describe '#current_version' do
67
+ describe 'on plain records' do
68
+ subject { $t.foo.current_version }
69
+ it { is_expected.to eq $t.foo }
70
+ end
71
+
72
+ describe 'from #as_of' do
73
+ subject { $t.foo.as_of(Time.now) }
74
+ it { is_expected.to eq $t.foo }
75
+ end
76
+
77
+ describe 'on historical records' do
78
+ subject { $t.foo.history.sample.current_version }
79
+ it { is_expected.to eq $t.foo }
80
+ end
81
+ end
82
+
83
+
84
+ describe '#historical?' do
85
+ subject { record.historical? }
86
+
87
+ describe 'on plain records' do
88
+ let(:record) { $t.foo }
89
+ it { is_expected.to be(false) }
90
+ end
91
+
92
+ describe 'on historical records' do
93
+ describe 'from #history' do
94
+ let(:record) { $t.foo.history.first }
95
+ it { is_expected.to be(true) }
96
+ end
97
+
98
+ describe 'from #as_of' do
99
+ let(:record) { $t.foo.as_of(Time.now) }
100
+ it { is_expected.to be(true) }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe 'does not interfere with AR standard behaviour' do
8
+ let(:all_foos) { [ $t.foo ] + $t.foos }
9
+ let(:all_bars) { [ $t.bar ] + $t.bars }
10
+
11
+ it { expect(Foo.count).to eq all_foos.size }
12
+ it { expect(Bar.count).to eq all_bars.size }
13
+
14
+ it { expect(Foo.includes(bars: :sub_bars)).to eq all_foos }
15
+ it { expect(Foo.includes(:bars).preload(bars: :sub_bars)).to eq all_foos }
16
+
17
+ it { expect(Foo.includes(:bars).first.name).to eq 'new foo' }
18
+ it { expect(Foo.includes(:bars).as_of($t.foo.ts[0]).first.name).to eq 'foo' }
19
+
20
+ it { expect(Foo.joins(:bars).map(&:bars).flatten).to eq all_bars }
21
+ it { expect(Foo.joins(:bars).first.bars.joins(:sub_bars).first.name).to eq 'new bar' }
22
+
23
+ it { expect(Foo.joins(bars: :sub_bars).first.bars.joins(:sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
24
+
25
+ it { expect(Foo.first.bars.includes(:sub_bars)).to eq [ $t.bar ] }
26
+ end
27
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe '#save' do
8
+ subject { $t.bar.history.first }
9
+
10
+ it do
11
+ with_revert do
12
+ subject.name = 'modified bar history'
13
+ subject.save
14
+ subject.reload
15
+
16
+ is_expected.to be_a(Bar::History)
17
+ expect(subject.name).to eq 'modified bar history'
18
+ end
19
+ end
20
+ end
21
+
22
+ describe '#save!' do
23
+ subject { $t.bar.history.second }
24
+
25
+ it do
26
+ with_revert do
27
+ subject.name = 'another modified bar history'
28
+ subject.save
29
+ subject.reload
30
+
31
+ is_expected.to be_a(Bar::History)
32
+ expect(subject.name).to eq 'another modified bar history'
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '#destroy' do
38
+ describe 'on historical records' do
39
+ subject { $t.foo.history.first.destroy }
40
+ it { expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord) }
41
+ end
42
+
43
+ describe 'on current records' do
44
+ rec = nil
45
+ before(:all) do
46
+ rec = ts_eval { Foo.create!(:name => 'alive foo', :fooity => 42) }
47
+ ts_eval(rec) { update_attributes!(:name => 'dying foo') }
48
+ end
49
+ after(:all) do
50
+ rec.history.delete_all
51
+ end
52
+
53
+ subject { rec.destroy }
54
+
55
+ it { expect { subject }.to_not raise_error }
56
+ it { expect { rec.reload }.to raise_error(ActiveRecord::RecordNotFound) }
57
+
58
+ describe 'does not delete its history' do
59
+ subject { record.name }
60
+
61
+ context do
62
+ let(:record) { rec.as_of(rec.ts.first) }
63
+ it { is_expected.to eq 'alive foo' }
64
+ end
65
+
66
+ context do
67
+ let(:record) { rec.as_of(rec.ts.last) }
68
+ it { is_expected.to eq 'dying foo' }
69
+ end
70
+
71
+ context do
72
+ let(:record) { Foo.as_of(rec.ts.first).where(:fooity => 42).first }
73
+ it { is_expected.to eq 'alive foo' }
74
+ end
75
+
76
+ context do
77
+ subject { Foo.history.where(:fooity => 42).map(&:name) }
78
+ it { is_expected.to eq ['alive foo', 'dying foo'] }
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ adapter.create_table 'plains' do |t|
8
+ t.string :foo
9
+ end
10
+
11
+ class ::Plain < ActiveRecord::Base
12
+ end
13
+
14
+ describe '.chrono?' do
15
+ subject { model.chrono? }
16
+
17
+ context 'on a temporal model' do
18
+ let(:model) { Foo }
19
+ it { is_expected.to be(true) }
20
+ end
21
+
22
+ context 'on a plain model' do
23
+ let(:model) { Plain }
24
+ it { is_expected.to be(false) }
25
+ end
26
+ end
27
+
28
+ describe '.history?' do
29
+ subject { model.history? }
30
+
31
+ context 'on a temporal parent model' do
32
+ let(:model) { Foo }
33
+ it { is_expected.to be(false) }
34
+ end
35
+
36
+ context 'on a temporal history model' do
37
+ let(:model) { Foo::History }
38
+ it { is_expected.to be(true) }
39
+ end
40
+
41
+ context 'on a plain model' do
42
+ let(:model) { Plain }
43
+ it { expect { subject }.to raise_error(NoMethodError) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe '#pred' do
8
+ context 'on the first history entry' do
9
+ subject { $t.foo.history.first.pred }
10
+ it { is_expected.to be(nil) }
11
+ end
12
+
13
+ context 'on the second history entry' do
14
+ subject { $t.foo.history.second.pred }
15
+ it { is_expected.to eq $t.foo.history.first }
16
+ end
17
+
18
+ context 'on the last history entry' do
19
+ subject { $t.foo.history.last.pred }
20
+ it { is_expected.to eq $t.foo.history[$t.foo.history.size - 2] }
21
+ end
22
+
23
+ context 'on records having history' do
24
+ subject { $t.bar.pred }
25
+ it { expect(subject.name).to eq 'bar bar' }
26
+ end
27
+
28
+ context 'when there is enough history' do
29
+ subject { $t.bar.pred.pred.pred.pred }
30
+ it { expect(subject.name).to eq 'bar' }
31
+ end
32
+
33
+ context 'when no history is recorded' do
34
+ let(:record) { Bar.create!(:name => 'quuuux') }
35
+
36
+ subject { record.pred }
37
+
38
+ it { is_expected.to be(nil) }
39
+
40
+ after { record.destroy.history.delete_all }
41
+ end
42
+ end
43
+
44
+ describe '#succ' do
45
+ context 'on the first history entry' do
46
+ subject { $t.foo.history.first.succ }
47
+
48
+ it { is_expected.to eq $t.foo.history.second }
49
+ end
50
+
51
+ context 'on the second history entry' do
52
+ subject { $t.foo.history.second.succ }
53
+
54
+ it { is_expected.to eq $t.foo.history.third }
55
+ end
56
+
57
+ context 'on the last history entry' do
58
+ subject { $t.foo.history.last.succ }
59
+
60
+ it { is_expected.to be(nil) }
61
+ end
62
+ end
63
+
64
+ describe '#first' do
65
+ subject { $t.foo.history.sample.first }
66
+ it { is_expected.to eq $t.foo.history.first }
67
+ end
68
+
69
+ describe '#last' do
70
+ subject { $t.foo.history.sample.last }
71
+ it { is_expected.to eq $t.foo.history.last }
72
+ end
73
+
74
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ # STI cases
5
+ #
6
+ # - https://github.com/ifad/chronomodel/issues/5
7
+ # - https://github.com/ifad/chronomodel/issues/47
8
+ #
9
+ describe 'models with STI' do
10
+ include ChronoTest::TimeMachine::Helpers
11
+
12
+ adapter.create_table 'elements', :temporal => true do |t|
13
+ t.string :title
14
+ t.string :type
15
+ end
16
+
17
+ class ::Element < ActiveRecord::Base
18
+ include ChronoModel::TimeMachine
19
+ end
20
+
21
+ class ::Publication < Element
22
+ end
23
+
24
+ describe '.descendants' do
25
+ subject { Element.descendants }
26
+
27
+ it { is_expected.to_not include(Element::History) }
28
+ it { is_expected.to include(Publication) }
29
+ end
30
+
31
+ describe '.descendants_with_history' do
32
+ subject { Element.descendants_with_history }
33
+
34
+ it { is_expected.to include(Element::History) }
35
+ it { is_expected.to include(Publication) }
36
+ end
37
+
38
+ describe 'timeline' do
39
+ let(:publication) do
40
+ pub = ts_eval { Publication.create! :title => 'wrong title' }
41
+ ts_eval(pub) { update_attributes! :title => 'correct title' }
42
+
43
+ pub
44
+ end
45
+
46
+ it { expect(publication.history.map(&:title)).to eq ['wrong title', 'correct title'] }
47
+ end
48
+
49
+ describe 'identity' do
50
+ adapter.create_table 'animals', temporal: true do |t|
51
+ t.string :type
52
+ end
53
+
54
+ class ::Animal < ActiveRecord::Base
55
+ include ChronoModel::TimeMachine
56
+ end
57
+
58
+ class ::Dog < Animal
59
+ end
60
+
61
+ class ::Goat < Animal
62
+ end
63
+
64
+ before do
65
+ Dog.create!
66
+ @later = Time.new
67
+ Goat.create!
68
+ end
69
+
70
+ after do
71
+ tables = ['temporal.animals', 'history.animals']
72
+ ActiveRecord::Base.connection.execute "truncate #{tables.join(', ')} cascade"
73
+ end
74
+
75
+ specify "select" do
76
+ expect(Animal.first).to be_a(Animal)
77
+ expect(Animal.as_of(@later).first).to be_a(Animal)
78
+
79
+ expect(Animal.where(type: 'Dog').first).to be_a(Dog)
80
+ expect(Dog.first).to be_a(Dog)
81
+ expect(Dog.as_of(@later).first).to be_a(Dog)
82
+
83
+ expect(Animal.where(type: 'Goat').first).to be_a(Goat)
84
+ expect(Goat.first).to be_a(Goat)
85
+ expect(Goat.as_of(@later).first).to be(nil)
86
+ expect(Goat.as_of(Time.now).first).to be_a(Goat)
87
+ end
88
+
89
+ specify "count" do
90
+ expect(Animal.count).to eq(2)
91
+ expect(Animal.as_of(@later).count).to eq(1)
92
+
93
+ expect(Dog.count).to eq(1)
94
+ expect(Dog.as_of(@later).count).to eq(1)
95
+
96
+ expect(Goat.count).to eq(1)
97
+ expect(Goat.as_of(@later).count).to eq(0)
98
+ end
99
+ end
100
+ end