dm-adapter-simpledb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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