sdbtools 0.1.0 → 0.2.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.
data/Rakefile CHANGED
@@ -19,6 +19,7 @@ END
19
19
  gem.add_dependency 'highline', '~> 1.5'
20
20
  gem.add_dependency 'progressbar', '~> 0.0.3'
21
21
  gem.add_dependency 'libxml-ruby', '~> 1.1'
22
+ gem.add_dependency 'arrayfields', '~> 4.7'
22
23
  gem.add_development_dependency "rspec", ">= 1.2.9"
23
24
  end
24
25
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -1,5 +1,6 @@
1
1
  require 'fattr'
2
2
  require 'right_aws'
3
+ require File.expand_path('selection', File.dirname(__FILE__))
3
4
 
4
5
  module SDBTools
5
6
 
@@ -8,16 +9,21 @@ module SDBTools
8
9
  class Operation
9
10
  include Enumerable
10
11
 
12
+ attr_reader :method
13
+ attr_reader :args
14
+ attr_reader :starting_token
15
+
11
16
  def initialize(sdb, method, *args)
12
17
  @options = args.last.is_a?(Hash) ? args.pop : {}
13
18
  @sdb = sdb
14
19
  @method = method
15
20
  @args = args
21
+ @starting_token = @options[:starting_token]
16
22
  end
17
23
 
18
24
  # Yields once for each result set, until there is no next token.
19
25
  def each
20
- next_token = @options[:starting_token]
26
+ next_token = starting_token
21
27
  begin
22
28
  args = @args.dup
23
29
  args << next_token
