hyperion-riak 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require 'hyperion/riak/datastore'
2
+
3
+ module Hyperion
4
+ module Riak
5
+ def self.new(opts={})
6
+ Datastore.new(opts)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,138 @@
1
+ require 'hyperion'
2
+ require 'hyperion/key'
3
+ require 'hyperion/riak/map_reduce_js'
4
+ require 'riak'
5
+ require 'hyperion/riak/optimized_filter_order'
6
+
7
+ module Hyperion
8
+ module Riak
9
+ class Datastore
10
+ def initialize(opts={})
11
+ opts ||= {}
12
+ @app = opts[:app] || ''
13
+ @client = ::Riak::Client.new(opts.reject {|k, v| k == :app})
14
+ @buckets = {}
15
+ end
16
+
17
+ def save(records)
18
+ records.map do |record|
19
+ Hyperion.new?(record) ? create(record) : update(record)
20
+ end
21
+ end
22
+
23
+ def find_by_key(key)
24
+ kind, riak_key = Hyperion::Key.decompose_key(key)
25
+ robject = bucket(kind).get(riak_key)
26
+ record_from_db(kind, robject.key, robject.data)
27
+ end
28
+
29
+ def find(query)
30
+ mr = new_mapreduce_with_returned_result(query)
31
+ mr.run.map do |record|
32
+ record_from_db(query.kind, record.delete('riak_key'), record)
33
+ end
34
+ end
35
+
36
+ def delete_by_key(key)
37
+ kind, riak_key = Hyperion::Key.decompose_key(key)
38
+ delete_with_riak_key(kind, riak_key)
39
+ nil
40
+ end
41
+
42
+ def delete(query)
43
+ mr = new_mapreduce_with_returned_result(query)
44
+ mr.run.each do |record|
45
+ delete_with_riak_key(query.kind, record['riak_key'])
46
+ end
47
+ nil
48
+ end
49
+
50
+ def count(query)
51
+ mr = new_mapreduce(query)
52
+ mr.reduce(MapReduceJs.count, :keep => true)
53
+ mr.run.first
54
+ end
55
+
56
+ private
57
+
58
+ def create(record)
59
+ kind = record[:kind]
60
+ robject = bucket(kind).new
61
+ store(kind, robject, record_to_db(record))
62
+ end
63
+
64
+ def update(record)
65
+ kind, riak_key = Hyperion::Key.decompose_key(record[:key])
66
+ robject = bucket(kind).get(riak_key)
67
+ store(kind, robject, robject.data.merge(record_to_db(record)))
68
+ end
69
+
70
+ def store(kind, robject, record_data)
71
+ robject.data = record_data
72
+ robject.indexes = record_data_to_index(record_data)
73
+ robject.store
74
+ record_from_db(kind, robject.key, robject.data)
75
+ end
76
+
77
+ def record_data_to_index(data)
78
+ data.reduce({}) do |new_record, (key, value)|
79
+ new_record[index_name(key)] = value
80
+ new_record
81
+ end
82
+ end
83
+
84
+ def delete_with_riak_key(kind, key)
85
+ bucket(kind).delete(key)
86
+ end
87
+
88
+ def new_mapreduce(query)
89
+ mr = ::Riak::MapReduce.new(@client)
90
+ add_query_filters(mr, query)
91
+ sorts = query.sorts
92
+ mr.reduce(MapReduceJs.sort(sorts)) unless sorts.empty?
93
+ mr.reduce(MapReduceJs.offset(query.offset)) if query.offset
94
+ mr.reduce(MapReduceJs.limit(query.limit)) if query.limit
95
+ mr
96
+ end
97
+
98
+ def add_query_filters(mr, query)
99
+ bucket_name = bucket_name(query.kind)
100
+ optimizer = OptimizedFilterOrder.new(query.filters, bucket_name)
101
+ field_index = index_name(optimizer.optimal_index_field)
102
+ mr.index(bucket_name, field_index, optimizer.optimal_index_value)
103
+ mr.map(MapReduceJs.filter(optimizer.filters))
104
+ end
105
+
106
+ def index_name(field_name)
107
+ if field_name != '$bucket'
108
+ "#{field_name}_bin"
109
+ else
110
+ field_name
111
+ end
112
+ end
113
+
114
+ def new_mapreduce_with_returned_result(query)
115
+ mr = new_mapreduce(query)
116
+ mr.reduce(MapReduceJs.pass_thru, :keep => true)
117
+ end
118
+
119
+ def record_to_db(record)
120
+ record.reject {|k, v| [:kind, :key].include?(k)}
121
+ end
122
+
123
+ def record_from_db(kind, riak_key, data)
124
+ key = Hyperion::Key.compose_key(kind, riak_key)
125
+ data.merge(:kind => kind, :key => key)
126
+ end
127
+
128
+ def bucket(kind)
129
+ name = bucket_name(kind)
130
+ @buckets[name] ||= @client.bucket(name)
131
+ end
132
+
133
+ def bucket_name(kind)
134
+ @app.to_s + kind
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,53 @@
1
+ require 'erb'
2
+
3
+ module Hyperion
4
+ module Riak
5
+ class MapReduceJs
6
+ class << self
7
+
8
+ def filter(filters)
9
+ template(:filter).result(binding)
10
+ end
11
+
12
+ def sort(sorts)
13
+ template(:sort).result(binding)
14
+ end
15
+
16
+ def offset(offset)
17
+ template(:offset).result(binding)
18
+ end
19
+
20
+ def offset(offset)
21
+ template(:offset).result(binding)
22
+ end
23
+
24
+ def limit(limit)
25
+ template(:limit).result(binding)
26
+ end
27
+
28
+ def count
29
+ template(:count).result
30
+ end
31
+
32
+ def pass_thru
33
+ template(:pass_thru).result
34
+ end
35
+
36
+ private
37
+
38
+ def template(name)
39
+ @templates ||= {}
40
+ @templates[name] ||= ERB.new(file_contents(template_path(name)))
41
+ end
42
+
43
+ def template_path(name)
44
+ File.expand_path(File.join('..', 'map_reduce', "#{name}.js.erb"), __FILE__)
45
+ end
46
+
47
+ def file_contents(path)
48
+ File.open(path, 'rb') { |f| f.read }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,111 @@
1
+ require 'hyperion/riak/optimized_range_filters'
2
+
3
+ module Hyperion
4
+ module Riak
5
+ class OptimizedFilterOrder
6
+
7
+ def initialize(filters, bucket_name)
8
+ @bucket_name = bucket_name
9
+ @filters = filters
10
+ end
11
+
12
+ def optimal_strategy
13
+ @optimal_strategy ||= OPTIMAL_ORDER.map do |strategy_klass|
14
+ strategy_klass.new(@filters, @bucket_name)
15
+ end.find do |strategy|
16
+ strategy.can_optimize?
17
+ end
18
+ end
19
+
20
+ def optimal_index_field
21
+ optimal_strategy.optimal_index_field
22
+ end
23
+
24
+ def optimal_index_value
25
+ optimal_strategy.optimal_index_value
26
+ end
27
+
28
+ def filters
29
+ optimal_strategy.filters
30
+ end
31
+
32
+ private
33
+ end
34
+
35
+ class EqualsStrategy
36
+ def initialize(filters, bucket_name)
37
+ @filters = filters
38
+ end
39
+
40
+ def can_optimize?
41
+ !first_equals_filter.nil?
42
+ end
43
+
44
+ def optimal_index_field
45
+ first_equals_filter.field
46
+ end
47
+
48
+ def optimal_index_value
49
+ first_equals_filter.value.to_s
50
+ end
51
+
52
+ def filters
53
+ @remaining_filters ||= (@filters - [first_equals_filter])
54
+ end
55
+
56
+ private
57
+
58
+ def first_equals_filter
59
+ @first_equals_filter ||= @filters.find { |f| f.operator == "=" }
60
+ end
61
+ end
62
+
63
+ class RangeStrategy
64
+ def initialize(filters, bucket_name)
65
+ @filters = filters
66
+ @optimizer = OptimizedRangeFilters.new(filters)
67
+ end
68
+
69
+ def can_optimize?
70
+ @optimizer.less_than_filter && @optimizer.greater_than_filter
71
+ end
72
+
73
+ def optimal_index_field
74
+ @optimizer.less_than_filter.field
75
+ end
76
+
77
+ def optimal_index_value
78
+ @value ||= @optimizer.less_than_filter.value.to_s .. @optimizer.greater_than_filter.value.to_s
79
+ end
80
+
81
+ def filters
82
+ @optimizer.remaining_filters
83
+ end
84
+ end
85
+
86
+ class BucketStrategy
87
+ def initialize(filters, bucket_name)
88
+ @filters = filters
89
+ @bucket_name = bucket_name
90
+ end
91
+
92
+ def can_optimize?
93
+ true
94
+ end
95
+
96
+ def optimal_index_field
97
+ '$bucket'
98
+ end
99
+
100
+ def optimal_index_value
101
+ @bucket_name
102
+ end
103
+
104
+ def filters
105
+ @filters
106
+ end
107
+ end
108
+
109
+ OPTIMAL_ORDER = [EqualsStrategy, RangeStrategy, BucketStrategy]
110
+ end
111
+ end
@@ -0,0 +1,42 @@
1
+ module Hyperion
2
+ module Riak
3
+ class OptimizedRangeFilters
4
+ def initialize(filters)
5
+ @filters = filters
6
+ end
7
+
8
+ def remaining_filters
9
+ @remaining_filters ||= @filters - [less_than_filter, greater_than_filter]
10
+ end
11
+
12
+ def less_than_filter
13
+ @less_than_filter ||= find_first_match(less_than_candidates, greater_than_candidates)
14
+ end
15
+
16
+ def greater_than_filter
17
+ @greater_than_filter ||= find_first_match(greater_than_candidates, less_than_candidates)
18
+ end
19
+
20
+ private
21
+
22
+ def find_first_match(filters_to_search, filters_to_match_against)
23
+ filters_to_search.find do |f1|
24
+ filters_to_match_against.any? { |f2| f1.field == f2.field }
25
+ end
26
+ end
27
+
28
+ def greater_than_candidates
29
+ @greater_than_filters ||= @filters.select do |filter|
30
+ filter.operator == '>'
31
+ end
32
+ end
33
+
34
+ def less_than_candidates
35
+ @less_than_candidates ||= @filters.select do |filter|
36
+ filter.operator == '<'
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,26 @@
1
+ def test_app_name
2
+ '_HTEST_'
3
+ end
4
+
5
+ BUCKETS = ['testing', 'other_testing']
6
+
7
+ def empty_buckets(ds)
8
+ client = ds.instance_variable_get(:@client)
9
+ BUCKETS.each do |bucket_name|
10
+ bucket_name = ds.send(:bucket_name, bucket_name)
11
+ bucket = client.bucket(bucket_name)
12
+ bucket.get_index('$bucket', bucket_name).each do |key|
13
+ bucket.delete(key)
14
+ end
15
+ end
16
+ end
17
+
18
+ def with_testable_riak_datastore
19
+ ds = Hyperion.new_datastore(:riak, :app => test_app_name, :protocol => :pbc)
20
+ around :each do |example|
21
+ Hyperion.datastore = ds
22
+ example.run
23
+ empty_buckets(ds)
24
+ Hyperion.datastore = nil
25
+ end
26
+ end
@@ -0,0 +1,122 @@
1
+ require 'hyperion/filter'
2
+ require 'hyperion/riak/optimized_filter_order'
3
+
4
+ describe Hyperion::Riak::OptimizedFilterOrder do
5
+
6
+ def filter(field, operator, value)
7
+ Hyperion::Filter.new(field, operator, value)
8
+ end
9
+
10
+ context "no filters" do
11
+ it 'returns "$bucket" for optimal_index_field' do
12
+ filters = []
13
+ bucket_name = "hamburgers"
14
+
15
+ o = described_class.new(filters, bucket_name)
16
+ o.optimal_index_field.should == '$bucket'
17
+ end
18
+
19
+ it 'returns bucket name for optimal_index_value' do
20
+ filters = []
21
+ bucket_name = "hamburgers"
22
+
23
+ o = described_class.new(filters, bucket_name)
24
+ o.optimal_index_value.should == bucket_name
25
+ end
26
+
27
+ it 'returns an empty collection for filters' do
28
+ filters = []
29
+ bucket_name = "hamburgers"
30
+
31
+ o = described_class.new(filters, bucket_name)
32
+ o.filters.should == []
33
+ end
34
+ end
35
+
36
+ context 'optimizes for an equals filter' do
37
+ it 'returns the index field' do
38
+ filters = [filter(:int, '=', 1)]
39
+ o = described_class.new(filters, '')
40
+ o.optimal_index_field.should == :int
41
+ end
42
+
43
+ it 'returns bucket name for optimal_index_value' do
44
+ filters = [filter(:int, '=', 1)]
45
+ bucket_name = "hamburgers"
46
+
47
+ o = described_class.new(filters, bucket_name)
48
+ o.optimal_index_value.should == '1'
49
+ end
50
+
51
+ it 'excludes the optimized filter' do
52
+ filters = [filter(:int, '=', 1)]
53
+ o = described_class.new(filters, '')
54
+ o.filters.should be_empty
55
+ end
56
+ end
57
+
58
+ context 'optimizes for a range filter' do
59
+ it 'returns the index field' do
60
+ filters = [filter(:int, '<', 1), filter(:int, '>', 2), filter(:int, '?', 3)]
61
+ o = described_class.new(filters, '')
62
+ o.optimal_index_field.should == :int
63
+ end
64
+
65
+ it 'returns bucket name for optimal_index_value' do
66
+ filters = [filter(:int, '<', 1), filter(:int, '>', 2), filter(:int, '?', 3)]
67
+
68
+ o = described_class.new(filters, '')
69
+ o.optimal_index_value.should == ('1'..'2')
70
+ end
71
+
72
+ it 'excludes the optimized filters' do
73
+ leftover_filter = filter(:int, '?', 3)
74
+ filters = [filter(:int, '<', 1), filter(:int, '>', 2), leftover_filter]
75
+ o = described_class.new(filters, '')
76
+ o.filters.should == [leftover_filter]
77
+ end
78
+ end
79
+
80
+ context 'cannot be optimized' do
81
+ it 'returns "$bucket" for index_field if no optimal filter' do
82
+ filters = [filter(:int, '?', 1)]
83
+ o = described_class.new(filters, '')
84
+ o.optimal_index_field.should == '$bucket'
85
+ end
86
+
87
+ it 'returns bucket name for optimal_index_value' do
88
+ filters = [filter(:int, '?', 1)]
89
+ o = described_class.new(filters, 'cheeseburgers')
90
+ o.optimal_index_value.should == 'cheeseburgers'
91
+ end
92
+
93
+ it 'filters contains all filters' do
94
+ filters = [filter(:int, '?', 1)]
95
+ o = described_class.new(filters, '')
96
+ o.filters.should == filters
97
+ end
98
+ end
99
+
100
+ it 'chooses equals filter over bucket' do
101
+ equal_filter = filter(:data, '=', 3)
102
+ o = described_class.new([equal_filter], '')
103
+ o.optimal_index_field.should == :data
104
+ o.optimal_index_value.should == '3'
105
+ o.filters.should == []
106
+ end
107
+
108
+ it 'chooses equals filter over range filter' do
109
+ equal_filter = filter(:data, '=', 3)
110
+ filters = [filter(:int, '<', 1), filter(:int, '>', 2)]
111
+ o = described_class.new(filters + [equal_filter], '')
112
+ o.optimal_index_field.should == :data
113
+ o.optimal_index_value.should == '3'
114
+ o.filters.should == filters
115
+ end
116
+
117
+ it 'chooses range filter over bucket filter' do
118
+ filters = [filter(:int, '<', 1), filter(:int, '>', 2)]
119
+ o = described_class.new(filters, '')
120
+ o.optimal_index_field.should == :int
121
+ end
122
+ end
@@ -0,0 +1,87 @@
1
+ require 'hyperion/filter'
2
+ require 'hyperion/riak/optimized_range_filters'
3
+
4
+ describe Hyperion::Riak::OptimizedRangeFilters do
5
+ let(:test_less_than_filter) { filter(:test, '<', 1) }
6
+ let(:test_greater_than_filter) { filter(:test, '>', 1) }
7
+ let(:test_equals_filter) { filter(:test, '=', 1) }
8
+ let(:other_test_less_than_filter) { filter(:other_test, '<', 1) }
9
+ let(:other_test_greater_than_filter) { filter(:other_test, '>', 1) }
10
+
11
+ it 'returns the filters' do
12
+ optimizer = described_class.new([])
13
+ optimizer.remaining_filters.should == []
14
+ end
15
+
16
+ def filter(field, operator, value)
17
+ Hyperion::Filter.new(field, operator, value)
18
+ end
19
+
20
+ it 'returns the optimal filters' do
21
+ optimizer = described_class.new([test_less_than_filter, test_greater_than_filter])
22
+ optimizer.less_than_filter.should == test_less_than_filter
23
+ optimizer.greater_than_filter.should == test_greater_than_filter
24
+ optimizer.remaining_filters.should == []
25
+ end
26
+
27
+ it 'returns the optimal filters' do
28
+ optimizer = described_class.new([test_greater_than_filter, test_less_than_filter])
29
+ optimizer.less_than_filter.should == test_less_than_filter
30
+ optimizer.greater_than_filter.should == test_greater_than_filter
31
+ optimizer.remaining_filters.should == []
32
+ end
33
+
34
+ it 'returns non-range filter as remaining filter' do
35
+ optimizer = described_class.new([
36
+ test_greater_than_filter,
37
+ test_less_than_filter,
38
+ test_equals_filter
39
+ ])
40
+ optimizer.remaining_filters.should == [test_equals_filter]
41
+ end
42
+
43
+ it 'does not return a less than filter if there is no greater than' do
44
+ filters = [test_less_than_filter, test_equals_filter]
45
+ optimizer = described_class.new(filters)
46
+ optimizer.remaining_filters.should == filters
47
+ optimizer.less_than_filter.should be_nil
48
+ end
49
+
50
+ it 'does not return a less than filter if there is no matching greater than' do
51
+ filters = [
52
+ filter(:test, '<', 1),
53
+ filter(:other_test, '>', 1),
54
+ test_equals_filter
55
+ ]
56
+ optimizer = described_class.new(filters)
57
+ optimizer.remaining_filters.should == filters
58
+ optimizer.less_than_filter.should be_nil
59
+ optimizer.greater_than_filter.should be_nil
60
+ end
61
+
62
+ it 'returns a matching range when there are other candidates' do
63
+ filters = [
64
+ test_less_than_filter,
65
+ other_test_less_than_filter,
66
+ other_test_greater_than_filter,
67
+ test_equals_filter
68
+ ]
69
+ optimizer = described_class.new(filters)
70
+ optimizer.remaining_filters.should == [test_less_than_filter, test_equals_filter]
71
+ optimizer.less_than_filter.should == other_test_less_than_filter
72
+ optimizer.greater_than_filter.should == other_test_greater_than_filter
73
+ end
74
+
75
+ it 'returns a matching range when there are other candidates' do
76
+ filters = [
77
+ test_greater_than_filter,
78
+ other_test_greater_than_filter,
79
+ test_less_than_filter,
80
+ test_equals_filter
81
+ ]
82
+ optimizer = described_class.new(filters)
83
+ optimizer.remaining_filters.should == [other_test_greater_than_filter, test_equals_filter]
84
+ optimizer.less_than_filter.should == test_less_than_filter
85
+ optimizer.greater_than_filter.should == test_greater_than_filter
86
+ end
87
+ end
@@ -0,0 +1,12 @@
1
+ require 'hyperion/dev/ds_spec'
2
+ require 'hyperion/riak'
3
+ require 'hyperion/riak/spec_helper'
4
+
5
+ describe Hyperion::Riak do
6
+
7
+ context 'live' do
8
+ with_testable_riak_datastore
9
+
10
+ include_examples 'Datastore'
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperion-riak
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Myles Megyesi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.11.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: hyperion-api
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.1.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.1.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: riak-client
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.4
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.4
62
+ description: Riak datastore for Hyperion
63
+ email:
64
+ - myles@8thlight.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - lib/hyperion/riak.rb
70
+ - lib/hyperion/riak/map_reduce_js.rb
71
+ - lib/hyperion/riak/optimized_filter_order.rb
72
+ - lib/hyperion/riak/optimized_range_filters.rb
73
+ - lib/hyperion/riak/spec_helper.rb
74
+ - lib/hyperion/riak/datastore.rb
75
+ - spec/hyperion/riak_spec.rb
76
+ - spec/hyperion/riak/optimized_filter_order_spec.rb
77
+ - spec/hyperion/riak/optimized_range_filters_spec.rb
78
+ homepage: https://github.com/mylesmegyesi/hyperion-ruby
79
+ licenses:
80
+ - Eclipse Public License
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: 1.8.7
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 1.8.24
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Riak datastore for Hypeiron
103
+ test_files:
104
+ - spec/hyperion/riak_spec.rb
105
+ - spec/hyperion/riak/optimized_filter_order_spec.rb
106
+ - spec/hyperion/riak/optimized_range_filters_spec.rb