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