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
@@ -1,13 +1,30 @@
1
1
  require 'spec_helper'
2
- require 'support/helpers'
2
+ require 'support/time_machine/structure'
3
3
 
4
4
  describe ChronoModel::TimeMachine::TimeQuery do
5
- include ChronoTest::Helpers::TimeMachine
5
+ include ChronoTest::TimeMachine::Helpers
6
6
 
7
- setup_schema!
8
- define_models!
7
+ adapter.create_table 'events' do |t|
8
+ t.string :name
9
+ t.daterange :interval
10
+ end
11
+
12
+ class ::Event < ActiveRecord::Base
13
+ extend ChronoModel::TimeMachine::TimeQuery
14
+ end
9
15
 
10
- # Create a set of events
16
+ # Main timeline quick test
17
+ #
18
+ it { expect(Foo.history.time_query(:after, :now, inclusive: true ).count).to eq 3 }
19
+ it { expect(Foo.history.time_query(:after, :now, inclusive: false).count).to eq 0 }
20
+ it { expect(Foo.history.time_query(:before, :now, inclusive: true ).count).to eq 5 }
21
+ it { expect(Foo.history.time_query(:before, :now, inclusive: false).count).to eq 2 }
22
+
23
+ it { expect(Foo.history.past.size).to eq 2 }
24
+
25
+ # Extended thorough test.
26
+ #
27
+ # Create a set of events and then run time queries on them.
11
28
  #
12
29
  think = Event.create! name: 'think', interval: (15.days.ago.to_date...13.days.ago.to_date)
