chrono_model 0.3.0

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