@@ -0,0 +1,189 @@
1
+ require 'arrayfields'
2
+ require 'logger'
3
+
4
+ module SDBTools
5
+ class Selection
6
+ include Enumerable
7
+
8
+ MAX_RESULT_LIMIT = 250
9
+ DEFAULT_RESULT_LIMIT = 100
10
+
11
+ attr_accessor :domain
12
+ attr_accessor :attributes
13
+ attr_accessor :conditions
14
+ attr_reader :limit
15
+ attr_accessor :offset
16
+ attr_accessor :order_by
17
+ attr_accessor :order
18
+ attr_accessor :sdb
19
+ attr_accessor :logger
20
+
21
+ # For testing
22
+ attr_writer :starting_token
23
+
24
+ def initialize(sdb, domain, options={})
25
+ @sdb = sdb
26
+ @domain = domain.to_s
27
+ @attributes = options.fetch(:attributes) { :all }
28
+ @conditions = Array(options[:conditions])
29
+ @order = options.fetch(:order) { :ascending }
30
+ @order_by = options.fetch(:order_by) { :none }
31
+ @order_by = @order_by.to_s unless @order_by == :none
32
+ self.limit = options.fetch(:limit) { DEFAULT_RESULT_LIMIT }
33
+ @offset = options.fetch(:offset) { 0 }.to_i
34
+ @logger = options.fetch(:logger){::Logger.new($stderr)}
35
+ end
36
+
37
+ def to_s
38
+ "SELECT #{output_list} FROM #{quote_name(domain)}#{match_expression}#{sort_instructions}#{limit_clause}"
39
+ end
40
+
41
+ def count_expression
42
+ "SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions}#{limit_clause_for_count}"
43
+ end
44
+
45
+ def offset_count_expression
46
+ "SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions} LIMIT #{offset}"
47
+ end
48
+
49
+ def count
50
+ @count ||= count_operation.inject(0){|count, results|
51
+ count += results[:items].first["Domain"]["Count"].first.to_i
52
+ }
53
+ end
54
+
55
+ alias_method :size, :count
56
+ alias_method :length, :count
57
+
58
+ def each
59
+ return if limit == 0
60
+ num_items = 0
61
+ select_operation.each do |results|
62
+ results[:items].each do |item|
63
+ yield(item.keys.first, item.values.first)
64
+ num_items += 1
65
+ return if limit != :none && num_items >= limit
66
+ end
67
+ end
68
+ end
69
+
70
+ def results
71
+ @results ||= inject(Arrayfields.new){|results, (name, value)|
72
+ results[name] = value
73
+ results
74
+ }
75
+ end
76
+
77
+ def count_operation
78
+ Operation.new(sdb, :select, count_expression, :starting_token => starting_token)
79
+ end
80
+
81
+ def offset_count_operation
82
+ Operation.new(sdb, :select, offset_count_expression)
83
+ end
84
+
85
+ def select_operation
86
+ Operation.new(sdb, :select, to_s, :starting_token => starting_token)
87
+ end
88
+
89
+ def starting_token
90
+ @starting_token ||=
91
+ case offset
92
+ when 0 then nil
93
+ else
94
+ op = offset_count_operation
95
+ count = 0
96
+ op.each do |results|
97
+ count += results[:items].first["Domain"]["Count"].first.to_i
98
+ if count == offset || results[:next_token].nil?
99
+ return results[:next_token]
100
+ end
101
+ end
102
+ raise "Failed to find offset #{offset}"
103
+ end
104
+ end
105
+
106
+ def limit=(new_limit)
107
+ # We can't yet support large limits. In order to do so, it will be necessary
108
+ # to implement limit chunking, where the limit is split across multiple
109
+ # requests of 250 items and a final request of limit % 250 items.
110
+ case new_limit
111
+ when :none, (0..MAX_RESULT_LIMIT) then
112
+ @limit = new_limit
113
+ else
114
+ raise RangeError, "Limit must be 0..250 or :none"
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def quote_name(name)
121
+ if name.to_s =~ /^[A-Z$_][A-Z0-9$_]*$/i
122
+ name.to_s
123
+ else
124
+ "`" + name.to_s.gsub("`", "``") + "`"
125
+ end
126
+ end
127
+
128
+ def quote_value(value)
129
+ '"' + value.to_s.gsub(/"/, '""') + '"'
130
+ end
131
+
132
+ def output_list
133
+ case attributes
134
+ when Array then attributes.map{|a| quote_name(a)}.join(", ")
135
+ when :all then "*"
136
+ else raise ScriptError, "Bad attributes: #{attributes.inspect}"
137
+ end
138
+ end
139
+
140
+ def match_expression
141
+ case conditions.size
142
+ when 0 then ""
143
+ else " WHERE #{prepared_conditions.join(' ')}"
144
+ end
145
+ end
146
+
147
+ def prepared_conditions
148
+ conditions.map { |condition|
149
+ case condition
150
+ when String then condition
151
+ when Array then
152
+ values = condition.dup
153
+ template = values.shift.to_s
154
+ template.gsub(/\?/) {|match|
155
+ quote_value(values.shift.to_s)
156
+ }
157
+ else
158
+ raise ScriptError, "Bad condition: #{condition.inspect}"
159
+ end
160
+ }
161
+ end
162
+
163
+ def limit_clause
164
+ case limit
165
+ when :none then " LIMIT #{MAX_RESULT_LIMIT}"
166
+ when DEFAULT_RESULT_LIMIT then ""
167
+ else
168
+ batch_limit = limit < MAX_RESULT_LIMIT ? limit : MAX_RESULT_LIMIT
169
+ " LIMIT #{batch_limit}"
170
+ end
171
+ end
172
+
173
+ def limit_clause_for_count
174
+ case limit
175
+ when :none then ""
176
+ else limit_clause
177
+ end
178
+ end
179
+
180
+ def sort_instructions
181
+ case order_by
182
+ when :none then ""
183
+ else
184
+ direction = (order == :ascending ? "ASC" : "DESC")
185
+ " ORDER BY #{quote_name(order_by)} #{direction}"
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,295 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module SDBTools
4
+ describe Selection do
5
+ def count_results(count, token=nil)
6
+ {
7
+ :items => [
8
+ "Domain" => {
9
+ "Count" => [count.to_s]
10
+ }
11
+ ],
12
+ :next_token => token
13
+ }
14
+ end
15
+
16
+ def select_results(names, token=nil)
17
+ {
18
+ :items => names.map{|n| {n => {"#{n}_attr" => ["#{n}_val"]}}},
19
+ :next_token => token
20
+ }
21
+ end
22
+
23
+ before :each do
24
+ @sdb = stub("SDB")
25
+ end
26
+
27
+ context "with domain THE_DOMAIN" do
28
+ before :each do
29
+ @it = Selection.new(@sdb, "THE_DOMAIN")
30
+ end
31
+
32
+ specify { @it.to_s.should == "SELECT * FROM THE_DOMAIN" }
33
+
34
+ it "should be able to generate a count expression" do
35
+ @it.count_expression.should be == "SELECT count(*) FROM THE_DOMAIN"
36
+ end
37
+
38
+ it "should be able to generate a count operation" do
39
+ op = @it.count_operation
40
+ op.method.should be == :select
41
+ op.args.should be == ["SELECT count(*) FROM THE_DOMAIN"]
42
+ op.starting_token.should be_nil
43
+ end
44
+
45
+ it "should generate a nil start token" do
46
+ @it.starting_token.should be_nil
47
+ end
48
+ end
49
+
50
+ context "with domain 1DO`MAIN" do
51
+ before :each do
52
+ @it = Selection.new(@sdb, "1DO`MAIN")
53
+ end
54
+
55
+ specify { @it.to_s.should == "SELECT * FROM `1DO``MAIN`" }
56
+ end
57
+
58
+ context "with explicit attributes" do
59
+ before :each do
60
+ @it = Selection.new(@sdb, "DOMAIN", :attributes => [:foo, :bar])
61
+ end
62
+
63
+ it "should select the attributes" do
64
+ @it.to_s.should == "SELECT foo, bar FROM DOMAIN"
65
+ end
66
+ end
67
+
68
+ context "with funky attribute names" do
69
+ before :each do
70
+ @it = Selection.new(@sdb, "DOMAIN",
71
+ :attributes => ["attr foo", "1`bar", "baz"])
72
+ end
73
+
74
+ it "should quote the funky attribute names" do
75
+ @it.to_s.should == %Q{SELECT `attr foo`, `1``bar`, baz FROM DOMAIN}
76
+ end
77
+ end
78
+
79
+ context "with conditions" do
80
+ before :each do
81
+ @it = Selection.new(@sdb, "DOMAIN",
82
+ :attributes => ["attr foo", "bar"],
83
+ :conditions => [
84
+ 'bar == "buz"',
85
+ "AND",
86
+ ['`attr foo` between ? and ?', 4, '1"2']])
87
+ end
88
+
89
+ it "should format quote and interpolate the conditions" do
90
+ @it.to_s.should be ==
91
+ %Q{SELECT `attr foo`, bar FROM DOMAIN WHERE bar == "buz" AND `attr foo` between "4" and "1""2"}
92
+ end
93
+
94
+ end # "
95
+
96
+ context "with a limit of :none" do
97
+ before :each do
98
+ @it = Selection.new(@sdb, "DOMAIN",
99
+ :attributes => [:foo, :bar],
100
+ :conditions => ["foo == 'bar'"],
101
+ :limit => :none)
102
+ end
103
+
104
+ it "should append a limit clause" do
105
+ @it.to_s.should be == "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 250"
106
+ end
107
+
108
+ it "should be able to generate a count expression" do
109
+ @it.count_expression.should be ==
110
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar'"
111
+ end
112
+ end
113
+
114
+ context "with a limit" do
115
+ before :each do
116
+ @it = Selection.new(@sdb, "DOMAIN",
117
+ :attributes => [:foo, :bar],
118
+ :conditions => ["foo == 'bar'"],
119
+ :limit => 10)
120
+ end
121
+
122
+ it "should append a limit clause" do
123
+ @it.to_s.should be == "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 10"
124
+ end
125
+
126
+ it "should be able to generate a count expression" do
127
+ @it.count_expression.should be ==
128
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' LIMIT 10"
129
+ end
130
+
131
+ it "should limit results" do
132
+ results = select_results(Array.new(100, "foo"))
133
+ @sdb.stub!(:select).and_return(results)
134
+ @it.to_a.size.should == 10
135
+ end
136
+ end
137
+
138
+ context "ordered by an attribute" do
139
+ before :each do
140
+ @it = Selection.new(@sdb, "DOMAIN",
141
+ :attributes => [:foo, :bar],
142
+ :conditions => ["foo == 'bar'"],
143
+ :order_by => "foo")
144
+ end
145
+
146
+ it "should append an order clause" do
147
+ @it.to_s.should be ==
148
+ "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' ORDER BY foo ASC"
149
+ end
150
+
151
+ it "should be able to generate a count expression" do
152
+ @it.count_expression.should be ==
153
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' ORDER BY foo ASC"
154
+ end
155
+ end
156
+
157
+ context "in descending order" do
158
+ before :each do
159
+ @it = Selection.new(@sdb, "DOMAIN",
160
+ :attributes => [:foo, :bar],
161
+ :conditions => ["foo == 'bar'"],
162
+ :order_by => "foo",
163
+ :order => :descending)
164
+ end
165
+
166
+ it "should append an order clause" do
167
+ @it.to_s.should be ==
168
+ "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC"
169
+ end
170
+
171
+ it "should be able to generate a count expression" do
172
+ @it.count_expression.should be ==
173
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC"
174
+ end
175
+
176
+ it "should be able to generate an offset count expression" do
177
+ @it.offset_count_expression.should be ==
178
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC LIMIT 0"
179
+ end
180
+
181
+ context "with a limit" do
182
+ before :each do
183
+ @it.limit = 250
184
+ end
185
+
186
+ specify { @it.to_s.should be ==
187
+ "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC LIMIT 250"
188
+ }
189
+ end
190
+ end
191
+
192
+ # We can't yet support large limits. In order to do so, it will be necessary
193
+ # to implement limit chunking, where the limit is split across multiple
194
+ # requests of 250 items and a final request of limit % 250 items.
195
+ it "should reject limits > 250" do
196
+ lambda do
197
+ Selection.new(@sdb, "DOMAIN", :limit => 251)
198
+ end.should raise_error
199
+ lambda do
200
+ Selection.new(@sdb, "DOMAIN").limit = 251
201
+ end.should raise_error
202
+ end
203
+
204
+ context "with an offset" do
205
+ before :each do
206
+ @it = Selection.new(@sdb, "DOMAIN",
207
+ :attributes => [:foo, :bar],
208
+ :conditions => ["foo == 'bar'"],
209
+ :limit => 10,
210
+ :offset => 200)
211
+
212
+ # Note: counts are relative to the last count, not absolute
213
+ @first_results = count_results(100, "TOKEN1")
214
+ @second_results = count_results(100, "TOKEN2")
215
+ @third_results = count_results(100, nil)
216
+ @sdb.stub!(:select).
217
+ and_return(@first_results, @second_results, @third_results)
218
+ end
219
+
220
+ it "should be able to generate an offset count expression" do
221
+ @it.offset_count_expression.should be ==
222
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' LIMIT 200"
223
+ end
224
+
225
+ it "should be able to generate an offset count operation" do
226
+ op = @it.offset_count_operation
227
+ op.method.should be == :select
228
+ op.args.should be == [
229
+ "SELECT count(*) FROM DOMAIN WHERE foo == 'bar' LIMIT 200"
230
+ ]
231
+ op.starting_token.should be_nil
232
+ end
233
+
234
+ it "should use an SDB count to determine starting token" do
235
+ @sdb = stub("SDB")
236
+ @sdb.should_receive(:select).
237
+ with(@it.offset_count_expression, nil).
238
+ ordered.
239
+ and_return(@first_results)
240
+ @sdb.should_receive(:select).
241
+ with(@it.offset_count_expression, "TOKEN1").
242
+ ordered.
243
+ and_return(@second_results)
244
+ @sdb.should_not_receive(:select).
245
+ with(@it.offset_count_expression, "TOKEN2")
246
+ @it.sdb = @sdb
247
+ @it.starting_token.should be == "TOKEN2"
248
+ end
249
+
250
+ it "should be able to generate a select operation" do
251
+ op = @it.select_operation
252
+ op.args.should be == [
253
+ "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 10"
254
+ ]
255
+ op.starting_token.should be == "TOKEN2"
256
+ end
257
+
258
+ it "should be able to count items in the selection" do
259
+ @sdb = stub("SDB")
260
+ @sdb.should_receive(:select).
261
+ with(@it.count_expression, "TOKEN2").
262
+ and_return(count_results(4, "TOKEN5"))
263
+ @sdb.should_receive(:select).
264
+ with(@it.count_expression, "TOKEN5").
265
+ and_return(count_results(5, nil))
266
+ @it.sdb = @sdb
267
+ @it.starting_token = "TOKEN2"
268
+ @it.size.should be == 9
269
+ @it.length.should be == 9
270
+ @it.count.should be == 9
271
+ end
272
+
273
+ it "should be able to get selection results" do
274
+ @sdb = stub("SDB")
275
+ @sdb.should_receive(:select).
276
+ with(@it.to_s, "TOKEN2").
277
+ and_return(select_results(["foo", "bar"], "TOKEN5"))
278
+ @sdb.should_receive(:select).
279
+ with(@it.to_s, "TOKEN5").
280
+ and_return(select_results(["baz", "buz"], nil))
281
+ @it.sdb = @sdb
282
+ @it.starting_token = "TOKEN2"
283
+ @it.results.values.should be == [
284
+ { "foo_attr" => ["foo_val"]},
285
+ { "bar_attr" => ["bar_val"]},
286
+ { "baz_attr" => ["baz_val"]},
287
+ { "buz_attr" => ["buz_val"]}
288
+ ]
289
+ @it.results.keys.should be == [
290
+ "foo", "bar", "baz", "buz"
291
+ ]
292
+ end
293
+ end
294
+ end
295
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sdbtools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Avdi Grimm
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-18 00:00:00 -05:00
12
+ date: 2010-01-19 00:00:00 -05:00
13
13
  default_executable: sdbtool
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -72,6 +72,16 @@ dependencies:
72
72
  - !ruby/object:Gem::Version
73
73
  version: "1.1"
74
74
  version:
75
+ - !ruby/object:Gem::Dependency
76
+ name: arrayfields
77
+ type: :runtime
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ~>
82
+ - !ruby/object:Gem::Version
83
+ version: "4.7"
84
+ version:
75
85
  - !ruby/object:Gem::Dependency
76
86
  name: rspec
77
87
  type: :development
@@ -98,7 +108,9 @@ files:
98
108
  - VERSION
99
109
  - bin/sdbtool
100
110
  - lib/sdbtools.rb
111
+ - lib/selection.rb
101
112
  - spec/operation_spec.rb
113
+ - spec/selection_spec.rb
102
114
  - spec/spec_helper.rb
103
115
  has_rdoc: true
104
116
  homepage: http://github.com/devver/sdbtools
@@ -129,5 +141,6 @@ signing_key:
129
141
  specification_version: 3
130
142
  summary: A high-level OO interface to Amazon SimpleDB
131
143
  test_files:
144
+ - spec/selection_spec.rb
132
145
  - spec/operation_spec.rb
133
146
  - spec/spec_helper.rb