chrono_model 0.5.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -1
- data/.travis.yml +7 -0
- data/Gemfile +10 -1
- data/LICENSE +3 -1
- data/README.md +239 -136
- data/README.sql +108 -94
- data/chrono_model.gemspec +5 -4
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +42 -0
- data/lib/chrono_model.rb +0 -8
- data/lib/chrono_model/adapter.rb +346 -212
- data/lib/chrono_model/patches.rb +21 -8
- data/lib/chrono_model/railtie.rb +1 -13
- data/lib/chrono_model/time_gate.rb +2 -2
- data/lib/chrono_model/time_machine.rb +153 -87
- data/lib/chrono_model/utils.rb +35 -8
- data/lib/chrono_model/version.rb +1 -1
- data/spec/adapter_spec.rb +154 -14
- data/spec/config.yml.example +1 -0
- data/spec/json_ops_spec.rb +48 -0
- data/spec/support/connection.rb +4 -9
- data/spec/support/helpers.rb +27 -2
- data/spec/support/matchers/column.rb +5 -2
- data/spec/support/matchers/index.rb +4 -0
- data/spec/support/matchers/schema.rb +4 -0
- data/spec/support/matchers/table.rb +94 -21
- data/spec/time_machine_spec.rb +62 -28
- data/spec/time_query_spec.rb +227 -0
- data/sql/json_ops.sql +56 -0
- data/sql/uninstall-json_ops.sql +24 -0
- metadata +44 -18
- data/lib/chrono_model/compatibility.rb +0 -31
@@ -35,6 +35,10 @@ module ChronoTest::Matchers
|
|
35
35
|
relation_exists? :in => public_schema
|
36
36
|
end
|
37
37
|
|
38
|
+
def description
|
39
|
+
'be in the public schema'
|
40
|
+
end
|
41
|
+
|
38
42
|
def failure_message_for_should
|
39
43
|
"expected #{table} to exist in the #{public_schema} schema"
|
40
44
|
end
|
@@ -55,6 +59,10 @@ module ChronoTest::Matchers
|
|
55
59
|
relation_exists? :in => temporal_schema
|
56
60
|
end
|
57
61
|
|
62
|
+
def description
|
63
|
+
'be in the temporal schema'
|
64
|
+
end
|
65
|
+
|
58
66
|
def failure_message_for_should
|
59
67
|
"expected #{table} to exist in the #{temporal_schema} schema"
|
60
68
|
end
|
@@ -73,14 +81,23 @@ module ChronoTest::Matchers
|
|
73
81
|
def matches?(table)
|
74
82
|
super(table)
|
75
83
|
|
76
|
-
table_exists?
|
84
|
+
table_exists? &&
|
85
|
+
inherits_from_temporal? &&
|
86
|
+
has_consistency_constraint? &&
|
87
|
+
has_history_indexes?
|
88
|
+
end
|
89
|
+
|
90
|
+
def description
|
91
|
+
'be in history schema'
|
77
92
|
end
|
78
93
|
|
79
94
|
def failure_message_for_should
|
80
95
|
"expected #{table} ".tap do |message|
|
81
96
|
message << [
|
82
97
|
("to exist in the #{history_schema} schema" unless @existance),
|
83
|
-
("to inherit from #{temporal_schema}.#{table}" unless @inheritance)
|
98
|
+
("to inherit from #{temporal_schema}.#{table}" unless @inheritance),
|
99
|
+
("to have a timeline consistency constraint" unless @constraint),
|
100
|
+
("to have history indexes" unless @indexes)
|
84
101
|
].compact.to_sentence
|
85
102
|
end
|
86
103
|
end
|
@@ -101,6 +118,58 @@ module ChronoTest::Matchers
|
|
101
118
|
)
|
102
119
|
SQL
|
103
120
|
end
|
121
|
+
|
122
|
+
def has_history_indexes?
|
123
|
+
binds = [ history_schema, table ]
|
124
|
+
|
125
|
+
indexes = select_values(<<-SQL, binds, 'Check history indexes')
|
126
|
+
SELECT indexdef FROM pg_indexes
|
127
|
+
WHERE schemaname = $1
|
128
|
+
AND tablename = $2
|
129
|
+
SQL
|
130
|
+
|
131
|
+
fqtn = [history_schema, table].join('.')
|
132
|
+
|
133
|
+
expected = [
|
134
|
+
"CREATE INDEX index_#{table}_temporal_on_lower_validity ON #{fqtn} USING btree (lower(validity))",
|
135
|
+
"CREATE INDEX index_#{table}_temporal_on_upper_validity ON #{fqtn} USING btree (upper(validity))",
|
136
|
+
"CREATE INDEX index_#{table}_temporal_on_validity ON #{fqtn} USING gist (validity)",
|
137
|
+
|
138
|
+
"CREATE INDEX #{table}_inherit_pkey ON #{fqtn} USING btree (id)",
|
139
|
+
"CREATE INDEX #{table}_instance_history ON #{fqtn} USING btree (id, recorded_at)",
|
140
|
+
"CREATE UNIQUE INDEX #{table}_pkey ON #{fqtn} USING btree (hid)",
|
141
|
+
"CREATE INDEX #{table}_recorded_at ON #{fqtn} USING btree (recorded_at)",
|
142
|
+
"CREATE INDEX #{table}_timeline_consistency ON #{fqtn} USING gist (id, validity)"
|
143
|
+
]
|
144
|
+
|
145
|
+
@indexes = (expected - indexes).empty?
|
146
|
+
end
|
147
|
+
|
148
|
+
def has_consistency_constraint?
|
149
|
+
binds = [
|
150
|
+
connection.timeline_consistency_constraint_name(table), # conname
|
151
|
+
history_schema, # connamespace
|
152
|
+
[history_schema, table].join('.'), # conrelid, attrelid
|
153
|
+
connection.primary_key(table) # attnum
|
154
|
+
]
|
155
|
+
|
156
|
+
@constraint = select_value(<<-SQL, binds, 'Check Consistency Constraint') == 't'
|
157
|
+
SELECT EXISTS (
|
158
|
+
SELECT 1 FROM pg_catalog.pg_constraint
|
159
|
+
WHERE conname = $1
|
160
|
+
AND contype = 'x'
|
161
|
+
AND conrelid = $3::regclass
|
162
|
+
AND connamespace = (
|
163
|
+
SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = $2
|
164
|
+
)
|
165
|
+
AND conkey = (
|
166
|
+
SELECT array_agg(attnum) FROM pg_catalog.pg_attribute
|
167
|
+
WHERE attname IN ($4, 'validity')
|
168
|
+
AND attrelid = $3::regclass
|
169
|
+
)
|
170
|
+
)
|
171
|
+
SQL
|
172
|
+
end
|
104
173
|
end
|
105
174
|
|
106
175
|
def have_history_backing
|
@@ -110,23 +179,27 @@ module ChronoTest::Matchers
|
|
110
179
|
|
111
180
|
# ##################################################################
|
112
181
|
# Checks that a table exists in the Public schema, is an updatable
|
113
|
-
# view and has an INSERT, UPDATE and DELETE
|
182
|
+
# view and has an INSERT, UPDATE and DELETE triggers.
|
114
183
|
#
|
115
184
|
class HavePublicInterface < Base
|
116
185
|
def matches?(table)
|
117
186
|
super(table)
|
118
187
|
|
119
|
-
view_exists? && [ is_updatable?,
|
188
|
+
view_exists? && [ is_updatable?, has_triggers? ].all?
|
189
|
+
end
|
190
|
+
|
191
|
+
def description
|
192
|
+
'be an updatable view'
|
120
193
|
end
|
121
194
|
|
122
195
|
def failure_message_for_should
|
123
196
|
"expected #{table} ".tap do |message|
|
124
197
|
message << [
|
125
|
-
("to exist in the #{public_schema} schema" unless @existance
|
126
|
-
('to be an updatable view' unless @updatable
|
127
|
-
('to have an INSERT
|
128
|
-
('to have an UPDATE
|
129
|
-
('to have a DELETE
|
198
|
+
("to exist in the #{public_schema} schema" unless @existance ),
|
199
|
+
('to be an updatable view' unless @updatable ),
|
200
|
+
('to have an INSERT trigger' unless @insert_trigger),
|
201
|
+
('to have an UPDATE trigger' unless @update_trigger),
|
202
|
+
('to have a DELETE trigger' unless @delete_trigger)
|
130
203
|
].compact.to_sentence
|
131
204
|
end
|
132
205
|
end
|
@@ -145,21 +218,21 @@ module ChronoTest::Matchers
|
|
145
218
|
SQL
|
146
219
|
end
|
147
220
|
|
148
|
-
def
|
149
|
-
|
150
|
-
SELECT
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
221
|
+
def has_triggers?
|
222
|
+
triggers = select_values(<<-SQL, [ public_schema, table ], 'Check triggers')
|
223
|
+
SELECT t.tgname
|
224
|
+
FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c, pg_catalog.pg_namespace n
|
225
|
+
WHERE t.tgrelid = c.relfilenode
|
226
|
+
AND n.oid = c.relnamespace
|
227
|
+
AND n.nspname = $1
|
228
|
+
AND c.relname = $2;
|
156
229
|
SQL
|
157
230
|
|
158
|
-
@
|
159
|
-
@
|
160
|
-
@
|
231
|
+
@insert_trigger = triggers.include? 'chronomodel_insert'
|
232
|
+
@update_trigger = triggers.include? 'chronomodel_update'
|
233
|
+
@delete_trigger = triggers.include? 'chronomodel_delete'
|
161
234
|
|
162
|
-
@
|
235
|
+
@insert_trigger && @update_trigger && @delete_trigger
|
163
236
|
end
|
164
237
|
end
|
165
238
|
|
data/spec/time_machine_spec.rb
CHANGED
@@ -24,6 +24,18 @@ describe ChronoModel::TimeMachine do
|
|
24
24
|
#
|
25
25
|
baz = Baz.create :name => 'baz', :bar => bar
|
26
26
|
|
27
|
+
describe '.chrono?' do
|
28
|
+
context 'on a temporal model' do
|
29
|
+
subject { Foo }
|
30
|
+
it { should be_chrono }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'on a plain model' do
|
34
|
+
subject { Plain }
|
35
|
+
it { should_not be_chrono }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
27
39
|
# Specs start here
|
28
40
|
#
|
29
41
|
describe '.chrono_models' do
|
@@ -164,12 +176,15 @@ describe ChronoModel::TimeMachine do
|
|
164
176
|
end
|
165
177
|
|
166
178
|
describe 'does not add as_of_time when there are aggregates' do
|
167
|
-
it { foo.history.select('max (id)').to_sql.should_not =~ /as_of_time/ }
|
168
|
-
it { foo.history.select('max
|
179
|
+
it { foo.history.select('max (id)').to_sql.should_not =~ /as_of_time/ } # The id is automatically added by ActiveRecord
|
180
|
+
it { foo.history.select('max(id) as foo, min(id) as bar').order(nil).first.attributes.keys.should == %w( foo bar id ) }
|
169
181
|
end
|
170
182
|
|
171
|
-
|
172
|
-
|
183
|
+
context 'with STI models' do
|
184
|
+
pub = ts_eval { Publication.create! :title => 'wrong title' }
|
185
|
+
ts_eval(pub) { update_attributes! :title => 'correct title' }
|
186
|
+
|
187
|
+
it { pub.history.map(&:title).should == ['wrong title', 'correct title'] }
|
173
188
|
end
|
174
189
|
|
175
190
|
describe 'allows a custom order list' do
|
@@ -177,12 +192,12 @@ describe ChronoModel::TimeMachine do
|
|
177
192
|
it { foo.history.order('id').to_sql.should =~ /order by id/i }
|
178
193
|
end
|
179
194
|
|
180
|
-
context '
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
it { pub.history.map(&:title).should == ['wrong title', 'correct title'] }
|
195
|
+
context '.sorted' do
|
196
|
+
describe 'orders by recorded_at, hid' do
|
197
|
+
it { foo.history.sorted.to_sql.should =~ /order by .+"recorded_at", .+"hid"/i }
|
198
|
+
end
|
185
199
|
end
|
200
|
+
|
186
201
|
end
|
187
202
|
|
188
203
|
describe '#pred' do
|
@@ -303,11 +318,10 @@ describe ChronoModel::TimeMachine do
|
|
303
318
|
|
304
319
|
timestamps_from = lambda {|*records|
|
305
320
|
ts = records.map(&:history).flatten!.inject([]) {|ret, rec|
|
306
|
-
ret.
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
}.sort.uniq[0..-2]
|
321
|
+
ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from)
|
322
|
+
ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to)
|
323
|
+
ret
|
324
|
+
}.sort.uniq
|
311
325
|
}
|
312
326
|
|
313
327
|
describe 'on records having an :has_many relationship' do
|
@@ -425,31 +439,44 @@ describe ChronoModel::TimeMachine do
|
|
425
439
|
end
|
426
440
|
end
|
427
441
|
|
428
|
-
|
429
|
-
|
430
|
-
|
442
|
+
describe 'timestamp methods' do
|
443
|
+
history_methods = %w( valid_from valid_to recorded_at )
|
444
|
+
current_methods = %w( as_of_time )
|
431
445
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
446
|
+
context 'on history records' do
|
447
|
+
let(:record) { foo.history.first }
|
448
|
+
|
449
|
+
(history_methods + current_methods).each do |attr|
|
450
|
+
describe ['#', attr].join do
|
451
|
+
subject { record.public_send(attr) }
|
436
452
|
|
437
453
|
it { should be_present }
|
438
454
|
it { should be_a(Time) }
|
439
455
|
it { should be_utc }
|
440
456
|
end
|
457
|
+
end
|
458
|
+
end
|
441
459
|
|
442
|
-
|
443
|
-
|
460
|
+
context 'on current records' do
|
461
|
+
let(:record) { foo }
|
444
462
|
|
445
|
-
|
463
|
+
history_methods.each do |attr|
|
464
|
+
describe ['#', attr].join do
|
465
|
+
subject { record.public_send(attr) }
|
466
|
+
|
467
|
+
it { expect { subject }.to raise_error(NoMethodError) }
|
446
468
|
end
|
447
|
-
|
448
|
-
}
|
469
|
+
end
|
449
470
|
|
450
|
-
|
451
|
-
|
471
|
+
current_methods.each do |attr|
|
472
|
+
describe ['#', attr].join do
|
473
|
+
subject { record.public_send(attr) }
|
474
|
+
|
475
|
+
it { should be_nil }
|
476
|
+
end
|
477
|
+
end
|
452
478
|
end
|
479
|
+
|
453
480
|
end
|
454
481
|
|
455
482
|
# Class methods
|
@@ -507,6 +534,13 @@ describe ChronoModel::TimeMachine do
|
|
507
534
|
it { Foo.history.all.map(&:name).should == foo_history }
|
508
535
|
it { Bar.history.all.map(&:name).should == bar_history }
|
509
536
|
end
|
537
|
+
|
538
|
+
describe '.time_query' do
|
539
|
+
it { Foo.history.time_query(:after, :now).count.should == 3 }
|
540
|
+
it { Foo.history.time_query(:before, :now).count.should == 5 }
|
541
|
+
it { Foo.history.past.size.should == 2 }
|
542
|
+
end
|
543
|
+
|
510
544
|
end
|
511
545
|
|
512
546
|
# Transactions
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/helpers'
|
3
|
+
|
4
|
+
describe ChronoModel::TimeMachine::TimeQuery do
|
5
|
+
include ChronoTest::Helpers::TimeMachine
|
6
|
+
|
7
|
+
setup_schema!
|
8
|
+
define_models!
|
9
|
+
|
10
|
+
# Create a set of events
|
11
|
+
#
|
12
|
+
think = Event.create! name: 'think', interval: (15.days.ago.to_date...13.days.ago.to_date)
|
13
|
+
plan = Event.create! name: 'plan', interval: (14.days.ago.to_date...12.days.ago.to_date)
|
14
|
+
collect = Event.create! name: 'collect', interval: (12.days.ago.to_date...10.days.ago.to_date)
|
15
|
+
start = Event.create! name: 'start', interval: (8.days.ago.to_date...7.days.ago.to_date)
|
16
|
+
build = Event.create! name: 'build', interval: (7.days.ago.to_date...Date.yesterday)
|
17
|
+
profit = Event.create! name: 'profit', interval: (Date.tomorrow...1.year.from_now.to_date)
|
18
|
+
|
19
|
+
describe :at do
|
20
|
+
describe 'with a single timestamp' do
|
21
|
+
subject { Event.time_query(:at, time.try(:to_date) || time, on: :interval).to_a }
|
22
|
+
|
23
|
+
context 'no records' do
|
24
|
+
let(:time) { 16.days.ago }
|
25
|
+
it { should be_empty }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'single record' do
|
29
|
+
let(:time) { 15.days.ago }
|
30
|
+
it { should == [think] }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'multiple overlapping records' do
|
34
|
+
let(:time) { 14.days.ago }
|
35
|
+
it { should =~ [think, plan] }
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'on an edge of an open interval' do
|
39
|
+
let(:time) { 10.days.ago }
|
40
|
+
it { should be_empty }
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'in an hole' do
|
44
|
+
let(:time) { 9.days.ago }
|
45
|
+
it { should be_empty }
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'today' do
|
49
|
+
let(:time) { Date.today }
|
50
|
+
it { should be_empty }
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'server-side :today' do
|
54
|
+
let(:time) { :today }
|
55
|
+
it { should be_empty }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'with a range' do
|
60
|
+
subject { Event.time_query(:at, times.map!(&:to_date), on: :interval, type: :daterange).to_a }
|
61
|
+
|
62
|
+
context 'that is empty' do
|
63
|
+
let(:times) { [ 14.days.ago, 14.days.ago ] }
|
64
|
+
it { should be_empty }
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'overlapping no records' do
|
68
|
+
let(:times) { [ 20.days.ago, 16.days.ago ] }
|
69
|
+
it { should be_empty }
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'overlapping a single record' do
|
73
|
+
let(:times) { [ 16.days.ago, 14.days.ago ] }
|
74
|
+
it { should == [think] }
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'overlapping more records' do
|
78
|
+
let(:times) { [ 16.days.ago, 11.days.ago ] }
|
79
|
+
it { should =~ [think, plan, collect] }
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'on the edge of an open interval and an hole' do
|
83
|
+
let(:times) { [ 10.days.ago, 9.days.ago ] }
|
84
|
+
it { should be_empty }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe :before do
|
90
|
+
subject { Event.time_query(:before, time.try(:to_date) || time, on: :interval, type: :daterange).to_a }
|
91
|
+
|
92
|
+
context '16 days ago' do
|
93
|
+
let(:time) { 16.days.ago }
|
94
|
+
it { should be_empty }
|
95
|
+
end
|
96
|
+
|
97
|
+
context '14 days ago' do
|
98
|
+
let(:time) { 14.days.ago }
|
99
|
+
it { should == [think] }
|
100
|
+
end
|
101
|
+
|
102
|
+
context '11 days ago' do
|
103
|
+
let(:time) { 11.days.ago }
|
104
|
+
it { should =~ [think, plan, collect] }
|
105
|
+
end
|
106
|
+
|
107
|
+
context '10 days ago' do
|
108
|
+
let(:time) { 10.days.ago }
|
109
|
+
it { should =~ [think, plan, collect] }
|
110
|
+
end
|
111
|
+
|
112
|
+
context '8 days ago' do
|
113
|
+
let(:time) { 8.days.ago }
|
114
|
+
it { should =~ [think, plan, collect] }
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'today' do
|
118
|
+
let(:time) { Date.today }
|
119
|
+
it { should =~ [think, plan, collect, start, build] }
|
120
|
+
end
|
121
|
+
|
122
|
+
context ':today' do
|
123
|
+
let(:time) { :today }
|
124
|
+
it { should =~ [think, plan, collect, start, build] }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe :after do
|
129
|
+
subject { Event.time_query(:after, time.try(:to_date) || time, on: :interval, type: :daterange).to_a }
|
130
|
+
|
131
|
+
context 'one month ago' do
|
132
|
+
let(:time) { 1.month.ago }
|
133
|
+
it { should =~ [think, plan, collect, start, build, profit] }
|
134
|
+
end
|
135
|
+
|
136
|
+
context '10 days ago' do
|
137
|
+
let(:time) { 10.days.ago }
|
138
|
+
it { should =~ [start, build, profit] }
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'yesterday' do
|
142
|
+
let(:time) { Date.yesterday }
|
143
|
+
it { should == [profit] }
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'today' do
|
147
|
+
let(:time) { Date.today }
|
148
|
+
it { should == [profit] }
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'server-side :today' do
|
152
|
+
let(:time) { :today }
|
153
|
+
it { should == [profit] }
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'tomorrow' do
|
157
|
+
let(:time) { Date.tomorrow }
|
158
|
+
it { should == [profit] }
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'one month from now' do
|
162
|
+
let(:time) { 1.month.from_now }
|
163
|
+
it { should == [profit] }
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'far future' do
|
167
|
+
let(:time) { 1.year.from_now }
|
168
|
+
it { should be_empty }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe :not do
|
173
|
+
context 'with a single timestamp' do
|
174
|
+
subject { Event.time_query(:not, time.try(:to_date) || time, on: :interval, type: :daterange).to_a }
|
175
|
+
|
176
|
+
context '14 days ago' do
|
177
|
+
let(:time) { 14.days.ago }
|
178
|
+
it { should =~ [collect, start, build, profit] }
|
179
|
+
end
|
180
|
+
|
181
|
+
context '9 days ago' do
|
182
|
+
let(:time) { 9.days.ago }
|
183
|
+
it { should =~ [think, plan, collect, start, build, profit] }
|
184
|
+
end
|
185
|
+
|
186
|
+
context '8 days ago' do
|
187
|
+
let(:time) { 8.days.ago }
|
188
|
+
it { should =~ [think, plan, collect, build, profit] }
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'today' do
|
192
|
+
let(:time) { Date.today }
|
193
|
+
it { should =~ [think, plan, collect, start, build, profit] }
|
194
|
+
end
|
195
|
+
|
196
|
+
context ':today' do
|
197
|
+
let(:time) { :today }
|
198
|
+
it { should =~ [think, plan, collect, start, build, profit] }
|
199
|
+
end
|
200
|
+
|
201
|
+
context '1 month from now' do
|
202
|
+
let(:time) { 1.month.from_now }
|
203
|
+
it { should =~ [think, plan, collect, start, build] }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context 'with a range' do
|
208
|
+
subject { Event.time_query(:not, time.map(&:to_date), on: :interval, type: :daterange).to_a }
|
209
|
+
|
210
|
+
context 'eliminating a single record' do
|
211
|
+
let(:time) { [1.month.ago, 14.days.ago] }
|
212
|
+
it { should =~ [plan, collect, start, build, profit] }
|
213
|
+
end
|
214
|
+
|
215
|
+
context 'eliminating multiple records' do
|
216
|
+
let(:time) { [1.month.ago, Date.today] }
|
217
|
+
it { should == [profit] }
|
218
|
+
end
|
219
|
+
|
220
|
+
context 'from an edge' do
|
221
|
+
let(:time) { [14.days.ago, 10.days.ago] }
|
222
|
+
it { should == [start, build, profit] }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|