plucky 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.bundle/config +4 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +2 -2
  4. data/Gemfile +13 -3
  5. data/Guardfile +13 -0
  6. data/README.md +1 -1
  7. data/Rakefile +3 -17
  8. data/examples/query.rb +1 -1
  9. data/lib/plucky.rb +13 -0
  10. data/lib/plucky/criteria_hash.rb +85 -56
  11. data/lib/plucky/normalizers/criteria_hash_key.rb +17 -0
  12. data/lib/plucky/normalizers/criteria_hash_value.rb +83 -0
  13. data/lib/plucky/normalizers/fields_value.rb +26 -0
  14. data/lib/plucky/normalizers/integer.rb +19 -0
  15. data/lib/plucky/normalizers/options_hash_key.rb +23 -0
  16. data/lib/plucky/normalizers/options_hash_value.rb +85 -0
  17. data/lib/plucky/normalizers/sort_value.rb +55 -0
  18. data/lib/plucky/options_hash.rb +56 -85
  19. data/lib/plucky/pagination/decorator.rb +3 -2
  20. data/lib/plucky/pagination/paginator.rb +15 -6
  21. data/lib/plucky/query.rb +93 -51
  22. data/lib/plucky/version.rb +1 -1
  23. data/script/criteria_hash.rb +21 -0
  24. data/{test → spec}/helper.rb +12 -8
  25. data/spec/plucky/criteria_hash_spec.rb +166 -0
  26. data/spec/plucky/normalizers/criteria_hash_key_spec.rb +37 -0
  27. data/spec/plucky/normalizers/criteria_hash_value_spec.rb +193 -0
  28. data/spec/plucky/normalizers/fields_value_spec.rb +45 -0
  29. data/spec/plucky/normalizers/integer_spec.rb +24 -0
  30. data/spec/plucky/normalizers/options_hash_key_spec.rb +23 -0
  31. data/spec/plucky/normalizers/options_hash_value_spec.rb +99 -0
  32. data/spec/plucky/normalizers/sort_value_spec.rb +94 -0
  33. data/spec/plucky/options_hash_spec.rb +64 -0
  34. data/{test/plucky/pagination/test_decorator.rb → spec/plucky/pagination/decorator_spec.rb} +8 -10
  35. data/spec/plucky/pagination/paginator_spec.rb +118 -0
  36. data/spec/plucky/query_spec.rb +839 -0
  37. data/spec/plucky_spec.rb +68 -0
  38. data/{test/test_symbol_operator.rb → spec/symbol_operator_spec.rb} +14 -16
  39. data/spec/symbol_spec.rb +9 -0
  40. metadata +58 -23
  41. data/test/plucky/pagination/test_paginator.rb +0 -120
  42. data/test/plucky/test_criteria_hash.rb +0 -359
  43. data/test/plucky/test_options_hash.rb +0 -302
  44. data/test/plucky/test_query.rb +0 -843
  45. data/test/test_plucky.rb +0 -48
  46. data/test/test_symbol.rb +0 -11
@@ -1,36 +1,43 @@
1
1
  # encoding: UTF-8
2
+ require 'set'
2
3
  require 'forwardable'
4
+
3
5
  module Plucky
4
6
  class Query
5
7
  include Enumerable
6
8
  extend Forwardable
7
9
 
