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,749 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'support/helpers'
|
3
|
-
|
4
|
-
describe ChronoModel::TimeMachine do
|
5
|
-
include ChronoTest::Helpers::TimeMachine
|
6
|
-
|
7
|
-
setup_schema!
|
8
|
-
define_models!
|
9
|
-
|
10
|
-
# Set up two associated records, with intertwined updates
|
11
|
-
#
|
12
|
-
foo = ts_eval { Foo.create! :name => 'foo', :fooity => 1 }
|
13
|
-
ts_eval(foo) { update_attributes! :name => 'foo bar' }
|
14
|
-
|
15
|
-
#
|
16
|
-
bar = ts_eval { Bar.create! :name => 'bar', :foo => foo }
|
17
|
-
ts_eval(bar) { update_attributes! :name => 'foo bar' }
|
18
|
-
|
19
|
-
#
|
20
|
-
subbar = ts_eval { SubBar.create! :name => 'sub-bar', :bar => bar }
|
21
|
-
ts_eval(subbar) { update_attributes! :name => 'bar sub-bar' }
|
22
|
-
|
23
|
-
ts_eval(foo) { update_attributes! :name => 'new foo' }
|
24
|
-
|
25
|
-
ts_eval(bar) { update_attributes! :name => 'bar bar' }
|
26
|
-
ts_eval(bar) { update_attributes! :name => 'new bar' }
|
27
|
-
|
28
|
-
ts_eval(subbar) { update_attributes! :name => 'sub-bar sub-bar' }
|
29
|
-
ts_eval(subbar) { update_attributes! :name => 'new sub-bar' }
|
30
|
-
|
31
|
-
#
|
32
|
-
foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
|
33
|
-
bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } }
|
34
|
-
|
35
|
-
#
|
36
|
-
baz = Baz.create :name => 'baz', :bar => bar
|
37
|
-
|
38
|
-
# Specs start here
|
39
|
-
#
|
40
|
-
describe '.chrono?' do
|
41
|
-
subject { model.chrono? }
|
42
|
-
|
43
|
-
context 'on a temporal model' do
|
44
|
-
let(:model) { Foo }
|
45
|
-
it { is_expected.to be(true) }
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'on a plain model' do
|
49
|
-
let(:model) { Plain }
|
50
|
-
it { is_expected.to be(false) }
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
describe '.history?' do
|
55
|
-
subject { model.history? }
|
56
|
-
|
57
|
-
context 'on a temporal parent model' do
|
58
|
-
let(:model) { Foo }
|
59
|
-
it { is_expected.to be(false) }
|
60
|
-
end
|
61
|
-
|
62
|
-
context 'on a temporal history model' do
|
63
|
-
let(:model) { Foo::History }
|
64
|
-
it { is_expected.to be(true) }
|
65
|
-
end
|
66
|
-
|
67
|
-
context 'on a plain model' do
|
68
|
-
let(:model) { Plain }
|
69
|
-
it { expect { subject }.to raise_error(NoMethodError) }
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe '.descendants' do
|
74
|
-
subject { Element.descendants }
|
75
|
-
it { is_expected.to_not include(Element::History) }
|
76
|
-
it { is_expected.to include(Publication) }
|
77
|
-
end
|
78
|
-
|
79
|
-
describe '.descendants_with_history' do
|
80
|
-
subject { Element.descendants_with_history }
|
81
|
-
it { is_expected.to include(Element::History) }
|
82
|
-
it { is_expected.to include(Publication) }
|
83
|
-
end
|
84
|
-
|
85
|
-
describe '.history_models' do
|
86
|
-
subject { ChronoModel.history_models }
|
87
|
-
|
88
|
-
it { is_expected.to eq(
|
89
|
-
'articles' => Article::History,
|
90
|
-
'foos' => Foo::History,
|
91
|
-
'defoos' => Defoo::History,
|
92
|
-
'bars' => Bar::History,
|
93
|
-
'elements' => Element::History,
|
94
|
-
'sections' => Section::History,
|
95
|
-
'sub_bars' => SubBar::History,
|
96
|
-
'animals' => Animal::History,
|
97
|
-
) }
|
98
|
-
end
|
99
|
-
|
100
|
-
describe 'does not interfere with AR standard behaviour' do
|
101
|
-
all_foos = [ foo ] + foos
|
102
|
-
all_bars = [ bar ] + bars
|
103
|
-
|
104
|
-
it { expect(Foo.count).to eq all_foos.size }
|
105
|
-
it { expect(Bar.count).to eq all_bars.size }
|
106
|
-
|
107
|
-
it { expect(Foo.includes(bars: :sub_bars)).to eq all_foos }
|
108
|
-
it { expect(Foo.includes(:bars).preload(bars: :sub_bars)).to eq all_foos }
|
109
|
-
|
110
|
-
it { expect(Foo.includes(:bars).first.name).to eq 'new foo' }
|
111
|
-
it { expect(Foo.includes(:bars).as_of(foo.ts[0]).first.name).to eq 'foo' }
|
112
|
-
|
113
|
-
it { expect(Foo.joins(:bars).map(&:bars).flatten).to eq all_bars }
|
114
|
-
it { expect(Foo.joins(:bars).first.bars.joins(:sub_bars).first.name).to eq 'new bar' }
|
115
|
-
|
116
|
-
it { expect(Foo.joins(bars: :sub_bars).first.bars.joins(:sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
|
117
|
-
|
118
|
-
it { expect(Foo.first.bars.includes(:sub_bars)).to eq [ bar ] }
|
119
|
-
|
120
|
-
end
|
121
|
-
|
122
|
-
describe '#as_of' do
|
123
|
-
describe 'accepts a Time instance' do
|
124
|
-
it { expect(foo.as_of(Time.now).name).to eq 'new foo' }
|
125
|
-
it { expect(bar.as_of(Time.now).name).to eq 'new bar' }
|
126
|
-
end
|
127
|
-
|
128
|
-
describe 'ignores time zones' do
|
129
|
-
it { expect(foo.as_of(Time.now.in_time_zone('America/Havana')).name).to eq 'new foo' }
|
130
|
-
it { expect(bar.as_of(Time.now.in_time_zone('America/Havana')).name).to eq 'new bar' }
|
131
|
-
end
|
132
|
-
|
133
|
-
describe 'returns records as they were before' do
|
134
|
-
it { expect(foo.as_of(foo.ts[0]).name).to eq 'foo' }
|
135
|
-
it { expect(foo.as_of(foo.ts[1]).name).to eq 'foo bar' }
|
136
|
-
it { expect(foo.as_of(foo.ts[2]).name).to eq 'new foo' }
|
137
|
-
|
138
|
-
it { expect(bar.as_of(bar.ts[0]).name).to eq 'bar' }
|
139
|
-
it { expect(bar.as_of(bar.ts[1]).name).to eq 'foo bar' }
|
140
|
-
it { expect(bar.as_of(bar.ts[2]).name).to eq 'bar bar' }
|
141
|
-
it { expect(bar.as_of(bar.ts[3]).name).to eq 'new bar' }
|
142
|
-
end
|
143
|
-
|
144
|
-
describe 'takes care of associated records' do
|
145
|
-
it { expect(foo.as_of(foo.ts[0]).bars).to eq [] }
|
146
|
-
it { expect(foo.as_of(foo.ts[1]).bars).to eq [] }
|
147
|
-
it { expect(foo.as_of(foo.ts[2]).bars).to eq [bar] }
|
148
|
-
|
149
|
-
it { expect(foo.as_of(foo.ts[2]).bars.first.name).to eq 'foo bar' }
|
150
|
-
|
151
|
-
|
152
|
-
it { expect(foo.as_of(bar.ts[0]).bars).to eq [bar] }
|
153
|
-
it { expect(foo.as_of(bar.ts[1]).bars).to eq [bar] }
|
154
|
-
it { expect(foo.as_of(bar.ts[2]).bars).to eq [bar] }
|
155
|
-
it { expect(foo.as_of(bar.ts[3]).bars).to eq [bar] }
|
156
|
-
|
157
|
-
it { expect(foo.as_of(bar.ts[0]).bars.first.name).to eq 'bar' }
|
158
|
-
it { expect(foo.as_of(bar.ts[1]).bars.first.name).to eq 'foo bar' }
|
159
|
-
it { expect(foo.as_of(bar.ts[2]).bars.first.name).to eq 'bar bar' }
|
160
|
-
it { expect(foo.as_of(bar.ts[3]).bars.first.name).to eq 'new bar' }
|
161
|
-
|
162
|
-
|
163
|
-
it { expect(bar.as_of(bar.ts[0]).foo).to eq foo }
|
164
|
-
it { expect(bar.as_of(bar.ts[1]).foo).to eq foo }
|
165
|
-
it { expect(bar.as_of(bar.ts[2]).foo).to eq foo }
|
166
|
-
it { expect(bar.as_of(bar.ts[3]).foo).to eq foo }
|
167
|
-
|
168
|
-
it { expect(bar.as_of(bar.ts[0]).foo.name).to eq 'foo bar' }
|
169
|
-
it { expect(bar.as_of(bar.ts[1]).foo.name).to eq 'foo bar' }
|
170
|
-
it { expect(bar.as_of(bar.ts[2]).foo.name).to eq 'new foo' }
|
171
|
-
it { expect(bar.as_of(bar.ts[3]).foo.name).to eq 'new foo' }
|
172
|
-
end
|
173
|
-
|
174
|
-
describe 'supports historical queries with includes()' do
|
175
|
-
it { expect(Foo.as_of(foo.ts[0]).includes(:bars).first.bars).to eq [] }
|
176
|
-
it { expect(Foo.as_of(foo.ts[1]).includes(:bars).first.bars).to eq [] }
|
177
|
-
it { expect(Foo.as_of(foo.ts[2]).includes(:bars).first.bars).to eq [bar] }
|
178
|
-
|
179
|
-
it { expect(Foo.as_of(bar.ts[0]).includes(:bars).first.bars.first.name).to eq 'bar' }
|
180
|
-
it { expect(Foo.as_of(bar.ts[1]).includes(:bars).first.bars.first.name).to eq 'foo bar' }
|
181
|
-
it { expect(Foo.as_of(bar.ts[2]).includes(:bars).first.bars.first.name).to eq 'bar bar' }
|
182
|
-
it { expect(Foo.as_of(bar.ts[3]).includes(:bars).first.bars.first.name).to eq 'new bar' }
|
183
|
-
|
184
|
-
|
185
|
-
it { expect(Foo.as_of(foo.ts[0]).includes(bars: :sub_bars).first.bars).to eq [] }
|
186
|
-
it { expect(Foo.as_of(foo.ts[1]).includes(bars: :sub_bars).first.bars).to eq [] }
|
187
|
-
it { expect(Foo.as_of(foo.ts[2]).includes(bars: :sub_bars).first.bars).to eq [bar] }
|
188
|
-
|
189
|
-
it { expect(Foo.as_of(bar.ts[0]).includes(bars: :sub_bars).first.bars.first.name).to eq 'bar' }
|
190
|
-
it { expect(Foo.as_of(bar.ts[1]).includes(bars: :sub_bars).first.bars.first.name).to eq 'foo bar' }
|
191
|
-
it { expect(Foo.as_of(bar.ts[2]).includes(bars: :sub_bars).first.bars.first.name).to eq 'bar bar' }
|
192
|
-
it { expect(Foo.as_of(bar.ts[3]).includes(bars: :sub_bars).first.bars.first.name).to eq 'new bar' }
|
193
|
-
|
194
|
-
|
195
|
-
it { expect(Bar.as_of(bar.ts[0]).includes(:foo).first.foo).to eq foo }
|
196
|
-
it { expect(Bar.as_of(bar.ts[1]).includes(:foo).first.foo).to eq foo }
|
197
|
-
it { expect(Bar.as_of(bar.ts[2]).includes(:foo).first.foo).to eq foo }
|
198
|
-
it { expect(Bar.as_of(bar.ts[3]).includes(:foo).first.foo).to eq foo }
|
199
|
-
|
200
|
-
it { expect(Bar.as_of(bar.ts[0]).includes(:foo).first.foo.name).to eq 'foo bar' }
|
201
|
-
it { expect(Bar.as_of(bar.ts[1]).includes(:foo).first.foo.name).to eq 'foo bar' }
|
202
|
-
it { expect(Bar.as_of(bar.ts[2]).includes(:foo).first.foo.name).to eq 'new foo' }
|
203
|
-
it { expect(Bar.as_of(bar.ts[3]).includes(:foo).first.foo.name).to eq 'new foo' }
|
204
|
-
|
205
|
-
|
206
|
-
it { expect(Bar.as_of(bar.ts[0]).includes(foo: :sub_bars).first.foo).to eq foo }
|
207
|
-
it { expect(Bar.as_of(bar.ts[1]).includes(foo: :sub_bars).first.foo).to eq foo }
|
208
|
-
it { expect(Bar.as_of(bar.ts[2]).includes(foo: :sub_bars).first.foo).to eq foo }
|
209
|
-
it { expect(Bar.as_of(bar.ts[3]).includes(foo: :sub_bars).first.foo).to eq foo }
|
210
|
-
|
211
|
-
it { expect(Bar.as_of(bar.ts[0]).includes(foo: :sub_bars).first.foo.name).to eq 'foo bar' }
|
212
|
-
it { expect(Bar.as_of(bar.ts[1]).includes(foo: :sub_bars).first.foo.name).to eq 'foo bar' }
|
213
|
-
it { expect(Bar.as_of(bar.ts[2]).includes(foo: :sub_bars).first.foo.name).to eq 'new foo' }
|
214
|
-
it { expect(Bar.as_of(bar.ts[3]).includes(foo: :sub_bars).first.foo.name).to eq 'new foo' }
|
215
|
-
|
216
|
-
it { expect(Foo.as_of(foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 0 }
|
217
|
-
it { expect(Foo.as_of(foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 0 }
|
218
|
-
it { expect(Foo.as_of(foo.ts[2]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 1 }
|
219
|
-
|
220
|
-
it { expect(Foo.as_of(foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil }
|
221
|
-
it { expect(Foo.as_of(foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil }
|
222
|
-
|
223
|
-
it { expect(Foo.as_of(subbar.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'sub-bar' }
|
224
|
-
it { expect(Foo.as_of(subbar.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'bar sub-bar' }
|
225
|
-
it { expect(Foo.as_of(subbar.ts[2]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'sub-bar sub-bar' }
|
226
|
-
it { expect(Foo.as_of(subbar.ts[3]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
|
227
|
-
end
|
228
|
-
|
229
|
-
it 'doesn\'t raise RecordNotFound when no history records are found' do
|
230
|
-
expect { foo.as_of(1.minute.ago) }.to_not raise_error
|
231
|
-
expect(foo.as_of(1.minute.ago)).to be(nil)
|
232
|
-
end
|
233
|
-
|
234
|
-
|
235
|
-
it 'raises ActiveRecord::RecordNotFound in the bang variant' do
|
236
|
-
expect { foo.as_of!(1.minute.ago) }.to raise_error(ActiveRecord::RecordNotFound)
|
237
|
-
end
|
238
|
-
|
239
|
-
describe 'it honors default_scopes' do
|
240
|
-
active = ts_eval { Defoo.create! :name => 'active 1', :active => true }
|
241
|
-
ts_eval(active) { update_attributes! :name => 'active 2' }
|
242
|
-
|
243
|
-
hidden = ts_eval { Defoo.create! :name => 'hidden 1', :active => false }
|
244
|
-
ts_eval(hidden) { update_attributes! :name => 'hidden 2' }
|
245
|
-
|
246
|
-
it { expect(Defoo.as_of(active.ts[0]).map(&:name)).to eq ['active 1'] }
|
247
|
-
it { expect(Defoo.as_of(active.ts[1]).map(&:name)).to eq ['active 2'] }
|
248
|
-
it { expect(Defoo.as_of(hidden.ts[0]).map(&:name)).to eq ['active 2'] }
|
249
|
-
it { expect(Defoo.as_of(hidden.ts[1]).map(&:name)).to eq ['active 2'] }
|
250
|
-
|
251
|
-
it { expect(Defoo.unscoped.as_of(active.ts[0]).map(&:name)).to eq ['active 1'] }
|
252
|
-
it { expect(Defoo.unscoped.as_of(active.ts[1]).map(&:name)).to eq ['active 2'] }
|
253
|
-
it { expect(Defoo.unscoped.as_of(hidden.ts[0]).map(&:name)).to eq ['active 2', 'hidden 1'] }
|
254
|
-
it { expect(Defoo.unscoped.as_of(hidden.ts[1]).map(&:name)).to eq ['active 2', 'hidden 2'] }
|
255
|
-
end
|
256
|
-
|
257
|
-
describe 'proxies from non-temporal models to temporal ones' do
|
258
|
-
it { expect(baz.as_of(bar.ts[0]).name).to eq 'baz' }
|
259
|
-
it { expect(baz.as_of(bar.ts[1]).name).to eq 'baz' }
|
260
|
-
it { expect(baz.as_of(bar.ts[2]).name).to eq 'baz' }
|
261
|
-
it { expect(baz.as_of(bar.ts[3]).name).to eq 'baz' }
|
262
|
-
|
263
|
-
it { expect(baz.as_of(bar.ts[0]).bar.name).to eq 'bar' }
|
264
|
-
it { expect(baz.as_of(bar.ts[1]).bar.name).to eq 'foo bar' }
|
265
|
-
it { expect(baz.as_of(bar.ts[2]).bar.name).to eq 'bar bar' }
|
266
|
-
it { expect(baz.as_of(bar.ts[3]).bar.name).to eq 'new bar' }
|
267
|
-
|
268
|
-
it { expect(baz.as_of(bar.ts[0]).bar.foo.name).to eq 'foo bar' }
|
269
|
-
it { expect(baz.as_of(bar.ts[1]).bar.foo.name).to eq 'foo bar' }
|
270
|
-
it { expect(baz.as_of(bar.ts[2]).bar.foo.name).to eq 'new foo' }
|
271
|
-
it { expect(baz.as_of(bar.ts[3]).bar.foo.name).to eq 'new foo' }
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
describe '#history' do
|
276
|
-
describe 'returns historical instances' do
|
277
|
-
it { expect(foo.history.size).to eq(3) }
|
278
|
-
it { expect(foo.history.map(&:name)).to eq ['foo', 'foo bar', 'new foo'] }
|
279
|
-
|
280
|
-
it { expect(bar.history.size).to eq(4) }
|
281
|
-
it { expect(bar.history.map(&:name)).to eq ['bar', 'foo bar', 'bar bar', 'new bar'] }
|
282
|
-
end
|
283
|
-
|
284
|
-
describe 'does not return read only records' do
|
285
|
-
it { expect(foo.history.all?(&:readonly?)).to be(false) }
|
286
|
-
it { expect(bar.history.all?(&:readonly?)).to be(false) }
|
287
|
-
end
|
288
|
-
|
289
|
-
describe 'takes care of associated records' do
|
290
|
-
subject { foo.history.map {|f| f.bars.first.try(:name)} }
|
291
|
-
it { is_expected.to eq [nil, 'foo bar', 'new bar'] }
|
292
|
-
end
|
293
|
-
|
294
|
-
describe 'does not return read only associated records' do
|
295
|
-
it { expect(foo.history[2].bars.all?(&:readonly?)).to_not be(true) }
|
296
|
-
it { expect(bar.history.all? {|b| b.foo.readonly?}).to_not be(true) }
|
297
|
-
end
|
298
|
-
|
299
|
-
describe 'allows a custom select list' do
|
300
|
-
it { expect(foo.history.select(:id).first.attributes.keys).to eq %w( id ) }
|
301
|
-
end
|
302
|
-
|
303
|
-
describe 'does not add as_of_time when there are aggregates' do
|
304
|
-
it { expect(foo.history.select('max(id)').to_sql).to_not match(/as_of_time/) }
|
305
|
-
|
306
|
-
it { expect(foo.history.except(:order).select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to eq %w( id foo bar ) }
|
307
|
-
end
|
308
|
-
|
309
|
-
context 'with STI models' do
|
310
|
-
pub = ts_eval { Publication.create! :title => 'wrong title' }
|
311
|
-
ts_eval(pub) { update_attributes! :title => 'correct title' }
|
312
|
-
|
313
|
-
it { expect(pub.history.map(&:title)).to eq ['wrong title', 'correct title'] }
|
314
|
-
end
|
315
|
-
|
316
|
-
context '.sorted' do
|
317
|
-
describe 'orders by recorded_at, hid' do
|
318
|
-
it { expect(foo.history.sorted.to_sql).to match(/order by .+"recorded_at" ASC, .+"hid" ASC/i) }
|
319
|
-
end
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
describe '#pred' do
|
324
|
-
context 'on the first history entry' do
|
325
|
-
subject { foo.history.first.pred }
|
326
|
-
it { is_expected.to be(nil) }
|
327
|
-
end
|
328
|
-
|
329
|
-
context 'on the second history entry' do
|
330
|
-
subject { foo.history.second.pred }
|
331
|
-
it { is_expected.to eq foo.history.first }
|
332
|
-
end
|
333
|
-
|
334
|
-
context 'on the last history entry' do
|
335
|
-
subject { foo.history.last.pred }
|
336
|
-
it { is_expected.to eq foo.history[foo.history.size - 2] }
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
describe '#succ' do
|
341
|
-
context 'on the first history entry' do
|
342
|
-
subject { foo.history.first.succ }
|
343
|
-
it { is_expected.to eq foo.history.second }
|
344
|
-
end
|
345
|
-
|
346
|
-
context 'on the second history entry' do
|
347
|
-
subject { foo.history.second.succ }
|
348
|
-
it { is_expected.to eq foo.history.third }
|
349
|
-
end
|
350
|
-
|
351
|
-
context 'on the last history entry' do
|
352
|
-
subject { foo.history.last.succ }
|
353
|
-
it { is_expected.to be(nil) }
|
354
|
-
end
|
355
|
-
end
|
356
|
-
|
357
|
-
describe '#first' do
|
358
|
-
subject { foo.history.sample.first }
|
359
|
-
it { is_expected.to eq foo.history.first }
|
360
|
-
end
|
361
|
-
|
362
|
-
describe '#last' do
|
363
|
-
subject { foo.history.sample.last }
|
364
|
-
it { is_expected.to eq foo.history.last }
|
365
|
-
end
|
366
|
-
|
367
|
-
describe '#current_version' do
|
368
|
-
describe 'on plain records' do
|
369
|
-
subject { foo.current_version }
|
370
|
-
it { is_expected.to eq foo }
|
371
|
-
end
|
372
|
-
|
373
|
-
describe 'from #as_of' do
|
374
|
-
subject { foo.as_of(Time.now) }
|
375
|
-
it { is_expected.to eq foo }
|
376
|
-
end
|
377
|
-
|
378
|
-
describe 'on historical records' do
|
379
|
-
subject { foo.history.sample.current_version }
|
380
|
-
it { is_expected.to eq foo }
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
describe '#historical?' do
|
385
|
-
subject { record.historical? }
|
386
|
-
|
387
|
-
describe 'on plain records' do
|
388
|
-
let(:record) { foo }
|
389
|
-
it { is_expected.to be(false) }
|
390
|
-
end
|
391
|
-
|
392
|
-
describe 'on historical records' do
|
393
|
-
describe 'from #history' do
|
394
|
-
let(:record) { foo.history.first }
|
395
|
-
it { is_expected.to be(true) }
|
396
|
-
end
|
397
|
-
|
398
|
-
describe 'from #as_of' do
|
399
|
-
let(:record) { foo.as_of(Time.now) }
|
400
|
-
it { is_expected.to be(true) }
|
401
|
-
end
|
402
|
-
end
|
403
|
-
end
|
404
|
-
|
405
|
-
describe '#destroy' do
|
406
|
-
describe 'on historical records' do
|
407
|
-
subject { foo.history.first.destroy }
|
408
|
-
it { expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord) }
|
409
|
-
end
|
410
|
-
|
411
|
-
describe 'on current records' do
|
412
|
-
rec = nil
|
413
|
-
before(:all) do
|
414
|
-
rec = ts_eval { Foo.create!(:name => 'alive foo', :fooity => 42) }
|
415
|
-
ts_eval(rec) { update_attributes!(:name => 'dying foo') }
|
416
|
-
end
|
417
|
-
after(:all) do
|
418
|
-
rec.history.delete_all
|
419
|
-
end
|
420
|
-
|
421
|
-
subject { rec.destroy }
|
422
|
-
|
423
|
-
it { expect { subject }.to_not raise_error }
|
424
|
-
it { expect { rec.reload }.to raise_error(ActiveRecord::RecordNotFound) }
|
425
|
-
|
426
|
-
describe 'does not delete its history' do
|
427
|
-
subject { record.name }
|
428
|
-
|
429
|
-
context do
|
430
|
-
let(:record) { rec.as_of(rec.ts.first) }
|
431
|
-
it { is_expected.to eq 'alive foo' }
|
432
|
-
end
|
433
|
-
|
434
|
-
context do
|
435
|
-
let(:record) { rec.as_of(rec.ts.last) }
|
436
|
-
it { is_expected.to eq 'dying foo' }
|
437
|
-
end
|
438
|
-
|
439
|
-
context do
|
440
|
-
let(:record) { Foo.as_of(rec.ts.first).where(:fooity => 42).first }
|
441
|
-
it { is_expected.to eq 'alive foo' }
|
442
|
-
end
|
443
|
-
|
444
|
-
context do
|
445
|
-
subject { Foo.history.where(:fooity => 42).map(&:name) }
|
446
|
-
it { is_expected.to eq ['alive foo', 'dying foo'] }
|
447
|
-
end
|
448
|
-
end
|
449
|
-
end
|
450
|
-
end
|
451
|
-
|
452
|
-
describe '#timeline' do
|
453
|
-
split = lambda {|ts| ts.map!{|t| [t.to_i, t.usec]} }
|
454
|
-
|
455
|
-
timestamps_from = lambda {|*records|
|
456
|
-
records.map(&:history).flatten!.inject([]) {|ret, rec|
|
457
|
-
ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from)
|
458
|
-
ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to)
|
459
|
-
ret
|
460
|
-
}.sort.uniq
|
461
|
-
}
|
462
|
-
|
463
|
-
describe 'on records having an :has_many relationship' do
|
464
|
-
describe 'by default returns timestamps of the record only' do
|
465
|
-
subject { split.call(foo.timeline) }
|
466
|
-
it { expect(subject.size).to eq foo.ts.size }
|
467
|
-
it { is_expected.to eq timestamps_from.call(foo) }
|
468
|
-
end
|
469
|
-
|
470
|
-
describe 'when asked, returns timestamps including the related objects' do
|
471
|
-
subject { split.call(foo.timeline(:with => :bars)) }
|
472
|
-
it { expect(subject.size).to eq(foo.ts.size + bar.ts.size) }
|
473
|
-
it { is_expected.to eq(timestamps_from.call(foo, *foo.bars)) }
|
474
|
-
end
|
475
|
-
end
|
476
|
-
|
477
|
-
describe 'on records using has_timeline :with' do
|
478
|
-
subject { split.call(bar.timeline) }
|
479
|
-
|
480
|
-
describe 'returns timestamps of the record and its associations' do
|
481
|
-
|
482
|
-
let!(:expected) do
|
483
|
-
creat = bar.history.first.valid_from
|
484
|
-
c_sec, c_usec = creat.to_i, creat.usec
|
485
|
-
|
486
|
-
timestamps_from.call(foo, bar).reject {|sec, usec|
|
487
|
-
sec < c_sec || ( sec == c_sec && usec < c_usec )
|
488
|
-
}
|
489
|
-
end
|
490
|
-
|
491
|
-
it { expect(subject.size).to eq expected.size }
|
492
|
-
it { is_expected.to eq expected }
|
493
|
-
end
|
494
|
-
end
|
495
|
-
|
496
|
-
describe 'on non-temporal records using has_timeline :with' do
|
497
|
-
subject { split.call(baz.timeline) }
|
498
|
-
|
499
|
-
describe 'returns timestamps of its temporal associations' do
|
500
|
-
it { expect(subject.size).to eq bar.ts.size }
|
501
|
-
it { is_expected.to eq timestamps_from.call(bar) }
|
502
|
-
end
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
describe '#last_changes' do
|
507
|
-
context 'on plain records' do
|
508
|
-
context 'having history' do
|
509
|
-
subject { bar.last_changes }
|
510
|
-
it { is_expected.to eq('name' => ['bar bar', 'new bar']) }
|
511
|
-
end
|
512
|
-
|
513
|
-
context 'without history' do
|
514
|
-
let(:record) { Bar.create!(:name => 'foreveralone') }
|
515
|
-
subject { record.last_changes }
|
516
|
-
it { is_expected.to be_nil }
|
517
|
-
after { record.destroy.history.delete_all } # UGLY
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
context 'on history records' do
|
522
|
-
context 'at the beginning of the timeline' do
|
523
|
-
subject { bar.history.first.last_changes }
|
524
|
-
it { is_expected.to be_nil }
|
525
|
-
end
|
526
|
-
|
527
|
-
context 'in the middle of the timeline' do
|
528
|
-
subject { bar.history.second.last_changes }
|
529
|
-
it { is_expected.to eq('name' => ['bar', 'foo bar']) }
|
530
|
-
end
|
531
|
-
end
|
532
|
-
end
|
533
|
-
|
534
|
-
describe '#changes_against' do
|
535
|
-
context 'can compare records against history' do
|
536
|
-
it { expect(bar.changes_against(bar.history.first)).to eq('name' => ['bar', 'new bar']) }
|
537
|
-
it { expect(bar.changes_against(bar.history.second)).to eq('name' => ['foo bar', 'new bar']) }
|
538
|
-
it { expect(bar.changes_against(bar.history.third)).to eq('name' => ['bar bar', 'new bar']) }
|
539
|
-
it { expect(bar.changes_against(bar.history.last)).to eq({}) }
|
540
|
-
end
|
541
|
-
|
542
|
-
context 'can compare history against history' do
|
543
|
-
it { expect(bar.history.first.changes_against(bar.history.third)).to eq('name' => ['bar bar', 'bar']) }
|
544
|
-
it { expect(bar.history.second.changes_against(bar.history.third)).to eq('name' => ['bar bar', 'foo bar']) }
|
545
|
-
it { expect(bar.history.third.changes_against(bar.history.third)).to eq({}) }
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
describe '#pred' do
|
550
|
-
context 'on records having history' do
|
551
|
-
subject { bar.pred }
|
552
|
-
it { expect(subject.name).to eq 'bar bar' }
|
553
|
-
end
|
554
|
-
|
555
|
-
context 'when there is enough history' do
|
556
|
-
subject { bar.pred.pred.pred.pred }
|
557
|
-
it { expect(subject.name).to eq 'bar' }
|
558
|
-
end
|
559
|
-
|
560
|
-
context 'when no history is recorded' do
|
561
|
-
let(:record) { Bar.create!(:name => 'quuuux') }
|
562
|
-
subject { record.pred }
|
563
|
-
it { is_expected.to be(nil) }
|
564
|
-
after { record.destroy.history.delete_all }
|
565
|
-
end
|
566
|
-
end
|
567
|
-
|
568
|
-
describe 'timestamp methods' do
|
569
|
-
history_methods = %w( valid_from valid_to recorded_at )
|
570
|
-
current_methods = %w( as_of_time )
|
571
|
-
|
572
|
-
context 'on history records' do
|
573
|
-
let(:record) { foo.history.first }
|
574
|
-
|
575
|
-
(history_methods + current_methods).each do |attr|
|
576
|
-
describe ['#', attr].join do
|
577
|
-
subject { record.public_send(attr) }
|
578
|
-
|
579
|
-
it { is_expected.to be_present }
|
580
|
-
it { is_expected.to be_a(Time) }
|
581
|
-
it { is_expected.to be_utc }
|
582
|
-
end
|
583
|
-
end
|
584
|
-
end
|
585
|
-
|
586
|
-
context 'on current records' do
|
587
|
-
let(:record) { foo }
|
588
|
-
|
589
|
-
history_methods.each do |attr|
|
590
|
-
describe ['#', attr].join do
|
591
|
-
subject { record.public_send(attr) }
|
592
|
-
|
593
|
-
it { expect { subject }.to raise_error(NoMethodError) }
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
|
-
current_methods.each do |attr|
|
598
|
-
describe ['#', attr].join do
|
599
|
-
subject { record.public_send(attr) }
|
600
|
-
|
601
|
-
it { is_expected.to be(nil) }
|
602
|
-
end
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
end
|
607
|
-
|
608
|
-
# Class methods
|
609
|
-
context do
|
610
|
-
describe '.as_of' do
|
611
|
-
it { expect(Foo.as_of(1.month.ago)).to eq [] }
|
612
|
-
|
613
|
-
it { expect(Foo.as_of(foos[0].ts[0])).to eq [foo, foos[0]] }
|
614
|
-
it { expect(Foo.as_of(foos[1].ts[0])).to eq [foo, foos[0], foos[1]] }
|
615
|
-
it { expect(Foo.as_of(Time.now )).to eq [foo, foos[0], foos[1]] }
|
616
|
-
|
617
|
-
it { expect(Bar.as_of(foos[1].ts[0])).to eq [bar] }
|
618
|
-
|
619
|
-
it { expect(Bar.as_of(bars[0].ts[0])).to eq [bar, bars[0]] }
|
620
|
-
it { expect(Bar.as_of(bars[1].ts[0])).to eq [bar, bars[0], bars[1]] }
|
621
|
-
it { expect(Bar.as_of(Time.now )).to eq [bar, bars[0], bars[1]] }
|
622
|
-
|
623
|
-
# Associations
|
624
|
-
context do
|
625
|
-
subject { foos[0].id }
|
626
|
-
|
627
|
-
it { expect(Foo.as_of(foos[0].ts[0]).find(subject).bars).to eq [] }
|
628
|
-
it { expect(Foo.as_of(foos[1].ts[0]).find(subject).bars).to eq [] }
|
629
|
-
it { expect(Foo.as_of(bars[0].ts[0]).find(subject).bars).to eq [bars[0]] }
|
630
|
-
it { expect(Foo.as_of(bars[1].ts[0]).find(subject).bars).to eq [bars[0]] }
|
631
|
-
it { expect(Foo.as_of(Time.now ).find(subject).bars).to eq [bars[0]] }
|
632
|
-
end
|
633
|
-
|
634
|
-
context do
|
635
|
-
subject { foos[1].id }
|
636
|
-
|
637
|
-
it { expect { Foo.as_of(foos[0].ts[0]).find(subject) }.to raise_error(ActiveRecord::RecordNotFound) }
|
638
|
-
it { expect { Foo.as_of(foos[1].ts[0]).find(subject) }.to_not raise_error }
|
639
|
-
|
640
|
-
it { expect(Foo.as_of(bars[0].ts[0]).find(subject).bars).to eq [] }
|
641
|
-
it { expect(Foo.as_of(bars[1].ts[0]).find(subject).bars).to eq [bars[1]] }
|
642
|
-
it { expect(Foo.as_of(Time.now ).find(subject).bars).to eq [bars[1]] }
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
describe '.history' do
|
647
|
-
let(:foo_history) {
|
648
|
-
['foo', 'foo bar', 'new foo', 'foo 0', 'foo 1']
|
649
|
-
}
|
650
|
-
|
651
|
-
let(:bar_history) {
|
652
|
-
['bar', 'foo bar', 'bar bar', 'new bar', 'bar 0', 'bar 1']
|
653
|
-
}
|
654
|
-
|
655
|
-
it { expect(Foo.history.all.map(&:name)).to eq foo_history }
|
656
|
-
it { expect(Bar.history.all.map(&:name)).to eq bar_history }
|
657
|
-
end
|
658
|
-
|
659
|
-
describe '.time_query' do
|
660
|
-
it { expect(Foo.history.time_query(:after, :now, inclusive: true ).count).to eq 3 }
|
661
|
-
it { expect(Foo.history.time_query(:after, :now, inclusive: false).count).to eq 0 }
|
662
|
-
it { expect(Foo.history.time_query(:before, :now, inclusive: true ).count).to eq 5 }
|
663
|
-
it { expect(Foo.history.time_query(:before, :now, inclusive: false).count).to eq 2 }
|
664
|
-
|
665
|
-
it { expect(Foo.history.past.size).to eq 2 }
|
666
|
-
end
|
667
|
-
|
668
|
-
end
|
669
|
-
|
670
|
-
# Transactions
|
671
|
-
context 'Within transactions' do
|
672
|
-
context 'multiple updates to an existing record' do
|
673
|
-
let!(:r1) do
|
674
|
-
Foo.create!(:name => 'xact test').tap do |record|
|
675
|
-
Foo.transaction do
|
676
|
-
record.update_attribute 'name', 'lost into oblivion'
|
677
|
-
record.update_attribute 'name', 'does work'
|
678
|
-
end
|
679
|
-
end
|
680
|
-
end
|
681
|
-
|
682
|
-
it "generate only a single history record" do
|
683
|
-
expect(r1.history.size).to eq(2)
|
684
|
-
|
685
|
-
expect(r1.history.first.name).to eq 'xact test'
|
686
|
-
expect(r1.history.last.name).to eq 'does work'
|
687
|
-
end
|
688
|
-
end
|
689
|
-
|
690
|
-
context 'insertion and subsequent update' do
|
691
|
-
let!(:r2) do
|
692
|
-
Foo.transaction do
|
693
|
-
Foo.create!(:name => 'lost into oblivion').tap do |record|
|
694
|
-
record.update_attribute 'name', 'I am Bar'
|
695
|
-
record.update_attribute 'name', 'I am Foo'
|
696
|
-
end
|
697
|
-
end
|
698
|
-
end
|
699
|
-
|
700
|
-
it 'generates a single history record' do
|
701
|
-
expect(r2.history.size).to eq(1)
|
702
|
-
expect(r2.history.first.name).to eq 'I am Foo'
|
703
|
-
end
|
704
|
-
end
|
705
|
-
|
706
|
-
context 'insertion and subsequent deletion' do
|
707
|
-
let!(:r3) do
|
708
|
-
Foo.transaction do
|
709
|
-
Foo.create!(:name => 'it never happened').destroy
|
710
|
-
end
|
711
|
-
end
|
712
|
-
|
713
|
-
it 'does not generate any history' do
|
714
|
-
expect(Foo.history.where(:id => r3.id)).to be_empty
|
715
|
-
end
|
716
|
-
end
|
717
|
-
end
|
718
|
-
|
719
|
-
# This group is below here to not to disturb the flow of the above specs.
|
720
|
-
#
|
721
|
-
context 'history modification' do
|
722
|
-
describe '#save' do
|
723
|
-
subject { bar.history.first }
|
724
|
-
|
725
|
-
before do
|
726
|
-
subject.name = 'modified bar history'
|
727
|
-
subject.save
|
728
|
-
subject.reload
|
729
|
-
end
|
730
|
-
|
731
|
-
it { is_expected.to be_a(Bar::History) }
|
732
|
-
it { expect(subject.name).to eq 'modified bar history' }
|
733
|
-
end
|
734
|
-
|
735
|
-
describe '#save!' do
|
736
|
-
subject { bar.history.second }
|
737
|
-
|
738
|
-
before do
|
739
|
-
subject.name = 'another modified bar history'
|
740
|
-
subject.save
|
741
|
-
subject.reload
|
742
|
-
end
|
743
|
-
|
744
|
-
it { is_expected.to be_a(Bar::History) }
|
745
|
-
it { expect(subject.name).to eq 'another modified bar history' }
|
746
|
-
end
|
747
|
-
end
|
748
|
-
|
749
|
-
end
|