plucky 0.5.2 → 0.6.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.
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