dm-adapter-simpledb 1.3.0 → 1.4.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/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