chrono_model 0.4.0 → 0.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/lib/chrono_model.rb CHANGED
@@ -3,6 +3,8 @@ require 'chrono_model/adapter'
3
3
  require 'chrono_model/compatibility'
4
4
  require 'chrono_model/patches'
5
5
  require 'chrono_model/time_machine'
6
+ require 'chrono_model/time_gate'
7
+ require 'chrono_model/utils'
6
8
 
7
9
  module ChronoModel
8
10
  class Error < ActiveRecord::ActiveRecordError #:nodoc:
@@ -24,11 +26,4 @@ silence_warnings do
24
26
  # We need to override the "scoped" method on AR::Association for temporal
25
27
  # associations to work as well
26
28
  ActiveRecord::Associations::Association = ChronoModel::Patches::Association
27
-
28
- # This implements correct WITH syntax on PostgreSQL
29
- Arel::Visitors::PostgreSQL = ChronoModel::Patches::Visitor
30
-
31
- # This adds .with support to ActiveRecord::Relation
32
- ActiveRecord::Relation.instance_eval { include ChronoModel::Patches::QueryMethods }
33
- ActiveRecord::Base.extend ChronoModel::Patches::Querying
34
29
  end
data/spec/adapter_spec.rb CHANGED
@@ -59,7 +59,7 @@ describe ChronoModel::Adapter do
59
59
 
60
60
  def native.to_proc
