chrono_model 0.5.3 → 0.8.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.
- 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
|