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 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: []