61
61
  proc {|t|
62
- t.string :test
62
+ t.string :test, :null => false
63
63
  t.integer :foo
64
64
  t.float :bar
65
65
  t.text :baz
@@ -325,8 +325,8 @@ describe ChronoModel::Adapter do
325
325
  it { should include(['id', 'integer']) }
326
326
  end
327
327
 
328
- with_temporal_table &assert
329
- with_plain_table &assert
328
+ with_temporal_table(&assert)
329
+ with_plain_table( &assert)
330
330
  end
331
331
 
332
332
  describe '.primary_key' do
@@ -336,8 +336,8 @@ describe ChronoModel::Adapter do
336
336
  it { should == 'id' }
337
337
  end
338
338
 
339
- with_temporal_table &assert
340
- with_plain_table &assert
339
+ with_temporal_table(&assert)
340
+ with_plain_table( &assert)
341
341
  end
342
342
 
343
343
  describe '.indexes' do
@@ -353,8 +353,8 @@ describe ChronoModel::Adapter do
353
353
  it { subject.map(&:columns).should =~ [['foo'], ['bar', 'baz']] }
354
354
  end
355
355
 
356
- with_temporal_table &assert
357
- with_plain_table &assert
356
+ with_temporal_table(&assert)
357
+ with_plain_table( &assert)
358
358
  end
359
359
 
360
360
  describe '.on_schema' do
@@ -395,4 +395,71 @@ describe ChronoModel::Adapter do
395
395
  end
396
396
  end
397
397
 
398
+
399
+ context 'INSERT multiple values' do
400
+ before :all do
401
+ adapter.create_table table, :temporal => true, &columns
402
+ end
403
+
404
+ after :all do
405
+ adapter.drop_table table
406
+ end
407
+
408
+ let(:current) { [ChronoModel::Adapter::TEMPORAL_SCHEMA, table].join('.') }
409
+ let(:history) { [ChronoModel::Adapter::HISTORY_SCHEMA, table].join('.') }
410
+
411
+ def count(table)
412
+ adapter.select_value("SELECT COUNT(*) FROM ONLY #{table}").to_i
413
+ end
414
+
415
+ def ids(table)
416
+ adapter.select_values("SELECT id FROM ONLY #{table} ORDER BY id")
417
+ end
418
+
419
+ context 'when succeeding' do
420
+ def insert
421
+ adapter.execute <<-SQL
422
+ INSERT INTO #{table} (test, foo) VALUES
423
+ ('test1', 1),
424
+ ('test2', 2);
425
+ SQL
426
+ end
427
+
428
+ it { expect { insert }.to_not raise_error }
429
+ it { count(current).should == 2 }
430
+ it { count(history).should == 2 }
431
+ end
432
+
433
+ context 'when failing' do
434
+ def insert
435
+ adapter.execute <<-SQL
436
+ INSERT INTO #{table} (test, foo) VALUES
437
+ ('test3', 3),
438
+ (NULL, 0);
439
+ SQL
440
+ end
441
+
442
+ it { expect { insert }.to raise_error }
443
+ it { count(current).should == 2 } # Because the previous
444
+ it { count(history).should == 2 } # records are preserved
445
+ end
446
+
447
+ context 'after a failure' do
448
+ def insert
449
+ adapter.execute <<-SQL
450
+ INSERT INTO #{table} (test, foo) VALUES
451
+ ('test4', 3),
452
+ ('test5', 4);
453
+ SQL
454
+ end
455
+
456
+ it { expect { insert }.to_not raise_error }
457
+
458
+ it { count(current).should == 4 }
459
+ it { count(history).should == 4 }
460
+
461
+ it { ids(current).should == ids(history) }
462
+ end
463
+ end
464
+
398
465
  end
@@ -6,7 +6,7 @@ module ChronoTest
6
6
  extend self
7
7
 
8
8
  AR = ActiveRecord::Base
9
- log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap{|f| File.truncate(f, 0)}
9
+ log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap{|f| File.open(f, "ab") { |ft| ft.truncate(0) }}
10
10
  AR.logger = ::Logger.new(log).tap do |l|
11
11
  l.level = 0
12
12
  end
@@ -69,6 +69,11 @@ module ChronoTest::Helpers
69
69
  t.string :name
70
70
  t.boolean :active
71
71
  end
72
+
73
+ adapter.create_table 'elements', :temporal => true do |t|
74
+ t.string :title
75
+ t.string :type
76
+ end
72
77
  end
73
78
 
74
79
  after(:all) do
@@ -90,10 +95,16 @@ module ChronoTest::Helpers
90
95
 
91
96
  belongs_to :foo
92
97
  has_one :baz
98
+
99
+ has_timeline :with => :foo
93
100
  end
94
101
 
95
102
  class ::Baz < ActiveRecord::Base
96
- belongs_to :baz
103
+ include ChronoModel::TimeGate
104
+
105
+ belongs_to :bar
106
+
107
+ has_timeline :with => :bar
97
108
  end
98
109
 
99
110
  class ::Defoo < ActiveRecord::Base
@@ -101,6 +112,14 @@ module ChronoTest::Helpers
101
112
 
102
113
  default_scope where(:active => true)
103
114
  end
115
+
116
+ # STI case (https://github.com/ifad/chronomodel/issues/5)
117
+ class ::Element < ActiveRecord::Base
118
+ include ChronoModel::TimeMachine
119
+ end
120
+
121
+ class ::Publication < Element
122
+ end
104
123
  }
105
124
 
106
125
  def define_models!
@@ -10,7 +10,12 @@ describe ChronoModel::TimeMachine do
10
10
  describe '.chrono_models' do
11
11
  subject { ChronoModel::TimeMachine.chrono_models }
12
12
 
13
- it { should == {'foos' => Foo::History, 'defoos' => Defoo::History, 'bars' => Bar::History} }
13
+ it { should == {
14
+ 'foos' => Foo::History,
15
+ 'defoos' => Defoo::History,
16
+ 'bars' => Bar::History,
17
+ 'elements' => Element::History
18
+ } }
14
19
  end
15
20
 
16
21
 
@@ -31,6 +36,10 @@ describe ChronoModel::TimeMachine do
31
36
  ts_eval(bar) { update_attributes! :name => 'new bar' }
32
37
  }
33
38
 
39
+ let!(:baz) {
40
+ Baz.create :name => 'baz', :bar => bar
41
+ }
42
+
34
43
  # Specs start here
35
44
  #
36
45
  describe '#as_of' do
@@ -110,6 +119,23 @@ describe ChronoModel::TimeMachine do
110
119
  it { Defoo.unscoped.as_of(hidden.ts[0]).map(&:name).should == ['active 2', 'hidden 1'] }
