relix 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### 1.2.0
2
+
3
+ * Improved keyer inheritance, including better legacy
4
+ support. (ntalbott)
5
+ * Improved memory efficiency by not storing unnecessary
6
+ data. (myronmarston)
7
+ * Added Ordered Indexes for easy range queries. (myronmarston)
8
+
1
9
  ### 1.1.1
2
10
 
3
11
  * Added keyers to the manifest file. (ntalbott)
data/README.md CHANGED
@@ -143,6 +143,17 @@ When there are multiple attributes, they are specified in a hash:
143
143
  end
144
144
 
145
145
 
146
+ ### Space efficiency
147
+
148
+ Model attributes that are indexed on but that never change can be marked as immutable to prevent them being stored (since they don't have to be reindexed). The primary key is marked immutable by default, but other attributes can be as well:
149
+
150
+ relix do
151
+ unique :token, immutable_attribute: true
152
+ end
153
+
154
+ This can also provide concurrency benefits since the keys for the indexes on immutable attributes don't have to be watched for concurrent modification.
155
+
156
+
146
157
  ## Index Types
147
158
 
148
159
  ### PrimaryKeyIndex
@@ -182,6 +193,30 @@ Unique indexes ignore nil values - they will not be indexed and an error is not
182
193
  **Supported Operators**: eq, all
183
194
  **Ordering**: can be ordered on any numeric attribute (default is the to_i of the indexed value)
184
195
 
196
+ ### OrderedIndex
197
+
198
+ Ordered indexes are specifically designed to support range queries. Like a MultiIndex, they support multiple matching
199
+ primary keys per indexed value. They are declared using #ordered in the relix block:
200
+
201
+ relix do
202
+ ordered :birthdate
203
+ end
204
+
205
+ **Supported Operators**: eq, lt, lte, gt, gte, order, limit, offset
206
+ **Ordering**: ordered ascending by the indexed value, but can be queried in
207
+ reverse order if you use `order(:desc)`.
208
+
209
+ Ordered indexes support a flexible fluent interface for specifying the query:
210
+
211
+ Person.lookup do |q|
212
+ q[:birthdate].
213
+ gte(Date.new(1990, 1, 1)).
214
+ lt(Date.new(1991, 1, 1).
215
+ order(:desc).
216
+ limit(10)
217
+ end
218
+
219
+ This query returns the primary keys of the 10 youngest people born in 1990.
185
220
 
186
221
  ## Keying
187
222
 
@@ -195,6 +230,8 @@ You can set the default keyer like so:
195
230
 
196
231
  Relix.default_keyer(Relix::Keyer::Compact)
197
232
 
233
+ Keyers are inherited, with child classes will use their parent's keyer unless a keyer is explicitly set on the child.
234
+
198
235
 
199
236
  ### Standard
200
237
 
@@ -208,4 +245,4 @@ Keys take up space, and especially since Redis holds the keyset in memory it can
208
245
 
209
246
  ### Legacy
210
247
 
211
- This (eventually to be deprecated and removed) strategy exactly mirrors the keying supported by Relix when first released.
248
+ This (eventually to be deprecated and removed) strategy exactly mirrors the keying supported by Relix when first released.
data/lib/relix.rb CHANGED
@@ -7,3 +7,4 @@ require 'relix/index'
7
7
  require 'relix/indexes/multi'
8
8
  require 'relix/indexes/unique'
9
9
  require 'relix/indexes/primary_key'
10
+ require 'relix/indexes/ordered'
data/lib/relix/index.rb CHANGED
@@ -8,10 +8,13 @@ module Relix
8
8
  @compact_kind ||= kind[0..0]
9
9
  end
10
10
 
11
+ attr_reader :model_name
11
12
  def initialize(set, base_name, accessor, options={})
12
13
  @set = set
13
14
  @base_name = base_name
15
+ @model_name = @set.klass.name
14
16
  @accessor = [accessor].flatten.collect{|a| a.to_s}
17
+ @attribute_immutable = !!options[:immutable_attribute]
15
18
  @options = options
16
19
  end
17
20
 
@@ -43,7 +46,11 @@ module Relix
43
46
  end.join(":")
44
47
  end
45
48
 
46
- def watch
49
+ def watch(*values)
50
+ watch_keys(*values) unless attribute_immutable?
51
+ end
52
+
53
+ def watch_keys(*values)
47
54
  nil
48
55
  end
49
56
 
@@ -55,6 +62,14 @@ module Relix
55
62
  nil
56
63
  end
57
64
 
65
+ def create_query_clause(redis)
66
+ Query::Clause.new(redis, self)
67
+ end
68
+
69
+ def attribute_immutable?
70
+ @attribute_immutable
71
+ end
72
+
58
73
  module Ordering
59
74
  def initialize(*args)
60
75
  super
@@ -65,6 +80,11 @@ module Relix
65
80
  if @order
66
81
  value = object.send(@order)
67
82
  end
83
+
84
+ score_for_value(value)
85
+ end
86
+
87
+ def score_for_value(value)
68
88
  case value
69
89
  when Numeric
70
90
  value
@@ -96,4 +116,4 @@ module Relix
96
116
 
97
117
  class UnorderableValueError < Relix::Error; end
98
118
  class MissingIndexValueError < Relix::Error; end
99
- end
119
+ end
@@ -1,11 +1,12 @@
1
1
  module Relix
2
2
  class IndexSet
3
3
  attr_accessor :redis
4
+ attr_reader :klass
4
5
  def initialize(klass, redis)
5
6
  @klass = klass
6
7
  @redis = redis
7
8
  @indexes = Hash.new
8
- @keyer = Keyer.default_for(@klass)
9
+ @keyer = Keyer.default_for(@klass) unless parent
9
10
  end
10
11
 
11
12
  def primary_key(accessor)
@@ -28,7 +29,7 @@ module Relix
28
29
  if value
29
30
  @keyer = value.new(@klass, options)
30
31
  else
31
- @keyer
32
+ (@keyer || parent.keyer)
32
33
  end
33
34
  end
34
35
 
@@ -65,13 +66,13 @@ module Relix
65
66
  current_values = @redis.hgetall(current_values_name)
66
67
 
67
68
  ops = indexes.collect do |name,index|
68
- ((watch = index.watch) && @redis.watch(*watch))
69
-
70
69
  value = index.read_normalized(object)
71
70
  old_value = current_values[name]
72
71
 
72
+ ((watch = index.watch(value, old_value)) && @redis.watch(*watch))
73
+
73
74
  next if value == old_value
74
- current_values[name] = value
75
+ current_values[name] = value unless index.attribute_immutable?
75
76
 
76
77
  next unless index.filter(@redis, object, value)
77
78
 
@@ -83,7 +84,7 @@ module Relix
83
84
 
84
85
  ops << proc do
85
86
  @redis.hmset(current_values_name, *current_values.flatten)
86
- end
87
+ end if current_values.any?
87
88
 
88
89
  ops
89
90
  end
@@ -105,8 +106,8 @@ module Relix
105
106
  current_values = @redis.hgetall(current_values_name)
106
107
 
107
108
  indexes.map do |name, index|
108
- ((watch = index.watch) && @redis.watch(*watch))
109
109
  old_value = current_values[name]
110
+ ((watch = index.watch(old_value)) && @redis.watch(*watch))
110
111
  proc { index.deindex(@redis, pk, object, old_value) }
111
112
  end.tap { |ops| ops << proc { @redis.del current_values_name } }
112
113
  end
@@ -125,7 +126,7 @@ module Relix
125
126
  end
126
127
 
127
128
  def current_values_name(pk)
128
- @keyer.values(pk)
129
+ keyer.values(pk, @klass)
129
130
  end
130
131
 
131
132
  private
@@ -2,6 +2,10 @@ module Relix
2
2
  class MultiIndex < Index
3
3
  include Ordering
4
4
 
5
+ def watch_keys(*values)
6
+ values.compact.map { |v| key_for(v) }
7
+ end
8
+
5
9
  def index(r, pk, object, value, old_value)
6
10
  r.zadd(key_for(value), score(object, value), pk)
7
11
  r.zrem(key_for(old_value), pk)
@@ -0,0 +1,101 @@
1
+ module Relix
2
+ class OrderedIndex < Index
3
+ include Ordering
4
+
5
+ def initialize(set, base_name, accessor, options={})
6
+ super
7
+ @order = accessor
8
+ end
9
+
10
+ def sorted_set_name
11
+ name
12
+ end
13
+
14
+ def watch_keys(*values)
15
+ sorted_set_name
16
+ end
17
+
18
+ def index(r, pk, object, value, old_value)
19
+ r.zadd(sorted_set_name, score(object, value), pk)
20
+ end
21
+
22
+ def deindex(r, pk, object, old_value)
23
+ r.zrem(sorted_set_name, pk)
24
+ end
25
+
26
+ def create_query_clause(redis)
27
+ QueryClause.new(redis, self)
28
+ end
29
+
30
+ class QueryClause
31
+ def initialize(redis, index)
32
+ @redis, @index = redis, index
33
+ @lt, @gt, @limit, @offset, @order = '+inf', '-inf', nil, nil, :asc
34
+ end
35
+
36
+ def lt(value)
37
+ @lt = "(#{@index.score_for_value(value)}"
38
+ self
39
+ end
40
+
41
+ def lte(value)
42
+ @lt = @index.score_for_value(value)
43
+ self
44
+ end
45
+
46
+ def gt(value)
47
+ @gt = "(#{@index.score_for_value(value)}"
48
+ self
49
+ end
50
+
51
+ def gte(value)
52
+ @gt = @index.score_for_value(value)
53
+ self
54
+ end
55
+
56
+ def eq(value)
57
+ lte(value)
58
+ gte(value)
59
+ end
60
+
61
+ def order(value)
62
+ unless [:asc, :desc].include?(value)
63
+ raise InvalidQueryOption.new("order must be :asc or :desc but was #{value.inspect}")
64
+ end
65
+
66
+ @order = value
67
+ self
68
+ end
69
+
70
+ def limit(value)
71
+ @limit = value
72
+ self
73
+ end
74
+
75
+ def offset(value)
76
+ @offset = value
77
+ self
78
+ end
79
+
80
+ def zrangebyscore_limit
81
+ # zrangebyscore uses offset/count rather than start/stop like zrange
82
+ offset, stop = @index.range_from_options(@redis, offset: @offset, limit: @limit)
83
+ count = stop == -1 ? -1 : (stop - offset + 1)
84
+ [offset, count]
85
+ end
86
+
87
+ def lookup
88
+ command, score_1, score_2 = case @order
89
+ when :desc then [:zrevrangebyscore, @lt, @gt]
90
+ when :asc then [:zrangebyscore, @gt, @lt]
91
+ end
92
+
93
+ @redis.send(command, @index.sorted_set_name, score_1, score_2, limit: zrangebyscore_limit)
94
+ end
95
+ end
96
+ end
97
+
98
+ register_index OrderedIndex
99
+ class InvalidQueryOption < Relix::Error; end
100
+ end
101
+
@@ -2,12 +2,17 @@ module Relix
2
2
  class PrimaryKeyIndex < Index
3
3
  include Ordering
4
4
 
5
- def watch
5
+ def initialize(set, base_name, accessor, options={})
6
+ options[:immutable_attribute] = true unless options.has_key?(:immutable_attribute)
7
+ super
8
+ end
9
+
10
+ def watch_keys(*values)
6
11
  name
7
12
  end
8
13
 
9
14
  def filter(r, object, value)
10
- !r.zrank(name, value)
15
+ !r.zscore(name, value)
11
16
  end
12
17
 
13
18
  def query(r, value)
@@ -31,4 +36,4 @@ module Relix
31
36
  end
32
37
  end
33
38
  register_index PrimaryKeyIndex
34
- end
39
+ end
@@ -10,7 +10,7 @@ module Relix
10
10
  @set.keyer.component(name, 'lookup')
11
11
  end
12
12
 
13
- def watch
13
+ def watch_keys(*values)
14
14
  hash_name
15
15
  end
16
16
 
data/lib/relix/keyer.rb CHANGED
@@ -18,16 +18,16 @@ module Relix
18
18
  @prefix = klass.name
19
19
  end
20
20
 
21
- def values(pk)
22
- "#{@prefix}:current_values:#{pk}"
21
+ def values(pk, klass)
22
+ "#{klass.name}:current_values:#{pk}"
23
23
  end
24
24
 
25
25
  def index(index, name)
26
26
  case index
27
27
  when PrimaryKeyIndex
28
- "#{index.class.name}:#{@prefix}:primary_key"
28
+ "#{index.class.name}:#{index.model_name}:primary_key"
29
29
  else
30
- "#{index.class.name}:#{@prefix}:#{name}"
30
+ "#{index.class.name}:#{index.model_name}:#{name}"
31
31
  end
32
32
  end
33
33
 
@@ -52,12 +52,12 @@ module Relix
52
52
  @prefix = klass.name
53
53
  end
54
54
 
55
- def values(pk)
55
+ def values(pk, klass)
56
56
  "#{@prefix}:values:#{pk}"
57
57
  end
58
58
 
59
59
  def index(index, name)
60
- "#{@prefix}:#{name}:#{index.class.kind}"
60
+ "#{index.model_name}:#{name}:#{index.class.kind}"
61
61
  end
62
62
 
63
63
  def component(name, component)
@@ -74,7 +74,7 @@ module Relix
74
74
  end
75
75
  end
76
76
 
77
- def values(pk)
77
+ def values(pk, klass)
78
78
  "#{@prefix}:v:#{pk}"
79
79
  end
80
80
 
data/lib/relix/query.rb CHANGED
@@ -8,7 +8,7 @@ module Relix
8
8
  def [](index_name)
9
9
  index = @model.indexes[index_name.to_s]
10
10
  raise MissingIndexError.new("No index declared for #{index_name}") unless index
11
- @clause = Clause.new(@model.redis, index)
11
+ @clause = index.create_query_clause(@model.redis)
12
12
  end
13
13
 
14
14
  def run
data/lib/relix/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Relix
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relix
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-05 00:00:00.000000000 Z
12
+ date: 2012-01-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hiredis
16
- requirement: &70249249266500 !ruby/object:Gem::Requirement
16
+ requirement: &70287488679200 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.4.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70249249266500
24
+ version_requirements: *70287488679200
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis
27
- requirement: &70249249266000 !ruby/object:Gem::Requirement
27
+ requirement: &70287488678700 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: 2.2.2
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70249249266000
35
+ version_requirements: *70287488678700
36
36
  description: ! 'Relix is a layer that can be added on to any model to make all the
37
37
  normal types of querying you want to do: equality, less than/greater than, in set,
38
38
  range, limit, etc., quick and painless. Relix depends on Redis to be awesome at
@@ -50,6 +50,7 @@ files:
50
50
  - lib/relix/core.rb
51
51
  - lib/relix/index.rb
52
52
  - lib/relix/indexes/multi.rb
53
+ - lib/relix/indexes/ordered.rb
53
54
  - lib/relix/indexes/primary_key.rb
54
55
  - lib/relix/indexes/unique.rb
55
56
  - lib/relix/index_set.rb
@@ -71,7 +72,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
72
  version: '0'
72
73
  segments:
73
74
  - 0
74
- hash: 187409947092406271
75
+ hash: -969086072538235743
75
76
  required_rubygems_version: !ruby/object:Gem::Requirement
76
77
  none: false
77
78
  requirements: