dm-adapter-simpledb 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,60 @@
1
+ == 1.4.0 2010-01-24
2
+
3
+ * Major enhancements:
4
+ * Completely rewritten query generation engine queries supports
5
+ arbitrarily deep nesting of complex AND/OR/NOT operators. This means that
6
+ code such as:
7
+
8
+ Post.all(:title => "foo") | Post.all(:title => "bar", :body => "baz")
9
+
10
+ will generate a SELECT statement similar to the following
11
+
12
+ SELECT title from mydomain where title = "foo" OR ( title = "bar" AND body = "baz" )
13
+
14
+ * New query engine supports range and IN predicates to the extent possible on
15
+ SimpleDB, including converting empty inclusion predicates to a form that
16
+ SimpleDB can understand. Exclusive ranges e.g. (0...5) now raise a
17
+ NotImplementedError instead of being silently flaky.
18
+
19
+ * Support for native condition expressions, e.g.:
20
+
21
+ Post.all(:conditions => 'title in "%banannas%"')
22
+
23
+ This includes support for variable interpolation:
24
+
25
+ Post.all(:conditions => ['title = ?', "foo"])
26
+
27
+ or:
28
+
29
+ post.all(:conditions => ['title = :title', {:title => "foo"}]
30
+
31
+ Interpolated values will be quoted according to SimpleDB value quoting
32
+ rules.
33
+
34
+ * Support for arbitrarily large limits. The concept of a query limit and a
35
+ batch limit have been completely separated in this release. If the batch
36
+ limit is set to 100 and a query is limited to 201 items, it will generate
37
+ three selects: two with "LIMIT 100" and one with "LIMIT 1".
38
+
39
+ * Vastly improved logging and benchmarking. For a given high-level operation,
40
+ such as a DataMapper "read", the adapter can output:
41
+
42
+ * Number of individual AWS calls made (e.g. individual SELECTs)
43
+ * Aggregate AWS box usage
44
+ * User CPU time
45
+ * System CPU time
46
+ * Wallclock time
47
+
48
+ * Minor enhancements:
49
+ * Even better quoting. With the new SELECT translator in place, all domain
50
+ names, attribute names, and values should be quoted properly according to
51
+ SimpleDB rules.
52
+ * No direct dependency on RightAws. All operations are performed via SDBTools
53
+ now.
54
+ * New "batch_limit" option to configure the maximum results requested per
55
+ SELECT call. Amazon sets a cap of 250 on this value.
56
+
57
+
1
58
  == 1.3.0 2010-01-19
2
59
 
3
60
  * 1 major enhancement:
@@ -6,6 +63,8 @@
6
63
 
7
64
  * 1 minor enhancement:
8
65
  * Better quoting of select statements using the 'sdbtools' library.
66
+ * "Null mode" can be set with :null => true. Null mode logs DB operations but
67
+ does not actually connect to AWS.
9
68
 
10
69
  == 1.2.0 2010-01-13
11
70
 
data/README CHANGED
@@ -7,8 +7,8 @@ A DataMapper adapter for Amazon's SimpleDB service.
7
7
  Features:
8
8
  * Uses the RightAWS gem for efficient SimpleDB operations.
9
9
  * Full set of CRUD operations
10
- * Supports all DataMapper query predicates.
11
- * Can translate many queries into efficient native SELECT operations.
10
+ * Supports nearly all DataMapper query predicates.
11
+ * Full support for complex nested union, intersaction, and negation in queries
12
12
  * Migrations
13
13
  * DataMapper identity map support for record caching
14
14
  * Lazy-loaded attributes
@@ -17,6 +17,7 @@ Features:
17
17
  * Basic aggregation support (Model.count("..."))
18
18
  * String "chunking" permits attributes to exceed the 1024-byte limit
19
19
  * Support for efficient :limit and :offset, for result set paging
20
+ * Robust quoting of names in values in selects
20
21
 
21
22
  Note: as of version 1.0.0, this gem supports supports the DataMapper 0.10.*
22
23
  series and breaks backwards compatibility with DataMapper 0.9.*.
@@ -39,9 +40,6 @@ http://github.com/devver/dm-adapter-simpledb/
39
40
 
40
41
  == TODO
41
42
 
42
- * More complete handling of NOT conditions in queries
43
- * Support for ORs, parens, and nested queries in general
44
- * Robust quoting in SELECT calls
45
43
  * Handle exclusive ranges natively
46
44
  Implement as inclusive range + filter step
47
45
  * Tests for associations
@@ -58,6 +56,7 @@ http://github.com/devver/dm-adapter-simpledb/
58
56
  * Silence SSL warnings
59
57
  See http://pivotallabs.com/users/carl/blog/articles/1079-standup-blog-11-24-2009-model-validations-without-backing-store-associations-to-array-and-ssl-with-aws
60
58
  * Token cache for reduced requests when given an offset
59
+ * Optimize key queries
61
60
 
62
61
  == Usage
63
62
 
data/Rakefile CHANGED
@@ -54,7 +54,6 @@ begin
54
54
  A DataMapper adapter for Amazon's SimpleDB service.
55
55
 
56
56
  Features:
57
- * Uses the RightAWS gem for efficient SimpleDB operations.
58
57
  * Full set of CRUD operations
59
58
  * Supports all DataMapper query predicates.
60
59
  * Can translate many queries into efficient native SELECT operations.
@@ -81,7 +80,7 @@ END
81
80
  gem.add_dependency('dm-migrations', '~> 0.10.0')
82
81
  gem.add_dependency('dm-types', '~> 0.10.0')
83
82
  gem.add_dependency('uuidtools', '~> 2.0')
84
- gem.add_dependency('sdbtools', '~> 0.2')
83
+ gem.add_dependency('sdbtools', '~> 0.4')
85
84
  end
86
85
  Jeweler::GemcutterTasks.new
87
86
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.0
1
+ 1.4.0
@@ -6,13 +6,14 @@ gem 'dm-core', '~> 0.10.0'
6
6
  require 'dm-core'
7
7
  require 'dm-aggregates'
8
8
  require 'digest/sha1'
9
- require 'right_aws'
10
9
  require 'uuidtools'
11
10
  require 'sdbtools'
12
11
 
12
+ $:.unshift(File.dirname(__FILE__))
13
13
  require 'dm-adapter-simpledb/sdb_array'
14
14
  require 'dm-adapter-simpledb/utils'
15
15
  require 'dm-adapter-simpledb/record'
16
16
  require 'dm-adapter-simpledb/table'
17
+ require 'dm-adapter-simpledb/where_expression'
17
18
  require 'dm-adapter-simpledb/migrations/simpledb_adapter'
18
19
  require 'dm-adapter-simpledb/adapters/simpledb_adapter'
@@ -3,7 +3,9 @@ module DataMapper
3
3
  class SimpleDBAdapter < AbstractAdapter
4
4
  include DmAdapterSimpledb::Utils
5
5
 
6
- attr_reader :sdb_options
6
+ attr_reader :sdb_options
7
+ attr_reader :batch_limit
8
+ attr_accessor :logger
7
9
 
8
10
  # For testing purposes ONLY. Seriously, don't enable this for production
9
11
  # code.
@@ -18,7 +20,8 @@ module DataMapper
18
20
  @sdb_options[:secret_key] = options.fetch(:secret_key) {
19
21
  options[:password]
20
22
  }
21
- @sdb_options[:logger] = options.fetch(:logger) { DataMapper.logger }
23
+ @logger = options.fetch(:logger) { DataMapper.logger }
24
+ @sdb_options[:logger] = @logger
22
25
  @sdb_options[:server] = options.fetch(:host) { 'sdb.amazonaws.com' }
23
26
  @sdb_options[:port] = options[:port] || 443 # port may be set but nil
24
27
  @sdb_options[:domain] = options.fetch(:domain) {
@@ -33,18 +36,27 @@ module DataMapper
33
36
  # RightAWS's nil-token replacement altogether, but that does not appear
34
37
  # to be an option.
35
38
  @sdb_options[:nil_representation] = "<[<[<NIL>]>]>"
39
+ @null_mode = options.fetch(:null) { false }
40
+ @batch_limit = options.fetch(:batch_limit) {
41
+ SDBTools::Selection::DEFAULT_RESULT_LIMIT
42
+ }.to_i
43
+
44
+ if @null_mode
45
+ logger.info "SimpleDB adapter for domain #{domain_name} is in null mode"
46
+ end
47
+
36
48
  @consistency_policy =
37
49
  normalised_options.fetch(:wait_for_consistency) { false }
38
50
  @sdb = options.fetch(:sdb_interface) { nil }
39
- if @sdb_options[:create_domain] && !domains.include?(@sdb_options[:domain])
40
- @sdb_options[:logger].info "Creating domain #{domain}"
41
- @sdb.create_domain(@sdb_options[:domain])
51
+ if @sdb_options[:create_domain] && !domains.include?(domain_name)
52
+ @sdb_options[:logger].info "Creating domain #{domain_name}"
53
+ database.create_domain(domain_name)
42
54
  end
43
55
  end
44
56
 
45
57
  def create(resources)
46
58
  created = 0
47
- time = Benchmark.realtime do
59
+ transaction("CREATE #{resources.size} objects") do
48
60
  resources.each do |resource|
49
61
  uuid = UUIDTools::UUID.timestamp_create
50
62
  initialize_serial(resource, uuid.to_i)
@@ -52,63 +64,63 @@ module DataMapper
52
64
  record = DmAdapterSimpledb::Record.from_resource(resource)
53
65
  attributes = record.writable_attributes
54
66
  item_name = record.item_name
55
- sdb.put_attributes(domain, item_name, attributes)
67
+ domain.put(item_name, attributes, :replace => true)
56
68
  created += 1
57
69
  end
58
70
  end
59
- DataMapper.logger.debug(format_log_entry("(#{created}) INSERT #{resources.inspect}", time))
60
71
  modified!
61
72
  created
62
73
  end
63
74
 
64
75
  def delete(collection)
65
76
  deleted = 0
66
- time = Benchmark.realtime do
77
+ transaction("DELETE #{collection.query.conditions}") do
67
78
  collection.each do |resource|
68
79
  record = DmAdapterSimpledb::Record.from_resource(resource)
69
80
  item_name = record.item_name
70
- sdb.delete_attributes(domain, item_name)
81
+ domain.delete(item_name)
71
82
  deleted += 1
72
83
  end
84
+
85
+ # TODO no reason we can't select a bunch of item names with an
86
+ # arbitrary query and then delete them.
73
87
  raise NotImplementedError.new('Only :eql on delete at the moment') if not_eql_query?(collection.query)
74
- end; DataMapper.logger.debug(format_log_entry("(#{deleted}) DELETE #{collection.query.conditions.inspect}", time))
88
+ end
75
89
  modified!
76
90
  deleted
77
91
  end
78
92
 
79
93
  def read(query)
80
94
  maybe_wait_for_consistency
81
- table = DmAdapterSimpledb::Table.new(query.model)
82
- conditions, order, unsupported_conditions =
83
- set_conditions_and_sort_order(query, table.simpledb_type)
84
- results = get_results(query, conditions, order)
85
- records = results.map{|result|
86
- DmAdapterSimpledb::Record.from_simpledb_hash(result)
87
- }
95
+ transaction("READ #{query.model.name} #{query.conditions}") do |t|
96
+ query = query.dup
88
97
 
89
- proto_resources = records.map{|record|
90
- record.to_resource_hash(query.fields)
91
- }
92
- query.conditions.operands.reject!{ |op|
93
- !unsupported_conditions.include?(op)
94
- }
98
+ selection = selection_from_query(query)
95
99
 
96
- # This used to be a simple call to Query#filter_records(), but that
97
- # caused the result limit to be re-imposed on an already limited result
98
- # set, with the upshot that too few records were returned. So here we do
99
- # everything filter_records() does EXCEPT limiting.
100
- records = proto_resources
101
- records = records.uniq if query.unique?
102
- records = query.match_records(records)
103
- records = query.sort_records(records)
100
+ records = selection.map{|name, attributes|
101
+ DmAdapterSimpledb::Record.from_simpledb_hash(name => attributes)
102
+ }
104
103
 
104
+ proto_resources = records.map{|record|
105
+ record.to_resource_hash(query.fields)
106
+ }
107
+
108
+ # This used to be a simple call to Query#filter_records(), but that
109
+ # caused the result limit to be re-imposed on an already limited result
110
+ # set, with the upshot that too few records were returned. So here we do
111
+ # everything filter_records() does EXCEPT limiting.
112
+ records = proto_resources
113
+ records = records.uniq if query.unique?
114
+ records = query.match_records(records)
115
+ records = query.sort_records(records)
105
116
 
106
- records
117
+ records
118
+ end
107
119
  end
108
120
 
109
121
  def update(attributes, collection)
110
122
  updated = 0
111
- time = Benchmark.realtime do
123
+ transaction("UPDATE #{collection.query} with #{attributes.inspect}") do
112
124
  collection.each do |resource|
113
125
  updated_resource = resource.dup
114
126
  updated_resource.attributes = attributes
@@ -117,40 +129,24 @@ module DataMapper
117
129
  attrs_to_delete = record.deletable_attributes
118
130
  item_name = record.item_name
119
131
  unless attrs_to_update.empty?
120
- sdb.put_attributes(domain, item_name, attrs_to_update, :replace)
132
+ domain.put(item_name, attrs_to_update, :replace => true)
121
133
  end
122
134
  unless attrs_to_delete.empty?
123
- sdb.delete_attributes(domain, item_name, attrs_to_delete)
135
+ domain.delete(item_name, attrs_to_delete)
124
136
  end
125
137
  updated += 1
126
138
  end
127
139
  raise NotImplementedError.new('Only :eql on delete at the moment') if not_eql_query?(collection.query)
128
140
  end
129
- DataMapper.logger.debug(format_log_entry("UPDATE #{collection.query.conditions.inspect} (#{updated} times)", time))
130
141
  modified!
131
142
  updated
132
143
  end
133
144
 
134
- def query(query_call, query_limit = 999999999)
135
- SDBTools::Operation.new(sdb, :select, query_call).inject([]){
136
- |a, results|
137
- a.concat(results[:items].map{|i| i.values.first})
138
- }[0...query_limit]
139
- end
140
-
141
145
  def aggregate(query)
142
- raise ArgumentError.new("Only count is supported") unless (query.fields.first.operator == :count)
143
- table = DmAdapterSimpledb::Table.new(query.model)
144
- sdb_type = table.simpledb_type
145
- conditions, order, unsupported_conditions = set_conditions_and_sort_order(query, sdb_type)
146
-
147
- query_call = "SELECT count(*) FROM #{domain} "
148
- query_call << "WHERE #{conditions.compact.join(' AND ')}" if conditions.length > 0
149
- results = nil
150
- time = Benchmark.realtime do
151
- results = sdb.select(query_call)
152
- end; DataMapper.logger.debug(format_log_entry(query_call, time))
153
- [results[:items][0].values.first["Count"].first.to_i]
146
+ raise NotImplementedError, "Only count is supported" unless (query.fields.first.operator == :count)
147
+ transaction("AGGREGATE") do |t|
148
+ [selection_from_query(query).count]
149
+ end
154
150
  end
155
151
 
156
152
  # For testing purposes only.
@@ -158,142 +154,25 @@ module DataMapper
158
154
  return unless @current_consistency_token
159
155
  token = :none
160
156
  begin
161
- results = sdb.get_attributes(domain, '__dm_consistency_token', '__dm_consistency_token')
157
+ results = domain.get('__dm_consistency_token', '__dm_consistency_token')
162
158
  tokens = Array(results[:attributes]['__dm_consistency_token'])
163
159
  end until tokens.include?(@current_consistency_token)
164
160
  end
165
161
 
166
162
  def domains
167
- result = []
168
- token = nil
169
- begin
170
- response = sdb.list_domains(nil, token)
171
- result.concat(response[:domains])
172
- token = response[:next_token]
173
- end while(token)
174
- result
163
+ database.domains
175
164
  end
176
165
 
177
166
  private
178
- # Returns the domain for the model
179
167
  def domain
180
- @sdb_options[:domain]
168
+ @domain ||= database.domain(@sdb_options[:domain])
181
169
  end
182
170
 
183
- #sets the conditions and order for the SDB query
184
- def set_conditions_and_sort_order(query, sdb_type)
185
- unsupported_conditions = []
186
- conditions = ["simpledb_type = '#{sdb_type}'"]
187
- # look for query.order.first and insure in conditions
188
- # raise if order if greater than 1
189
-
190
- if query.order && query.order.length > 0
191
- query_object = query.order[0]
192
- #anything sorted on must be a condition for SDB
193
- conditions << "#{query_object.target.name} IS NOT NULL"
194
- order = "ORDER BY #{query_object.target.name} #{query_object.operator}"
195
- else
196
- order = ""
197
- end
198
- query.conditions.each do |op|
199
- case op.slug
200
- when :regexp
201
- unsupported_conditions << op
202
- when :eql
203
- conditions << if op.value.nil?
204
- "#{op.subject.name} IS NULL"
205
- else
206
- "#{op.subject.name} = '#{op.value}'"
207
- end
208
- when :not then
209
- comp = op.operands.first
210
- if comp.slug == :like
211
- conditions << "#{comp.subject.name} not like '#{comp.value}'"
212
- next
213
- end
214
- case comp.value
215
- when Range, Set, Array, Regexp
216
- unsupported_conditions << op
217
- when nil
218
- conditions << "#{comp.subject.name} IS NOT NULL"
219
- else
220
- conditions << "#{comp.subject.name} != '#{comp.value}'"
221
- end
222
- when :gt then conditions << "#{op.subject.name} > '#{op.value}'"
223
- when :gte then conditions << "#{op.subject.name} >= '#{op.value}'"
224
- when :lt then conditions << "#{op.subject.name} < '#{op.value}'"
225
- when :lte then conditions << "#{op.subject.name} <= '#{op.value}'"
226
- when :like then conditions << "#{op.subject.name} like '#{op.value}'"
227
- when :in
228
- case op.value
229
- when Array, Set
230
- values = op.value.collect{|v| "'#{v}'"}.join(',')
231
- values = "'__NULL__'" if values.empty?
232
- conditions << "#{op.subject.name} IN (#{values})"
233
- when Range
234
- if op.value.exclude_end?
235
- unsupported_conditions << op
236
- else
237
- conditions << "#{op.subject.name} between '#{op.value.first}' and '#{op.value.last}'"
238
- end
239
- else
240
- raise ArgumentError, "Unsupported inclusion op: #{op.value.inspect}"
241
- end
242
- when :or
243
- # TODO There's no reason not to support OR
244
- unsupported_conditions << op
245
- else raise "Invalid query op: #{op.inspect}"
246
- end
247
- end
248
- [conditions,order,unsupported_conditions]
171
+ # Returns the domain for the model
172
+ def domain_name
173
+ @sdb_options[:domain]
249
174
  end
250
-
251
- #gets all results or proper number of results depending on the :limit
252
- def get_results(query, conditions, order)
253
- fields_to_request = query.fields.map{|f| f.field}
254
- fields_to_request << DmAdapterSimpledb::Record::METADATA_KEY
255
-
256
- selection = SDBTools::Selection.new(
257
- sdb,
258
- domain,
259
- :attributes => fields_to_request)
260
-
261
- if query.order && query.order.length > 0
262
- query_object = query.order[0]
263
- #anything sorted on must be a condition for SDB
264
- conditions << "#{query_object.target.name} IS NOT NULL"
265
- selection.order_by = query_object.target.name
266
- selection.order = case query_object.operator
267
- when :asc then :ascending
268
- when :desc then :descending
269
- else raise "Unrecognized sort direction"
270
- end
271
- end
272
- selection.conditions = conditions.compact.inject([]){|conds, cond|
273
- conds << "AND" unless conds.empty?
274
- conds << cond
275
- }
276
- if query.limit.nil?
277
- selection.limit = :none
278
- else
279
- selection.limit = query.limit
280
- end
281
- unless query.offset.nil?
282
- selection.offset = query.offset
283
- end
284
-
285
- items = []
286
- time = Benchmark.realtime do
287
- # TODO update Record to be created from name/attributes pair
288
- selection.each do |name, value|
289
- items << {name => value}
290
- end
291
- end
292
- DataMapper.logger.debug(format_log_entry(selection.to_s, time))
293
175
 
294
- items
295
- end
296
-
297
176
  # Creates an item name for a query
298
177
  def item_name_for_query(query)
299
178
  sdb_type = simpledb_type(query.model)
@@ -313,23 +192,23 @@ module DataMapper
313
192
  selectors = [ :gt, :gte, :lt, :lte, :not, :like, :in ]
314
193
  return (selectors - conditions).size != selectors.size
315
194
  end
195
+
196
+ def database
197
+ options = sdb ? {:sdb_interface => sdb} : {}
198
+ @database ||= SDBTools::Database.new(
199
+ @sdb_options[:access_key],
200
+ @sdb_options[:secret_key],
201
+ options)
202
+ end
316
203
 
317
204
  # Returns an SimpleDB instance to work with
318
205
  def sdb
319
- access_key = @sdb_options[:access_key]
320
- secret_key = @sdb_options[:secret_key]
321
- @sdb ||= RightAws::SdbInterface.new(access_key,secret_key,@sdb_options)
322
- @sdb
206
+ @sdb ||= (@null_mode ? NullSdbInterface.new(logger) : nil)
323
207
  end
324
208
 
325
- def format_log_entry(query, ms = 0)
326
- 'SDB (%.1fs) %s' % [ms, query.squeeze(' ')]
327
- end
328
-
329
209
  def update_consistency_token
330
210
  @current_consistency_token = UUIDTools::UUID.timestamp_create.to_s
331
- sdb.put_attributes(
332
- domain,
211
+ domain.put(
333
212
  '__dm_consistency_token',
334
213
  {'__dm_consistency_token' => [@current_consistency_token]})
335
214
  end
@@ -368,6 +247,92 @@ module DataMapper
368
247
  end
369
248
  end
370
249
 
250
+ # WARNING This method updates +query+ as a side-effect
251
+ def selection_from_query(query)
252
+ query.update(extra_conditions(query))
253
+ where_expression =
254
+ DmAdapterSimpledb::WhereExpression.new(query.conditions, :logger => logger)
255
+ selection_options = {
256
+ :attributes => fields_to_request(query),
257
+ :conditions => where_expression,
258
+ :batch_limit => batch_limit,
259
+ :limit => query_limit(query),
260
+ :logger => logger
261
+ }
262
+ selection_options.merge!(sort_instructions(query))
263
+ selection = domain.selection(selection_options)
264
+ selection.offset = query.offset unless query.offset.nil?
265
+ query.clear
266
+ query.update(:conditions => where_expression.unsupported_conditions)
267
+ selection
268
+ end
269
+
270
+ def transaction(description, &block)
271
+ on_close = SDBTools::Transaction.log_transaction_close(logger)
272
+ SDBTools::Transaction.open(description, on_close, &block)
273
+ end
274
+
275
+ def fields_to_request(query)
276
+ fields = []
277
+ fields.concat(query.fields.map{|f|
278
+ f.field if f.respond_to?(:field)
279
+ }.compact)
280
+ fields.concat(DmAdapterSimpledb::Record::META_KEYS)
281
+ fields.uniq!
282
+ fields
283
+ end
284
+
285
+ def sort_instructions(query)
286
+ direction = first_order_direction(query)
287
+ if direction
288
+ order_by = direction.target.field
289
+ order = case direction.operator
290
+ when :asc then :ascending
291
+ when :desc then :descending
292
+ else raise "Unrecognized sort direction"
293
+ end
294
+ {:order_by => order_by, :order => order}
295
+ else
296
+ {}
297
+ end
298
+ end
299
+
300
+ def extra_conditions(query)
301
+ # SimpleDB requires all sort-by attributes to also be included in a
302
+ # predicate.
303
+ conditions = if (direction = first_order_direction(query))
304
+ { direction.target.field.to_sym.not => nil }
305
+ else
306
+ {}
307
+ end
308
+ table = DmAdapterSimpledb::Table.new(query.model)
309
+ meta_key = DmAdapterSimpledb::Record::METADATA_KEY
310
+
311
+ # The simpledb_type key is deprecated
312
+ old_table_key = DmAdapterSimpledb::Record::STORAGE_NAME_KEY
313
+
314
+ quoted_table_key = SDBTools::Selection.quote_name(old_table_key)
315
+ quoted_key = SDBTools::Selection.quote_name(meta_key)
316
+ conditions.merge!(
317
+ :conditions => [
318
+ "( #{quoted_key} = ? OR #{quoted_table_key} = ? )",
319
+ table.token,
320
+ table.simpledb_type
321
+ ])
322
+ conditions
323
+ end
324
+
325
+ def query_limit(query)
326
+ query.limit.nil? ? :none : query.limit
327
+ end
328
+
329
+ # SimpleDB only supports a single sort-by field. Further sorting has to be
330
+ # handled locally.
331
+ def first_order_direction(query)
332
+ Array(query.order).first
333
+ end
334
+
335
+
371
336
  end # class SimpleDBAdapter
372
337
 
373
338