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 +59 -0
- data/README +4 -5
- data/Rakefile +1 -2
- data/VERSION +1 -1
- data/lib/dm-adapter-simpledb.rb +2 -1
- data/lib/dm-adapter-simpledb/adapters/simpledb_adapter.rb +154 -189
- data/lib/dm-adapter-simpledb/rake.rb +0 -41
- data/lib/dm-adapter-simpledb/record.rb +1 -1
- data/lib/dm-adapter-simpledb/table.rb +2 -1
- data/lib/dm-adapter-simpledb/utils.rb +44 -0
- data/lib/dm-adapter-simpledb/where_expression.rb +180 -0
- data/scripts/console +23 -0
- data/scripts/limits_benchmark +51 -0
- data/scripts/union_benchmark +39 -0
- data/spec/integration/compliance_spec.rb +1 -0
- data/spec/integration/limit_and_order_spec.rb +1 -1
- data/spec/integration/simpledb_adapter_spec.rb +4 -22
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/unit/dm_adapter_simpledb/where_expression_spec.rb +368 -0
- data/spec/unit/simpledb_adapter_spec.rb +3 -2
- data/spec/unit/unit_spec_helper.rb +1 -0
- metadata +9 -4
@@ -1,43 +1,2 @@
|
|
1
1
|
namespace :simpledb do
|
2
|
-
desc "Migrate records to be compatable with current DM/SimpleDB adapter"
|
3
|
-
task :migrate, :domain do |t, args|
|
4
|
-
raise "THIS IS A WORK IN PROGRESS AND WILL DESTROY YOUR DATA"
|
5
|
-
require 'progressbar'
|
6
|
-
require 'right_aws'
|
7
|
-
require 'dm-adapter-simpledb/record'
|
8
|
-
|
9
|
-
puts "Initializing connection..."
|
10
|
-
domain = args.domain
|
11
|
-
sdb = RightAws::SdbInterface.new
|
12
|
-
puts "Counting records..."
|
13
|
-
num_legacy_records = 0
|
14
|
-
query = "select count(*) from #{domain} where (simpledb_type is not null) and (__dm_metadata is null)"
|
15
|
-
next_token = nil
|
16
|
-
while(results = sdb.select(query, next_token)) do
|
17
|
-
next_token = results[:next_token]
|
18
|
-
count = results[:items].first["Domain"]["Count"].first.to_i
|
19
|
-
num_legacy_records += count
|
20
|
-
break if next_token.nil?
|
21
|
-
end
|
22
|
-
puts "Found #{num_legacy_records} to migrate"
|
23
|
-
|
24
|
-
pbar = ProgressBar.new("migrate", num_legacy_records)
|
25
|
-
query = "select * from #{domain} where (simpledb_type is not null) and (__dm_metadata is null)"
|
26
|
-
while(results = sdb.select(query, next_token)) do
|
27
|
-
next_token = results[:next_token]
|
28
|
-
items = results[:items]
|
29
|
-
items.each do |item|
|
30
|
-
legacy_record = DmAdapterSimpledb::Record.from_simpledb_hash(item)
|
31
|
-
new_record = legacy_record.migrate
|
32
|
-
updates = new_record.writable_attributes
|
33
|
-
deletes = new_record.deletable_attributes
|
34
|
-
sdb.put_attributes(domain, new_record.item_name, updates)
|
35
|
-
sdb.delete_attributes(domain, new_record.item_name, deletes)
|
36
|
-
pbar.inc
|
37
|
-
end
|
38
|
-
break if next_token.nil?
|
39
|
-
end
|
40
|
-
pbar.finish
|
41
|
-
|
42
|
-
end
|
43
2
|
end
|
@@ -20,8 +20,9 @@ module DmAdapterSimpledb
|
|
20
20
|
|
21
21
|
# Returns a string so we know what type of
|
22
22
|
def simpledb_type
|
23
|
-
model.storage_name(
|
23
|
+
model.storage_name(DataMapper.repository.name)
|
24
24
|
end
|
25
|
+
alias_method :storage_name, :simpledb_type
|
25
26
|
|
26
27
|
def repository_name
|
27
28
|
# TODO this should probably take into account the adapter
|
@@ -1,5 +1,49 @@
|
|
1
1
|
module DmAdapterSimpledb
|
2
2
|
module Utils
|
3
|
+
class NullObject
|
4
|
+
def method_missing(*args, &block)
|
5
|
+
self
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class NullSdbInterface
|
10
|
+
def initialize(logger=NullObject.new)
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def select(*args, &block)
|
15
|
+
@logger.debug "[SELECT] #{args.inspect}"
|
16
|
+
{
|
17
|
+
:items => []
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_attributes(*args, &block)
|
22
|
+
@logger.debug "[GET_ATTRIBUTES] #{args.inspect}"
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
def list_domains(*args, &block)
|
27
|
+
@logger.debug "[LIST_DOMAINS] #{args.inspect}"
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
|
31
|
+
def put_attributes(*args, &block)
|
32
|
+
@logger.debug "[PUT_ATTRIBUTES] #{args.inspect}"
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_attributes(*args, &block)
|
37
|
+
@logger.debug "[DELETE_ATTRIBUTES] #{args.inspect}"
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_domain(*args, &block)
|
42
|
+
@logger.debug "[CREATE_DOMAIN] #{args.inspect}"
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
3
47
|
def transform_hash(original, options={}, &block)
|
4
48
|
original.inject({}){|result, (key,value)|
|
5
49
|
value = if (options[:deep] && Hash === value)
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module DmAdapterSimpledb
|
2
|
+
class WhereExpression
|
3
|
+
include DataMapper::Query::Conditions
|
4
|
+
|
5
|
+
attr_reader :conditions
|
6
|
+
attr_accessor :logger
|
7
|
+
|
8
|
+
def initialize(conditions, options={})
|
9
|
+
@conditions = conditions
|
10
|
+
@logger = options.fetch(:logger){ DataMapper.logger }
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
node_to_expression(conditions)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsupported_conditions(node=conditions, top=true)
|
18
|
+
case node
|
19
|
+
when InclusionComparison
|
20
|
+
if node.value.is_a?(Range) && node.value.exclude_end?
|
21
|
+
logger.warn "Exclusive ranges are not supported natively by the SimpleDB adapter"
|
22
|
+
node.dup
|
23
|
+
end
|
24
|
+
when RegexpComparison
|
25
|
+
logger.warn "Regexp comparisons are not supported natively by the SimpleDB adapter"
|
26
|
+
node.dup
|
27
|
+
when AbstractOperation
|
28
|
+
op_copy = node.dup
|
29
|
+
op_copy.clear
|
30
|
+
operands_copy =
|
31
|
+
node.operands.map{|o| unsupported_conditions(o,false)}.compact
|
32
|
+
if operands_copy.size > 0
|
33
|
+
op_copy.operands.merge(operands_copy)
|
34
|
+
op_copy
|
35
|
+
else
|
36
|
+
top ? Operation.new(:and) : nil
|
37
|
+
end
|
38
|
+
else top ? Operation.new(:and) : nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def node_to_expression(node)
|
45
|
+
case node
|
46
|
+
when AbstractOperation then operation_to_expression(node)
|
47
|
+
when AbstractComparison then comparison_to_expression(node)
|
48
|
+
when Array then array_to_expression(node)
|
49
|
+
when nil then nil
|
50
|
+
else raise NotImplementedError, "Unrecognized node: #{node.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def operation_to_expression(operation)
|
55
|
+
case operation
|
56
|
+
when NotOperation
|
57
|
+
operand = operation.sorted_operands.first # NOT only allowed to have 1 operand
|
58
|
+
if operand.is_a?(EqualToComparison)
|
59
|
+
if operand.value.nil?
|
60
|
+
comparison_to_expression(operand, "IS NOT")
|
61
|
+
else
|
62
|
+
comparison_to_expression(operand, "!=")
|
63
|
+
end
|
64
|
+
elsif empty_inclusion?(operand)
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
"NOT #{node_to_expression(operand)}"
|
68
|
+
end
|
69
|
+
when AndOperation then
|
70
|
+
join_operands(operation, "AND")
|
71
|
+
when OrOperation then
|
72
|
+
join_operands(operation, "OR")
|
73
|
+
when nil then nil
|
74
|
+
else raise NotImplementedError, "Unhandled operation: #{operation.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def join_operands(operation, delimiter)
|
79
|
+
operands = operation.sorted_operands
|
80
|
+
joined_operands = Array(operands).map{|o|
|
81
|
+
catch(:unsupported) { node_to_expression(o) }
|
82
|
+
}.compact.join(" #{delimiter} ")
|
83
|
+
if operation.parent
|
84
|
+
"( #{joined_operands} )"
|
85
|
+
else
|
86
|
+
joined_operands
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def comparison_to_expression(
|
91
|
+
comparison,
|
92
|
+
operator=comparison_operator(comparison),
|
93
|
+
values = value_to_expression(comparison.value))
|
94
|
+
field = SDBTools::Selection.quote_name(comparison.subject.field)
|
95
|
+
if empty_inclusion?(comparison)
|
96
|
+
"#{field} IS NULL"
|
97
|
+
else
|
98
|
+
"#{field} #{operator} #{values}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def array_to_expression(array)
|
103
|
+
template, *replacements = *array.dup
|
104
|
+
if replacements.size == 1 && replacements.first.is_a?(Array)
|
105
|
+
replacements = replacements.first
|
106
|
+
end
|
107
|
+
if replacements.size == 1 && replacements[0].is_a?(Hash)
|
108
|
+
fill_template_from_hash(template, replacements[0])
|
109
|
+
else
|
110
|
+
fill_template_from_array(template, replacements)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def fill_template_from_array(template, replacements)
|
115
|
+
template.to_s.gsub("?") {|match| quote_value(replacements.shift.to_s) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def fill_template_from_hash(template, replacements)
|
119
|
+
template.to_s.gsub(/:\w+/) { |match|
|
120
|
+
quote_value(replacements.fetch(match[1..-1].to_sym){match})
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def value_to_expression(value)
|
125
|
+
case value
|
126
|
+
when nil then "NULL"
|
127
|
+
when Range then
|
128
|
+
"#{quote_value(value.begin)} AND #{quote_value(value.end)}"
|
129
|
+
when Array then
|
130
|
+
value = value.map{|v| value_to_expression(v) }.join(", ")
|
131
|
+
value = "(#{value})"
|
132
|
+
else quote_value(value)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def quote_value(value)
|
137
|
+
SDBTools::Selection.quote_value(value)
|
138
|
+
end
|
139
|
+
|
140
|
+
def comparison_operator(comparison)
|
141
|
+
case comparison
|
142
|
+
when EqualToComparison then
|
143
|
+
case comparison.value
|
144
|
+
when nil then "IS"
|
145
|
+
else "="
|
146
|
+
end
|
147
|
+
when GreaterThanComparison then ">"
|
148
|
+
when LessThanComparison then "<"
|
149
|
+
when GreaterThanOrEqualToComparison then ">="
|
150
|
+
when LessThanOrEqualToComparison then "<="
|
151
|
+
when LikeComparison then "LIKE"
|
152
|
+
when InclusionComparison then
|
153
|
+
case comparison.value
|
154
|
+
when Range then
|
155
|
+
if comparison.value.exclude_end?
|
156
|
+
# We have tried to support exclusive ranges by simply adding
|
157
|
+
# the range to the unsupported_conditions list so that excluded
|
158
|
+
# values will be caught in post-filtering. However, DataMapper
|
159
|
+
# is apparently casting the range begin/end values to Strings, even
|
160
|
+
# when the property type is an Integer.
|
161
|
+
|
162
|
+
# Or it may be that something else is going wrong. But the upshot
|
163
|
+
# is that the post-filtering step doesn't work.
|
164
|
+
raise NotImplementedError,
|
165
|
+
"Exclusive ranges are not supported by the SimpleDB adapter"
|
166
|
+
else
|
167
|
+
"BETWEEN"
|
168
|
+
end
|
169
|
+
else "IN"
|
170
|
+
end
|
171
|
+
when RegexpComparison then throw :unsupported
|
172
|
+
else raise NotImplementedError, "Unhandled comparison: #{comparison.inspect}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def empty_inclusion?(node)
|
177
|
+
node.is_a?(InclusionComparison) && Array(node.value).empty?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/scripts/console
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require File.expand_path('../lib/dm-adapter-simpledb', File.dirname(__FILE__))
|
4
|
+
require 'logger'
|
5
|
+
require 'irb'
|
6
|
+
|
7
|
+
include DataMapper
|
8
|
+
|
9
|
+
class Post
|
10
|
+
include Resource
|
11
|
+
|
12
|
+
property :title, String, :key => true
|
13
|
+
property :body, Text
|
14
|
+
end
|
15
|
+
|
16
|
+
DataMapper.setup(
|
17
|
+
:default,
|
18
|
+
:domain => "example",
|
19
|
+
:adapter => 'simpledb',
|
20
|
+
:null => true,
|
21
|
+
:logger => ::Logger.new($stderr, ::Logger::DEBUG))
|
22
|
+
|
23
|
+
IRB.start
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require File.expand_path('../lib/dm-adapter-simpledb', File.dirname(__FILE__))
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
include DataMapper
|
7
|
+
|
8
|
+
class Post
|
9
|
+
include Resource
|
10
|
+
|
11
|
+
property :title, String, :key => true
|
12
|
+
property :body, Text
|
13
|
+
end
|
14
|
+
|
15
|
+
logger = ::Logger.new($stderr, ::Logger::DEBUG)
|
16
|
+
|
17
|
+
DataMapper.setup(
|
18
|
+
:default,
|
19
|
+
:domain => "dm_simpledb_adapter_benchmark",
|
20
|
+
:adapter => 'simpledb',
|
21
|
+
:create_domain => true,
|
22
|
+
:logger => logger,
|
23
|
+
:batch_limit => 10)
|
24
|
+
|
25
|
+
logger.info "Writing records"
|
26
|
+
100.times do |i|
|
27
|
+
Post.create(:title => "title#{i}", :body => "body#{i}")
|
28
|
+
end
|
29
|
+
|
30
|
+
logger.info "Waiting a bit for consistency"
|
31
|
+
sleep 2
|
32
|
+
|
33
|
+
SDBTools::Transaction.on_close =
|
34
|
+
SDBTools::Transaction.log_transaction_close(logger)
|
35
|
+
SDBTools::Transaction.open("benchmark limited query") do |t|
|
36
|
+
# With a batch limit of 10, this should result in two queries of LIMIT 10 and
|
37
|
+
# one with LIMIT 5
|
38
|
+
query = Post.all(:limit => 25)
|
39
|
+
item_count = query.to_a.size
|
40
|
+
logger.info "Found #{item_count} items"
|
41
|
+
end
|
42
|
+
|
43
|
+
SDBTools::Transaction.on_close =
|
44
|
+
SDBTools::Transaction.log_transaction_close(logger)
|
45
|
+
SDBTools::Transaction.open("benchmark query with limit and offset") do |t|
|
46
|
+
# With a batch limit of 10, this should result in two queries of LIMIT 10 and
|
47
|
+
# one with LIMIT 5
|
48
|
+
query = Post.all(:limit => 25, :order => :title.asc, :offset => 25)
|
49
|
+
item_count = query.to_a.size
|
50
|
+
logger.info "Found #{item_count} items"
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require File.expand_path('../lib/dm-adapter-simpledb', File.dirname(__FILE__))
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
include DataMapper
|
7
|
+
|
8
|
+
class Post
|
9
|
+
include Resource
|
10
|
+
|
11
|
+
property :title, String, :key => true
|
12
|
+
property :body, Text
|
13
|
+
end
|
14
|
+
|
15
|
+
logger = ::Logger.new($stderr, ::Logger::DEBUG)
|
16
|
+
|
17
|
+
DataMapper.setup(
|
18
|
+
:default,
|
19
|
+
:domain => "dm_simpledb_adapter_benchmark",
|
20
|
+
:adapter => 'simpledb',
|
21
|
+
:create_domain => true,
|
22
|
+
:logger => logger)
|
23
|
+
|
24
|
+
logger.info "Writing records"
|
25
|
+
100.times do |i|
|
26
|
+
Post.create(:title => "title#{i}", :body => "body#{i}")
|
27
|
+
end
|
28
|
+
|
29
|
+
logger.info "Waiting a bit for consistency"
|
30
|
+
sleep 2
|
31
|
+
|
32
|
+
logger.info "benchmarking OR"
|
33
|
+
SDBTools::Transaction.on_close =
|
34
|
+
SDBTools::Transaction.log_transaction_close(logger)
|
35
|
+
SDBTools::Transaction.open("benchmark OR") do |t|
|
36
|
+
query = Post.all(:title.like => "%2%") | Post.all(:title.like => "%5%")
|
37
|
+
item_count = query.to_a.size
|
38
|
+
logger.info "Found #{item_count} items"
|
39
|
+
end
|
@@ -8,6 +8,7 @@ describe DataMapper::Adapters::SimpleDBAdapter do
|
|
8
8
|
@adapter = DataMapper::Repository.adapters[:default]
|
9
9
|
@old_consistency_policy = @adapter.consistency_policy
|
10
10
|
@adapter.consistency_policy = :automatic
|
11
|
+
# @adapter.logger = ::Logger.new($stderr)
|
11
12
|
end
|
12
13
|
|
13
14
|
after :all do
|
@@ -40,7 +40,7 @@ describe 'with multiple records saved' do
|
|
40
40
|
|
41
41
|
it 'should handle max item if limit is large case' do
|
42
42
|
persons = Hero.all(:limit => 150)
|
43
|
-
persons.length.should ==3
|
43
|
+
persons.length.should == 3
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'should handle ordering asc results with a limit' do
|
@@ -138,33 +138,15 @@ EOF
|
|
138
138
|
persons.length.should == 0
|
139
139
|
end
|
140
140
|
|
141
|
-
describe '#query' do
|
142
|
-
before(:each) do
|
143
|
-
@domain = Friend.repository(:default).adapter.sdb_options[:domain]
|
144
|
-
end
|
145
|
-
it "should return an array of records" do
|
146
|
-
records = Friend.repository(:default).adapter.query("SELECT age, wealth from #{@domain} where age = '25'")
|
147
|
-
records.should == [{"wealth"=>["25.0"], "age"=>["25"]}]
|
148
|
-
end
|
149
|
-
it "should return empty array if no matches" do
|
150
|
-
records = Friend.repository(:default).adapter.query("SELECT age, wealth from #{@domain} where age = '15'")
|
151
|
-
records.should be_empty
|
152
|
-
end
|
153
|
-
it "should raise an error if query is invalid" do
|
154
|
-
lambda do
|
155
|
-
records = Friend.repository(:default).adapter.query("SELECT gaga")
|
156
|
-
end.should raise_error(RightAws::AwsError)
|
157
|
-
end
|
158
|
-
end
|
159
141
|
describe 'aggregate' do
|
160
142
|
it "should respond to count(*)" do
|
161
143
|
Friend.count.should == 1
|
162
144
|
end
|
163
145
|
it "should not respond to any other aggregates" do
|
164
|
-
lambda { Friend.min(:age) }.should raise_error(
|
165
|
-
lambda { Friend.max(:age) }.should raise_error(
|
166
|
-
lambda { Friend.avg(:age) }.should raise_error(
|
167
|
-
lambda { Friend.sum(:age) }.should raise_error(
|
146
|
+
lambda { Friend.min(:age) }.should raise_error(NotImplementedError)
|
147
|
+
lambda { Friend.max(:age) }.should raise_error(NotImplementedError)
|
148
|
+
lambda { Friend.avg(:age) }.should raise_error(NotImplementedError)
|
149
|
+
lambda { Friend.sum(:age) }.should raise_error(NotImplementedError)
|
168
150
|
end
|
169
151
|
end
|
170
152
|
end
|