relix 1.0.0 → 1.0.1
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.
- data/lib/relix/core.rb +37 -0
- data/lib/relix/index.rb +90 -0
- data/lib/relix/index_set.rb +99 -0
- data/lib/relix/indexes/multi.rb +21 -0
- data/lib/relix/indexes/primary_key.rb +30 -0
- data/lib/relix/indexes/unique.rb +45 -0
- data/lib/relix/query.rb +51 -0
- data/lib/relix/redis.rb +24 -0
- data/lib/relix/version.rb +1 -1
- metadata +14 -6
data/lib/relix/core.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Relix
|
2
|
+
def self.included(klass)
|
3
|
+
super
|
4
|
+
klass.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.index_types
|
8
|
+
@index_types ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.register_index(name, index)
|
12
|
+
index_types[name.to_sym] = index
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def relix(&block)
|
17
|
+
@relix ||= IndexSet.new(self)
|
18
|
+
if block_given?
|
19
|
+
@relix.instance_eval(&block)
|
20
|
+
else
|
21
|
+
@relix
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def lookup(&block)
|
26
|
+
relix.lookup(&block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def relix
|
31
|
+
self.class.relix
|
32
|
+
end
|
33
|
+
|
34
|
+
def index!
|
35
|
+
relix.index!(self)
|
36
|
+
end
|
37
|
+
end
|
data/lib/relix/index.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Relix
|
2
|
+
class Index
|
3
|
+
def initialize(name, accessor, options={})
|
4
|
+
@name = "#{self.class.name}:#{name}"
|
5
|
+
@accessor = [accessor].flatten.collect{|a| a.to_s}
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(object)
|
10
|
+
@accessor.inject({}){|h,e| h[e] = object.send(e); h}
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_normalized(object)
|
14
|
+
normalize(read(object))
|
15
|
+
end
|
16
|
+
|
17
|
+
def normalize(value)
|
18
|
+
value_hash = case value
|
19
|
+
when Hash
|
20
|
+
value.inject({}){|h, (k,v)| h[k.to_s] = v; h}
|
21
|
+
else
|
22
|
+
{@accessor.first => value}
|
23
|
+
end
|
24
|
+
@accessor.collect do |k|
|
25
|
+
if value_hash.include?(k)
|
26
|
+
value_hash[k].to_s
|
27
|
+
else
|
28
|
+
raise MissingIndexValueError, "Missing #{k} when looking up by #{@name}"
|
29
|
+
end
|
30
|
+
end.join(":")
|
31
|
+
end
|
32
|
+
|
33
|
+
def watch
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def filter(r, object, value)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def query(r, value)
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def key_for(value)
|
46
|
+
"#{@name}:#{value}"
|
47
|
+
end
|
48
|
+
|
49
|
+
module Ordering
|
50
|
+
def initialize(*args)
|
51
|
+
super
|
52
|
+
@order = @options[:order]
|
53
|
+
end
|
54
|
+
|
55
|
+
def score(object, value)
|
56
|
+
if @order
|
57
|
+
value = object.send(@order)
|
58
|
+
end
|
59
|
+
case value
|
60
|
+
when Numeric
|
61
|
+
value
|
62
|
+
when Time
|
63
|
+
value.to_f
|
64
|
+
else
|
65
|
+
if value.respond_to?(:to_i)
|
66
|
+
value.to_i
|
67
|
+
elsif value.respond_to?(:to_time)
|
68
|
+
value.to_time.to_f
|
69
|
+
elsif @order
|
70
|
+
raise UnorderableValueError.new("Unable to convert #{value} in to a number for ordering.")
|
71
|
+
else
|
72
|
+
0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def range_from_options(options, value=nil)
|
78
|
+
start = (options[:offset] || 0)
|
79
|
+
if f = options[:from]
|
80
|
+
start = (position(f, value) + 1)
|
81
|
+
end
|
82
|
+
stop = (options[:limit] ? (start + options[:limit] - 1) : -1)
|
83
|
+
[start, stop]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class UnorderableValueError < StandardError; end
|
89
|
+
class MissingIndexValueError < StandardError; end
|
90
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Relix
|
2
|
+
class IndexSet
|
3
|
+
def initialize(klass)
|
4
|
+
@klass = klass
|
5
|
+
@indexes = Hash.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def primary_key(accessor)
|
9
|
+
add_index(:primary_key, 'primary_key', on: accessor)
|
10
|
+
end
|
11
|
+
alias pk primary_key
|
12
|
+
|
13
|
+
def method_missing(m, *args)
|
14
|
+
if Relix.index_types.keys.include?(m.to_sym)
|
15
|
+
add_index(m, *args)
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_index(index_type, name, options={})
|
22
|
+
accessor = (options.delete(:on) || name)
|
23
|
+
@indexes[name.to_s] = Relix.index_types[index_type].new(key_prefix(name), accessor, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def indexes
|
27
|
+
(parent ? parent.indexes.merge(@indexes) : @indexes)
|
28
|
+
end
|
29
|
+
|
30
|
+
def lookup(&block)
|
31
|
+
unless primary_key = indexes['primary_key']
|
32
|
+
raise MissingPrimaryKeyError.new("You must declare a primary key for #{@klass.name}")
|
33
|
+
end
|
34
|
+
if block
|
35
|
+
query = Query.new(self)
|
36
|
+
yield(query)
|
37
|
+
query.run
|
38
|
+
else
|
39
|
+
primary_key.all
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def index!(object)
|
44
|
+
unless primary_key_index = indexes['primary_key']
|
45
|
+
raise MissingPrimaryKeyError.new("You must declare a primary key for #{@klass.name}")
|
46
|
+
end
|
47
|
+
pk = primary_key_index.read_normalized(object)
|
48
|
+
current_values_name = "#{key_prefix('current_values')}:#{pk}"
|
49
|
+
|
50
|
+
Relix.redis do |r|
|
51
|
+
loop do
|
52
|
+
r.watch current_values_name
|
53
|
+
current_values = r.hgetall(current_values_name)
|
54
|
+
indexers = []
|
55
|
+
indexes.each do |name,index|
|
56
|
+
((watch = index.watch) && r.watch(*watch))
|
57
|
+
|
58
|
+
value = index.read_normalized(object)
|
59
|
+
old_value = current_values[name]
|
60
|
+
|
61
|
+
next if value == old_value
|
62
|
+
current_values[name] = value
|
63
|
+
|
64
|
+
next unless index.filter(r, object, value)
|
65
|
+
|
66
|
+
query_value = index.query(r, value)
|
67
|
+
indexers << proc do
|
68
|
+
index.index(r, pk, object, value, old_value, *query_value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
r.multi do
|
72
|
+
indexers.each do |indexer|
|
73
|
+
indexer.call
|
74
|
+
end
|
75
|
+
r.hmset(current_values_name, *current_values.flatten)
|
76
|
+
end.each do |result|
|
77
|
+
raise RedisIndexingError.new(result.message) if Exception === result
|
78
|
+
end
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def key_prefix(name)
|
85
|
+
"#{@klass.name}:#{name}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def parent
|
89
|
+
unless @parent || @parent == false
|
90
|
+
parent = @klass.superclass
|
91
|
+
@parent = (parent.respond_to?(:relix) ? parent.relix : false)
|
92
|
+
end
|
93
|
+
@parent
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class MissingPrimaryKeyError < StandardError; end
|
98
|
+
class RedisIndexingError < StandardError; end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Relix
|
2
|
+
class MultiIndex < Index
|
3
|
+
include Ordering
|
4
|
+
|
5
|
+
def index(r, pk, object, value, old_value)
|
6
|
+
r.zadd(key_for(value), score(object, value), pk)
|
7
|
+
r.zrem(key_for(old_value), pk)
|
8
|
+
end
|
9
|
+
|
10
|
+
def eq(value, options={})
|
11
|
+
Relix.redis.zrange(key_for(value), *range_from_options(options, value))
|
12
|
+
end
|
13
|
+
|
14
|
+
def position(pk, value)
|
15
|
+
position = Relix.redis.zrank(key_for(value), pk)
|
16
|
+
raise MissingIndexValueError, "Cannot find key #{pk} in index for #{value}" unless position
|
17
|
+
position
|
18
|
+
end
|
19
|
+
end
|
20
|
+
register_index :multi, MultiIndex
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Relix
|
2
|
+
class PrimaryKeyIndex < Index
|
3
|
+
include Ordering
|
4
|
+
|
5
|
+
def watch
|
6
|
+
@name
|
7
|
+
end
|
8
|
+
|
9
|
+
def filter(r, object, value)
|
10
|
+
!r.zrank(@name, value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def query(r, value)
|
14
|
+
r.zcard(@name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def index(r, pk, object, value, old_value, rank)
|
18
|
+
r.zadd(@name, rank, pk)
|
19
|
+
end
|
20
|
+
|
21
|
+
def all(options={})
|
22
|
+
Relix.redis.zrange(@name, *range_from_options(options))
|
23
|
+
end
|
24
|
+
|
25
|
+
def eq(value, options)
|
26
|
+
[value]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
register_index :primary_key, PrimaryKeyIndex
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Relix
|
2
|
+
class UniqueIndex < Index
|
3
|
+
include Ordering
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@sorted_set_name = "#{@name}:zset"
|
8
|
+
@hash_name = "#{@name}:hash"
|
9
|
+
end
|
10
|
+
|
11
|
+
def watch
|
12
|
+
@hash_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter(r, object, value)
|
16
|
+
return true if read(object).values.any?{|e| e.nil?}
|
17
|
+
if r.hexists(@hash_name, value)
|
18
|
+
raise NotUniqueError.new("'#{value}' is not unique in index #{@name}")
|
19
|
+
end
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def index(r, pk, object, value, old_value)
|
24
|
+
if read(object).values.all?{|e| !e.nil?}
|
25
|
+
r.hset(@hash_name, value, pk)
|
26
|
+
r.zadd(@sorted_set_name, score(object, value), pk)
|
27
|
+
else
|
28
|
+
r.hdel(@hash_name, value)
|
29
|
+
r.zrem(@sorted_set_name, pk)
|
30
|
+
end
|
31
|
+
r.hdel(@hash_name, old_value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def all(options={})
|
35
|
+
Relix.redis.zrange(@sorted_set_name, *range_from_options(options))
|
36
|
+
end
|
37
|
+
|
38
|
+
def eq(value, options={})
|
39
|
+
[Relix.redis.hget(@hash_name, value)].compact
|
40
|
+
end
|
41
|
+
end
|
42
|
+
register_index :unique, UniqueIndex
|
43
|
+
|
44
|
+
class NotUniqueError < StandardError; end
|
45
|
+
end
|
data/lib/relix/query.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Relix
|
2
|
+
class Query
|
3
|
+
def initialize(model)
|
4
|
+
@model = model
|
5
|
+
@offset = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](index_name)
|
9
|
+
index = @model.indexes[index_name.to_s]
|
10
|
+
raise MissingIndexError.new("No index declared for #{index_name}") unless index
|
11
|
+
@clause = Clause.new(index)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
if @clause
|
16
|
+
@clause.lookup
|
17
|
+
else
|
18
|
+
@model.indexes['primary_key'].lookup
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Clause
|
23
|
+
def initialize(index)
|
24
|
+
@index = index
|
25
|
+
@options = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def eq(value, options={})
|
29
|
+
@value = @index.normalize(value)
|
30
|
+
@options = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def all(options={})
|
34
|
+
@all = true
|
35
|
+
@options = options
|
36
|
+
end
|
37
|
+
|
38
|
+
def lookup
|
39
|
+
if @options[:limit] == 0
|
40
|
+
[]
|
41
|
+
elsif @all
|
42
|
+
@index.all(@options)
|
43
|
+
else
|
44
|
+
@index.eq(@value, @options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class MissingIndexError < StandardError; end
|
51
|
+
end
|
data/lib/relix/redis.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'hiredis'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
module Relix
|
5
|
+
def self.redis
|
6
|
+
unless @redis
|
7
|
+
@redis = ::Redis.new(port: @redis_port)
|
8
|
+
@redis.select @redis_db if @redis_db
|
9
|
+
end
|
10
|
+
if block_given?
|
11
|
+
yield(@redis)
|
12
|
+
else
|
13
|
+
@redis
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.port=(value)
|
18
|
+
@redis_port = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.db=(value)
|
22
|
+
@redis_db = value
|
23
|
+
end
|
24
|
+
end
|
data/lib/relix/version.rb
CHANGED
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.0.
|
4
|
+
version: 1.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,18 +13,18 @@ date: 2011-11-04 00:00:00.000000000Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hiredis
|
16
|
-
requirement: &
|
16
|
+
requirement: &70135708785180 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
21
|
+
version: 0.4.1
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70135708785180
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: redis
|
27
|
-
requirement: &
|
27
|
+
requirement: &70135708783420 !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: *
|
35
|
+
version_requirements: *70135708783420
|
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
|
@@ -46,6 +46,14 @@ extra_rdoc_files: []
|
|
46
46
|
files:
|
47
47
|
- README.md
|
48
48
|
- lib/relix.rb
|
49
|
+
- lib/relix/core.rb
|
50
|
+
- lib/relix/index.rb
|
51
|
+
- lib/relix/indexes/multi.rb
|
52
|
+
- lib/relix/indexes/primary_key.rb
|
53
|
+
- lib/relix/indexes/unique.rb
|
54
|
+
- lib/relix/index_set.rb
|
55
|
+
- lib/relix/query.rb
|
56
|
+
- lib/relix/redis.rb
|
49
57
|
- lib/relix/version.rb
|
50
58
|
homepage: http://github.com/ntalbott/relix
|
51
59
|
licenses: []
|