13
30
  plan = Event.create! name: 'plan', interval: (14.days.ago.to_date...12.days.ago.to_date)
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ describe '#timeline' do
8
+ split = lambda {|ts| ts.map!{|t| [t.to_i, t.usec]} }
9
+
10
+ timestamps_from = lambda {|*records|
11
+ records.map(&:history).flatten!.inject([]) {|ret, rec|
12
+ ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from)
13
+ ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to)
14
+ ret
15
+ }.sort.uniq
16
+ }
17
+
18
+ describe 'on records having an :has_many relationship' do
19
+ describe 'by default returns timestamps of the record only' do
20
+ subject { split.call($t.foo.timeline) }
21
+
22
+ it { expect(subject.size).to eq $t.foo.ts.size }
23
+ it { is_expected.to eq timestamps_from.call($t.foo) }
24
+ end
25
+
26
+ describe 'when asked, returns timestamps including the related objects' do
27
+ subject { split.call($t.foo.timeline(with: :bars)) }
28
+
29
+ it { expect(subject.size).to eq($t.foo.ts.size + $t.bar.ts.size) }
30
+ it { is_expected.to eq(timestamps_from.call($t.foo, *$t.foo.bars)) }
31
+ end
32
+ end
33
+
34
+ describe 'on records using has_timeline :with' do
35
+ subject { split.call($t.bar.timeline) }
36
+
37
+ describe 'returns timestamps of the record and its associations' do
38
+
39
+ let!(:expected) do
40
+ creat = $t.bar.history.first.valid_from
41
+ c_sec, c_usec = creat.to_i, creat.usec
42
+
43
+ timestamps_from.call($t.foo, $t.bar).reject {|sec, usec|
44
+ sec < c_sec || ( sec == c_sec && usec < c_usec )
45
+ }
46
+ end
47
+
48
+ it { expect(subject.size).to eq expected.size }
49
+ it { is_expected.to eq expected }
50
+ end
51
+ end
52
+
53
+ describe 'on non-temporal records using has_timeline :with' do
54
+ subject { split.call($t.baz.timeline) }
55
+
56
+ describe 'returns timestamps of its temporal associations' do
57
+ it { expect(subject.size).to eq $t.bar.ts.size }
58
+ it { is_expected.to eq timestamps_from.call($t.bar) }
59
+ end
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ history_methods = %w( valid_from valid_to recorded_at )
8
+ current_methods = %w( as_of_time )
9
+
10
+ context 'on history records' do
11
+ let(:record) { $t.foo.history.first }
12
+
13
+ (history_methods + current_methods).each do |attr|
14
+ describe ['#', attr].join do
15
+ subject { record.public_send(attr) }
16
+
17
+ it { is_expected.to be_present }
18
+ it { is_expected.to be_a(Time) }
19
+ it { is_expected.to be_utc }
20
+ end
21
+ end
22
+ end
23
+
24
+ context 'on current records' do
25
+ let(:record) { $t.foo }
26
+
27
+ history_methods.each do |attr|
28
+ describe ['#', attr].join do
29
+ subject { record.public_send(attr) }
30
+
31
+ it { expect { subject }.to raise_error(NoMethodError) }
32
+ end
33
+ end
34
+
35
+ current_methods.each do |attr|
36
+ describe ['#', attr].join do
37
+ subject { record.public_send(attr) }
38
+
39
+ it { is_expected.to be(nil) }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+ require 'support/time_machine/structure'
3
+
4
+ describe ChronoModel::TimeMachine do
5
+ include ChronoTest::TimeMachine::Helpers
6
+
7
+ # Transactions
8
+ context 'multiple updates to an existing record' do
9
+ let!(:r1) do
10
+ Foo.create!(:name => 'xact test').tap do |record|
11
+ Foo.transaction do
12
+ record.update_attribute 'name', 'lost into oblivion'
13
+ record.update_attribute 'name', 'does work'
14
+ end
15
+ end
16
+ end
17
+
18
+ it "generate only a single history record" do
19
+ expect(r1.history.size).to eq(2)
20
+
21
+ expect(r1.history.first.name).to eq 'xact test'
22
+ expect(r1.history.last.name).to eq 'does work'
23
+ end
24
+
25
+ after do
26
+ r1.destroy
27
+ r1.history.delete_all
28
+ end
29
+ end
30
+
31
+ context 'insertion and subsequent update' do
32
+ let!(:r2) do
33
+ Foo.transaction do
34
+ Foo.create!(:name => 'lost into oblivion').tap do |record|
35
+ record.update_attribute 'name', 'I am Bar'
36
+ record.update_attribute 'name', 'I am Foo'
37
+ end
38
+ end
39
+ end
40
+
41
+ it 'generates a single history record' do
42
+ expect(r2.history.size).to eq(1)
43
+ expect(r2.history.first.name).to eq 'I am Foo'
44
+ end
45
+
46
+ after do
47
+ r2.destroy
48
+ r2.history.delete_all
49
+ end
50
+ end
51
+
52
+ context 'insertion and subsequent deletion' do
53
+ let!(:r3) do
54
+ Foo.transaction do
55
+ Foo.create!(:name => 'it never happened').destroy
56
+ end
57
+ end
58
+
59
+ it 'does not generate any history' do
60
+ expect(Foo.history.where(:id => r3.id)).to be_empty
61
+ end
62
+
63
+ after do
64
+ r3.destroy
65
+ r3.history.delete_all
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,53 @@
1
+ module ChronoTest::Adapter
2
+
3
+ module Helpers
4
+ def self.included(base)
5
+ base.extend DSL
6
+
7
+ base.instance_eval do
8
+ delegate :adapter, :to => ChronoTest
9
+ delegate :columns, :table, :pk_type, :to => DSL
10
+ end
11
+ end
12
+
13
+ module DSL
14
+ def with_temporal_table(&block)
15
+ context ':temporal => true' do
16
+ before(:all) { adapter.create_table(table, :temporal => true, &DSL.columns) }
17
+ after(:all) { adapter.drop_table table }
18
+
19
+ instance_eval(&block)
20
+ end
21
+ end
22
+
23
+ def with_plain_table(&block)
24
+ context ':temporal => false' do
25
+ before(:all) { adapter.create_table(table, :temporal => false, &DSL.columns) }
26
+ after(:all) { adapter.drop_table table }
27
+
28
+ instance_eval(&block)
29
+ end
30
+ end
31
+
32
+ def self.table(table = nil)
33
+ @table = table if table
34
+ @table
35
+ end
36
+
37
+ def self.columns(&block)
38
+ @columns = block.call if block
39
+ @columns
40
+ end
41
+
42
+ def self.pk_type
43
+ @pk_type ||= if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1
44
+ 'bigint'
45
+ else
46
+ 'integer'
47
+ end
48
+ end
49
+ delegate :columns, :table, :pk_type, :to => self
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,44 @@
1
+ require 'support/adapter/helpers'
2
+
3
+ # This module contains the definition of a test structure that is used by the
4
+ # adapter methods tests, that look up in the database directly whether the
5
+ # expected objects have been created.
6
+ #
7
+ # The structure defintiion below serves as a blueprint of what it can be
8
+ # defined, ans as a reference of what it is expected to have been created by
9
+ # the +ChronoModel::Adapter+ methods.
10
+ #
11
+ module ChronoTest::Adapter
12
+
13
+ module Structure
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ table 'test_table'
18
+ subject { table }
19
+
20
+ columns do
21
+ native = [
22
+ ['test', 'character varying'],
23
+ ['foo', 'integer'],
24
+ ['bar', 'double precision'],
25
+ ['baz', 'text']
26
+ ]
27
+
28
+ def native.to_proc
29
+ proc {|t|
30
+ t.string :test, null: false, default: 'default-value'
31
+ t.integer :foo
32
+ t.float :bar
33
+ t.text :baz
34
+ t.integer :ary, array: true, null: false, default: []
35
+ t.boolean :bool, null: false, default: false
36
+ }
37
+ end
38
+
39
+ native
40
+ end
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,47 @@
1
+ module ChronoTest::TimeMachine
2
+
3
+ # This module contains helpers used throughout the
4
+ # +ChronoModel::TimeMachine+ specs.
5
+ #
6
+ module Helpers
7
+ def self.included(base)
8
+ base.extend(self)
9
+ end
10
+
11
+ def adapter
12
+ ChronoTest.connection
13
+ end
14
+
15
+ def with_revert
16
+ adapter.transaction do
17
+ adapter.create_savepoint 'revert'
18
+
19
+ yield
20
+
21
+ adapter.exec_rollback_to_savepoint 'revert'
22
+ end
23
+ end
24
+
25
+ # If a context object is given, evaluates the given
26
+ # block in its instance context, then defines a `ts`
27
+ # on it, backed by an Array, and adds the current
28
+ # database timestamp to it.
29
+ #
30
+ # If a context object is not given, the block is
31
+ # evaluated in the current context and the above
32
+ # mangling is done on the blocks' return value.
33
+ #
34
+ def ts_eval(ctx = nil, &block)
35
+ ret = (ctx || self).instance_eval(&block)
36
+ (ctx || ret).tap do |obj|
37
+ obj.singleton_class.instance_eval do
38
+ define_method(:ts) { @_ts ||= [] }
39
+ end unless obj.methods.include?(:ts)
40
+
41
+ now = ChronoTest.connection.select_value('select now()::timestamp') + 'Z'
42
+ obj.ts.push(Time.parse(now))
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,111 @@
1
+ require 'support/time_machine/helpers'
2
+
3
+ # This module contains the test DDL and models used by most of the
4
+ # +TimeMachine+ specs.
5
+ #
6
+ # The models exercise different ActiveRecord features.
7
+ #
8
+ # They look candidate of unwinding by their respective specs, however this
9
+ # test suite aims also at testing within a "real" use case scenario, in which
10
+ # multiple models are defined and they interact - with their AR side effects,
11
+ # still ChronoModel should provide the expected results.
12
+ #
13
+ # The +$t+ global variable holds a timeline of events that have happened in
14
+ # the form of .create! and update_attributes, that aim to mimic the most of
15
+ # AR with the least of the effort. Full coverage exercises are most welcome.
16
+ #
17
+ module ChronoTest::TimeMachine
18
+ include ChronoTest::TimeMachine::Helpers
19
+
20
+ # Set up database structure
21
+ #
22
+ adapter.create_table 'foos', :temporal => true do |t|
23
+ t.string :name
24
+ t.integer :fooity
25
+ end
26
+
27
+ class ::Foo < ActiveRecord::Base
28
+ include ChronoModel::TimeMachine
29
+
30
+ has_many :bars
31
+ has_many :sub_bars, :through => :bars
32
+ end
33
+
34
+
35
+ adapter.create_table 'bars', :temporal => true do |t|
36
+ t.string :name
37
+ t.references :foo
38
+ end
39
+
40
+ class ::Bar < ActiveRecord::Base
41
+ include ChronoModel::TimeMachine
42
+
43
+ belongs_to :foo
44
+ has_many :sub_bars
45
+ has_one :baz
46
+
47
+ has_timeline :with => :foo
48
+ end
49
+
50
+
51
+ adapter.create_table 'sub_bars', :temporal => true do |t|
52
+ t.string :name
53
+ t.references :bar
54
+ end
55
+
56
+ class ::SubBar < ActiveRecord::Base
57
+ include ChronoModel::TimeMachine
58
+
59
+ belongs_to :bar
60
+
61
+ has_timeline :with => :bar
62
+ end
63
+
64
+
65
+ adapter.create_table 'bazs' do |t|
66
+ t.string :name
67
+ t.references :bar
68
+ end
69
+
70
+ class ::Baz < ActiveRecord::Base
71
+ include ChronoModel::TimeGate
72
+
73
+ belongs_to :bar
74
+
75
+ has_timeline :with => :bar
76
+ end
77
+
78
+ # Master timeline, used in multiple specs. It is defined here
79
+ # as a global variable to be able to be shared across specs.
80
+ #
81
+ $t = Struct.new(:foo, :bar, :baz, :subbar, :foos, :bars).new
82
+
83
+ # Set up associated records, with intertwined updates
84
+ #
85
+ $t.foo = ts_eval { Foo.create! :name => 'foo', :fooity => 1 }
86
+ ts_eval($t.foo) { update_attributes! :name => 'foo bar' }
87
+
88
+ #
89
+ $t.bar = ts_eval { Bar.create! :name => 'bar', :foo => $t.foo }
90
+ ts_eval($t.bar) { update_attributes! :name => 'foo bar' }
91
+
92
+ #
93
+ $t.subbar = ts_eval { SubBar.create! :name => 'sub-bar', :bar => $t.bar }
94
+ ts_eval($t.subbar) { update_attributes! :name => 'bar sub-bar' }
95
+
96
+ ts_eval($t.foo) { update_attributes! :name => 'new foo' }
97
+
98
+ ts_eval($t.bar) { update_attributes! :name => 'bar bar' }
99
+ ts_eval($t.bar) { update_attributes! :name => 'new bar' }
100
+
101
+ ts_eval($t.subbar) { update_attributes! :name => 'sub-bar sub-bar' }
102
+ ts_eval($t.subbar) { update_attributes! :name => 'new sub-bar' }
103
+
104
+ #
105
+ $t.foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
106
+ $t.bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => $t.foos[i] } }
107
+
108
+ #
109
+ $t.baz = Baz.create :name => 'baz', :bar => $t.bar
110
+
111
+ end