chrono_model 1.2.0 → 1.2.1

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