relix 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Relix
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
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.0.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: &70323249027760 !ruby/object:Gem::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.3.2
21
+ version: 0.4.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70323249027760
24
+ version_requirements: *70135708785180
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis
27
- requirement: &70323249027180 !ruby/object:Gem::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: *70323249027180
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: []