chrono_model 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ require 'support/matchers/base'
2
+
3
+ module ChronoTest::Matchers
4
+
5
+ module Table
6
+ class Base < ChronoTest::Matchers::Base
7
+
8
+ protected
9
+ # Database statements
10
+ #
11
+ def relation_exists?(options)
12
+ schema = options[:in]
13
+ kind = options[:kind] == :view ? 'v' : 'r'
14
+
15
+ select_value(<<-SQL, [ table, schema ], 'Check table exists') == 't'
16
+ SELECT EXISTS (
17
+ SELECT 1
18
+ FROM pg_class c
19
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
20
+ WHERE c.relkind = '#{kind}'
21
+ AND c.relname = $1
22
+ AND n.nspname = $2
23
+ )
24
+ SQL
25
+ end
26
+ end
27
+
28
+ # ##################################################################
29
+ # Checks that a table exists in the Public schema
30
+ #
31
+ class HavePublicBacking < Base
32
+ def matches?(table)
33
+ super(table)
34
+
35
+ relation_exists? :in => public_schema
36
+ end
37
+
38
+ def failure_message_for_should
39
+ "expected #{table} to exist in the #{public_schema} schema"
40
+ end
41
+ end
42
+
43
+ def have_public_backing
44
+ HavePublicBacking.new
45
+ end
46
+
47
+
48
+ # ##################################################################
49
+ # Checks that a table exists in the Temporal schema
50
+ #
51
+ class HaveTemporalBacking < Base
52
+ def matches?(table)
53
+ super(table)
54
+
55
+ relation_exists? :in => temporal_schema
56
+ end
57
+
58
+ def failure_message_for_should
59
+ "expected #{table} to exist in the #{temporal_schema} schema"
60
+ end
61
+ end
62
+
63
+ def have_temporal_backing
64
+ HaveTemporalBacking.new
65
+ end
66
+
67
+
68
+ # ##################################################################
69
+ # Checks that a table exists in the History schema and inherits from
70
+ # the one in the Temporal schema
71
+ #
72
+ class HaveHistoryBacking < Base
73
+ def matches?(table)
74
+ super(table)
75
+
76
+ table_exists? && inherits_from_temporal?
77
+ end
78
+
79
+ def failure_message_for_should
80
+ "expected #{table} ".tap do |message|
81
+ message << [
82
+ ("to exist in the #{history_schema} schema" unless @existance),
83
+ ("to inherit from #{temporal_schema}.#{table}" unless @inheritance)
84
+ ].compact.to_sentence
85
+ end
86
+ end
87
+
88
+ private
89
+ def table_exists?
90
+ @existance = relation_exists? :in => history_schema
91
+ end
92
+
93
+ def inherits_from_temporal?
94
+ binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"]
95
+
96
+ @inheritance = select_value(<<-SQL, binds, 'Check inheritance') == 't'
97
+ SELECT EXISTS (
98
+ SELECT 1 FROM pg_catalog.pg_inherits
99
+ WHERE inhrelid = $1::regclass::oid
100
+ AND inhparent = $2::regclass::oid
101
+ )
102
+ SQL
103
+ end
104
+ end
105
+
106
+ def have_history_backing
107
+ HaveHistoryBacking.new
108
+ end
109
+
110
+
111
+ # ##################################################################
112
+ # Checks that a table exists in the Public schema, is an updatable
113
+ # view and has an INSERT, UPDATE and DELETE rule.
114
+ #
115
+ class HavePublicInterface < Base
116
+ def matches?(table)
117
+ super(table)
118
+
119
+ view_exists? && [ is_updatable?, has_rules? ].all?
120
+ end
121
+
122
+ def failure_message_for_should
123
+ "expected #{table} ".tap do |message|
124
+ message << [
125
+ ("to exist in the #{public_schema} schema" unless @existance ),
126
+ ('to be an updatable view' unless @updatable ),
127
+ ('to have an INSERT rule' unless @insert_rule),
128
+ ('to have an UPDATE rule' unless @update_rule),
129
+ ('to have a DELETE rule' unless @delete_rule)
130
+ ].compact.to_sentence
131
+ end
132
+ end
133
+
134
+ private
135
+ def view_exists?
136
+ @existance = relation_exists? :in => public_schema, :kind => :view
137
+ end
138
+
139
+ def is_updatable?
140
+ binds = [ public_schema, table ]
141
+
142
+ @updatable = select_value(<<-SQL, binds, 'Check updatable') == 'YES'
143
+ SELECT is_updatable FROM information_schema.views
144
+ WHERE table_schema = $1 AND table_name = $2
145
+ SQL
146
+ end
147
+
148
+ def has_rules?
149
+ rules = select_values(<<-SQL, [ public_schema, table ], 'Check rules')
150
+ SELECT UNNEST(REGEXP_MATCHES(
151
+ definition, 'ON (INSERT|UPDATE|DELETE) TO #{table} DO INSTEAD'
152
+ ))
153
+ FROM pg_catalog.pg_rules
154
+ WHERE schemaname = $1
155
+ AND tablename = $2
156
+ SQL
157
+
158
+ @insert_rule = rules.include? 'INSERT'
159
+ @update_rule = rules.include? 'UPDATE'
160
+ @delete_rule = rules.include? 'DELETE'
161
+
162
+ @insert_rule && @update_rule && @delete_rule
163
+ end
164
+ end
165
+
166
+ def have_public_interface
167
+ HavePublicInterface.new
168
+ end
169
+ end
170
+
171
+ end
@@ -0,0 +1,299 @@
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
+ describe '.chrono_models' do
11
+ subject { ChronoModel::TimeMachine.chrono_models }
12
+
13
+ it { should == {'foos' => Foo, 'bars' => Bar} }
14
+ end
15
+
16
+
17
+ # Set up two associated records, with intertwined updates
18
+ #
19
+ let!(:foo) {
20
+ foo = ts_eval { Foo.create! :name => 'foo', :fooity => 1 }
21
+ ts_eval(foo) { update_attributes! :name => 'foo bar' }
22
+ }
23
+
24
+ let!(:bar) {
25
+ bar = ts_eval { Bar.create! :name => 'bar', :foo => foo }
26
+ ts_eval(bar) { update_attributes! :name => 'foo bar' }
27
+
28
+ ts_eval(foo) { update_attributes! :name => 'new foo' }
29
+
30
+ ts_eval(bar) { update_attributes! :name => 'bar bar' }
31
+ ts_eval(bar) { update_attributes! :name => 'new bar' }
32
+ }
33
+
34
+ # Specs start here
35
+ #
36
+ describe '#as_of' do
37
+ describe 'accepts a Time instance' do
38
+ it { foo.as_of(Time.now).name.should == 'new foo' }
39
+ it { bar.as_of(Time.now).name.should == 'new bar' }
40
+ end
41
+
42
+ describe 'ignores time zones' do
43
+ it { foo.as_of(Time.now.in_time_zone('America/Havana')).name.should == 'new foo' }
44
+ it { bar.as_of(Time.now.in_time_zone('America/Havana')).name.should == 'new bar' }
45
+ end
46
+
47
+ describe 'returns records as they were before' do
48
+ it { foo.as_of(foo.ts[0]).name.should == 'foo' }
49
+ it { foo.as_of(foo.ts[1]).name.should == 'foo bar' }
50
+ it { foo.as_of(foo.ts[2]).name.should == 'new foo' }
51
+
52
+ it { bar.as_of(bar.ts[0]).name.should == 'bar' }
53
+ it { bar.as_of(bar.ts[1]).name.should == 'foo bar' }
54
+ it { bar.as_of(bar.ts[2]).name.should == 'bar bar' }
55
+ it { bar.as_of(bar.ts[3]).name.should == 'new bar' }
56
+ end
57
+
58
+ describe 'takes care of associated records' do
59
+ it { foo.as_of(foo.ts[0]).bars.should == [] }
60
+ it { foo.as_of(foo.ts[1]).bars.should == [] }
61
+ it { foo.as_of(foo.ts[2]).bars.should == [bar] }
62
+
63
+ it { foo.as_of(foo.ts[2]).bars.first.name.should == 'foo bar' }
64
+
65
+
66
+ it { foo.as_of(bar.ts[0]).bars.should == [bar] }
67
+ it { foo.as_of(bar.ts[1]).bars.should == [bar] }
68
+ it { foo.as_of(bar.ts[2]).bars.should == [bar] }
69
+ it { foo.as_of(bar.ts[3]).bars.should == [bar] }
70
+
71
+ it { foo.as_of(bar.ts[0]).bars.first.name.should == 'bar' }
72
+ it { foo.as_of(bar.ts[1]).bars.first.name.should == 'foo bar' }
73
+ it { foo.as_of(bar.ts[2]).bars.first.name.should == 'bar bar' }
74
+ it { foo.as_of(bar.ts[3]).bars.first.name.should == 'new bar' }
75
+
76
+
77
+ it { bar.as_of(bar.ts[0]).foo.should == foo }
78
+ it { bar.as_of(bar.ts[1]).foo.should == foo }
79
+ it { bar.as_of(bar.ts[2]).foo.should == foo }
80
+ it { bar.as_of(bar.ts[3]).foo.should == foo }
81
+
82
+ it { bar.as_of(bar.ts[0]).foo.name.should == 'foo bar' }
83
+ it { bar.as_of(bar.ts[1]).foo.name.should == 'foo bar' }
84
+ it { bar.as_of(bar.ts[2]).foo.name.should == 'new foo' }
85
+ it { bar.as_of(bar.ts[3]).foo.name.should == 'new foo' }
86
+ end
87
+
88
+ it 'raises RecordNotFound when no history records are found' do
89
+ expect { foo.as_of(1.minute.ago) }.to raise_error
90
+ end
91
+ end
92
+
93
+ describe '#history' do
94
+ describe 'returns historical instances' do
95
+ it { foo.history.should have(3).entries }
96
+ it { foo.history.map(&:name).should == ['foo', 'foo bar', 'new foo'] }
97
+
98
+ it { bar.history.should have(4).entries }
99
+ it { bar.history.map(&:name).should == ['bar', 'foo bar', 'bar bar', 'new bar'] }
100
+ end
101
+
102
+ describe 'returns read only records' do
103
+ it { foo.history.all?(&:readonly?).should be_true }
104
+ it { bar.history.all?(&:readonly?).should be_true }
105
+ end
106
+
107
+ describe 'takes care of associated records' do
108
+ subject { foo.history.map {|f| f.bars.first.try(:name)} }
109
+ it { should == [nil, 'foo bar', 'new bar'] }
110
+ end
111
+
112
+ describe 'returns read only associated records' do
113
+ it { foo.history[2].bars.all?(&:readonly?).should be_true }
114
+ it { bar.history.all? {|b| b.foo.readonly?}.should be_true }
115
+ end
116
+ end
117
+
118
+ describe '#historical?' do
119
+ describe 'on plain records' do
120
+ subject { foo.historical? }
121
+ it { should be_false }
122
+ end
123
+
124
+ describe 'on historical records' do
125
+ describe 'from #history' do
126
+ subject { foo.history.first }
127
+ it { should be_true }
128
+ end
129
+
130
+ describe 'from #as_of' do
131
+ subject { foo.as_of(Time.now) }
132
+ it { should be_true }
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#destroy' do
138
+ describe 'on historical records' do
139
+ subject { foo.history.first.destroy }
140
+ it { expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord) }
141
+ end
142
+
143
+ describe 'on current records' do
144
+ let!(:rec) {
145
+ rec = ts_eval { Foo.create!(:name => 'alive foo', :fooity => 42) }
146
+ ts_eval(rec) { update_attributes!(:name => 'dying foo') }
147
+ }
148
+
149
+ subject { rec.destroy }
150
+
151
+ it { expect { subject }.to_not raise_error }
152
+ it { expect { rec.reload }.to raise_error(ActiveRecord::RecordNotFound) }
153
+
154
+ describe 'does not delete its history' do
155
+ context do
156
+ subject { rec.as_of(rec.ts.first) }
157
+ its(:name) { should == 'alive foo' }
158
+ end
159
+
160
+ context do
161
+ subject { rec.as_of(rec.ts.last) }
162
+ its(:name) { should == 'dying foo' }
163
+ end
164
+
165
+ context do
166
+ subject { Foo.as_of(rec.ts.first).where(:fooity => 42).first }
167
+ its(:name) { should == 'alive foo' }
168
+ end
169
+
170
+ context do
171
+ subject { Foo.history.where(:fooity => 42).map(&:name) }
172
+ it { should == ['alive foo', 'dying foo'] }
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ describe '#history_timestamps' do
179
+ timestamps_from = lambda {|*records|
180
+ records.map(&:history).flatten!.inject([]) {|ret, rec|
181
+ ret.concat [rec.valid_from, rec.valid_to]
182
+ }.sort.uniq[0..-2]
183
+ }
184
+
185
+ describe 'on records having an :has_many relationship' do
186
+ subject { foo.history_timestamps }
187
+
188
+ describe 'returns timestamps of the record and its associations' do
189
+ its(:size) { should == foo.ts.size + bar.ts.size }
190
+ it { should == timestamps_from.call(foo, bar) }
191
+ end
192
+ end
193
+
194
+ describe 'on records having a :belongs_to relationship' do
195
+ subject { bar.history_timestamps }
196
+
197
+ describe 'returns timestamps of the record only' do
198
+ its(:size) { should == bar.ts.size }
199
+ it { should == timestamps_from.call(bar) }
200
+ end
201
+ end
202
+ end
203
+
204
+ context do
205
+ history_attrs = ChronoModel::TimeMachine::HISTORY_ATTRIBUTES
206
+
207
+ let!(:history) { foo.history.first }
208
+ let!(:current) { foo }
209
+
210
+ history_attrs.each do |attr|
211
+ describe ['#', attr].join do
212
+ describe 'on history records' do
213
+ subject { history.public_send(attr) }
214
+
215
+ it { should be_present }
216
+ it { should be_a(Time) }
217
+ it { should be_utc }
218
+ end
219
+
220
+ describe 'on current records' do
221
+ subject { current.public_send(attr) }
222
+ it { should be_nil }
223
+ end
224
+ end
225
+ end
226
+
227
+ describe '#initialize_dup' do
228
+ describe 'on history records' do
229
+ subject { history.dup }
230
+
231
+ history_attrs.each do |attr|
232
+ its(attr) { should be_nil }
233
+ end
234
+
235
+ it { should_not be_readonly }
236
+ it { should be_new_record }
237
+
238
+ end
239
+ end
240
+ end
241
+
242
+ # Class methods
243
+ context do
244
+ let!(:foos) { Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } } }
245
+ let!(:bars) { Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } } }
246
+
247
+ after(:all) { foos.each(&:destroy); bars.each(&:destroy) }
248
+
249
+ describe '.as_of' do
250
+ it { Foo.as_of(1.month.ago).should == [] }
251
+
252
+ it { Foo.as_of(foos[0].ts[0]).should == [foo, foos[0]] }
253
+ it { Foo.as_of(foos[1].ts[0]).should == [foo, foos[0], foos[1]] }
254
+ it { Foo.as_of(Time.now ).should == [foo, foos[0], foos[1]] }
255
+
256
+ it { Bar.as_of(foos[1].ts[0]).should == [bar] }
257
+
258
+ it { Bar.as_of(bars[0].ts[0]).should == [bar, bars[0]] }
259
+ it { Bar.as_of(bars[1].ts[0]).should == [bar, bars[0], bars[1]] }
260
+ it { Bar.as_of(Time.now ).should == [bar, bars[0], bars[1]] }
261
+
262
+ # Associations
263
+ context do
264
+ subject { foos[0] }
265
+
266
+ it { Foo.as_of(foos[0].ts[0]).find(subject).bars.should == [] }
267
+ it { Foo.as_of(foos[1].ts[0]).find(subject).bars.should == [] }
268
+ it { Foo.as_of(bars[0].ts[0]).find(subject).bars.should == [bars[0]] }
269
+ it { Foo.as_of(bars[1].ts[0]).find(subject).bars.should == [bars[0]] }
270
+ it { Foo.as_of(Time.now ).find(subject).bars.should == [bars[0]] }
271
+ end
272
+
273
+ context do
274
+ subject { foos[1] }
275
+
276
+ it { expect { Foo.as_of(foos[0].ts[0]).find(subject) }.to raise_error(ActiveRecord::RecordNotFound) }
277
+ it { expect { Foo.as_of(foos[1].ts[0]).find(subject) }.to_not raise_error }
278
+
279
+ it { Foo.as_of(bars[0].ts[0]).find(subject).bars.should == [] }
280
+ it { Foo.as_of(bars[1].ts[0]).find(subject).bars.should == [bars[1]] }
281
+ it { Foo.as_of(Time.now ).find(subject).bars.should == [bars[1]] }
282
+ end
283
+ end
284
+
285
+ describe '.history' do
286
+ let(:foo_history) {
287
+ ['foo', 'foo bar', 'new foo', 'alive foo', 'dying foo', 'foo 0', 'foo 1']
288
+ }
289
+
290
+ let(:bar_history) {
291
+ ['bar', 'foo bar', 'bar bar', 'new bar', 'bar 0', 'bar 1']
292
+ }
293
+
294
+ it { Foo.history.map(&:name).should == foo_history }
295
+ it { Bar.history.map(&:name).should == bar_history }
296
+ end
297
+ end
298
+
299
+ end