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
@@ -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