sdbtools 0.3.0 → 0.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/VERSION +1 -1
- data/lib/sdbtools.rb +7 -141
- data/lib/sdbtools/database.rb +49 -0
- data/lib/sdbtools/domain.rb +74 -0
- data/lib/sdbtools/measured_sdb_interface.rb +48 -0
- data/lib/sdbtools/operation.rb +34 -0
- data/lib/sdbtools/selection.rb +212 -0
- data/lib/sdbtools/transaction.rb +119 -0
- data/script/console +8 -0
- data/spec/domain_spec.rb +39 -0
- data/spec/operation_spec.rb +10 -0
- data/spec/selection_spec.rb +41 -12
- metadata +11 -3
- data/lib/selection.rb +0 -197
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/sdbtools.rb
CHANGED
@@ -1,149 +1,15 @@
|
|
1
1
|
require 'fattr'
|
2
2
|
require 'right_aws'
|
3
|
-
|
3
|
+
$:.unshift(File.expand_path(File.dirname(__FILE__)))
|
4
|
+
require 'sdbtools/operation'
|
5
|
+
require 'sdbtools/database'
|
6
|
+
require 'sdbtools/domain'
|
7
|
+
require 'sdbtools/selection'
|
8
|
+
require 'sdbtools/transaction'
|
9
|
+
require 'sdbtools/measured_sdb_interface'
|
4
10
|
|
5
11
|
module SDBTools
|
6
12
|
|
7
|
-
# An Operation represents a SimpleDB operation and handles the details of
|
8
|
-
# re-requesting "next tokens" until the operation is complete.
|
9
|
-
class Operation
|
10
|
-
include Enumerable
|
11
|
-
|
12
|
-
attr_reader :method
|
13
|
-
attr_reader :args
|
14
|
-
attr_reader :starting_token
|
15
|
-
|
16
|
-
def initialize(sdb, method, *args)
|
17
|
-
@options = args.last.is_a?(Hash) ? args.pop : {}
|
18
|
-
@sdb = sdb
|
19
|
-
@method = method
|
20
|
-
@args = args
|
21
|
-
@starting_token = @options[:starting_token]
|
22
|
-
end
|
23
|
-
|
24
|
-
# Yields once for each result set, until there is no next token.
|
25
|
-
def each
|
26
|
-
next_token = starting_token
|
27
|
-
begin
|
28
|
-
args = @args.dup
|
29
|
-
args << next_token
|
30
|
-
results = @sdb.send(@method, *args)
|
31
|
-
yield(results)
|
32
|
-
next_token = results[:next_token]
|
33
|
-
end while next_token
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
class Database
|
39
|
-
def initialize(access_key=nil, secret_key=nil, options={})
|
40
|
-
@sdb = RightAws::SdbInterface.new(access_key, secret_key, options)
|
41
|
-
@logger = options.fetch(:logger){::Logger.new($stderr)}
|
42
|
-
end
|
43
|
-
|
44
|
-
def domains
|
45
|
-
domains_op = Operation.new(@sdb, :list_domains, nil)
|
46
|
-
domains_op.inject([]) {|domains, results|
|
47
|
-
domains.concat(results[:domains])
|
48
|
-
domains
|
49
|
-
}
|
50
|
-
end
|
51
|
-
|
52
|
-
def domain(domain_name)
|
53
|
-
Domain.new(@sdb, domain_name)
|
54
|
-
end
|
55
|
-
|
56
|
-
def domain_exists?(domain_name)
|
57
|
-
domains.include?(domain_name)
|
58
|
-
end
|
59
|
-
|
60
|
-
def create_domain(domain_name)
|
61
|
-
@sdb.create_domain(domain_name)
|
62
|
-
domain(domain_name)
|
63
|
-
end
|
64
|
-
|
65
|
-
def delete_domain(domain_name)
|
66
|
-
@sdb.delete_domain(domain_name)
|
67
|
-
end
|
68
|
-
|
69
|
-
def make_dump(domain, filename)
|
70
|
-
Dump.new(domain, filename, @logger)
|
71
|
-
end
|
72
|
-
|
73
|
-
def make_load(domain, filename)
|
74
|
-
Load.new(domain, filename, @logger)
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
end
|
79
|
-
|
80
|
-
class Domain
|
81
|
-
attr_reader :name
|
82
|
-
|
83
|
-
def initialize(sdb, name)
|
84
|
-
@sdb = sdb
|
85
|
-
@name = name
|
86
|
-
@item_names = nil
|
87
|
-
@count = nil
|
88
|
-
end
|
89
|
-
|
90
|
-
def [](item_name)
|
91
|
-
@sdb.get_attributes(name, item_name)[:attributes]
|
92
|
-
end
|
93
|
-
|
94
|
-
def item_names
|
95
|
-
return @item_names if @item_names
|
96
|
-
query = Operation.new(@sdb, :query, @name, nil, nil)
|
97
|
-
@item_names = query.inject([]) {|names, results|
|
98
|
-
names.concat(results[:items])
|
99
|
-
names
|
100
|
-
}
|
101
|
-
end
|
102
|
-
|
103
|
-
def count
|
104
|
-
return @count if @count
|
105
|
-
op = Operation.new(@sdb, :select, "select count(*) from #{name}")
|
106
|
-
@count = op.inject(0) {|count, results|
|
107
|
-
count += results[:items].first["Domain"]["Count"].first.to_i
|
108
|
-
count
|
109
|
-
}
|
110
|
-
end
|
111
|
-
|
112
|
-
def items(item_names)
|
113
|
-
names = item_names.map{|n| "'#{n}'"}.join(', ')
|
114
|
-
query = "select * from #{name} where itemName() in (#{names})"
|
115
|
-
select = Operation.new(@sdb, :select, query)
|
116
|
-
select.inject({}) {|items, results|
|
117
|
-
results[:items].each do |item|
|
118
|
-
item_name = item.keys.first
|
119
|
-
item_value = item.values.first
|
120
|
-
items[item_name] = item_value
|
121
|
-
end
|
122
|
-
items
|
123
|
-
}
|
124
|
-
end
|
125
|
-
|
126
|
-
def put(item_name, attributes)
|
127
|
-
@sdb.put_attributes(@name, item_name, attributes)
|
128
|
-
end
|
129
|
-
|
130
|
-
def select(query)
|
131
|
-
op = Operation.new(@sdb, :select, "select * from #{name} where #{query}")
|
132
|
-
op.inject([]){|items,results|
|
133
|
-
batch_items = results[:items].map{|pair|
|
134
|
-
item = pair.values.first
|
135
|
-
item.merge!({'itemName()' => pair.keys.first})
|
136
|
-
item
|
137
|
-
}
|
138
|
-
items.concat(batch_items)
|
139
|
-
}
|
140
|
-
end
|
141
|
-
|
142
|
-
def delete(item_name)
|
143
|
-
@sdb.delete_attributes(@name, item_name)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
13
|
class Task
|
148
14
|
fattr :callback => lambda{}
|
149
15
|
fattr :chunk_size => 100
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module SDBTools
|
2
|
+
class Database
|
3
|
+
attr_reader :sdb
|
4
|
+
attr_accessor :logger
|
5
|
+
|
6
|
+
def initialize(access_key=nil, secret_key=nil, options={})
|
7
|
+
@logger = (options[:logger] ||= ::Logger.new($stderr))
|
8
|
+
@sdb = MeasuredSdbInterface.new(
|
9
|
+
options.delete(:sdb_interface) {
|
10
|
+
RightAws::SdbInterface.new(access_key, secret_key, options)
|
11
|
+
})
|
12
|
+
end
|
13
|
+
|
14
|
+
def domains
|
15
|
+
domains_op = Operation.new(@sdb, :list_domains, nil)
|
16
|
+
domains_op.inject([]) {|domains, (results, operation)|
|
17
|
+
domains.concat(results[:domains])
|
18
|
+
domains
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def domain(domain_name)
|
23
|
+
Domain.new(@sdb, domain_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def domain_exists?(domain_name)
|
27
|
+
domains.include?(domain_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_domain(domain_name)
|
31
|
+
@sdb.create_domain(domain_name)
|
32
|
+
domain(domain_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_domain(domain_name)
|
36
|
+
@sdb.delete_domain(domain_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def make_dump(domain, filename)
|
40
|
+
Dump.new(domain, filename, @logger)
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_load(domain, filename)
|
44
|
+
Load.new(domain, filename, @logger)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module SDBTools
|
2
|
+
class Domain
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(sdb, name)
|
6
|
+
@sdb = sdb
|
7
|
+
@name = name
|
8
|
+
@item_names = nil
|
9
|
+
@count = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](item_name)
|
13
|
+
@sdb.get_attributes(name, item_name)[:attributes]
|
14
|
+
end
|
15
|
+
|
16
|
+
def item_names
|
17
|
+
return @item_names if @item_names
|
18
|
+
query = Operation.new(@sdb, :query, @name, nil, nil)
|
19
|
+
@item_names = query.inject([]) {|names, results|
|
20
|
+
names.concat(results[:items])
|
21
|
+
names
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def count
|
26
|
+
return @count if @count
|
27
|
+
@count = selection.count
|
28
|
+
end
|
29
|
+
|
30
|
+
def items(item_names)
|
31
|
+
names = item_names.map{|n| "'#{n}'"}.join(', ')
|
32
|
+
query = "select * from #{name} where itemName() in (#{names})"
|
33
|
+
select = Operation.new(@sdb, :select, query)
|
34
|
+
select.inject({}) {|items, results|
|
35
|
+
results[:items].each do |item|
|
36
|
+
item_name = item.keys.first
|
37
|
+
item_value = item.values.first
|
38
|
+
items[item_name] = item_value
|
39
|
+
end
|
40
|
+
items
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(item_name, attribute_name=nil)
|
45
|
+
@sdb.get_attributes(@name, item_name, attribute_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def put(item_name, attributes, options={})
|
49
|
+
replace = options[:replace] ? :replace : false
|
50
|
+
@sdb.put_attributes(@name, item_name, attributes, replace)
|
51
|
+
end
|
52
|
+
|
53
|
+
def selection(options={})
|
54
|
+
Selection.new(@sdb, name, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Somewhat deprecated. Use #selection() instead
|
58
|
+
def select(query)
|
59
|
+
op = Operation.new(@sdb, :select, "select * from #{name} where #{query}")
|
60
|
+
op.inject([]){|items,(results, operation)|
|
61
|
+
batch_items = results[:items].map{|pair|
|
62
|
+
item = pair.values.first
|
63
|
+
item.merge!({'itemName()' => pair.keys.first})
|
64
|
+
item
|
65
|
+
}
|
66
|
+
items.concat(batch_items)
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def delete(item_name, attributes={})
|
71
|
+
@sdb.delete_attributes(@name, item_name, attributes)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'sdbtools/transaction'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module SDBTools
|
5
|
+
class MeasuredSdbInterface < DelegateClass(RightAws::SdbInterface)
|
6
|
+
def create_domain(*args, &block)
|
7
|
+
Transaction.open("create_domain") do |t|
|
8
|
+
t.measure_aws_call{super(*args, &block)}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete_domain(*args, &block)
|
13
|
+
Transaction.open("delete_domain") do |t|
|
14
|
+
t.measure_aws_call{super(*args, &block)}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def list_domains(*args, &block)
|
19
|
+
Transaction.open("list_domains") do |t|
|
20
|
+
t.measure_aws_call{super(*args, &block)}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def put_attributes(*args, &block)
|
25
|
+
Transaction.open("put_attributes") do |t|
|
26
|
+
t.measure_aws_call{super(*args, &block)}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_attributes(*args, &block)
|
31
|
+
Transaction.open("get_attributes") do |t|
|
32
|
+
t.measure_aws_call{super(*args, &block)}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def select(*args, &block)
|
37
|
+
Transaction.open("select #{args.first}") do |t|
|
38
|
+
t.measure_aws_call{super(*args, &block)}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def query(*args, &block)
|
43
|
+
Transaction.open("query") do |t|
|
44
|
+
t.measure_aws_call{super(*args, &block)}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SDBTools
|
2
|
+
# An Operation represents a SimpleDB operation and handles the details of
|
3
|
+
# re-requesting "next tokens" until the operation is complete.
|
4
|
+
class Operation
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :method
|
8
|
+
attr_reader :args
|
9
|
+
attr_reader :starting_token
|
10
|
+
|
11
|
+
def initialize(sdb, method, *args)
|
12
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
13
|
+
@sdb = sdb
|
14
|
+
@method = method
|
15
|
+
@args = args
|
16
|
+
@starting_token = @options[:starting_token]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Yields once for each result set, until there is no next token.
|
20
|
+
def each
|
21
|
+
Transaction.open(":#{method} operation") do |t|
|
22
|
+
next_token = starting_token
|
23
|
+
begin
|
24
|
+
args = @args.dup
|
25
|
+
args << next_token
|
26
|
+
results = @sdb.send(@method, *args)
|
27
|
+
yield(results, self)
|
28
|
+
next_token = results[:next_token]
|
29
|
+
end while next_token
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'arrayfields'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module SDBTools
|
5
|
+
class Selection
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
MAX_BATCH_LIMIT = 250
|
9
|
+
DEFAULT_RESULT_LIMIT = 100
|
10
|
+
|
11
|
+
attr_accessor :domain
|
12
|
+
attr_accessor :attributes
|
13
|
+
attr_accessor :conditions
|
14
|
+
attr_reader :limit
|
15
|
+
attr_reader :batch_limit
|
16
|
+
attr_accessor :offset
|
17
|
+
attr_accessor :order_by
|
18
|
+
attr_accessor :order
|
19
|
+
attr_accessor :sdb
|
20
|
+
attr_accessor :logger
|
21
|
+
|
22
|
+
# For testing
|
23
|
+
attr_writer :starting_token
|
24
|
+
|
25
|
+
def self.quote_name(name)
|
26
|
+
if name.to_s =~ /^[A-Z$_][A-Z0-9$_]*$/i
|
27
|
+
name.to_s
|
28
|
+
else
|
29
|
+
"`" + name.to_s.gsub("`", "``") + "`"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.quote_value(value)
|
34
|
+
'"' + value.to_s.gsub(/"/, '""') + '"'
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(sdb, domain, options={})
|
38
|
+
@sdb = sdb
|
39
|
+
@domain = domain.to_s
|
40
|
+
@attributes = options.fetch(:attributes) { :all }
|
41
|
+
@conditions = options[:conditions].to_s
|
42
|
+
@order = options.fetch(:order) { :ascending }
|
43
|
+
@order_by = options.fetch(:order_by) { :none }
|
44
|
+
@order_by = @order_by.to_s unless @order_by == :none
|
45
|
+
self.limit = options.fetch(:limit) { :none }
|
46
|
+
self.batch_limit = options.fetch(:batch_limit) { DEFAULT_RESULT_LIMIT }.to_i
|
47
|
+
@offset = options.fetch(:offset) { 0 }.to_i
|
48
|
+
@logger = options.fetch(:logger){::Logger.new($stderr)}
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s(query_limit=limit, offset=0)
|
52
|
+
"SELECT #{output_list}" \
|
53
|
+
" FROM #{quote_name(domain)}" \
|
54
|
+
"#{match_expression}" \
|
55
|
+
"#{sort_instructions}" \
|
56
|
+
"#{limit_clause(query_limit,offset)}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def count_expression
|
60
|
+
"SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions}#{limit_clause_for_count}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def offset_count_expression
|
64
|
+
"SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions} LIMIT #{offset}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def count
|
68
|
+
Transaction.open(count_expression) do |t|
|
69
|
+
@count ||= count_operation.inject(0){|count, (results, operation)|
|
70
|
+
count += results[:items].first["Domain"]["Count"].first.to_i
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :size, :count
|
76
|
+
alias_method :length, :count
|
77
|
+
|
78
|
+
def each
|
79
|
+
return if limit == 0
|
80
|
+
num_items = 0
|
81
|
+
Transaction.open(to_s) do
|
82
|
+
select_operation(limit, num_items).each do |results, operation|
|
83
|
+
results[:items].each do |item|
|
84
|
+
yield(item.keys.first, item.values.first)
|
85
|
+
num_items += 1
|
86
|
+
return if limit != :none && num_items >= limit
|
87
|
+
end
|
88
|
+
operation.args[0] = to_s(limit, num_items)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue RightAws::AwsError => error
|
92
|
+
if error.message =~ /InvalidQueryExpression/
|
93
|
+
raise error, error.message.to_s + " (#{to_s(limit, num_items)})", error.backtrace
|
94
|
+
else
|
95
|
+
raise
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def results
|
100
|
+
@results ||= inject(Arrayfields.new){|results, (name, value)|
|
101
|
+
results[name] = value
|
102
|
+
results
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def count_operation
|
107
|
+
Operation.new(sdb, :select, count_expression, :starting_token => starting_token)
|
108
|
+
end
|
109
|
+
|
110
|
+
def offset_count_operation
|
111
|
+
Operation.new(sdb, :select, offset_count_expression)
|
112
|
+
end
|
113
|
+
|
114
|
+
def select_operation(query_limit=limit, offset=0)
|
115
|
+
Operation.new(sdb, :select, to_s(query_limit, offset), :starting_token => starting_token)
|
116
|
+
end
|
117
|
+
|
118
|
+
def starting_token
|
119
|
+
@starting_token ||=
|
120
|
+
case offset
|
121
|
+
when 0 then nil
|
122
|
+
else
|
123
|
+
op = offset_count_operation
|
124
|
+
count = 0
|
125
|
+
op.each do |results, operation|
|
126
|
+
count += results[:items].first["Domain"]["Count"].first.to_i
|
127
|
+
if count == offset || results[:next_token].nil?
|
128
|
+
return results[:next_token]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
raise "Failed to find offset #{offset}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def limit=(new_limit)
|
136
|
+
case new_limit
|
137
|
+
when :none, Integer then
|
138
|
+
@limit = new_limit
|
139
|
+
else
|
140
|
+
raise ArgumentError, "Limit must be integer or :none"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def batch_limit=(new_limit)
|
145
|
+
case new_limit
|
146
|
+
when (1..MAX_BATCH_LIMIT) then
|
147
|
+
@batch_limit = new_limit
|
148
|
+
else
|
149
|
+
raise RangeError, "Limit must be 1..#{MAX_BATCH_LIMIT}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def quote_name(name)
|
156
|
+
self.class.quote_name(name)
|
157
|
+
end
|
158
|
+
|
159
|
+
def quote_value(value)
|
160
|
+
self.class.quote_value(value)
|
161
|
+
end
|
162
|
+
|
163
|
+
def output_list
|
164
|
+
case attributes
|
165
|
+
when Array then attributes.map{|a| quote_name(a)}.join(", ")
|
166
|
+
when :all then "*"
|
167
|
+
else raise ScriptError, "Bad attributes: #{attributes.inspect}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def match_expression
|
172
|
+
if conditions.empty? then "" else " WHERE #{conditions}" end
|
173
|
+
end
|
174
|
+
|
175
|
+
def limit_clause(query_limit=:none, offset=0)
|
176
|
+
case query_limit
|
177
|
+
when :none then format_limit_clause(batch_limit)
|
178
|
+
else
|
179
|
+
remaining_query_limit = query_limit - offset
|
180
|
+
this_batch_limit =
|
181
|
+
remaining_query_limit < batch_limit ? remaining_query_limit : batch_limit
|
182
|
+
format_limit_clause(this_batch_limit)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def format_limit_clause(this_batch_limit)
|
187
|
+
case this_batch_limit
|
188
|
+
when DEFAULT_RESULT_LIMIT then ""
|
189
|
+
else " LIMIT #{this_batch_limit}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# There are special rules for the LIMIT clause when executing a count(*)
|
194
|
+
# select. Normally it specifies batch size, capped at 250. But for count(*)
|
195
|
+
# expressions it specifies the max count, which may be arbitrarily large
|
196
|
+
def limit_clause_for_count
|
197
|
+
case limit
|
198
|
+
when :none then ""
|
199
|
+
else " LIMIT #{limit}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def sort_instructions
|
204
|
+
case order_by
|
205
|
+
when :none then ""
|
206
|
+
else
|
207
|
+
direction = (order == :ascending ? "ASC" : "DESC")
|
208
|
+
" ORDER BY #{quote_name(order_by)} #{direction}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
module SDBTools
|
4
|
+
|
5
|
+
# SimpleDB is not remotely transactional. A Transaction in this context is
|
6
|
+
# just a way to group together a series of SimpleDB requests for the purpose
|
7
|
+
# of benchmarking.
|
8
|
+
class Transaction
|
9
|
+
|
10
|
+
def self.open(description, on_close=self.on_close)
|
11
|
+
transaction = self.new(description, on_close)
|
12
|
+
transaction_stack.push(transaction)
|
13
|
+
yield(transaction)
|
14
|
+
ensure
|
15
|
+
transaction = transaction_stack.pop
|
16
|
+
transaction.close
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the default on_close action. An on_close action receives a Transaction
|
20
|
+
# object which is being closed
|
21
|
+
def self.on_close=(action)
|
22
|
+
@on_close_action = action
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get the default on_close action
|
26
|
+
def self.on_close
|
27
|
+
if current
|
28
|
+
current.on_close
|
29
|
+
else
|
30
|
+
@on_close_action ||= lambda{|t|}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Usage:
|
35
|
+
# Transaction.on_close = Transaction.log_transaction_close(logger)
|
36
|
+
def self.log_transaction_close(logger, cutoff_level=:none)
|
37
|
+
pattern = "%s \"%s\" (User %0.6u; System %0.6y; CPU %0.6t; Clock %0.6r; Box %0.6f; Reqs %d; Items %d)"
|
38
|
+
lambda do |t|
|
39
|
+
if cutoff_level == :none || cutoff_level <= t.nesting_level
|
40
|
+
prefix = "*" * (t.nesting_level + 1)
|
41
|
+
logger.info(
|
42
|
+
t.times.format(
|
43
|
+
pattern,
|
44
|
+
prefix,
|
45
|
+
t.description,
|
46
|
+
t.box_usage,
|
47
|
+
t.request_count,
|
48
|
+
t.item_count))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.add_stats(box_usage, request_count, item_count, times)
|
54
|
+
current and current.add_stats(box_usage, request_count, item_count, times)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.current
|
58
|
+
transaction_stack.last
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.transaction_stack
|
62
|
+
Thread.current[:sdbtools_transaction_stack] ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :description
|
66
|
+
attr_reader :box_usage
|
67
|
+
attr_reader :request_count
|
68
|
+
attr_reader :item_count
|
69
|
+
attr_reader :times
|
70
|
+
attr_reader :on_close
|
71
|
+
attr_reader :nesting_level
|
72
|
+
|
73
|
+
def initialize(description, on_close_action=self.class.on_close)
|
74
|
+
@description = description
|
75
|
+
@box_usage = 0.0
|
76
|
+
@request_count = 0
|
77
|
+
@item_count = 0
|
78
|
+
@on_close = on_close_action
|
79
|
+
@times = Benchmark::Tms.new
|
80
|
+
@nesting_level = self.class.transaction_stack.size
|
81
|
+
end
|
82
|
+
|
83
|
+
def close
|
84
|
+
self.class.add_stats(@box_usage, @request_count, @item_count, @times)
|
85
|
+
@on_close.call(self)
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_stats(box_usage, request_count, item_count, times=Benchmark::Tms.new)
|
90
|
+
@request_count += request_count.to_i
|
91
|
+
@box_usage += box_usage.to_f
|
92
|
+
@item_count += item_count
|
93
|
+
@times += times
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_stats_from_aws_response(response)
|
97
|
+
response = response.nil? ? {} : response
|
98
|
+
box_usage = response[:box_usage].to_f
|
99
|
+
item_count = Array(response[:items]).size + Array(response[:domains]).size
|
100
|
+
item_count += 1 if response.key?(:attributes)
|
101
|
+
add_stats(box_usage, 1, item_count)
|
102
|
+
response
|
103
|
+
end
|
104
|
+
|
105
|
+
def measure_aws_call
|
106
|
+
add_stats_from_aws_response(time{yield})
|
107
|
+
end
|
108
|
+
|
109
|
+
# Benchmark the block and add its times to the transaction
|
110
|
+
def time
|
111
|
+
result = nil
|
112
|
+
self.times.add! do
|
113
|
+
result = yield
|
114
|
+
end
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
data/script/console
ADDED
data/spec/domain_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module SDBTools
|
4
|
+
describe Domain do
|
5
|
+
before :each do
|
6
|
+
@sdb = stub("SDB")
|
7
|
+
end
|
8
|
+
|
9
|
+
context "given a name and an SDB interface" do
|
10
|
+
before :each do
|
11
|
+
@it = Domain.new(@sdb, "foo")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be able to get selected item attributes" do
|
15
|
+
@sdb.should_receive(:get_attributes).
|
16
|
+
with("foo", "bar", "baz")
|
17
|
+
@it.get("bar", "baz")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should be able to delete selected item attributes" do
|
21
|
+
@sdb.should_receive(:delete_attributes).
|
22
|
+
with("foo", "bar", {"baz" => ["buz"]})
|
23
|
+
@it.delete("bar", {"baz" => ["buz"]})
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be able to write selected item attributes" do
|
27
|
+
@sdb.should_receive(:put_attributes).
|
28
|
+
with("foo", "bar", {"baz" => ["buz"]}, false)
|
29
|
+
@it.put("bar", {"baz" => ["buz"]})
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be able to overwrite selected item attributes" do
|
33
|
+
@sdb.should_receive(:put_attributes).
|
34
|
+
with("foo", "bar", {"baz" => ["buz"]}, :replace)
|
35
|
+
@it.put("bar", {"baz" => ["buz"]}, :replace => true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/operation_spec.rb
CHANGED
@@ -16,5 +16,15 @@ module SDBTools
|
|
16
16
|
@it.each do break; end
|
17
17
|
end
|
18
18
|
end
|
19
|
+
|
20
|
+
it "should pass itself into the given block" do
|
21
|
+
op = :unset
|
22
|
+
@sdb.stub!(:select).and_return({})
|
23
|
+
@it = Operation.new(@sdb, :select, "ARG")
|
24
|
+
op = @it.each do |results, operation|
|
25
|
+
break operation
|
26
|
+
end
|
27
|
+
op.should equal(@it)
|
28
|
+
end
|
19
29
|
end
|
20
30
|
end
|
data/spec/selection_spec.rb
CHANGED
@@ -80,10 +80,7 @@ module SDBTools
|
|
80
80
|
before :each do
|
81
81
|
@it = Selection.new(@sdb, "DOMAIN",
|
82
82
|
:attributes => ["attr foo", "bar"],
|
83
|
-
:conditions =>
|
84
|
-
'bar == "buz"',
|
85
|
-
"AND",
|
86
|
-
['`attr foo` between ? and ?', 4, '1"2']])
|
83
|
+
:conditions => 'bar == "buz" AND `attr foo` between "4" and "1""2"')
|
87
84
|
end
|
88
85
|
|
89
86
|
it "should format quote and interpolate the conditions" do
|
@@ -102,7 +99,7 @@ module SDBTools
|
|
102
99
|
end
|
103
100
|
|
104
101
|
it "should append a limit clause" do
|
105
|
-
@it.to_s.should be == "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar'
|
102
|
+
@it.to_s.should be == "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar'"
|
106
103
|
end
|
107
104
|
|
108
105
|
it "should be able to generate a count expression" do
|
@@ -135,6 +132,38 @@ module SDBTools
|
|
135
132
|
end
|
136
133
|
end
|
137
134
|
|
135
|
+
context "with a high limit" do
|
136
|
+
before :each do
|
137
|
+
@it = Selection.new(@sdb, "DOMAIN",
|
138
|
+
:attributes => [:foo, :bar],
|
139
|
+
:conditions => ["foo == 'bar'"],
|
140
|
+
:limit => 251,
|
141
|
+
:batch_limit => 250)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should append a limit clause" do
|
145
|
+
@it.to_s.should be == "SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 250"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should be able to generate a count expression" do
|
149
|
+
@it.count_expression.should be ==
|
150
|
+
"SELECT count(*) FROM DOMAIN WHERE foo == 'bar' LIMIT 251"
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should limit results" do
|
154
|
+
@sdb = stub("SDB")
|
155
|
+
@sdb.should_receive(:select).
|
156
|
+
with("SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 250", nil).
|
157
|
+
and_return(select_results(("1".."250"), "TOKEN1"))
|
158
|
+
@sdb.should_receive(:select).
|
159
|
+
with("SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' LIMIT 1", "TOKEN1").
|
160
|
+
and_return(select_results(["251"], nil))
|
161
|
+
@it.sdb = @sdb
|
162
|
+
@it.results.size.should == 251
|
163
|
+
@it.results.keys.should == ("1".."251").to_a
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
138
167
|
context "ordered by an attribute" do
|
139
168
|
before :each do
|
140
169
|
@it = Selection.new(@sdb, "DOMAIN",
|
@@ -180,11 +209,11 @@ module SDBTools
|
|
180
209
|
|
181
210
|
context "with a limit" do
|
182
211
|
before :each do
|
183
|
-
@it.limit =
|
212
|
+
@it.limit = 12
|
184
213
|
end
|
185
214
|
|
186
215
|
specify { @it.to_s.should be ==
|
187
|
-
"SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC LIMIT
|
216
|
+
"SELECT foo, bar FROM DOMAIN WHERE foo == 'bar' ORDER BY foo DESC LIMIT 12"
|
188
217
|
}
|
189
218
|
end
|
190
219
|
end
|
@@ -192,12 +221,12 @@ module SDBTools
|
|
192
221
|
# We can't yet support large limits. In order to do so, it will be necessary
|
193
222
|
# to implement limit chunking, where the limit is split across multiple
|
194
223
|
# requests of 250 items and a final request of limit % 250 items.
|
195
|
-
it "should reject limits > 250" do
|
224
|
+
it "should reject batch limits > 250" do
|
196
225
|
lambda do
|
197
|
-
Selection.new(@sdb, "DOMAIN", :
|
226
|
+
Selection.new(@sdb, "DOMAIN", :batch_limit => 251)
|
198
227
|
end.should raise_error
|
199
228
|
lambda do
|
200
|
-
Selection.new(@sdb, "DOMAIN").
|
229
|
+
Selection.new(@sdb, "DOMAIN").batch_limit = 251
|
201
230
|
end.should raise_error
|
202
231
|
end
|
203
232
|
|
@@ -273,10 +302,10 @@ module SDBTools
|
|
273
302
|
it "should be able to get selection results" do
|
274
303
|
@sdb = stub("SDB")
|
275
304
|
@sdb.should_receive(:select).
|
276
|
-
with(@it.to_s, "TOKEN2").
|
305
|
+
with(@it.to_s(10,0), "TOKEN2").
|
277
306
|
and_return(select_results(["foo", "bar"], "TOKEN5"))
|
278
307
|
@sdb.should_receive(:select).
|
279
|
-
with(@it.to_s, "TOKEN5").
|
308
|
+
with(@it.to_s(10,2), "TOKEN5").
|
280
309
|
and_return(select_results(["baz", "buz"], nil))
|
281
310
|
@it.sdb = @sdb
|
282
311
|
@it.starting_token = "TOKEN2"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sdbtools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Avdi Grimm
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-01-
|
12
|
+
date: 2010-01-24 00:00:00 -05:00
|
13
13
|
default_executable: sdbtool
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -78,7 +78,14 @@ files:
|
|
78
78
|
- VERSION
|
79
79
|
- bin/sdbtool
|
80
80
|
- lib/sdbtools.rb
|
81
|
-
- lib/
|
81
|
+
- lib/sdbtools/database.rb
|
82
|
+
- lib/sdbtools/domain.rb
|
83
|
+
- lib/sdbtools/measured_sdb_interface.rb
|
84
|
+
- lib/sdbtools/operation.rb
|
85
|
+
- lib/sdbtools/selection.rb
|
86
|
+
- lib/sdbtools/transaction.rb
|
87
|
+
- script/console
|
88
|
+
- spec/domain_spec.rb
|
82
89
|
- spec/operation_spec.rb
|
83
90
|
- spec/selection_spec.rb
|
84
91
|
- spec/spec_helper.rb
|
@@ -111,6 +118,7 @@ signing_key:
|
|
111
118
|
specification_version: 3
|
112
119
|
summary: A high-level OO interface to Amazon SimpleDB
|
113
120
|
test_files:
|
121
|
+
- spec/domain_spec.rb
|
114
122
|
- spec/selection_spec.rb
|
115
123
|
- spec/operation_spec.rb
|
116
124
|
- spec/spec_helper.rb
|
data/lib/selection.rb
DELETED
@@ -1,197 +0,0 @@
|
|
1
|
-
require 'arrayfields'
|
2
|
-
require 'logger'
|
3
|
-
|
4
|
-
module SDBTools
|
5
|
-
class Selection
|
6
|
-
include Enumerable
|
7
|
-
|
8
|
-
MAX_RESULT_LIMIT = 250
|
9
|
-
DEFAULT_RESULT_LIMIT = 100
|
10
|
-
|
11
|
-
attr_accessor :domain
|
12
|
-
attr_accessor :attributes
|
13
|
-
attr_accessor :conditions
|
14
|
-
attr_reader :limit
|
15
|
-
attr_accessor :offset
|
16
|
-
attr_accessor :order_by
|
17
|
-
attr_accessor :order
|
18
|
-
attr_accessor :sdb
|
19
|
-
attr_accessor :logger
|
20
|
-
|
21
|
-
# For testing
|
22
|
-
attr_writer :starting_token
|
23
|
-
|
24
|
-
def self.quote_name(name)
|
25
|
-
if name.to_s =~ /^[A-Z$_][A-Z0-9$_]*$/i
|
26
|
-
name.to_s
|
27
|
-
else
|
28
|
-
"`" + name.to_s.gsub("`", "``") + "`"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.quote_value(value)
|
33
|
-
'"' + value.to_s.gsub(/"/, '""') + '"'
|
34
|
-
end
|
35
|
-
|
36
|
-
def initialize(sdb, domain, options={})
|
37
|
-
@sdb = sdb
|
38
|
-
@domain = domain.to_s
|
39
|
-
@attributes = options.fetch(:attributes) { :all }
|
40
|
-
@conditions = Array(options[:conditions])
|
41
|
-
@order = options.fetch(:order) { :ascending }
|
42
|
-
@order_by = options.fetch(:order_by) { :none }
|
43
|
-
@order_by = @order_by.to_s unless @order_by == :none
|
44
|
-
self.limit = options.fetch(:limit) { DEFAULT_RESULT_LIMIT }
|
45
|
-
@offset = options.fetch(:offset) { 0 }.to_i
|
46
|
-
@logger = options.fetch(:logger){::Logger.new($stderr)}
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_s
|
50
|
-
"SELECT #{output_list} FROM #{quote_name(domain)}#{match_expression}#{sort_instructions}#{limit_clause}"
|
51
|
-
end
|
52
|
-
|
53
|
-
def count_expression
|
54
|
-
"SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions}#{limit_clause_for_count}"
|
55
|
-
end
|
56
|
-
|
57
|
-
def offset_count_expression
|
58
|
-
"SELECT count(*) FROM #{quote_name(domain)}#{match_expression}#{sort_instructions} LIMIT #{offset}"
|
59
|
-
end
|
60
|
-
|
61
|
-
def count
|
62
|
-
@count ||= count_operation.inject(0){|count, results|
|
63
|
-
count += results[:items].first["Domain"]["Count"].first.to_i
|
64
|
-
}
|
65
|
-
end
|
66
|
-
|
67
|
-
alias_method :size, :count
|
68
|
-
alias_method :length, :count
|
69
|
-
|
70
|
-
def each
|
71
|
-
return if limit == 0
|
72
|
-
num_items = 0
|
73
|
-
select_operation.each do |results|
|
74
|
-
results[:items].each do |item|
|
75
|
-
yield(item.keys.first, item.values.first)
|
76
|
-
num_items += 1
|
77
|
-
return if limit != :none && num_items >= limit
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def results
|
83
|
-
@results ||= inject(Arrayfields.new){|results, (name, value)|
|
84
|
-
results[name] = value
|
85
|
-
results
|
86
|
-
}
|
87
|
-
end
|
88
|
-
|
89
|
-
def count_operation
|
90
|
-
Operation.new(sdb, :select, count_expression, :starting_token => starting_token)
|
91
|
-
end
|
92
|
-
|
93
|
-
def offset_count_operation
|
94
|
-
Operation.new(sdb, :select, offset_count_expression)
|
95
|
-
end
|
96
|
-
|
97
|
-
def select_operation
|
98
|
-
Operation.new(sdb, :select, to_s, :starting_token => starting_token)
|
99
|
-
end
|
100
|
-
|
101
|
-
def starting_token
|
102
|
-
@starting_token ||=
|
103
|
-
case offset
|
104
|
-
when 0 then nil
|
105
|
-
else
|
106
|
-
op = offset_count_operation
|
107
|
-
count = 0
|
108
|
-
op.each do |results|
|
109
|
-
count += results[:items].first["Domain"]["Count"].first.to_i
|
110
|
-
if count == offset || results[:next_token].nil?
|
111
|
-
return results[:next_token]
|
112
|
-
end
|
113
|
-
end
|
114
|
-
raise "Failed to find offset #{offset}"
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def limit=(new_limit)
|
119
|
-
# We can't yet support large limits. In order to do so, it will be necessary
|
120
|
-
# to implement limit chunking, where the limit is split across multiple
|
121
|
-
# requests of 250 items and a final request of limit % 250 items.
|
122
|
-
case new_limit
|
123
|
-
when :none, (0..MAX_RESULT_LIMIT) then
|
124
|
-
@limit = new_limit
|
125
|
-
else
|
126
|
-
raise RangeError, "Limit must be 0..250 or :none"
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
def quote_name(name)
|
133
|
-
self.class.quote_name(name)
|
134
|
-
end
|
135
|
-
|
136
|
-
def quote_value(value)
|
137
|
-
self.class.quote_value(value)
|
138
|
-
end
|
139
|
-
|
140
|
-
def output_list
|
141
|
-
case attributes
|
142
|
-
when Array then attributes.map{|a| quote_name(a)}.join(", ")
|
143
|
-
when :all then "*"
|
144
|
-
else raise ScriptError, "Bad attributes: #{attributes.inspect}"
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
def match_expression
|
149
|
-
case conditions.size
|
150
|
-
when 0 then ""
|
151
|
-
else " WHERE #{prepared_conditions.join(' ')}"
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
def prepared_conditions
|
156
|
-
conditions.map { |condition|
|
157
|
-
case condition
|
158
|
-
when String then condition
|
159
|
-
when Array then
|
160
|
-
values = condition.dup
|
161
|
-
template = values.shift.to_s
|
162
|
-
template.gsub(/\?/) {|match|
|
163
|
-
quote_value(values.shift.to_s)
|
164
|
-
}
|
165
|
-
else
|
166
|
-
raise ScriptError, "Bad condition: #{condition.inspect}"
|
167
|
-
end
|
168
|
-
}
|
169
|
-
end
|
170
|
-
|
171
|
-
def limit_clause
|
172
|
-
case limit
|
173
|
-
when :none then " LIMIT #{MAX_RESULT_LIMIT}"
|
174
|
-
when DEFAULT_RESULT_LIMIT then ""
|
175
|
-
else
|
176
|
-
batch_limit = limit < MAX_RESULT_LIMIT ? limit : MAX_RESULT_LIMIT
|
177
|
-
" LIMIT #{batch_limit}"
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def limit_clause_for_count
|
182
|
-
case limit
|
183
|
-
when :none then ""
|
184
|
-
else limit_clause
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def sort_instructions
|
189
|
-
case order_by
|
190
|
-
when :none then ""
|
191
|
-
else
|
192
|
-
direction = (order == :ascending ? "ASC" : "DESC")
|
193
|
-
" ORDER BY #{quote_name(order_by)} #{direction}"
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|