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.
- data/.autotest +0 -0
- data/.gitignore +8 -0
- data/README +156 -0
- data/Rakefile +77 -0
- data/VERSION +1 -0
- data/aws_config.sample +3 -0
- data/dm-adapter-simpledb.gemspec +99 -0
- data/lib/simpledb_adapter.rb +469 -0
- data/lib/simpledb_adapter/sdb_array.rb +52 -0
- data/scripts/simple_benchmark.rb +84 -0
- data/spec/associations_spec.rb +15 -0
- data/spec/compliance_spec.rb +18 -0
- data/spec/date_spec.rb +51 -0
- data/spec/limit_and_order_spec.rb +110 -0
- data/spec/migrations_spec.rb +41 -0
- data/spec/multiple_records_spec.rb +111 -0
- data/spec/nils_spec.rb +45 -0
- data/spec/sdb_array_spec.rb +71 -0
- data/spec/simpledb_adapter_spec.rb +162 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +67 -0
- data/tasks/devver.rake +167 -0
- metadata +146 -0
data/.autotest
ADDED
File without changes
|
data/.gitignore
ADDED
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
|
+
|
data/Rakefile
ADDED
@@ -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
|
data/aws_config.sample
ADDED
@@ -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
|