zermelo 1.1.0 → 1.2.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +76 -52
- data/lib/zermelo/associations/association_data.rb +4 -3
- data/lib/zermelo/associations/class_methods.rb +37 -50
- data/lib/zermelo/associations/index.rb +3 -1
- data/lib/zermelo/associations/multiple.rb +247 -0
- data/lib/zermelo/associations/range_index.rb +44 -0
- data/lib/zermelo/associations/singular.rb +193 -0
- data/lib/zermelo/associations/unique_index.rb +4 -3
- data/lib/zermelo/backend.rb +120 -0
- data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
- data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
- data/lib/zermelo/backends/stub.rb +43 -0
- data/lib/zermelo/filter.rb +194 -0
- data/lib/zermelo/filters/index_range.rb +22 -0
- data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
- data/lib/zermelo/filters/redis.rb +173 -0
- data/lib/zermelo/filters/steps/list_step.rb +48 -30
- data/lib/zermelo/filters/steps/set_step.rb +148 -89
- data/lib/zermelo/filters/steps/sort_step.rb +2 -2
- data/lib/zermelo/record.rb +53 -0
- data/lib/zermelo/records/attributes.rb +32 -0
- data/lib/zermelo/records/class_methods.rb +12 -25
- data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
- data/lib/zermelo/records/instance_methods.rb +9 -8
- data/lib/zermelo/records/key.rb +3 -1
- data/lib/zermelo/records/redis.rb +17 -0
- data/lib/zermelo/records/stub.rb +17 -0
- data/lib/zermelo/version.rb +1 -1
- data/spec/lib/zermelo/associations/index_spec.rb +70 -1
- data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
- data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
- data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
- data/spec/lib/zermelo/filter_spec.rb +363 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
- data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
- data/spec/spec_helper.rb +9 -1
- data/spec/support/mock_logger.rb +48 -0
- metadata +31 -46
- data/lib/zermelo/associations/belongs_to.rb +0 -115
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
- data/lib/zermelo/associations/has_many.rb +0 -120
- data/lib/zermelo/associations/has_one.rb +0 -109
- data/lib/zermelo/associations/has_sorted_set.rb +0 -124
- data/lib/zermelo/backends/base.rb +0 -115
- data/lib/zermelo/filters/base.rb +0 -212
- data/lib/zermelo/filters/redis_filter.rb +0 -111
- data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
- data/lib/zermelo/records/base.rb +0 -62
- data/lib/zermelo/records/redis_record.rb +0 -27
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +0 -434
- data/spec/lib/zermelo/records/key_spec.rb +0 -6
- data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
- data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
- data/spec/lib/zermelo/version_spec.rb +0 -6
- data/spec/lib/zermelo_spec.rb +0 -6
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'zermelo/records/redis'
|
3
|
+
require 'zermelo/associations/range_index'
|
4
|
+
|
5
|
+
describe Zermelo::Associations::RangeIndex do
|
6
|
+
|
7
|
+
context 'redis', :redis => true do
|
8
|
+
|
9
|
+
let(:redis) { Zermelo.redis }
|
10
|
+
|
11
|
+
let(:time) { Time.now }
|
12
|
+
|
13
|
+
module ZermeloExamples
|
14
|
+
class RedisRangeIndex
|
15
|
+
include Zermelo::Records::Redis
|
16
|
+
define_attributes :created_at => :timestamp
|
17
|
+
validates :created_at, :presence => true
|
18
|
+
range_index_by :created_at
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'adds an entry to a sorted set indexing an attribute' do
|
23
|
+
example = ZermeloExamples::RedisRangeIndex.new(:id => '1',
|
24
|
+
:created_at => time)
|
25
|
+
expect(example).to be_valid
|
26
|
+
expect(example.save).to be true
|
27
|
+
|
28
|
+
expect(redis.exists('redis_range_index::indices:by_created_at')).to be true
|
29
|
+
expect(redis.zrange('redis_range_index::indices:by_created_at', 0, -1, :withscores => true)).to eq([
|
30
|
+
['1', time.to_f]
|
31
|
+
])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'removes an entry from a sorted set indexing an attribute' do
|
35
|
+
time_2 = time + 100
|
36
|
+
|
37
|
+
example = ZermeloExamples::RedisRangeIndex.new(:id => '1',
|
38
|
+
:created_at => time)
|
39
|
+
example.save
|
40
|
+
|
41
|
+
example_2 = ZermeloExamples::RedisRangeIndex.new(:id => '2',
|
42
|
+
:created_at => time_2)
|
43
|
+
example_2.save
|
44
|
+
|
45
|
+
expect(redis.zrange('redis_range_index::indices:by_created_at', 0, -1, :withscores => true)).to eq([
|
46
|
+
['1', time.to_f],
|
47
|
+
['2', time_2.to_f]
|
48
|
+
])
|
49
|
+
|
50
|
+
example.destroy
|
51
|
+
|
52
|
+
expect(redis.zrange('redis_range_index::indices:by_created_at', 0, -1, :withscores => true)).to eq([
|
53
|
+
['2', time_2.to_f]
|
54
|
+
])
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'changes an entry in a sorted set indexing an attribute' do
|
58
|
+
example = ZermeloExamples::RedisRangeIndex.new(:id => '1',
|
59
|
+
:created_at => time)
|
60
|
+
example.save
|
61
|
+
|
62
|
+
expect(redis.zrange('redis_range_index::indices:by_created_at', 0, -1, :withscores => true)).to eq([
|
63
|
+
['1', time.to_f]
|
64
|
+
])
|
65
|
+
|
66
|
+
time_2 = time + 100
|
67
|
+
example.created_at = time_2
|
68
|
+
example.save
|
69
|
+
|
70
|
+
expect(redis.zrange('redis_range_index::indices:by_created_at', 0, -1, :withscores => true)).to eq([
|
71
|
+
['1', time_2.to_f]
|
72
|
+
])
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'zermelo/records/redis'
|
3
|
+
require 'zermelo/associations/singular'
|
4
|
+
|
5
|
+
describe Zermelo::Associations::Singular do
|
6
|
+
|
7
|
+
context 'has_one' do
|
8
|
+
|
9
|
+
shared_examples "has_one functions work", :has_one => true do
|
10
|
+
|
11
|
+
it 'returns associated ids for multiple parent ids' do
|
12
|
+
create_parent(:id => '8')
|
13
|
+
create_parent(:id => '9')
|
14
|
+
create_parent(:id => '10')
|
15
|
+
|
16
|
+
time = Time.now.to_i
|
17
|
+
|
18
|
+
create_child(parent_class.find_by_id('9'), :id => '3')
|
19
|
+
create_child(parent_class.find_by_id('10'), :id => '4')
|
20
|
+
|
21
|
+
assoc_ids = parent_class.intersect(:id => ['8', '9', '10']).
|
22
|
+
associated_ids_for(:child)
|
23
|
+
expect(assoc_ids).to eq('8' => nil,
|
24
|
+
'9' => '3',
|
25
|
+
'10' => '4')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'calls before/after_read callbacks when the value is read' # do
|
29
|
+
# create_parent(:id => '8')
|
30
|
+
# parent = parent_class.find_by_id('8')
|
31
|
+
|
32
|
+
# expect(parent.read).to be_nil
|
33
|
+
# expect(parent.child).to be_nil
|
34
|
+
# expect(parent.read).to eq([:pre, :post])
|
35
|
+
# end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'redis', :redis => true, :has_one => true do
|
40
|
+
|
41
|
+
let(:redis) { Zermelo.redis }
|
42
|
+
|
43
|
+
module ZermeloExamples
|
44
|
+
class AssociationsHasOneParentRedis
|
45
|
+
include Zermelo::Records::Redis
|
46
|
+
has_one :child, :class_name => 'ZermeloExamples::AssociationsHasOneChildRedis',
|
47
|
+
:inverse_of => :parent
|
48
|
+
end
|
49
|
+
|
50
|
+
class AssociationsHasOneChildRedis
|
51
|
+
include Zermelo::Records::Redis
|
52
|
+
belongs_to :parent, :class_name => 'ZermeloExamples::AssociationsHasOneParentRedis',
|
53
|
+
:inverse_of => :child
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
let(:parent_class) { ZermeloExamples::AssociationsHasOneParentRedis }
|
58
|
+
let(:child_class) { ZermeloExamples::AssociationsHasOneChildRedis }
|
59
|
+
|
60
|
+
# parent and child keys
|
61
|
+
let(:pk) { 'associations_has_one_parent_redis' }
|
62
|
+
let(:ck) { 'associations_has_one_child_redis' }
|
63
|
+
|
64
|
+
def create_parent(attrs = {})
|
65
|
+
redis.sadd("#{pk}::attrs:ids", attrs[:id])
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_child(parent, attrs = {})
|
69
|
+
redis.hset("#{ck}:#{attrs[:id]}:assocs:belongs_to", 'parent_id', parent.id)
|
70
|
+
redis.hset("#{pk}:#{parent.id}:assocs:has_one", 'child_id', attrs[:id])
|
71
|
+
redis.sadd("#{ck}::attrs:ids", attrs[:id])
|
72
|
+
end
|
73
|
+
|
74
|
+
it "sets and retrieves a record via a has_one association" do
|
75
|
+
create_parent(:id => '8')
|
76
|
+
|
77
|
+
child = child_class.new(:id => '22')
|
78
|
+
expect(child.save).to be true
|
79
|
+
|
80
|
+
parent = parent_class.find_by_id('8')
|
81
|
+
parent.child = child
|
82
|
+
|
83
|
+
expect(redis.keys('*')).to match_array([
|
84
|
+
"#{pk}::attrs:ids",
|
85
|
+
"#{pk}:8:assocs:has_one",
|
86
|
+
"#{ck}::attrs:ids",
|
87
|
+
"#{ck}:22:assocs:belongs_to"
|
88
|
+
])
|
89
|
+
|
90
|
+
expect(redis.hgetall("#{pk}:8:assocs:has_one")).to eq("child_id" => "22")
|
91
|
+
|
92
|
+
expect(redis.smembers("#{ck}::attrs:ids")).to eq(["22"])
|
93
|
+
|
94
|
+
expect(redis.hgetall("#{ck}:22:assocs:belongs_to")).to eq(
|
95
|
+
'parent_id' => '8'
|
96
|
+
)
|
97
|
+
|
98
|
+
parent2 = parent_class.find_by_id('8')
|
99
|
+
child2 = parent2.child
|
100
|
+
expect(child2).not_to be_nil
|
101
|
+
|
102
|
+
expect(child2.id).to eq('22')
|
103
|
+
expect(child2.parent.id).to eq('8')
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'clears the belongs_to association when the child record is deleted' do
|
107
|
+
create_parent(:id => '8')
|
108
|
+
parent = parent_class.find_by_id('8')
|
109
|
+
create_child(parent, :id => '3')
|
110
|
+
child = child_class.find_by_id('3')
|
111
|
+
|
112
|
+
expect(redis.keys).to match_array([
|
113
|
+
"#{pk}::attrs:ids",
|
114
|
+
"#{pk}:8:assocs:has_one",
|
115
|
+
"#{ck}::attrs:ids",
|
116
|
+
"#{ck}:3:assocs:belongs_to"
|
117
|
+
])
|
118
|
+
|
119
|
+
child.destroy
|
120
|
+
|
121
|
+
expect(redis.keys).to match_array([
|
122
|
+
"#{pk}::attrs:ids",
|
123
|
+
])
|
124
|
+
end
|
125
|
+
|
126
|
+
it "clears the belongs_to association when the parent record is deleted" do
|
127
|
+
create_parent(:id => '8')
|
128
|
+
parent = parent_class.find_by_id('8')
|
129
|
+
create_child(parent, :id => '3')
|
130
|
+
|
131
|
+
expect(redis.keys).to match_array([
|
132
|
+
"#{pk}::attrs:ids",
|
133
|
+
"#{pk}:8:assocs:has_one",
|
134
|
+
"#{ck}::attrs:ids",
|
135
|
+
"#{ck}:3:assocs:belongs_to"
|
136
|
+
])
|
137
|
+
|
138
|
+
parent.destroy
|
139
|
+
|
140
|
+
expect(redis.keys).to match_array([
|
141
|
+
"#{ck}::attrs:ids"
|
142
|
+
])
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -1,6 +1,62 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'zermelo/records/redis'
|
2
3
|
require 'zermelo/associations/unique_index'
|
3
4
|
|
4
|
-
describe Zermelo::Associations::UniqueIndex
|
5
|
+
describe Zermelo::Associations::UniqueIndex do
|
5
6
|
|
6
|
-
|
7
|
+
context 'redis', :redis => true do
|
8
|
+
|
9
|
+
let(:redis) { Zermelo.redis }
|
10
|
+
|
11
|
+
module ZermeloExamples
|
12
|
+
class RedisUniqueIndex
|
13
|
+
include Zermelo::Records::Redis
|
14
|
+
define_attributes :name => :string
|
15
|
+
validates :name, :presence => true
|
16
|
+
unique_index_by :name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'adds an entry to a hash indexing an attribute' do
|
21
|
+
example = ZermeloExamples::RedisUniqueIndex.new(:id => '1',
|
22
|
+
:name => 'John Smith')
|
23
|
+
expect(example).to be_valid
|
24
|
+
expect(example.save).to be true
|
25
|
+
|
26
|
+
expect(redis.exists('redis_unique_index::indices:by_name')).to be true
|
27
|
+
expect(redis.hgetall('redis_unique_index::indices:by_name')).to eq('string:John%20Smith' => '1')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'removes an entry from a hash indexing an attribute' do
|
31
|
+
example = ZermeloExamples::RedisUniqueIndex.new(:id => '1',
|
32
|
+
:name => 'John Smith')
|
33
|
+
example.save
|
34
|
+
|
35
|
+
example_2 = ZermeloExamples::RedisUniqueIndex.new(:id => '2',
|
36
|
+
:name => 'Roger Wilco')
|
37
|
+
example_2.save
|
38
|
+
|
39
|
+
expect(redis.hgetall('redis_unique_index::indices:by_name')).to eq('string:John%20Smith' => '1',
|
40
|
+
'string:Roger%20Wilco' => '2')
|
41
|
+
|
42
|
+
example.destroy
|
43
|
+
|
44
|
+
expect(redis.hgetall('redis_unique_index::indices:by_name')).to eq({'string:Roger%20Wilco' => '2'})
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'changes an entry in a hash indexing an attribute' do
|
48
|
+
example = ZermeloExamples::RedisUniqueIndex.new(:id => '1',
|
49
|
+
:name => 'John Smith')
|
50
|
+
example.save
|
51
|
+
|
52
|
+
expect(redis.hgetall('redis_unique_index::indices:by_name')).to eq('string:John%20Smith' => '1')
|
53
|
+
|
54
|
+
example.name = 'Jane Jones'
|
55
|
+
example.save
|
56
|
+
|
57
|
+
expect(redis.hgetall('redis_unique_index::indices:by_name')).to eq('string:Jane%20Jones' => '1')
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'zermelo/filter'
|
3
|
+
require 'zermelo/records/redis'
|
4
|
+
require 'zermelo/records/influxdb'
|
5
|
+
require 'zermelo/associations/range_index'
|
6
|
+
|
7
|
+
describe Zermelo::Filter do
|
8
|
+
|
9
|
+
shared_examples 'filter functions work', :filter => true do
|
10
|
+
|
11
|
+
let(:time) { Time.now }
|
12
|
+
|
13
|
+
let(:active) {
|
14
|
+
create_example(:id => '8', :name => 'John Jones', :active => true,
|
15
|
+
:created_at => (time - 200).to_f)
|
16
|
+
}
|
17
|
+
|
18
|
+
let(:inactive) {
|
19
|
+
create_example(:id => '9', :name => 'James Brown', :active => false,
|
20
|
+
:created_at => (time - 100).to_f)
|
21
|
+
}
|
22
|
+
|
23
|
+
before do
|
24
|
+
active; inactive
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns all record ids" do
|
28
|
+
examples = example_class.ids
|
29
|
+
expect(examples).not_to be_nil
|
30
|
+
expect(examples).to be_an(Array)
|
31
|
+
expect(examples.size).to eq(2)
|
32
|
+
expect(examples).to contain_exactly('8', '9')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns a count of records" do
|
36
|
+
example_count = example_class.count
|
37
|
+
expect(example_count).not_to be_nil
|
38
|
+
expect(example_count).to be_an(Integer)
|
39
|
+
expect(example_count).to eq(2)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns all records" do
|
43
|
+
examples = example_class.all
|
44
|
+
expect(examples).not_to be_nil
|
45
|
+
expect(examples).to be_an(Array)
|
46
|
+
expect(examples.size).to eq(2)
|
47
|
+
expect(examples.map(&:id)).to contain_exactly('9', '8')
|
48
|
+
end
|
49
|
+
|
50
|
+
it "finds a record by id" do
|
51
|
+
example = example_class.find_by_id('8')
|
52
|
+
expect(example).not_to be_nil
|
53
|
+
expect(example.id).to eq('8')
|
54
|
+
expect(example.name).to eq('John Jones')
|
55
|
+
end
|
56
|
+
|
57
|
+
it "finds records by a uniquely indexed value" do
|
58
|
+
examples = example_class.intersect(:name => 'John Jones').all
|
59
|
+
expect(examples).not_to be_nil
|
60
|
+
expect(examples).to be_an(Array)
|
61
|
+
expect(examples.size).to eq(1)
|
62
|
+
example = examples.first
|
63
|
+
expect(example.id).to eq('8')
|
64
|
+
expect(example.name).to eq('John Jones')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "filters all class records by indexed attribute values" do
|
68
|
+
example = example_class.intersect(:active => true).all
|
69
|
+
expect(example).not_to be_nil
|
70
|
+
expect(example).to be_an(Array)
|
71
|
+
expect(example.size).to eq(1)
|
72
|
+
expect(example.map(&:id)).to eq(['8'])
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'filters by id attribute values' do
|
76
|
+
example = example_class.intersect(:id => '9').all
|
77
|
+
expect(example).not_to be_nil
|
78
|
+
expect(example).to be_an(Array)
|
79
|
+
expect(example.size).to eq(1)
|
80
|
+
expect(example.map(&:id)).to eq(['9'])
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'filters by multiple id attribute values' do
|
84
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true,
|
85
|
+
:created_at => (time - 50).to_f)
|
86
|
+
|
87
|
+
examples = example_class.intersect(:id => ['8', '10']).all
|
88
|
+
expect(examples).not_to be_nil
|
89
|
+
expect(examples).to be_an(Array)
|
90
|
+
expect(examples.size).to eq(2)
|
91
|
+
expect(examples.map(&:id)).to match_array(['8', '10'])
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'supports sequential intersection and union operations' do
|
95
|
+
examples = example_class.intersect(:active => true).
|
96
|
+
union(:active => false).all
|
97
|
+
expect(examples).not_to be_nil
|
98
|
+
expect(examples).to be_an(Array)
|
99
|
+
expect(examples.size).to eq(2)
|
100
|
+
expect(examples.map(&:id)).to match_array(['8', '9'])
|
101
|
+
end
|
102
|
+
|
103
|
+
it "chains two intersect filters together" do
|
104
|
+
example = example_class.intersect(:active => true).
|
105
|
+
intersect(:name => 'John Jones').all
|
106
|
+
expect(example).not_to be_nil
|
107
|
+
expect(example).to be_an(Array)
|
108
|
+
expect(example.size).to eq(1)
|
109
|
+
expect(example.map(&:id)).to eq(['8'])
|
110
|
+
end
|
111
|
+
|
112
|
+
it "allows multiple attributes in an intersect filter" do
|
113
|
+
example = example_class.intersect(:active => true,
|
114
|
+
:name => 'John Jones').all
|
115
|
+
expect(example).not_to be_nil
|
116
|
+
expect(example).to be_an(Array)
|
117
|
+
expect(example.size).to eq(1)
|
118
|
+
expect(example.map(&:id)).to eq(['8'])
|
119
|
+
end
|
120
|
+
|
121
|
+
it "chains an intersect and a diff filter together" do
|
122
|
+
create_example(:id => '3', :name => 'Fred Bloggs',
|
123
|
+
:active => 'true')
|
124
|
+
|
125
|
+
example = example_class.intersect(:active => true).diff(:name => 'Fred Bloggs').all
|
126
|
+
expect(example).not_to be_nil
|
127
|
+
expect(example).to be_an(Array)
|
128
|
+
expect(example.size).to eq(1)
|
129
|
+
expect(example.map(&:id)).to eq(['8'])
|
130
|
+
end
|
131
|
+
|
132
|
+
it "does not return a spurious record count when records don't exist" do
|
133
|
+
scope = example_class.intersect(:id => ['3000', '5000'])
|
134
|
+
expect(scope.all).to be_empty
|
135
|
+
expect(scope.count).to eq 0
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'finds records by regex match against a uniquely indexed value' do
|
139
|
+
examples = example_class.intersect(:name => /hn Jones/).all
|
140
|
+
expect(examples).not_to be_nil
|
141
|
+
expect(examples).to be_an(Array)
|
142
|
+
expect(examples.size).to eq(1)
|
143
|
+
example = examples.first
|
144
|
+
expect(example.id).to eq('8')
|
145
|
+
expect(example.name).to eq('John Jones')
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'cannot find records by regex match against non-string values' do
|
149
|
+
expect {
|
150
|
+
example_class.intersect(:active => /alse/).all
|
151
|
+
}.to raise_error("Can't query non-string values via regexp")
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'can append to a filter chain fragment more than once' do
|
155
|
+
inter = example_class.intersect(:active => true)
|
156
|
+
expect(inter.ids).to eq(['8'])
|
157
|
+
|
158
|
+
union = inter.union(:name => 'James Brown')
|
159
|
+
expect(union.ids).to match_array(['8', '9'])
|
160
|
+
|
161
|
+
diff = inter.diff(:id => ['8'])
|
162
|
+
expect(diff.ids).to eq([])
|
163
|
+
end
|
164
|
+
|
165
|
+
it "ANDs multiple union arguments, not ORs them" do
|
166
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true)
|
167
|
+
examples = example_class.intersect(:id => ['8']).
|
168
|
+
union(:id => ['9', '10'], :active => true).all
|
169
|
+
expect(examples).not_to be_nil
|
170
|
+
expect(examples).to be_an(Array)
|
171
|
+
expect(examples.size).to eq(2)
|
172
|
+
expect(examples.map(&:id)).to match_array(['8', '10'])
|
173
|
+
end
|
174
|
+
|
175
|
+
it "ANDs multiple diff arguments, not ORs them" do
|
176
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true)
|
177
|
+
examples = example_class.intersect(:id => ['8', '9', '10']).
|
178
|
+
diff(:id => ['9', '10'], :active => false).all
|
179
|
+
expect(examples).not_to be_nil
|
180
|
+
expect(examples).to be_an(Array)
|
181
|
+
expect(examples.size).to eq(2)
|
182
|
+
expect(examples.map(&:id)).to match_array(['8', '10'])
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'supports a regex as argument in union after intersect' do
|
186
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true)
|
187
|
+
examples = example_class.intersect(:id => ['8']).
|
188
|
+
union(:id => ['9', '10'], :name => [nil, /^Jam/]).all
|
189
|
+
expect(examples).not_to be_nil
|
190
|
+
expect(examples).to be_an(Array)
|
191
|
+
expect(examples.size).to eq(2)
|
192
|
+
expect(examples.map(&:id)).to match_array(['8', '9'])
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'allows intersection operations across multiple values for an attribute' do
|
196
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true)
|
197
|
+
|
198
|
+
examples = example_class.intersect(:name => ['Jay Johns', 'James Brown']).all
|
199
|
+
expect(examples).not_to be_nil
|
200
|
+
expect(examples).to be_an(Array)
|
201
|
+
expect(examples.size).to eq(2)
|
202
|
+
expect(examples.map(&:id)).to match_array(['9', '10'])
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'allows union operations across multiple values for an attribute' do
|
206
|
+
create_example(:id => '10', :name => 'Jay Johns', :active => true)
|
207
|
+
|
208
|
+
examples = example_class.intersect(:active => false).
|
209
|
+
union(:name => ['Jay Johns', 'James Brown']).all
|
210
|
+
expect(examples).not_to be_nil
|
211
|
+
expect(examples).to be_an(Array)
|
212
|
+
expect(examples.size).to eq(2)
|
213
|
+
expect(examples.map(&:id)).to match_array(['9', '10'])
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'excludes particular records' do
|
217
|
+
example = example_class.diff(:active => true).all
|
218
|
+
expect(example).not_to be_nil
|
219
|
+
expect(example).to be_an(Array)
|
220
|
+
expect(example.size).to eq(1)
|
221
|
+
expect(example.map(&:id)).to eq(['9'])
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
context 'redis', :redis => true, :filter => true do
|
227
|
+
|
228
|
+
let(:redis) { Zermelo.redis }
|
229
|
+
|
230
|
+
module ZermeloExamples
|
231
|
+
class FilterRedis
|
232
|
+
include Zermelo::Records::Redis
|
233
|
+
define_attributes :name => :string,
|
234
|
+
:active => :boolean,
|
235
|
+
:created_at => :timestamp
|
236
|
+
validates :name, :presence => true
|
237
|
+
validates :active, :inclusion => {:in => [true, false]}
|
238
|
+
index_by :active
|
239
|
+
range_index_by :created_at
|
240
|
+
unique_index_by :name
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
let(:example_class) { ZermeloExamples::FilterRedis }
|
245
|
+
|
246
|
+
# parent and child keys
|
247
|
+
let(:ek) { 'filter_redis' }
|
248
|
+
|
249
|
+
def create_example(attrs = {})
|
250
|
+
redis.hmset("#{ek}:#{attrs[:id]}:attrs",
|
251
|
+
{'name' => attrs[:name], 'active' => attrs[:active]}.to_a.flatten)
|
252
|
+
redis.sadd("#{ek}::indices:by_active:boolean:#{!!attrs[:active]}", attrs[:id])
|
253
|
+
name = attrs[:name].gsub(/%/, '%%').gsub(/ /, '%20').gsub(/:/, '%3A')
|
254
|
+
redis.hset("#{ek}::indices:by_name", "string:#{name}", attrs[:id])
|
255
|
+
redis.zadd("#{ek}::indices:by_created_at", attrs[:created_at].to_f, attrs[:id])
|
256
|
+
redis.sadd("#{ek}::attrs:ids", attrs[:id])
|
257
|
+
end
|
258
|
+
|
259
|
+
# following only working in Redis for now
|
260
|
+
it 'sorts records by an attribute' do
|
261
|
+
example = example_class.sort(:name, :order => 'alpha').all
|
262
|
+
expect(example).not_to be_nil
|
263
|
+
expect(example).to be_an(Array)
|
264
|
+
expect(example.size).to eq(2)
|
265
|
+
expect(example.map(&:id)).to eq(['9', '8'])
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'sorts by multiple fields' do
|
269
|
+
data = {:active => true, :created_at => Time.now}
|
270
|
+
create_example(data.merge(:id => '1', :name => 'abc'))
|
271
|
+
create_example(data.merge(:id => '2', :name => 'def'))
|
272
|
+
create_example(data.merge(:id => '3', :name => 'abc'))
|
273
|
+
create_example(data.merge(:id => '4', :name => 'def'))
|
274
|
+
|
275
|
+
expect(example_class.sort(:name => :asc, :id => :desc).map(&:id)).to eq(
|
276
|
+
['9', '8', '3', '1', '4', '2']
|
277
|
+
)
|
278
|
+
end
|
279
|
+
|
280
|
+
# NB sort is case-sensitive, may want a non-case sensitive version
|
281
|
+
it "returns paginated query responses" do
|
282
|
+
data = {:active => true, :created_at => Time.now}
|
283
|
+
create_example(data.merge(:id => '1', :name => 'mno'))
|
284
|
+
create_example(data.merge(:id => '2', :name => 'abc'))
|
285
|
+
create_example(data.merge(:id => '3', :name => 'jkl'))
|
286
|
+
create_example(data.merge(:id => '4', :name => 'ghi'))
|
287
|
+
create_example(data.merge(:id => '5', :name => 'def'))
|
288
|
+
|
289
|
+
expect(example_class.sort(:id).page(1, :per_page => 3).map(&:id)).to eq(['1', '2', '3'])
|
290
|
+
expect(example_class.sort(:id).page(2, :per_page => 2).map(&:id)).to eq(['3', '4'])
|
291
|
+
expect(example_class.sort(:id).page(3, :per_page => 2).map(&:id)).to eq(['5', '8'])
|
292
|
+
expect(example_class.sort(:id).page(3, :per_page => 3).map(&:id)).to eq(['9'])
|
293
|
+
|
294
|
+
expect(example_class.sort(:name).page(1, :per_page => 3).map(&:id)).to eq(['9', '8', '2'])
|
295
|
+
expect(example_class.sort(:name).page(2, :per_page => 3).map(&:id)).to eq(['5', '4', '3'])
|
296
|
+
expect(example_class.sort(:name).page(3, :per_page => 3).map(&:id)).to eq(['1'])
|
297
|
+
expect(example_class.sort(:name).page(4, :per_page => 3).map(&:id)).to eq([])
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'filters by records created before a certain time' do
|
301
|
+
examples = example_class.intersect(:created_at => Zermelo::Filters::IndexRange.new(nil, time - 150, :by_score => true))
|
302
|
+
expect(examples.count).to eq(1)
|
303
|
+
expect(examples.map(&:id)).to eq(['8'])
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'filters by records created after a certain time' do
|
307
|
+
examples = example_class.intersect(:created_at => Zermelo::Filters::IndexRange.new(time - 150, nil, :by_score => true))
|
308
|
+
expect(examples.count).to eq(1)
|
309
|
+
expect(examples.map(&:id)).to eq(['9'])
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'raises an error when trying to filter on a non-indexed value' do
|
313
|
+
expect {
|
314
|
+
example_class.intersect(:email => 'jjones@example.com').all
|
315
|
+
}.to raise_error("'email' property is not indexed")
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
context 'influxdb', :influxdb => true, :filter => true do
|
321
|
+
|
322
|
+
let(:influxdb) { Zermelo.influxdb }
|
323
|
+
|
324
|
+
module ZermeloExamples
|
325
|
+
class FilterInfluxDB
|
326
|
+
include Zermelo::Records::InfluxDB
|
327
|
+
define_attributes :name => :string,
|
328
|
+
:active => :boolean,
|
329
|
+
:created_at => :timestamp
|
330
|
+
validates :name, :presence => true
|
331
|
+
validates :active, :inclusion => {:in => [true, false]}
|
332
|
+
index_by :active
|
333
|
+
range_index_by :created_at
|
334
|
+
unique_index_by :name
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
let(:example_class) { ZermeloExamples::FilterInfluxDB }
|
339
|
+
|
340
|
+
# parent and child keys
|
341
|
+
let(:ek) { 'filter_influx_db' }
|
342
|
+
|
343
|
+
def create_example(attrs = {})
|
344
|
+
attrs[:active] = attrs[:active].to_s unless attrs[:active].nil?
|
345
|
+
attrs[:time] = attrs[:created_at].to_i
|
346
|
+
Zermelo.influxdb.write_point("#{ek}/#{attrs[:id]}", attrs)
|
347
|
+
end
|
348
|
+
|
349
|
+
# need to fix the influxdb driver to work with these (see Redis examples above)
|
350
|
+
it 'sorts records by an attribute'
|
351
|
+
it 'sorts by multiple fields'
|
352
|
+
it "returns paginated query responses"
|
353
|
+
it 'filters by records created before a certain time'
|
354
|
+
it 'filters by records created after a certain time'
|
355
|
+
|
356
|
+
it 'raises an error when trying to filter on a non-indexed value' do
|
357
|
+
expect {
|
358
|
+
example_class.intersect(:email => 'jjones@example.com').all
|
359
|
+
}.to raise_error("Field email doesn't exist in series filter_influx_db/10")
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
363
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'zermelo/locks/redis_lock'
|
3
|
-
require 'zermelo/records/
|
3
|
+
require 'zermelo/records/redis'
|
4
4
|
|
5
5
|
describe Zermelo::Locks::RedisLock, :redis => true do
|
6
6
|
|
@@ -8,12 +8,12 @@ describe Zermelo::Locks::RedisLock, :redis => true do
|
|
8
8
|
|
9
9
|
module Zermelo
|
10
10
|
class RedisLockExample
|
11
|
-
include Zermelo::Records::
|
11
|
+
include Zermelo::Records::Redis
|
12
12
|
define_attributes :name => :string
|
13
13
|
end
|
14
14
|
|
15
15
|
class AnotherExample
|
16
|
-
include Zermelo::Records::
|
16
|
+
include Zermelo::Records::Redis
|
17
17
|
define_attributes :age => :integer
|
18
18
|
end
|
19
19
|
end
|