111
120
  it { Defoo.unscoped.as_of(hidden.ts[1]).map(&:name).should == ['active 2', 'hidden 2'] }
112
121
  end
122
+
123
+ describe 'proxies from non-temporal models to temporal ones' do
124
+ it { baz.as_of(bar.ts[0]).name.should == 'baz' }
125
+ it { baz.as_of(bar.ts[1]).name.should == 'baz' }
126
+ it { baz.as_of(bar.ts[2]).name.should == 'baz' }
127
+ it { baz.as_of(bar.ts[3]).name.should == 'baz' }
128
+
129
+ it { baz.as_of(bar.ts[0]).bar.name.should == 'bar' }
130
+ it { baz.as_of(bar.ts[1]).bar.name.should == 'foo bar' }
131
+ it { baz.as_of(bar.ts[2]).bar.name.should == 'bar bar' }
132
+ it { baz.as_of(bar.ts[3]).bar.name.should == 'new bar' }
133
+
134
+ it { baz.as_of(bar.ts[0]).bar.foo.name.should == 'foo bar' }
135
+ it { baz.as_of(bar.ts[1]).bar.foo.name.should == 'foo bar' }
136
+ it { baz.as_of(bar.ts[2]).bar.foo.name.should == 'new foo' }
137
+ it { baz.as_of(bar.ts[3]).bar.foo.name.should == 'new foo' }
138
+ end
113
139
  end
114
140
 
115
141
  describe '#history' do
@@ -135,6 +161,82 @@ describe ChronoModel::TimeMachine do
135
161
  it { foo.history[2].bars.all?(&:readonly?).should be_true }
136
162
  it { bar.history.all? {|b| b.foo.readonly?}.should be_true }
137
163
  end
164
+
165
+ describe 'allows a custom select list' do
166
+ it { foo.history.select(:id).first.attributes.keys.should == %w( id as_of_time ) }
167
+ end
168
+
169
+ describe 'does not add as_of_time when there are aggregates' do
170
+ it { foo.history.select('max (id)').to_sql.should_not =~ /as_of_time/ }
171
+ it { foo.history.select('max (id) as foo, min(id) as bar').first.attributes.keys.should == %w( foo bar ) }
172
+ end
173
+
174
+ describe 'orders by recorded_at, hid by default' do
175
+ it { foo.history.to_sql.should =~ /order by.*recorded_at,.*hid/i }
176
+ end
177
+
178
+ describe 'allows a custom order list' do
179
+ it { expect { foo.history.order('id') }.to_not raise_error }
180
+ it { foo.history.order('id').to_sql.should =~ /order by id/i }
181
+ end
182
+
183
+ context 'with STI models' do
184
+ let!(:pub) {
185
+ pub = ts_eval { Publication.create! :title => 'wrong title' }
186
+ ts_eval(pub) { update_attributes! :title => 'correct title' }
187
+ }
188
+
189
+ it { pub.history.map(&:title).should == ['wrong title', 'correct title'] }
190
+ end
191
+ end
192
+
193
+ describe '#pred' do
194
+ context 'on the first history entry' do
195
+ subject { foo.history.first.pred }
196
+ it { should be_nil }
197
+ end
198
+
199
+ context 'on the second history entry' do
200
+ subject { foo.history.second.pred }
201
+ it { should == foo.history.first }
202
+ end
203
+
204
+ context 'on the last history entry' do
205
+ subject { foo.history.last.pred }
206
+ it { should == foo.history[foo.history.size - 2] }
207
+ end
208
+ end
209
+
210
+ describe '#succ' do
211
+ context 'on the first history entry' do
212
+ subject { foo.history.first.succ }
213
+ it { should == foo.history.second }
214
+ end
215
+
216
+ context 'on the second history entry' do
217
+ subject { foo.history.second.succ }
218
+ it { should == foo.history.third }
219
+ end
220
+
221
+ context 'on the last history entry' do
222
+ subject { foo.history.last.succ }
223
+ it { should be_nil }
224
+ end
225
+ end
226
+
227
+ describe '#first' do
228
+ subject { foo.history.sample.first }
229
+ it { should == foo.history.first }
230
+ end
231
+
232
+ describe '#last' do
233
+ subject { foo.history.sample.last }
234
+ it { should == foo.history.last }
235
+ end
236
+
237
+ describe '#record' do
238
+ subject { foo.history.sample.record }
239
+ it { should == foo }
138
240
  end
