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.
- checksums.yaml +4 -4
- data/Rakefile +2 -1
- data/lib/chrono_model/time_machine.rb +1 -1
- data/lib/chrono_model/time_machine/history_model.rb +4 -0
- data/lib/chrono_model/version.rb +1 -1
- data/spec/chrono_model/adapter/base_spec.rb +157 -0
- data/spec/chrono_model/adapter/ddl_spec.rb +243 -0
- data/spec/chrono_model/adapter/indexes_spec.rb +72 -0
- data/spec/chrono_model/adapter/migrations_spec.rb +312 -0
- data/spec/chrono_model/history_models_spec.rb +32 -0
- data/spec/chrono_model/time_machine/as_of_spec.rb +188 -0
- data/spec/chrono_model/time_machine/changes_spec.rb +50 -0
- data/spec/chrono_model/{adapter → time_machine}/counter_cache_race_spec.rb +2 -2
- data/spec/chrono_model/time_machine/default_scope_spec.rb +37 -0
- data/spec/chrono_model/time_machine/history_spec.rb +104 -0
- data/spec/chrono_model/time_machine/keep_cool_spec.rb +27 -0
- data/spec/chrono_model/time_machine/manipulations_spec.rb +84 -0
- data/spec/chrono_model/time_machine/model_identification_spec.rb +46 -0
- data/spec/chrono_model/time_machine/sequence_spec.rb +74 -0
- data/spec/chrono_model/time_machine/sti_spec.rb +100 -0
- data/spec/chrono_model/{time_query_spec.rb → time_machine/time_query_spec.rb} +22 -5
- data/spec/chrono_model/time_machine/timeline_spec.rb +63 -0
- data/spec/chrono_model/time_machine/timestamps_spec.rb +43 -0
- data/spec/chrono_model/time_machine/transactions_spec.rb +69 -0
- data/spec/support/adapter/helpers.rb +53 -0
- data/spec/support/adapter/structure.rb +44 -0
- data/spec/support/time_machine/helpers.rb +47 -0
- data/spec/support/time_machine/structure.rb +111 -0
- metadata +48 -14
- data/spec/chrono_model/adapter/sti_bug_spec.rb +0 -49
- data/spec/chrono_model/adapter_spec.rb +0 -788
- data/spec/chrono_model/time_machine_spec.rb +0 -749
- data/spec/support/helpers.rb +0 -198
@@ -1,13 +1,30 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'support/
|
2
|
+
require 'support/time_machine/structure'
|
3
3
|
|
4
4
|
describe ChronoModel::TimeMachine::TimeQuery do
|
5
|
-
include ChronoTest::Helpers
|
5
|
+
include ChronoTest::TimeMachine::Helpers
|
6
6
|
|
7
|
-
|
8
|
-
|
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
|
-
#
|
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
|