dm-adapter-simpledb 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|