139
241
 
140
242
  describe '#historical?' do
@@ -197,32 +299,133 @@ describe ChronoModel::TimeMachine do
197
299
  end
198
300
  end
199
301
 
200
- describe '#history_timestamps' do
302
+ describe '#timeline' do
303
+ split = lambda {|ts| ts.map!{|t| [t.to_i, t.usec]} }
304
+
201
305
  timestamps_from = lambda {|*records|
202
- records.map(&:history).flatten!.inject([]) {|ret, rec|
203
- ret.concat [rec.valid_from, rec.valid_to]
306
+ ts = records.map(&:history).flatten!.inject([]) {|ret, rec|
307
+ ret.concat [
308
+ [rec.valid_from.to_i, rec.valid_from.usec + 2],
309
+ [rec.valid_to.to_i, rec.valid_to.usec + 2]
310
+ ]
204
311
  }.sort.uniq[0..-2]
205
312
  }
206
313
 
207
314
  describe 'on records having an :has_many relationship' do
208
- subject { foo.history_timestamps }
315
+ describe 'by default returns timestamps of the record only' do
316
+ subject { split.call(foo.timeline) }
317
+ its(:size) { should == foo.ts.size }
318
+ it { should == timestamps_from.call(foo) }
319
+ end
209
320
 
210
- describe 'returns timestamps of the record and its associations' do
321
+ describe 'when asked, returns timestamps including the related objects' do
322
+ subject { split.call(foo.timeline(:with => :bars)) }
211
323
  its(:size) { should == foo.ts.size + bar.ts.size }
212
- it { should == timestamps_from.call(foo, bar) }
324
+ it { should == timestamps_from.call(foo, *foo.bars) }
213
325
  end
214
326
  end
215
327
 
216
- describe 'on records having a :belongs_to relationship' do
217
- subject { bar.history_timestamps }
328
+ describe 'on records using has_timeline :with' do
329
+ subject { split.call(bar.timeline) }
218
330
 
219
331
  describe 'returns timestamps of the record and its associations' do
220
- its(:size) { should == foo.ts.size + bar.ts.size }
221
- it { should == timestamps_from.call(foo, bar) }
332
+
333
+ let!(:expected) do
334
+ creat = bar.history.first.valid_from
335
+ c_sec, c_usec = creat.to_i, creat.usec
336
+
337
+ timestamps_from.call(foo, bar).reject {|sec, usec|
338
+ sec < c_sec || ( sec == c_sec && usec < c_usec )
339
+ }
340
+ end
341
+
342
+ its(:size) { should == expected.size }
343
+ it { should == expected }
344
+ end
345
+ end
346
+
347
+ describe 'on non-temporal records using has_timeline :with' do
348
+ subject { split.call(baz.timeline) }
349
+
350
+ describe 'returns timestamps of its temporal associations' do
351
+ its(:size) { should == bar.ts.size }
352
+ it { should == timestamps_from.call(bar) }
353
+ end
354
+ end
355
+ end
356
+
357
+ describe '#last_changes' do
358
+ context 'on plain records' do
359
+ context 'having history' do
360
+ subject { bar.last_changes }
361
+ it { should == {'name' => ['bar bar', 'new bar']} }
362
+ end
363
+
364
+ context 'without history' do
365
+ let(:record) { Bar.create!(:name => 'foreveralone') }
366
+ subject { record.last_changes }
367
+ it { should be_nil }
368
+ after { record.destroy.history.delete_all } # UGLY
369
+ end
370
+ end
371
+
372
+ context 'on history records' do
373
+ context 'at the beginning of the timeline' do
374
+ subject { bar.history.first.last_changes }
375
+ it { should be_nil }
376
+ end
377
+
378
+ context 'in the middle of the timeline' do
379
+ subject { bar.history.second.last_changes }
380
+ it { should == {'name' => ['bar', 'foo bar']} }
222
381
  end
223
382
  end
224
383
  end
225
384
 
385
+ describe '#changes_against' do
386
+ context 'can compare records against history' do
387
+ it { bar.changes_against(bar.history.first).should ==
388
+ {'name' => ['bar', 'new bar']} }
389
+
390
+ it { bar.changes_against(bar.history.second).should ==
391
+ {'name' => ['foo bar', 'new bar']} }
392
+
393
+ it { bar.changes_against(bar.history.third).should ==
394
+ {'name' => ['bar bar', 'new bar']} }
395
+
396
+ it { bar.changes_against(bar.history.last).should == {} }
397
+ end
398
+
399
+ context 'can compare history against history' do
400
+ it { bar.history.first.changes_against(bar.history.third).should ==
401
+ {'name' => ['bar bar', 'bar']} }
402
+
403
+ it { bar.history.second.changes_against(bar.history.third).should ==
404
+ {'name' => ['bar bar', 'foo bar']} }
405
+
406
+ it { bar.history.third.changes_against(bar.history.third).should == {} }
407
+ end
408
+ end
409
+
410
+ describe '#pred' do
411
+ context 'on records having history' do
412
+ subject { bar.pred }
413
+ its(:name) { should == 'bar bar' }
414
+ end
415
+
416
+ context 'when there is enough history' do
417
+ subject { bar.pred.pred.pred.pred }
418
+ its(:name) { should == 'bar' }
419
+ end
420
+
421
+ context 'when no history is recorded' do
422
+ let(:record) { Bar.create!(:name => 'quuuux') }
423
+ subject { record.pred }
424
+ it { should be_nil }
425
+ after { record.destroy.history.delete_all }
426
+ end
427
+ end
428
+
226
429
  context do
227
430
  let!(:history) { foo.history.first }
228
431
  let!(:current) { foo }
@@ -239,7 +442,8 @@ describe ChronoModel::TimeMachine do
239
442
 
240
443
  describe 'on current records' do
241
444
  subject { current.public_send(attr) }
242
- it { expect { subject }.to raise_error(NoMethodError) }
445
+
446
+ it { should be_nil }
243
447
  end
244
448
  }
