hyperion-riak 0.1.0

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.
@@ -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