chrono_model 0.4.0 → 0.5.0.beta

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