dm-adapter-simpledb 1.0.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.
File without changes
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ aws_sdb.log
3
+ aws_config
4
+ coverage
5
+ log/**
6
+ pkg/**
7
+ /THROW_AWAY_SDB_DOMAIN
8
+ /TODO
data/README ADDED
@@ -0,0 +1,156 @@
1
+ = dm-adapter-simpledb
2
+
3
+ == What
4
+
5
+ A DataMapper adapter for Amazon's SimpleDB service.
6
+
7
+ Features:
8
+ * Uses the RightAWS gem for efficient SimpleDB operations.
9
+ * Full set of CRUD operations
10
+ * Supports all DataMapper query predicates.
11
+ * Can translate many queries into efficient native SELECT operations.
12
+ * Migrations
13
+ * DataMapper identity map support for record caching
14
+ * Lazy-loaded attributes
15
+ * DataMapper Serial property support via UUIDs.
16
+ * Array properties
17
+ * Basic aggregation support (Model.count("..."))
18
+ * String "chunking" permits attributes to exceed the 1024-byte limit
19
+
20
+ Note: as of version 1.0.0, this gem supports supports the DataMapper 0.10.*
21
+ series and breaks backwards compatibility with DataMapper 0.9.*.
22
+
23
+ == Who
24
+
25
+ Originally written by Jeremy Boles.
26
+
27
+ Contributers:
28
+ Edward Ocampo-Gooding (edward)
29
+ Dan Mayer (danmayer)
30
+ Thomas Olausson (latompa)
31
+ Avdi Grimm (avdi)
32
+
33
+
34
+ == Where
35
+
36
+ dm-adapter-simpledb is currently maintained by the Devver team and lives at:
37
+ http://github.com/devver/dm-adapter-simpledb/
38
+
39
+ == TODO
40
+
41
+ * Backwards-compatibility option for nils stored as "nil" string
42
+ * More complete handling of NOT conditions in queries
43
+ * Robust quoting in SELECT calls
44
+ * Handle exclusive ranges natively
45
+ Implement as inclusive range + filter step
46
+ * Tests for associations
47
+ * Split up into multiple files
48
+ * Option for smart lexicographical storage for numbers
49
+ - Zero-pad integers
50
+ - Store floats using exponential notation
51
+ * Option to store Date/Time/DateTime as ISO8601
52
+ * Full aggregate support (min/max/etc)
53
+ * Option to use libxml if available
54
+ * Parallelized queries for increased throughput
55
+ * Support of normalized 1:1 table:domain schemes that works with associations
56
+
57
+ == Usage
58
+
59
+ === Standalone
60
+
61
+ require 'rubygems'
62
+ require 'dm-core'
63
+
64
+ DataMapper.setup(:default, 'simpledb://ACCESS_KEY:SECRET_KEY@sdb.amazon.com/DOMAIN')
65
+
66
+ [Same as the following, but skip the database.yml]
67
+
68
+ === In a Merb application
69
+ See sample Merb application using Merb-Auth and protected resources on SimpleDB:
70
+ http://github.com/danmayer/merb-simpledb-dm_example/tree/master
71
+
72
+ Setup database.yml with the SimpleDB DataMapper adapter:
73
+
74
+ adapter: simpledb
75
+ database: 'default'
76
+ access_key: (a 20-character, alphanumeric sequence)
77
+ secret_key: (a 40-character sequence)
78
+ domain: 'my_amazon_sdb_domain'
79
+ base_url: 'http://sdb.amazon.com'
80
+
81
+ Create a model
82
+
83
+ class Tree
84
+ include DataMapper::Resource
85
+
86
+ storage_name "trees" # manually setting the domain
87
+
88
+ property :id, Serial
89
+ property :name, String, :nullable => false
90
+ end
91
+
92
+ Use interactively (with merb -i)
93
+
94
+ $ merb -i
95
+
96
+ maple = Tree.new
97
+ maple.name = "Acer rubrum"
98
+ maple.save
99
+
100
+ all_trees = Tree.all() # calls #read_all
101
+ a_tree = Tree.first(:name => "Acer rubrum")
102
+ yanked_tree = Tree.remote(:name => "Acer rubrum")
103
+
104
+ == Running the tests
105
+ Add these two lines to your .bash_profile as the spec_helper relies on them
106
+
107
+ $ export AMAZON_ACCESS_KEY_ID='YOUR_ACCESS_KEY'
108
+ $ export AMAZON_SECRET_ACCESS_KEY='YOUR_SECRET_ACCESS_KEY'
109
+
110
+ Configure the domain to use for integration tests. THIS DOMAIN WILL BE
111
+ DELETED AND RECREATED BY THE TESTS, so do not choose a domain which contains
112
+ data you care about. Configure the domain by creating a file named
113
+ THROW_AWAY_SDB_DOMAIN in the projet root:
114
+
115
+ $ echo dm_simpledb_adapter_test > THROW_AWAY_SDB_DOMAIN
116
+
117
+ Run the tests:
118
+
119
+ rake spec
120
+
121
+ NOTE: While every attempt has been made to make the tests robust, Amazon
122
+ SimpleDB is by it's nature an unreliable service. Sometimes it can take a
123
+ very long time for updates to be reflected by queries, and sometimes calls
124
+ just time out. If the tests fail, try them again a few times before reporting
125
+ it as a bug. Also try running the spec files individually.
126
+
127
+ == Bibliography
128
+
129
+ Relating to Amazon SimpleDB
130
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1292&ref=featured
131
+ Approaching SimpleDB from a relational database background
132
+
133
+ Active Record Persistence with Amazon SimpleDB
134
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1367&categoryID=152
135
+
136
+ Building for Performance and Reliability with Amazon SimpleDB
137
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1394&categoryID=152
138
+
139
+ Query 101: Building Amazon SimpleDB Queries
140
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1231&categoryID=152
141
+
142
+ Query 201: Tips & Tricks for Amazon SimpleDB Query
143
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1232&categoryID=152
144
+ Latter portion describes parallelization advantages of normalized domains – the
145
+ downside being the added complexity at the application layer (this library’s).
146
+
147
+ Using SimpleDB and Rails in No Time with ActiveResource
148
+ http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1242&categoryID=152
149
+ Exemplifies using the Single Table Inheritance pattern within a single SimpleDB
150
+ domain by storing the model type in an attribute called '_resource' and using a
151
+ “SHA512 hash function on the request body combined with a timestamp and a
152
+ configurable salt” for the id.
153
+
154
+ RightScale Ruby library to access Amazon EC2, S3, SQS, and SDB
155
+ http://developer.amazonwebservices.com/connect/entry!default.jspa?categoryID=140&externalID=1014&fromSearchPage=true
156
+
@@ -0,0 +1,77 @@
1
+ require 'spec'
2
+ require 'spec/rake/spectask'
3
+ require 'pathname'
4
+ load 'tasks/devver.rake'
5
+
6
+ ROOT = Pathname(__FILE__).dirname.expand_path
7
+ require ROOT + 'lib/simpledb_adapter'
8
+
9
+ task :default => [ :spec ]
10
+
11
+ desc 'Run specifications'
12
+ Spec::Rake::SpecTask.new(:spec) do |t|
13
+ if File.exists?('spec/spec.opts')
14
+ t.spec_opts << '--options' << 'spec/spec.opts'
15
+ end
16
+ t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s)
17
+
18
+ begin
19
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
20
+ t.rcov_opts << '--exclude' << 'spec'
21
+ t.rcov_opts << '--text-summary'
22
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
23
+ rescue Exception
24
+ # rcov not installed
25
+ end
26
+ end
27
+
28
+ desc 'Run specifications without Rcov'
29
+ Spec::Rake::SpecTask.new(:spec_no_rcov) do |t|
30
+ if File.exists?('spec/spec.opts')
31
+ t.spec_opts << '--options' << 'spec/spec.opts'
32
+ end
33
+ t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s)
34
+ end
35
+
36
+ begin
37
+ require 'jeweler'
38
+ Jeweler::Tasks.new do |gem|
39
+ gem.name = "dm-adapter-simpledb"
40
+ gem.summary = "DataMapper adapter for Amazon SimpleDB"
41
+ gem.email = "devs@devver.net"
42
+ gem.homepage = "http://github.com/devver/dm-adapter-simpledb"
43
+ gem.description = <<END
44
+ A DataMapper adapter for Amazon's SimpleDB service.
45
+
46
+ Features:
47
+ * Uses the RightAWS gem for efficient SimpleDB operations.
48
+ * Full set of CRUD operations
49
+ * Supports all DataMapper query predicates.
50
+ * Can translate many queries into efficient native SELECT operations.
51
+ * Migrations
52
+ * DataMapper identity map support for record caching
53
+ * Lazy-loaded attributes
54
+ * DataMapper Serial property support via UUIDs.
55
+ * Array properties
56
+ * Basic aggregation support (Model.count("..."))
57
+ * String "chunking" permits attributes to exceed the 1024-byte limit
58
+
59
+ Note: as of version 1.0.0, this gem supports supports the DataMapper 0.10.*
60
+ series and breaks backwards compatibility with DataMapper 0.9.*.
61
+ END
62
+ gem.authors = [
63
+ "Jeremy Boles",
64
+ "Edward Ocampo-Gooding",
65
+ "Dan Mayer",
66
+ "Thomas Olausson",
67
+ "Avdi Grimm"
68
+ ]
69
+ gem.add_dependency('dm-core', '~> 0.10.0')
70
+ gem.add_dependency('dm-aggregates', '~> 0.10.0')
71
+ gem.add_dependency('uuidtools', '~> 2.0')
72
+ gem.add_dependency('right_aws', '~> 1.10')
73
+ end
74
+ Jeweler::GemcutterTasks.new
75
+ rescue LoadError
76
+ puts "Jeweler, or one of it's dependencies, is not available."
77
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,3 @@
1
+ 1_YOUR_ACCESS_KEY_2
2
+ I_YOUR_SECRET_KEY_0
3
+
@@ -0,0 +1,99 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{dm-adapter-simpledb}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jeremy Boles", "Edward Ocampo-Gooding", "Dan Mayer", "Thomas Olausson", "Avdi Grimm"]
12
+ s.date = %q{2009-11-16}
13
+ s.description = %q{A DataMapper adapter for Amazon's SimpleDB service.
14
+
15
+ Features:
16
+ * Uses the RightAWS gem for efficient SimpleDB operations.
17
+ * Full set of CRUD operations
18
+ * Supports all DataMapper query predicates.
19
+ * Can translate many queries into efficient native SELECT operations.
20
+ * Migrations
21
+ * DataMapper identity map support for record caching
22
+ * Lazy-loaded attributes
23
+ * DataMapper Serial property support via UUIDs.
24
+ * Array properties
25
+ * Basic aggregation support (Model.count("..."))
26
+ * String "chunking" permits attributes to exceed the 1024-byte limit
27
+
28
+ Note: as of version 1.0.0, this gem supports supports the DataMapper 0.10.*
29
+ series and breaks backwards compatibility with DataMapper 0.9.*.
30
+ }
31
+ s.email = %q{devs@devver.net}
32
+ s.extra_rdoc_files = [
33
+ "README"
34
+ ]
35
+ s.files = [
36
+ ".autotest",
37
+ ".gitignore",
38
+ "README",
39
+ "Rakefile",
40
+ "VERSION",
41
+ "aws_config.sample",
42
+ "dm-adapter-simpledb.gemspec",
43
+ "lib/simpledb_adapter.rb",
44
+ "lib/simpledb_adapter/sdb_array.rb",
45
+ "scripts/simple_benchmark.rb",
46
+ "spec/associations_spec.rb",
47
+ "spec/compliance_spec.rb",
48
+ "spec/date_spec.rb",
49
+ "spec/limit_and_order_spec.rb",
50
+ "spec/migrations_spec.rb",
51
+ "spec/multiple_records_spec.rb",
52
+ "spec/nils_spec.rb",
53
+ "spec/sdb_array_spec.rb",
54
+ "spec/simpledb_adapter_spec.rb",
55
+ "spec/spec.opts",
56
+ "spec/spec_helper.rb",
57
+ "tasks/devver.rake"
58
+ ]
59
+ s.homepage = %q{http://github.com/devver/dm-adapter-simpledb}
60
+ s.rdoc_options = ["--charset=UTF-8"]
61
+ s.require_paths = ["lib"]
62
+ s.rubygems_version = %q{1.3.5}
63
+ s.summary = %q{DataMapper adapter for Amazon SimpleDB}
64
+ s.test_files = [
65
+ "spec/nils_spec.rb",
66
+ "spec/limit_and_order_spec.rb",
67
+ "spec/compliance_spec.rb",
68
+ "spec/simpledb_adapter_spec.rb",
69
+ "spec/date_spec.rb",
70
+ "spec/sdb_array_spec.rb",
71
+ "spec/migrations_spec.rb",
72
+ "spec/spec_helper.rb",
73
+ "spec/multiple_records_spec.rb",
74
+ "spec/associations_spec.rb"
75
+ ]
76
+
77
+ if s.respond_to? :specification_version then
78
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
79
+ s.specification_version = 3
80
+
81
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
82
+ s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.0"])
83
+ s.add_runtime_dependency(%q<dm-aggregates>, ["~> 0.10.0"])
84
+ s.add_runtime_dependency(%q<uuidtools>, ["~> 2.0"])
85
+ s.add_runtime_dependency(%q<right_aws>, ["~> 1.10"])
86
+ else
87
+ s.add_dependency(%q<dm-core>, ["~> 0.10.0"])
88
+ s.add_dependency(%q<dm-aggregates>, ["~> 0.10.0"])
89
+ s.add_dependency(%q<uuidtools>, ["~> 2.0"])
90
+ s.add_dependency(%q<right_aws>, ["~> 1.10"])
91
+ end
92
+ else
93
+ s.add_dependency(%q<dm-core>, ["~> 0.10.0"])
94
+ s.add_dependency(%q<dm-aggregates>, ["~> 0.10.0"])
95
+ s.add_dependency(%q<uuidtools>, ["~> 2.0"])
96
+ s.add_dependency(%q<right_aws>, ["~> 1.10"])
97
+ end
98
+ end
99
+
@@ -0,0 +1,469 @@
1
+ require 'rubygems'
2
+ require 'dm-core'
3
+ require 'digest/sha1'
4
+ require 'dm-aggregates'
5
+ require 'right_aws'
6
+ require 'uuidtools'
7
+ require File.expand_path('simpledb_adapter/sdb_array', File.dirname(__FILE__))
8
+
9
+ module DataMapper
10
+
11
+ module Migrations
12
+ #integrated from http://github.com/edward/dm-simpledb/tree/master
13
+ module SimpledbAdapter
14
+
15
+ module ClassMethods
16
+
17
+ end
18
+
19
+ def self.included(other)
20
+ other.extend ClassMethods
21
+
22
+ DataMapper.extend(::DataMapper::Migrations::SingletonMethods)
23
+
24
+ [ :Repository, :Model ].each do |name|
25
+ ::DataMapper.const_get(name).send(:include, Migrations.const_get(name))
26
+ end
27
+ end
28
+
29
+ # Returns whether the storage_name exists.
30
+ # @param storage_name<String> a String defining the name of a domain
31
+ # @return <Boolean> true if the storage exists
32
+ def storage_exists?(storage_name)
33
+ domains = sdb.list_domains[:domains]
34
+ domains.detect {|d| d == storage_name }!=nil
35
+ end
36
+
37
+ def create_model_storage(model)
38
+ sdb.create_domain(@sdb_options[:domain])
39
+ end
40
+
41
+ #On SimpleDB you probably don't want to destroy the whole domain
42
+ #if you are just adding fields it is automatically supported
43
+ #default to non destructive migrate, to destroy run
44
+ #rake db:automigrate destroy=true
45
+ def destroy_model_storage(model)
46
+ if ENV['destroy']!=nil && ENV['destroy']=='true'
47
+ sdb.delete_domain(@sdb_options[:domain])
48
+ end
49
+ end
50
+
51
+ end # module Migration
52
+ end # module Migration
53
+
54
+ module Adapters
55
+ class SimpleDBAdapter < AbstractAdapter
56
+
57
+ attr_reader :sdb_options
58
+
59
+ # For testing purposes ONLY. Seriously, don't enable this for production
60
+ # code.
61
+ attr_accessor :consistency_policy
62
+
63
+ def initialize(name, normalised_options)
64
+ super
65
+ @sdb_options = {}
66
+ @sdb_options[:access_key] = options.fetch(:access_key) {
67
+ options[:user]
68
+ }
69
+ @sdb_options[:secret_key] = options.fetch(:secret_key) {
70
+ options[:password]
71
+ }
72
+ @sdb_options[:logger] = options.fetch(:logger) { DataMapper.logger }
73
+ @sdb_options[:server] = options.fetch(:host) { 'sdb.amazonaws.com' }
74
+ @sdb_options[:port] = options[:port] || 443 # port may be set but nil
75
+ @sdb_options[:domain] = options.fetch(:domain) {
76
+ options[:path].to_s.gsub(%r{(^/+)|(/+$)},"") # remove slashes
77
+ }
78
+ @consistency_policy =
79
+ normalised_options.fetch(:wait_for_consistency) { false }
80
+ end
81
+
82
+ def create(resources)
83
+ created = 0
84
+ time = Benchmark.realtime do
85
+ resources.each do |resource|
86
+ uuid = UUIDTools::UUID.timestamp_create
87
+ initialize_serial(resource, uuid.to_i)
88
+ item_name = item_name_for_resource(resource)
89
+ sdb_type = simpledb_type(resource.model)
90
+ attributes = resource.attributes.merge(:simpledb_type => sdb_type)
91
+ attributes = adjust_to_sdb_attributes(attributes)
92
+ attributes.reject!{|name, value| value.nil?}
93
+ sdb.put_attributes(domain, item_name, attributes)
94
+ created += 1
95
+ end
96
+ end
97
+ DataMapper.logger.debug(format_log_entry("(#{created}) INSERT #{resources.inspect}", time))
98
+ modified!
99
+ created
100
+ end
101
+
102
+ def delete(collection)
103
+ deleted = 0
104
+ time = Benchmark.realtime do
105
+ collection.each do |resource|
106
+ item_name = item_name_for_resource(resource)
107
+ sdb.delete_attributes(domain, item_name)
108
+ deleted += 1
109
+ end
110
+ raise NotImplementedError.new('Only :eql on delete at the moment') if not_eql_query?(collection.query)
111
+ end; DataMapper.logger.debug(format_log_entry("(#{deleted}) DELETE #{collection.query.conditions.inspect}", time))
112
+ modified!
113
+ deleted
114
+ end
115
+
116
+ def read(query)
117
+ maybe_wait_for_consistency
118
+ sdb_type = simpledb_type(query.model)
119
+
120
+ conditions, order, unsupported_conditions =
121
+ set_conditions_and_sort_order(query, sdb_type)
122
+ results = get_results(query, conditions, order)
123
+ proto_resources = results.map do |result|
124
+ name, attributes = *result.to_a.first
125
+ proto_resource = query.fields.inject({}) do |proto_resource, property|
126
+ value = attributes[property.field.to_s]
127
+ if value != nil
128
+ if value.size > 1
129
+ if property.type == String
130
+ value = chunks_to_string(value)
131
+ else
132
+ value = value.map {|v| property.typecast(v) }
133
+ end
134
+ else
135
+ value = property.typecast(value.first)
136
+ end
137
+ else
138
+ value = property.typecast(nil)
139
+ end
140
+ proto_resource[property.name.to_s] = value
141
+ proto_resource
142
+ end
143
+ proto_resource
144
+ end
145
+ query.conditions.operands.reject!{ |op|
146
+ !unsupported_conditions.include?(op)
147
+ }
148
+ records = query.filter_records(proto_resources)
149
+
150
+ records
151
+ end
152
+
153
+ def update(attributes, collection)
154
+ updated = 0
155
+ attrs_to_update, attrs_to_delete = prepare_attributes(attributes)
156
+ time = Benchmark.realtime do
157
+ collection.each do |resource|
158
+ item_name = item_name_for_resource(resource)
159
+ unless attrs_to_update.empty?
160
+ sdb.put_attributes(domain, item_name, attrs_to_update, :replace)
161
+ end
162
+ unless attrs_to_delete.empty?
163
+ sdb.delete_attributes(domain, item_name, attrs_to_delete)
164
+ end
165
+ updated += 1
166
+ end
167
+ raise NotImplementedError.new('Only :eql on delete at the moment') if not_eql_query?(collection.query)
168
+ end
169
+ DataMapper.logger.debug(format_log_entry("UPDATE #{collection.query.conditions.inspect} (#{updated} times)", time))
170
+ modified!
171
+ updated
172
+ end
173
+
174
+ def query(query_call, query_limit = 999999999)
175
+ select(query_call, query_limit).collect{|x| x.values[0]}
176
+ end
177
+
178
+ def aggregate(query)
179
+ raise ArgumentError.new("Only count is supported") unless (query.fields.first.operator == :count)
180
+ sdb_type = simpledb_type(query.model)
181
+ conditions, order, unsupported_conditions = set_conditions_and_sort_order(query, sdb_type)
182
+
183
+ query_call = "SELECT count(*) FROM #{domain} "
184
+ query_call << "WHERE #{conditions.compact.join(' AND ')}" if conditions.length > 0
185
+ results = nil
186
+ time = Benchmark.realtime do
187
+ results = sdb.select(query_call)
188
+ end; DataMapper.logger.debug(format_log_entry(query_call, time))
189
+ [results[:items][0].values.first["Count"].first.to_i]
190
+ end
191
+
192
+ # For testing purposes only.
193
+ def wait_for_consistency
194
+ return unless @current_consistency_token
195
+ token = :none
196
+ begin
197
+ results = sdb.get_attributes(domain, '__dm_consistency_token', '__dm_consistency_token')
198
+ tokens = results[:attributes]['__dm_consistency_token']
199
+ end until tokens.include?(@current_consistency_token)
200
+ end
201
+
202
+ private
203
+
204
+ # hack for converting and storing strings longer than 1024 one thing to
205
+ # note if you use string longer than 1019 chars you will loose the ability
206
+ # to do full text matching on queries as the string can be broken at any
207
+ # place during chunking
208
+ def adjust_to_sdb_attributes(attrs)
209
+ attrs.each_pair do |key, value|
210
+ if value.kind_of?(String)
211
+ # Strings need to be inside arrays in order to prevent RightAws from
212
+ # inadvertantly splitting them on newlines when it calls
213
+ # Array(value).
214
+ attrs[key] = [value]
215
+ end
216
+ if value.is_a?(String) && value.length > 1019
217
+ chunked = string_to_chunks(value)
218
+ attrs[key] = chunked
219
+ end
220
+ end
221
+ attrs
222
+ end
223
+
224
+ def string_to_chunks(value)
225
+ chunks = value.to_s.scan(%r/.{1,1019}/) # 1024 - '1024:'.size
226
+ i = -1
227
+ fmt = '%04d:'
228
+ chunks.map!{|chunk| [(fmt % (i += 1)), chunk].join}
229
+ raise ArgumentError, 'that is just too big yo!' if chunks.size >= 256
230
+ chunks
231
+ end
232
+
233
+ def chunks_to_string(value)
234
+ begin
235
+ chunks =
236
+ Array(value).flatten.map do |chunk|
237
+ index, text = chunk.split(%r/:/, 2)
238
+ [Float(index).to_i, text]
239
+ end
240
+ chunks.replace chunks.sort_by{|index, text| index}
241
+ string_result = chunks.map!{|index, text| text}.join
242
+ string_result
243
+ rescue ArgumentError, TypeError
244
+ #return original value, they could have put strings in the system not using the adapter or previous versions
245
+ #that are larger than chunk size, but less than 1024
246
+ value
247
+ end
248
+ end
249
+
250
+ # Returns the domain for the model
251
+ def domain
252
+ @sdb_options[:domain]
253
+ end
254
+
255
+ #sets the conditions and order for the SDB query
256
+ def set_conditions_and_sort_order(query, sdb_type)
257
+ unsupported_conditions = []
258
+ conditions = ["simpledb_type = '#{sdb_type}'"]
259
+ # look for query.order.first and insure in conditions
260
+ # raise if order if greater than 1
261
+
262
+ if query.order && query.order.length > 0
263
+ query_object = query.order[0]
264
+ #anything sorted on must be a condition for SDB
265
+ conditions << "#{query_object.target.name} IS NOT NULL"
266
+ order = "ORDER BY #{query_object.target.name} #{query_object.operator}"
267
+ else
268
+ order = ""
269
+ end
270
+ query.conditions.each do |op|
271
+ case op.slug
272
+ when :regexp
273
+ unsupported_conditions << op
274
+ when :eql
275
+ conditions << if op.value.nil?
276
+ "#{op.subject.name} IS NULL"
277
+ else
278
+ "#{op.subject.name} = '#{op.value}'"
279
+ end
280
+ when :not then
281
+ comp = op.operands.first
282
+ if comp.slug == :like
283
+ conditions << "#{comp.subject.name} not like '#{comp.value}'"
284
+ next
285
+ end
286
+ case comp.value
287
+ when Range, Set, Array, Regexp
288
+ unsupported_conditions << op
289
+ when nil
290
+ conditions << "#{comp.subject.name} IS NOT NULL"
291
+ else
292
+ conditions << "#{comp.subject.name} != '#{comp.value}'"
293
+ end
294
+ when :gt then conditions << "#{op.subject.name} > '#{op.value}'"
295
+ when :gte then conditions << "#{op.subject.name} >= '#{op.value}'"
296
+ when :lt then conditions << "#{op.subject.name} < '#{op.value}'"
297
+ when :lte then conditions << "#{op.subject.name} <= '#{op.value}'"
298
+ when :like then conditions << "#{op.subject.name} like '#{op.value}'"
299
+ when :in
300
+ case op.value
301
+ when Array, Set
302
+ values = op.value.collect{|v| "'#{v}'"}.join(',')
303
+ values = "'__NULL__'" if values.empty?
304
+ conditions << "#{op.subject.name} IN (#{values})"
305
+ when Range
306
+ if op.value.exclude_end?
307
+ unsupported_conditions << op
308
+ else
309
+ conditions << "#{op.subject.name} between '#{op.value.first}' and '#{op.value.last}'"
310
+ end
311
+ else
312
+ raise ArgumentError, "Unsupported inclusion op: #{op.value.inspect}"
313
+ end
314
+ else raise "Invalid query op: #{op.inspect}"
315
+ end
316
+ end
317
+ [conditions,order,unsupported_conditions]
318
+ end
319
+
320
+ def select(query_call, query_limit)
321
+ items = []
322
+ time = Benchmark.realtime do
323
+ sdb_continuation_key = nil
324
+ while (results = sdb.select(query_call, sdb_continuation_key)) do
325
+ sdb_continuation_key = results[:next_token]
326
+ items += results[:items]
327
+ break if items.length > query_limit
328
+ break if sdb_continuation_key.nil?
329
+ end
330
+ end; DataMapper.logger.debug(format_log_entry(query_call, time))
331
+ items[0...query_limit]
332
+ end
333
+
334
+ #gets all results or proper number of results depending on the :limit
335
+ def get_results(query, conditions, order)
336
+ output_list = query.fields.map{|f| f.field}.join(', ')
337
+ query_call = "SELECT #{output_list} FROM #{domain} "
338
+ query_call << "WHERE #{conditions.compact.join(' AND ')}" if conditions.length > 0
339
+ query_call << " #{order}"
340
+ if query.limit!=nil
341
+ query_limit = query.limit
342
+ query_call << " LIMIT #{query.limit}"
343
+ else
344
+ #on large items force the max limit
345
+ query_limit = 999999999 #TODO hack for query.limit being nil
346
+ #query_call << " limit 2500" #this doesn't work with continuation keys as it halts at the limit passed not just a limit per query.
347
+ end
348
+ records = select(query_call, query_limit)
349
+ end
350
+
351
+ # Creates an item name for a query
352
+ def item_name_for_query(query)
353
+ sdb_type = simpledb_type(query.model)
354
+
355
+ item_name = "#{sdb_type}+"
356
+ keys = keys_for_model(query.model)
357
+ conditions = query.conditions.sort {|a,b| a[1].name.to_s <=> b[1].name.to_s }
358
+ item_name += conditions.map do |property|
359
+ property[2].to_s
360
+ end.join('-')
361
+ Digest::SHA1.hexdigest(item_name)
362
+ end
363
+
364
+ # Creates an item name for a resource
365
+ def item_name_for_resource(resource)
366
+ sdb_type = simpledb_type(resource.model)
367
+
368
+ item_name = "#{sdb_type}+"
369
+ keys = keys_for_model(resource.model)
370
+ item_name += keys.map do |property|
371
+ property.get(resource)
372
+ end.join('-')
373
+
374
+ Digest::SHA1.hexdigest(item_name)
375
+ end
376
+
377
+ # Returns the keys for model sorted in alphabetical order
378
+ def keys_for_model(model)
379
+ model.key(self.name).sort {|a,b| a.name.to_s <=> b.name.to_s }
380
+ end
381
+
382
+ def not_eql_query?(query)
383
+ # Curosity check to make sure we are only dealing with a delete
384
+ conditions = query.conditions.map {|c| c.slug }.uniq
385
+ selectors = [ :gt, :gte, :lt, :lte, :not, :like, :in ]
386
+ return (selectors - conditions).size != selectors.size
387
+ end
388
+
389
+ # Returns an SimpleDB instance to work with
390
+ def sdb
391
+ access_key = @sdb_options[:access_key]
392
+ secret_key = @sdb_options[:secret_key]
393
+ @sdb ||= RightAws::SdbInterface.new(access_key,secret_key,@sdb_options)
394
+ @sdb
395
+ end
396
+
397
+ # Returns a string so we know what type of
398
+ def simpledb_type(model)
399
+ model.storage_name(model.repository.name)
400
+ end
401
+
402
+ def format_log_entry(query, ms = 0)
403
+ 'SDB (%.1fs) %s' % [ms, query.squeeze(' ')]
404
+ end
405
+
406
+ def prepare_attributes(attributes)
407
+ attributes = attributes.to_a.map {|a| [a.first.name.to_s, a.last]}.to_hash
408
+ attributes = adjust_to_sdb_attributes(attributes)
409
+ updates, deletes = attributes.partition{|name,value|
410
+ !value.nil? && !(value.respond_to?(:to_ary) && value.to_ary.empty?)
411
+ }
412
+ attrs_to_update = Hash[updates]
413
+ attrs_to_delete = Hash[deletes].keys
414
+ [attrs_to_update, attrs_to_delete]
415
+ end
416
+
417
+ def update_consistency_token
418
+ @current_consistency_token = UUIDTools::UUID.timestamp_create.to_s
419
+ sdb.put_attributes(
420
+ domain,
421
+ '__dm_consistency_token',
422
+ {'__dm_consistency_token' => [@current_consistency_token]})
423
+ end
424
+
425
+ def maybe_wait_for_consistency
426
+ if consistency_policy == :automatic && @current_consistency_token
427
+ wait_for_consistency
428
+ end
429
+ end
430
+
431
+ # SimpleDB supports "eventual consistency", which mean your data will be
432
+ # there... eventually. Obviously this can make tests a little flaky. One
433
+ # option is to just wait a fixed amount of time after every write, but
434
+ # this can quickly add up to a lot of waiting. The strategy implemented
435
+ # here is based on the theory that while consistency is only eventual,
436
+ # chances are writes will at least be linear. That is, once the results of
437
+ # write #2 show up we can probably assume that the results of write #1 are
438
+ # in as well.
439
+ #
440
+ # When a consistency policy is enabled, the adapter writes a new unique
441
+ # "consistency token" to the database after every write (i.e. every
442
+ # create, update, or delete). If the policy is :manual, it only writes the
443
+ # consistency token. If the policy is :automatic, writes will not return
444
+ # until the token has been successfully read back.
445
+ #
446
+ # When waiting for the consistency token to show up, we use progressively
447
+ # longer timeouts until finally giving up and raising an exception.
448
+ def modified!
449
+ case @consistency_policy
450
+ when :manual, :automatic then
451
+ update_consistency_token
452
+ when false then
453
+ # do nothing
454
+ else
455
+ raise "Invalid :wait_for_consistency option: #{@consistency_policy.inspect}"
456
+ end
457
+ end
458
+
459
+ end # class SimpleDBAdapter
460
+
461
+ # Required naming scheme.
462
+ SimpledbAdapter = SimpleDBAdapter
463
+
464
+ const_added(:SimpledbAdapter)
465
+
466
+ end # module Adapters
467
+
468
+
469
+ end # module DataMapper