245
449
  }
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
5
- prerelease:
4
+ version: 0.5.0.beta
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Marcello Barnaba
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-30 00:00:00.000000000 Z
12
+ date: 2013-02-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
- requirement: &79419830 !ruby/object:Gem::Requirement
16
+ requirement: &77788870 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '3.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *79419830
24
+ version_requirements: *77788870
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: pg
27
- requirement: &79493370 !ruby/object:Gem::Requirement
27
+ requirement: &77787530 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *79493370
35
+ version_requirements: *77787530
36
36
  description: Give your models as-of date temporal extensions. Built entirely for PostgreSQL
37
37
  >= 9.0
38
38
  email:
@@ -55,7 +55,9 @@ files:
55
55
  - lib/chrono_model/compatibility.rb
56
56
  - lib/chrono_model/patches.rb
57
57
  - lib/chrono_model/railtie.rb
58
+ - lib/chrono_model/time_gate.rb
58
59
  - lib/chrono_model/time_machine.rb
60
+ - lib/chrono_model/utils.rb
59
61
  - lib/chrono_model/version.rb
60
62
  - spec/adapter_spec.rb
61
63
  - spec/config.yml.example
@@ -83,9 +85,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
85
  required_rubygems_version: !ruby/object:Gem::Requirement
84
86
  none: false
85
87
  requirements:
86
- - - ! '>='
88
+ - - ! '>'
87
89
  - !ruby/object:Gem::Version
88
- version: '0'
90
+ version: 1.3.1
89
91
  requirements: []
90
92
  rubyforge_project:
91
93
  rubygems_version: 1.8.10