8
- OptionKeys = [
10
+ # Private
11
+ OptionKeys = Set[
9
12
  :select, :offset, :order, # MM
10
13
  :fields, :skip, :limit, :sort, :hint, :snapshot, # Ruby Driver
11
- :batch_size, :timeout, :transformer, # Ruby Driver
12
- :max_scan, :show_disk_loc, :return_key, # Ruby Driver
14
+ :batch_size, :timeout, :max_scan, :return_key, # Ruby Driver
15
+ :transformer, :show_disk_loc, :comment, :read, # Ruby Driver
16
+ :tag_sets, :acceptable_latency, # Ruby Driver
13
17
  ]
14
18
 
15
19
  attr_reader :criteria, :options, :collection
16
- def_delegator :criteria, :simple?
17
- def_delegator :options, :fields?
20
+
21
+ def_delegator :@criteria, :simple?
22
+ def_delegator :@options, :fields?
18
23
  def_delegators :to_a, :include?
19
24
 
20
- def initialize(collection, opts={})
25
+ # Public
26
+ def initialize(collection, query_options = {})
21
27
  @collection, @options, @criteria = collection, OptionsHash.new, CriteriaHash.new
22
- opts.each { |key, value| self[key] = value }
28
+ query_options.each { |key, value| self[key] = value }
23
29
  end
24
30
 
25
- def initialize_copy(source)
31
+ def initialize_copy(original)
26
32
  super
27
33
  @criteria = @criteria.dup
28
34
  @options = @options.dup
29
35
  end
30
36
 
37
+ # Public
31
38
  def object_ids(*keys)
32
- return criteria.object_ids if keys.empty?
33
- criteria.object_ids = *keys
39
+ return @criteria.object_ids if keys.empty?
40
+ @criteria.object_ids = *keys
34
41
  self
35
42
  end
36
43
 
@@ -48,30 +55,39 @@ module Plucky
48
55
  limit = opts.delete(:per_page) || per_page
49
56
  query = clone.amend(opts)
50
57
  paginator = Pagination::Paginator.new(query.count, page, limit)
51
- query.amend(:limit => paginator.limit, :skip => paginator.skip).all.tap do |docs|
52
- docs.extend(Pagination::Decorator)
53
- docs.paginator(paginator)
54
- end
58
+ docs = query.amend({
59
+ :limit => paginator.limit,
60
+ :skip => paginator.skip,
61
+ }).all
62
+
63
+ docs.extend(Pagination::Decorator)
64
+ docs.paginator(paginator)
65
+ docs
55
66
  end
56
67
 
57
68
  def find_each(opts={}, &block)
58
69
  query = clone.amend(opts)
59
- cursor = query.collection.find(query.criteria.to_hash, query.options.to_hash)
70
+ cursor = query.cursor
71
+
60
72
  if block_given?
61
73
  cursor.each { |doc| yield doc }
62
74
  cursor.rewind!
63
75
  end
76
+
64
77
  cursor
65
78
  end
66
79
 
67
80
  def find_one(opts={})
68
81
  query = clone.amend(opts)
69
- query.collection.find_one(query.criteria.to_hash, query.options.to_hash)
82
+ query.collection.find_one(query.criteria_hash, query.options_hash)
70
83
  end
71
84
 
72
85
  def find(*ids)
73
86
  return nil if ids.empty?
74
- if ids.size == 1 && !ids[0].is_a?(Array)
87
+
88
+ single_id_find = ids.size == 1 && !ids[0].is_a?(Array)
89
+
90
+ if single_id_find
75
91
  first(:_id => ids[0])
76
92
  else
77
93
  all(:_id => ids.flatten)
@@ -96,12 +112,12 @@ module Plucky
96
112
 
97
113
  def remove(opts={}, driver_opts={})
98
114
  query = clone.amend(opts)
99
- query.collection.remove(query.criteria.to_hash, driver_opts)
115
+ query.collection.remove(query.criteria_hash, driver_opts)
100
116
  end
101
117
 
102
118
  def count(opts={})
103
119
  query = clone.amend(opts)
104
- cursor = query.collection.find(query.criteria.to_hash, query.options.to_hash)
120
+ cursor = query.cursor
105
121
  cursor.count
106
122
  end
107
123
 
@@ -111,7 +127,7 @@ module Plucky
111
127
 
112
128
  def distinct(key, opts = {})
113
129
  query = clone.amend(opts)
114
- query.collection.distinct(key, query.criteria.to_hash)
130
+ query.collection.distinct(key, query.criteria_hash)
115
131
  end
116
132
 
117
133
  def fields(*args)
@@ -119,11 +135,11 @@ module Plucky
119
135
  end
120
136
 
121
137
  def ignore(*args)
122
- set_fields(args, 0)
138
+ set_field_inclusion(args, 0)
123
139
  end
124
140
 
125
141
  def only(*args)
126
- set_fields(args, 1)
142
+ set_field_inclusion(args, 1)
127
143
  end
128
144
 
129
145
  def limit(count=nil)
@@ -132,9 +148,10 @@ module Plucky
132
148
 
133
149
  def reverse
134
150
  clone.tap do |query|
135
- query[:sort] = query[:sort].map do |s|
136
- [s[0], -s[1]]
137
- end unless query.options[:sort].nil?
151
+ sort = query[:sort]
152
+ unless sort.nil?
153
+ query.options[:sort] = sort.map { |s| [s[0], -s[1]] }
154
+ end
138
155
  end
139
156
  end
140
157
 
@@ -149,9 +166,7 @@ module Plucky
149
166
  alias order sort
150
167
 
151
168
  def where(hash={})
152
- clone.tap do |query|
153
- query.criteria.merge!(CriteriaHash.new(hash))
154
- end
169
+ clone.tap { |query| query.criteria.merge!(CriteriaHash.new(hash)) }
155
170
  end
156
171
  alias filter where
157
172
 
@@ -159,8 +174,8 @@ module Plucky
159
174
  count.zero?
160
175
  end
161
176
 
162
- def exists?(options={})
163
- !count(options).zero?
177
+ def exists?(query_options={})
178
+ !count(query_options).zero?
164
179
  end
165
180
  alias :exist? :exists?
166
181
 
@@ -172,7 +187,7 @@ module Plucky
172
187
 
173
188
  def update(document, driver_opts={})
174
189
  query = clone
175
- query.collection.update(query.criteria.to_hash, document, driver_opts)
190
+ query.collection.update(query.criteria_hash, document, driver_opts)
176
191
  end
177
192
 
178
193
  def amend(opts={})
@@ -181,35 +196,29 @@ module Plucky
181
196
  end
182
197
 
183
198
  def [](key)
184
- key = key.to_sym if key.respond_to?(:to_sym)
185
- if OptionKeys.include?(key)
186
- @options[key]
187
- else
188
- @criteria[key]
189
- end
199
+ key = symbolized_key(key)
200
+ source = hash_for_key(key)
201
+ source[key]
190
202
  end
191
203
 
192
204
  def []=(key, value)
193
- key = key.to_sym if key.respond_to?(:to_sym)
194
- if OptionKeys.include?(key)
195
- @options[key] = value
196
- else
197
- @criteria[key] = value
198
- end
205
+ key = symbolized_key(key)
206
+ source = hash_for_key(key)
207
+ source[key] = value
199
208
  end
200
209
 
201
210
  def merge(other)
202
- merged_criteria = criteria.merge(other.criteria).to_hash
203
- merged_options = options.merge(other.options).to_hash
211
+ merged_criteria = @criteria.merge(other.criteria).to_hash
212
+ merged_options = @options.merge(other.options).to_hash
204
213
  clone.amend(merged_criteria).amend(merged_options)
205
214
  end
206
215
 
207
216
  def to_hash
208
- criteria.to_hash.merge(options.to_hash)
217
+ criteria_hash.merge(options_hash)
209
218
  end
210
219
 
211
220
  def explain
212
- collection.find(criteria.to_hash, options.to_hash).explain
221
+ @collection.find(criteria_hash, options_hash).explain
213
222
  end
214
223
 
215
224
  def inspect
@@ -219,11 +228,44 @@ module Plucky
219
228
  "#<#{self.class}#{as_nice_string}>"
220
229
  end
221
230
 
231
+ def criteria_hash
232
+ @criteria.to_hash
233
+ end
234
+
235
+ def options_hash
236
+ @options.to_hash
237
+ end
238
+
239
+ def cursor
240
+ @collection.find(criteria_hash, options_hash)
241
+ end
242
+
222
243
  private
223
- def set_fields(field_list, value)
224
- the_fields = {}
225
- field_list.each {|field| the_fields[field.to_sym] = value}
226
- clone.tap { |query| query.options[:fields] = the_fields}
244
+
245
+ # Private
246
+ def hash_for_key(key)
247
+ options_key?(key) ? @options : @criteria
248
+ end
249
+
250
+ # Private
251
+ def symbolized_key(key)
252
+ if key.respond_to?(:to_sym)
253
+ key.to_sym
254
+ else
255
+ key
256
+ end
257
+ end
258
+
259
+ # Private
260
+ def options_key?(key)
261
+ OptionKeys.include?(key)
262
+ end
263
+
264
+ # Private
265
+ def set_field_inclusion(fields, value)
266
+ fields_option = {}
267
+ fields.each { |field| fields_option[symbolized_key(field)] = value }
268
+ clone.tap { |query| query.options[:fields] = fields_option }
227
269
  end
228
270
  end
229
271
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: UTF-8
2
2
  module Plucky
3
- Version = '0.5.2'
3
+ Version = '0.6.0'
4
4
  end
@@ -0,0 +1,21 @@
1
+ require 'pp'
2
+ require 'pathname'
3
+ require 'benchmark'
4
+ require 'rubygems'
5
+ require 'bundler'
6
+
7
+ Bundler.require :default, :performance
8
+
9
+ root_path = Pathname(__FILE__).dirname.join('..').expand_path
10
+ lib_path = root_path.join('lib')
11
+ $:.unshift(lib_path)
12
+ require 'plucky'
13
+
14
+ criteria = Plucky::CriteriaHash.new(:foo => 'bar')
15
+
16
+ PerfTools::CpuProfiler.start("/tmp/criteria_hash") do
17
+ 1_000_000.times { criteria[:foo] = 'bar' }
18
+ end
19
+
20
+ puts system "pprof.rb --gif /tmp/criteria_hash > /tmp/criteria_hash.gif"
21
+ puts system "open /tmp/criteria_hash.gif"
@@ -1,15 +1,15 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
 
4
6
  Bundler.require(:default, :test)
5
7
 
6
- $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
7
8
  require 'plucky'
8
9
 
9
10
  require 'fileutils'
10
11
  require 'logger'
11
12
  require 'pp'
12
- require 'set'
13
13
 
14
14
  log_dir = File.expand_path('../../log', __FILE__)
15
15
  FileUtils.mkdir_p(log_dir)
@@ -20,17 +20,21 @@ LogBuddy.init :logger => Log
20
20
  connection = Mongo::Connection.new('127.0.0.1', 27017, :logger => Log)
21
21
  DB = connection.db('test')
22
22
 
23
- class Test::Unit::TestCase
24
- def setup
23
+ RSpec.configure do |config|
24
+ config.filter_run :focused => true
25
+ config.alias_example_to :fit, :focused => true
26
+ config.alias_example_to :xit, :pending => true
27
+ config.run_all_when_everything_filtered = true
28
+
29
+ config.before(:suite) do
25
30
  DB.collections.map do |collection|
26
- collection.remove
27
31
  collection.drop_indexes
28
32
  end
29
33
  end
30
34
 
31
- def oh(*args)
32
- BSON::OrderedHash.new.tap do |hash|
33
- args.each { |a| hash[a[0]] = a[1] }
35
+ config.before(:each) do
36
+ DB.collections.map do |collection|
37
+ collection.remove
34
38
  end
35
39
  end
36
40
  end
@@ -0,0 +1,166 @@
1
+ require 'helper'
2
+
3
+ describe Plucky::CriteriaHash do
4
+ context "#initialize_copy" do
5
+ before do
6
+ @original = described_class.new({
7
+ :comments => {:_id => 1}, :tags => ['mongo', 'ruby'],
8
+ }, :object_ids => [:_id])
9
+ @cloned = @original.clone
10
+ end
11
+
12
+ it "duplicates source hash" do
13
+ @cloned.source.should_not equal(@original.source)
14
+ end
15
+
16
+ it "duplicates options hash" do
17
+ @cloned.options.should_not equal(@original.options)
18
+ end
19
+
20
+ it "clones duplicable? values" do
21
+ @cloned[:comments].should_not equal(@original[:comments])
22
+ @cloned[:tags].should_not equal(@original[:tags])
23
+ end
24
+ end
25
+
26
+ context "#object_ids=" do
27
+ it "works with array" do
28
+ criteria = described_class.new
29
+ criteria.object_ids = [:_id]
30
+ criteria.object_ids.should == [:_id]
31
+ end
32
+
33
+ it "flattens multi-dimensional array" do
34
+ criteria = described_class.new
35
+ criteria.object_ids = [[:_id]]
36
+ criteria.object_ids.should == [:_id]
37
+ end
38
+
39
+ it "raises argument error if not array" do
40
+ expect { described_class.new.object_ids = {} }.to raise_error(ArgumentError)
41
+ expect { described_class.new.object_ids = nil }.to raise_error(ArgumentError)
42
+ expect { described_class.new.object_ids = 'foo' }.to raise_error(ArgumentError)
43
+ end
44
+ end
45
+
46
+ context "#[]=" do
47
+ context "with key and value" do
48
+ let(:key_normalizer) { lambda { |*args| :normalized_key } }
49
+ let(:value_normalizer) { lambda { |*args| 'normalized_value' } }
50
+
51
+ it "sets normalized key to normalized value in source" do
52
+ criteria = described_class.new({}, :value_normalizer => value_normalizer, :key_normalizer => key_normalizer)
53
+ criteria[:foo] = 'bar'
54
+ criteria.source[:normalized_key].should eq('normalized_value')
55
+ end
56
+ end
57
+
58
+ context "with conditions" do
59
+ it "sets each of conditions keys in source" do
60
+ criteria = described_class.new
61
+ criteria[:conditions] = {:_id => 'john', :foo => 'bar'}
62
+ criteria.source[:_id].should eq('john')
63
+ criteria.source[:foo].should eq('bar')
64
+ end
65
+ end
66
+
67
+ context "with symbol operators" do
68
+ it "sets nests key with operator and value" do
69
+ criteria = described_class.new
70
+ criteria[:age.gt] = 20
71
+ criteria[:age.lt] = 10
72
+ criteria.source[:age].should eq({:$gt => 20, :$lt => 10})
73
+ end
74
+ end
75
+ end
76
+
77
+ context "#merge" do
78
+ it "works when no keys match" do
79
+ c1 = described_class.new(:foo => 'bar')
80
+ c2 = described_class.new(:baz => 'wick')
81
+ c1.merge(c2).source.should eq(:foo => 'bar', :baz => 'wick')
82
+ end
83
+
84
+ it "turns matching keys with simple values into array" do
85
+ c1 = described_class.new(:foo => 'bar')
86
+ c2 = described_class.new(:foo => 'baz')
87
+ c1.merge(c2).source.should eq(:foo => {:$in => %w[bar baz]})
88
+ end
89
+
90
+ it "uniques matching key values" do
91
+ c1 = described_class.new(:foo => 'bar')
92
+ c2 = described_class.new(:foo => 'bar')
93
+ c1.merge(c2).source.should eq(:foo => {:$in => %w[bar]})
94
+ end
95
+
96
+ it "correctly merges arrays and non-arrays" do
97
+ c1 = described_class.new(:foo => 'bar')
98
+ c2 = described_class.new(:foo => %w[bar baz])
99
+ c1.merge(c2).source.should eq(:foo => {:$in => %w[bar baz]})
100
+ c2.merge(c1).source.should eq(:foo => {:$in => %w[bar baz]})
101
+ end
102
+
103
+ it "is able to merge two modifier hashes" do
104
+ c1 = described_class.new(:$in => [1, 2])
105
+ c2 = described_class.new(:$in => [2, 3])
106
+ c1.merge(c2).source.should eq(:$in => [1, 2, 3])
107
+ end
108
+
109
+ it "is able to merge two modifier hashes with hash values" do
110
+ c1 = described_class.new(:arr => {:$elemMatch => {:foo => 'bar'}})
111
+ c2 = described_class.new(:arr => {:$elemMatch => {:omg => 'ponies'}})
112
+ c1.merge(c2).source.should eq(:arr => {:$elemMatch => {:foo => 'bar', :omg => 'ponies'}})
113
+ end
114
+
115
+ it "merges matching keys with a single modifier" do
116
+ c1 = described_class.new(:foo => {:$in => [1, 2, 3]})
117
+ c2 = described_class.new(:foo => {:$in => [1, 4, 5]})
118
+ c1.merge(c2).source.should eq(:foo => {:$in => [1, 2, 3, 4, 5]})
119
+ end
120
+
121
+ it "merges matching keys with multiple modifiers" do
122
+ c1 = described_class.new(:foo => {:$in => [1, 2, 3]})
123
+ c2 = described_class.new(:foo => {:$all => [1, 4, 5]})
124
+ c1.merge(c2).source.should eq(:foo => {:$in => [1, 2, 3], :$all => [1, 4, 5]})
125
+ end
126
+
127
+ it "does not update mergee" do
128
+ c1 = described_class.new(:foo => 'bar')
129
+ c2 = described_class.new(:foo => 'baz')
130
+ c1.merge(c2).should_not equal(c1)
131
+ c1[:foo].should == 'bar'
132
+ end
133
+ end
134
+
135
+ context "#merge!" do
136
+ it "updates mergee" do
137
+ c1 = described_class.new(:foo => 'bar')
138
+ c2 = described_class.new(:foo => 'baz')
139
+ c1.merge!(c2).should equal(c1)
140
+ c1[:foo].should == {:$in => ['bar', 'baz']}
141
+ end
142
+ end
143
+
144
+ context "#simple?" do
145
+ it "returns true if only filtering by _id" do
146
+ described_class.new(:_id => 'id').should be_simple
147
+ end
148
+
149
+ it "returns true if only filtering by Sci" do
150
+ described_class.new(:_id => 'id', :_type => 'Foo').should be_simple
151
+ described_class.new(:_type => 'Foo', :_id => 'id').should be_simple # reverse order
152
+ end
153
+
154
+ it "returns false if querying by more than max number of simple keys" do
155
+ described_class.new(:one => 1, :two => 2, :three => 3).should_not be_simple
156
+ end
157
+
158
+ it "returns false if querying by anthing other than _id/Sci" do
159
+ described_class.new(:foo => 'bar').should_not be_simple
160
+ end
161
+
162
+ it "returns false if querying only by _type" do
163
+ described_class.new(:_type => 'Foo').should_not be_simple
164
+ end
165
+ end
166
+ end