simplerdb 0.1
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 +3 -0
- data/Manifest.txt +16 -0
- data/README.txt +60 -0
- data/Rakefile +29 -0
- data/bin/simplerdb +8 -0
- data/lib/simplerdb/Rakefile +14 -0
- data/lib/simplerdb/client_exception.rb +10 -0
- data/lib/simplerdb/db.rb +142 -0
- data/lib/simplerdb/query_language.rb +266 -0
- data/lib/simplerdb/server.rb +33 -0
- data/lib/simplerdb/servlet.rb +203 -0
- data/lib/simplerdb.rb +3 -0
- data/test/functional_test.rb +81 -0
- data/test/query_evaluator_test.rb +73 -0
- data/test/query_parser_test.rb +64 -0
- data/test/simplerdb_test.rb +80 -0
- metadata +90 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
bin/simplerdb
|
6
|
+
lib/simplerdb.rb
|
7
|
+
lib/simplerdb/Rakefile
|
8
|
+
lib/simplerdb/client_exception.rb
|
9
|
+
lib/simplerdb/db.rb
|
10
|
+
lib/simplerdb/query_language.rb
|
11
|
+
lib/simplerdb/servlet.rb
|
12
|
+
lib/simplerdb/server.rb
|
13
|
+
test/query_evaluator_test.rb
|
14
|
+
test/query_parser_test.rb
|
15
|
+
test/simplerdb_test.rb
|
16
|
+
test/functional_test.rb
|
data/README.txt
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
<b>simplerdb</b>
|
2
|
+
by Gary Elliott (gary@tourb.us)
|
3
|
+
http://simplerdb.rubyforge.org
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
SimplerDB is an in-memory implementation of Amazon's SimpleDB REST API.
|
8
|
+
You can use it to test your application offline (for free!) or experiment with SimpleDB
|
9
|
+
without a beta account.
|
10
|
+
|
11
|
+
== FEATURES:
|
12
|
+
|
13
|
+
* Implements the current SimpleDB API specification as defined by http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide.
|
14
|
+
* Start/stop a server from within your ruby unit tests
|
15
|
+
* Run a standalone server for use by any SimpleDB client
|
16
|
+
|
17
|
+
== SYNOPSIS:
|
18
|
+
|
19
|
+
In order to use SimplerDB, you will need to replace Amazon's service url (http://sds.amazonaws.com)
|
20
|
+
with http://localhost:8087 in your client. RightAWS[http://rightaws.rubyforge.org/right_aws_gem_doc]
|
21
|
+
is the only ruby client I'm aware of that lets you do this.
|
22
|
+
|
23
|
+
To write a ruby-based unit test using SimplerDB, you can use the following setup/teardown methods to
|
24
|
+
start and stop the server for each test.
|
25
|
+
|
26
|
+
def setup
|
27
|
+
@server = SimplerDB::Server.new(8087)
|
28
|
+
@thread = Thread.new { @server.start }
|
29
|
+
end
|
30
|
+
|
31
|
+
def teardown
|
32
|
+
@server.shutdown
|
33
|
+
@thread.join
|
34
|
+
end
|
35
|
+
|
36
|
+
You will need to configure your SimpleDB client to point to http://localhost:8087 for the test,
|
37
|
+
but otherwise your code will work just as if it was accessing Amazon's live web service.
|
38
|
+
|
39
|
+
For a more complete example of a functional test using RightAWS take a look at
|
40
|
+
http://simplerdb.rubyforge.org/svn/trunk/test/functional_test.rb
|
41
|
+
|
42
|
+
Installed with the gem is the <tt>simplerdb</tt> command, which starts a standalone SimplerDB server.
|
43
|
+
The optional command-line argument allows you to set HTTP port (8087 by default).
|
44
|
+
|
45
|
+
== REQUIREMENTS:
|
46
|
+
|
47
|
+
* Dhaka[http://dhaka.rubyforge.org]
|
48
|
+
* Builder[http://builder.rubyforge.org]
|
49
|
+
|
50
|
+
All dependencies should be installed automatically during the SimplerDB gem installation.
|
51
|
+
|
52
|
+
== INSTALL:
|
53
|
+
|
54
|
+
* <tt>sudo gem install simplerdb</tt>
|
55
|
+
|
56
|
+
== CAVEATS:
|
57
|
+
|
58
|
+
* Since I don't have a SimplerDB beta account this software has not been tested in comparsion to the live web service and the behavior might differ. If you discover such a scenario please report it as a bug.
|
59
|
+
* SimplerDB has only been tested with ruby SimpleDB clients.
|
60
|
+
* Query performance with large numbers of items (more than 1000) in a domain could be poor.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/simplerdb.rb'
|
6
|
+
|
7
|
+
|
8
|
+
task :default => [:test]
|
9
|
+
|
10
|
+
Hoe.new('simplerdb', SimplerDB::VERSION) do |p|
|
11
|
+
p.rubyforge_name = 'simplerdb'
|
12
|
+
p.author = 'Gary Elliott'
|
13
|
+
p.email = 'gary@tourb.us'
|
14
|
+
p.summary = "Test your SimpleDB application offline"
|
15
|
+
p.description = p.summary
|
16
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
17
|
+
p.remote_rdoc_dir = '' # Release to root
|
18
|
+
p.version = SimplerDB::VERSION
|
19
|
+
p.extra_deps = []
|
20
|
+
p.extra_deps << ['dhaka', '>= 2.2.1']
|
21
|
+
p.extra_deps << ['builder', '>= 2.0']
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
Rake::TestTask.new do |t|
|
26
|
+
t.libs << "lib"
|
27
|
+
t.test_files = FileList['test/*test.rb']
|
28
|
+
t.verbose = true
|
29
|
+
end
|
data/bin/simplerdb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => [:gen]
|
5
|
+
|
6
|
+
task :gen do
|
7
|
+
require 'query_grammar'
|
8
|
+
|
9
|
+
parser = Dhaka::Parser.new(QueryGrammar)
|
10
|
+
File.open('query_parser.rb', 'w') { |file| file << parser.compile_to_ruby_source_as(:QueryParser) }
|
11
|
+
|
12
|
+
lexer = Dhaka::Lexer.new(QueryLexerSpec)
|
13
|
+
File.open('query_lexer.rb', 'w') {|file| file << lexer.compile_to_ruby_source_as(:QueryLexer)}
|
14
|
+
end
|
data/lib/simplerdb/db.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'simplerdb/query_language'
|
3
|
+
|
4
|
+
module SimplerDB
|
5
|
+
|
6
|
+
class Domain
|
7
|
+
attr_accessor :name, :items
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
@items = Hash.new { |hash, key| hash[key] = Item.new(key) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Item
|
16
|
+
attr_accessor :name, :attributes
|
17
|
+
|
18
|
+
def initialize(name)
|
19
|
+
@name = name
|
20
|
+
@attributes = Hash.new { |hash, key| hash[key] = [] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def put_attribute(attr, replace = false)
|
24
|
+
@attributes[attr.name].clear if replace
|
25
|
+
@attributes[attr.name] << attr
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_attribute(attr)
|
29
|
+
if attr.value.nil?
|
30
|
+
@attributes[attr.name].clear
|
31
|
+
else
|
32
|
+
@attributes[attr.name].delete(attr)
|
33
|
+
end
|
34
|
+
|
35
|
+
if @attributes[attr.name].size == 0
|
36
|
+
@attributes.delete(attr.name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_attributes(attribute_name = nil)
|
41
|
+
attrs = []
|
42
|
+
|
43
|
+
@attributes.each do |key, value|
|
44
|
+
if (attribute_name.nil? || attribute_name == key)
|
45
|
+
attrs += value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
return attrs
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Attribute
|
54
|
+
attr_accessor :name, :value
|
55
|
+
|
56
|
+
def initialize(name, value)
|
57
|
+
@name = name
|
58
|
+
@value = value
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
@name == other.name && @value == other.value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class AttributeParam < Attribute
|
67
|
+
attr_accessor :replace
|
68
|
+
|
69
|
+
def initialize(name, value, replace = false)
|
70
|
+
super(name, value)
|
71
|
+
@replace = replace
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_attr
|
75
|
+
Attribute.new(self.name, self.value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# The main SimplerDB class, containing methods that correspond to the
|
80
|
+
# AWS API.
|
81
|
+
class DB
|
82
|
+
include Singleton
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
reset
|
86
|
+
end
|
87
|
+
|
88
|
+
# For testing
|
89
|
+
def reset
|
90
|
+
@domains = {}
|
91
|
+
@query_executor = QueryExecutor.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_domain(name)
|
95
|
+
@domains[name] ||= Domain.new(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def delete_domain(name)
|
99
|
+
@domains.delete(name)
|
100
|
+
end
|
101
|
+
|
102
|
+
def list_domains(max = 100, token = 0)
|
103
|
+
doms = []
|
104
|
+
count = 0
|
105
|
+
token = 0 unless token
|
106
|
+
@domains.keys.each do |domain|
|
107
|
+
break if doms.size == max
|
108
|
+
doms << domain if count >= token.to_i
|
109
|
+
count += 1
|
110
|
+
end
|
111
|
+
|
112
|
+
if (count >= @domains.size)
|
113
|
+
return doms,nil
|
114
|
+
else
|
115
|
+
return doms,count
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_attributes(domain_name, item_name, attribute_name = nil)
|
120
|
+
domain = @domains[domain_name]
|
121
|
+
item = domain.items[item_name]
|
122
|
+
return item.get_attributes(attribute_name)
|
123
|
+
end
|
124
|
+
|
125
|
+
def put_attributes(domain_name, item_name, attribute_params)
|
126
|
+
domain = @domains[domain_name]
|
127
|
+
item = domain.items[item_name]
|
128
|
+
attribute_params.each { |attr| item.put_attribute(attr.to_attr, attr.replace) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete_attributes(domain_name, item_name, attributes)
|
132
|
+
domain = @domains[domain_name]
|
133
|
+
item = domain.items[item_name]
|
134
|
+
attributes.each { |attr| item.delete_attribute(attr) }
|
135
|
+
end
|
136
|
+
|
137
|
+
def query(domain, query, max = 100, token = 0)
|
138
|
+
@query_executor.do_query(query, @domains[domain], max, token)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dhaka'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module SimplerDB
|
6
|
+
# Uses the lexer/parser/evaluator to perform the query and do simple paging
|
7
|
+
class QueryExecutor
|
8
|
+
ERROR_MARKER = ">>>"
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@lexer = Dhaka::Lexer.new(QueryLexerSpec)
|
12
|
+
@parser = Dhaka::Parser.new(QueryGrammar)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Execute the query
|
16
|
+
def do_query(query, domain, max = 100, token = 0)
|
17
|
+
parse_result = @parser.parse(@lexer.lex(query))
|
18
|
+
token = 0 if token.nil?
|
19
|
+
|
20
|
+
case parse_result
|
21
|
+
when Dhaka::TokenizerErrorResult
|
22
|
+
raise tokenize_error_message(parse_result.unexpected_char_index, query)
|
23
|
+
when Dhaka::ParseErrorResult
|
24
|
+
raise parse_error_message(parse_result.unexpected_token, query)
|
25
|
+
end
|
26
|
+
|
27
|
+
items = QueryEvaluator.new(domain).evaluate(parse_result)
|
28
|
+
results = []
|
29
|
+
count = 0
|
30
|
+
items.each do |item|
|
31
|
+
break if results.size == max
|
32
|
+
results << item if count >= token
|
33
|
+
count += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
if (count == items.size)
|
37
|
+
return results,nil
|
38
|
+
else
|
39
|
+
return results,count
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# From dhaka examples
|
44
|
+
def parse_error_message(unexpected_token, program)
|
45
|
+
if unexpected_token.symbol_name == Dhaka::END_SYMBOL_NAME
|
46
|
+
"Unexpected end of file."
|
47
|
+
else
|
48
|
+
"Unexpected token #{unexpected_token.symbol_name}:\n#{program.dup.insert(unexpected_token.input_position - 1, ERROR_MARKER)}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def tokenize_error_message(unexpected_char_index, program)
|
53
|
+
"Unexpected character #{program[unexpected_char_index - 1].chr}:\n#{program.dup.insert(unexpected_char_index - 1, ERROR_MARKER)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def evaluation_error_message(evaluation_result, program)
|
57
|
+
"#{evaluation_result.exception}:\n#{program.dup.insert(evaluation_result.node.tokens[0].input_position, ERROR_MARKER)}"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
# The SimpleDB query language grammar, as defined at
|
63
|
+
# http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_Query.html
|
64
|
+
class QueryGrammar < Dhaka::Grammar
|
65
|
+
for_symbol(Dhaka::START_SYMBOL_NAME) do
|
66
|
+
start %w| predicates |
|
67
|
+
end
|
68
|
+
|
69
|
+
for_symbol('predicates') do
|
70
|
+
single_predicate %w| predicate |
|
71
|
+
not_predicate %w| not predicate |
|
72
|
+
intersection %w| predicate intersection predicates |
|
73
|
+
not_intersection %w| not predicate intersection predicates |
|
74
|
+
union %w| predicate union predicates |
|
75
|
+
not_union %w| not predicate union predicates |
|
76
|
+
end
|
77
|
+
|
78
|
+
for_symbol('predicate') do
|
79
|
+
attribute_comparison %w| [ attribute_comparison ] |
|
80
|
+
end
|
81
|
+
|
82
|
+
for_symbol('attribute_comparison') do
|
83
|
+
single_comparison %w| identifier comp_op constant |
|
84
|
+
not_comparison %w| not identifier comp_op constant |
|
85
|
+
and_comparison %w| identifier comp_op constant and attribute_comparison |
|
86
|
+
not_and_comparison %w| not identifier comp_op constant and attribute_comparison |
|
87
|
+
or_comparison %w| identifier comp_op constant or attribute_comparison |
|
88
|
+
not_or_comparison %w| not identifier comp_op constant or attribute_comparison |
|
89
|
+
end
|
90
|
+
|
91
|
+
for_symbol('comp_op') do
|
92
|
+
equal %w| = |
|
93
|
+
greater_than %w| > |
|
94
|
+
less_than %w| < |
|
95
|
+
greater_or_equal %w| >= |
|
96
|
+
less_or_equal %w| <= |
|
97
|
+
not_equal %w| != |
|
98
|
+
starts_with %w| starts-with |
|
99
|
+
end
|
100
|
+
|
101
|
+
for_symbol('identifier') do
|
102
|
+
identifier %w| quoted_string |
|
103
|
+
end
|
104
|
+
|
105
|
+
for_symbol('constant') do
|
106
|
+
constant %w| quoted_string |
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# The lexer for the query language.
|
112
|
+
class QueryLexerSpec < Dhaka::LexerSpecification
|
113
|
+
|
114
|
+
%w| = > < >= <= != starts-with |.each do |op|
|
115
|
+
for_pattern(op) do
|
116
|
+
create_token(op)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
for_pattern('\[') do
|
121
|
+
create_token('[')
|
122
|
+
end
|
123
|
+
|
124
|
+
for_pattern('\]') do
|
125
|
+
create_token(']')
|
126
|
+
end
|
127
|
+
|
128
|
+
for_pattern('\s+') do
|
129
|
+
# ignore whitespace
|
130
|
+
end
|
131
|
+
|
132
|
+
KEYWORDS = %w| not and or union intersection |
|
133
|
+
KEYWORDS.each do |keyword|
|
134
|
+
for_pattern(keyword) do
|
135
|
+
create_token(keyword)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
for_pattern("'(\\\\'|[^'])+'") do
|
140
|
+
create_token 'quoted_string'
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# The query evaluator. This class acts on the parse tree and will return
|
146
|
+
# a list of item names that match the query from the evaluate method.
|
147
|
+
class QueryEvaluator < Dhaka::Evaluator
|
148
|
+
self.grammar = QueryGrammar
|
149
|
+
|
150
|
+
def initialize(domain = nil)
|
151
|
+
@domain = domain
|
152
|
+
@predicate_identifier = nil
|
153
|
+
@all_items = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
define_evaluation_rules do
|
157
|
+
|
158
|
+
for_single_predicate do
|
159
|
+
evaluate(child_nodes[0])
|
160
|
+
end
|
161
|
+
|
162
|
+
for_not_predicate do
|
163
|
+
results = evaluate(child_nodes[1])
|
164
|
+
all_items.difference(results)
|
165
|
+
end
|
166
|
+
|
167
|
+
for_intersection do
|
168
|
+
results = evaluate(child_nodes[0])
|
169
|
+
results.intersection(evaluate(child_nodes[2]))
|
170
|
+
end
|
171
|
+
|
172
|
+
# TODO Nots are probably not handled correctly. Need to play with AWS to find out for sure.
|
173
|
+
for_not_intersection do
|
174
|
+
results = evaluate(child_nodes[1])
|
175
|
+
all_items.difference(results.intersection(evaluate(child_nodes[3])))
|
176
|
+
end
|
177
|
+
|
178
|
+
for_union do
|
179
|
+
results = evaluate(child_nodes[0])
|
180
|
+
results.union(evaluate(child_nodes[2]))
|
181
|
+
end
|
182
|
+
|
183
|
+
for_not_union do
|
184
|
+
results = evaluate(child_nodes[1])
|
185
|
+
all_items.difference(results.union(evaluate(child_nodes[3])))
|
186
|
+
end
|
187
|
+
|
188
|
+
for_attribute_comparison do
|
189
|
+
evaluate(child_nodes[1])
|
190
|
+
end
|
191
|
+
|
192
|
+
for_single_comparison do
|
193
|
+
do_comparison(evaluate(child_nodes[0]), evaluate(child_nodes[1]), evaluate(child_nodes[2]))
|
194
|
+
end
|
195
|
+
|
196
|
+
for_not_comparison do
|
197
|
+
do_comparison(evaluate(child_nodes[1]), evaluate(child_nodes[2]), evaluate(child_nodes[3]), true)
|
198
|
+
end
|
199
|
+
|
200
|
+
for_and_comparison do
|
201
|
+
results = do_comparison(evaluate(child_nodes[0]), evaluate(child_nodes[1]), evaluate(child_nodes[2]))
|
202
|
+
results.intersection(evaluate(child_nodes[4]))
|
203
|
+
end
|
204
|
+
|
205
|
+
for_not_and_comparison do
|
206
|
+
results = do_comparison(evaluate(child_nodes[1]), evaluate(child_nodes[2]), evaluate(child_nodes[3]), true)
|
207
|
+
results.intersection(evaluate(child_nodes[5]))
|
208
|
+
end
|
209
|
+
|
210
|
+
for_or_comparison do
|
211
|
+
results = do_comparison(evaluate(child_nodes[0]), evaluate(child_nodes[1]), evaluate(child_nodes[2]))
|
212
|
+
results.union(evaluate(child_nodes[4]))
|
213
|
+
end
|
214
|
+
|
215
|
+
for_not_or_comparison do
|
216
|
+
results = do_comparison(evaluate(child_nodes[1]), evaluate(child_nodes[2]), evaluate(child_nodes[3]), true)
|
217
|
+
results.union(evaluate(child_nodes[5]))
|
218
|
+
end
|
219
|
+
|
220
|
+
for_equal { lambda { |v1, v2| v1 == v2 } }
|
221
|
+
for_greater_than { lambda { |v1, v2| v1 > v2 } }
|
222
|
+
for_less_than { lambda { |v1, v2| v1 < v2 } }
|
223
|
+
for_greater_or_equal { lambda { |v1, v2| v1 >= v2 } }
|
224
|
+
for_less_or_equal { lambda { |v1, v2| v1 <= v2 } }
|
225
|
+
# TODO ['a1' != 'v2'] should return false if a1 has values v1 AND v2
|
226
|
+
for_not_equal { lambda { |v1, v2| v1 != v2 } }
|
227
|
+
for_starts_with { lambda { |v1, v2| v2[0...v1.length] == v1 } }
|
228
|
+
|
229
|
+
for_identifier { val(child_nodes[0]) }
|
230
|
+
for_constant { val(child_nodes[0]) }
|
231
|
+
end
|
232
|
+
|
233
|
+
def val(node)
|
234
|
+
node.token.value.to_s[1..-2]
|
235
|
+
end
|
236
|
+
|
237
|
+
def all_items
|
238
|
+
unless @all_items
|
239
|
+
@all_items = @domain.items.collect { |k,v| k }.to_set
|
240
|
+
end
|
241
|
+
|
242
|
+
return @all_items
|
243
|
+
end
|
244
|
+
|
245
|
+
# Apply the given comparison params to every item in the domain
|
246
|
+
def do_comparison(identifier, op, constant, negate = false)
|
247
|
+
results = Set.new
|
248
|
+
|
249
|
+
if @domain
|
250
|
+
@domain.items.each_value do |item|
|
251
|
+
attrs = item.attributes[identifier]
|
252
|
+
|
253
|
+
attrs.each do |attr|
|
254
|
+
match = op.call(constant, attr.value)
|
255
|
+
if (match && !negate) || (negate && !match)
|
256
|
+
results << item
|
257
|
+
break
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
results
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'simplerdb/servlet'
|
3
|
+
require 'webrick'
|
4
|
+
|
5
|
+
# Runs the SimplerDB server
|
6
|
+
module SimplerDB
|
7
|
+
class Server
|
8
|
+
|
9
|
+
# Create the server running on the given port
|
10
|
+
def initialize(port = nil)
|
11
|
+
@port = port || 8087
|
12
|
+
end
|
13
|
+
|
14
|
+
# Run the server on the configured port.
|
15
|
+
def start
|
16
|
+
config = { :Port => @port }
|
17
|
+
@server = WEBrick::HTTPServer.new(config)
|
18
|
+
@server.mount("/", SimplerDB::RESTServlet)
|
19
|
+
|
20
|
+
['INT', 'TERM'].each do |signal|
|
21
|
+
trap(signal) { @server.shutdown }
|
22
|
+
end
|
23
|
+
|
24
|
+
@server.start
|
25
|
+
end
|
26
|
+
|
27
|
+
# Shut down the server
|
28
|
+
def shutdown
|
29
|
+
@server.shutdown
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'webrick'
|
3
|
+
require 'builder'
|
4
|
+
require 'simplerdb/db'
|
5
|
+
require 'simplerdb/client_exception'
|
6
|
+
|
7
|
+
# Force every request to be a GET. Otherwise WEBrick can't handle requests
|
8
|
+
# from certain clients
|
9
|
+
module WEBrick
|
10
|
+
class HTTPRequest
|
11
|
+
alias old_parse parse
|
12
|
+
def parse(socket=nil)
|
13
|
+
old_parse(socket)
|
14
|
+
@request_method = "GET"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module SimplerDB
|
20
|
+
|
21
|
+
# A WEBrick servlet to handle API requests over REST
|
22
|
+
class RESTServlet < WEBrick::HTTPServlet::AbstractServlet
|
23
|
+
|
24
|
+
def do_GET(req, res)
|
25
|
+
action = req.query["Action"]
|
26
|
+
|
27
|
+
begin
|
28
|
+
if action.nil?
|
29
|
+
# Raise an error
|
30
|
+
raise ClientException.new(:MissingAction, "No action was supplied with this request")
|
31
|
+
else
|
32
|
+
# Process the action appropriately
|
33
|
+
xml = eval("do_#{action.downcase}(req)")
|
34
|
+
res.body = xml
|
35
|
+
res.status = 200
|
36
|
+
end
|
37
|
+
rescue ClientException => e
|
38
|
+
res.body = error_xml(e.code, e.msg)
|
39
|
+
res.status = 400 # What is the right status?
|
40
|
+
end
|
41
|
+
|
42
|
+
res['content-type'] = 'text/xml'
|
43
|
+
end
|
44
|
+
|
45
|
+
alias do_POST do_GET
|
46
|
+
alias do_DELETE do_GET
|
47
|
+
|
48
|
+
# Handle CreateDomain requests
|
49
|
+
def do_createdomain(req)
|
50
|
+
name = req.query["DomainName"]
|
51
|
+
DB.instance.create_domain(name)
|
52
|
+
build_result("CreateDomain")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Handle ListDomains requests
|
56
|
+
def do_listdomains(req)
|
57
|
+
max = req.query["MaxNumberOfDomains"]
|
58
|
+
next_token = req.query["NextToken"]
|
59
|
+
|
60
|
+
max = max.to_i if max
|
61
|
+
domains,token = DB.instance.list_domains(max, next_token)
|
62
|
+
|
63
|
+
build_result("ListDomains") do |doc|
|
64
|
+
doc.ListDomainsResult do
|
65
|
+
if domains
|
66
|
+
domains.each do |domain|
|
67
|
+
doc.DomainName domain
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if token
|
72
|
+
doc.NextToken token
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# DeleteDomains requests
|
80
|
+
def do_deletedomain(req)
|
81
|
+
name = req.query["DomainName"]
|
82
|
+
DB.instance.delete_domain(name)
|
83
|
+
build_result("DeleteDomain")
|
84
|
+
end
|
85
|
+
|
86
|
+
# PutAttributes requests
|
87
|
+
def do_putattributes(req)
|
88
|
+
domain_name = req.query["DomainName"]
|
89
|
+
item_name = req.query["ItemName"]
|
90
|
+
attrs = parse_attribute_args(req)
|
91
|
+
DB.instance.put_attributes(domain_name, item_name, attrs)
|
92
|
+
build_result("PutAttributes")
|
93
|
+
end
|
94
|
+
|
95
|
+
# DeleteAttributes requests
|
96
|
+
def do_deleteattributes(req)
|
97
|
+
domain_name = req.query["DomainName"]
|
98
|
+
item_name = req.query["ItemName"]
|
99
|
+
attrs = parse_attribute_args(req)
|
100
|
+
DB.instance.delete_attributes(domain_name, item_name, attrs)
|
101
|
+
build_result("DeleteAttributes")
|
102
|
+
end
|
103
|
+
|
104
|
+
# GetAttributes request
|
105
|
+
def do_getattributes(req)
|
106
|
+
domain_name = req.query["DomainName"]
|
107
|
+
item_name = req.query["ItemName"]
|
108
|
+
attr_name = req.query["AttributeName"]
|
109
|
+
attrs = DB.instance.get_attributes(domain_name, item_name)
|
110
|
+
|
111
|
+
build_result("GetAttributes") do |doc|
|
112
|
+
doc.GetAttributesResult do
|
113
|
+
if attrs
|
114
|
+
attrs.each do |attr|
|
115
|
+
doc.Attribute do
|
116
|
+
doc.Name attr.name
|
117
|
+
doc.Value attr.value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Query request
|
126
|
+
def do_query(req)
|
127
|
+
domain_name = req.query["DomainName"]
|
128
|
+
max_items = req.query["MaxItems"].to_i
|
129
|
+
max_items = 100 if (max_items < 1 || max_items > 250)
|
130
|
+
next_token = req.query["NextToken"]
|
131
|
+
query = req.query["QueryExpression"]
|
132
|
+
|
133
|
+
results,token = DB.instance.query(domain_name, query, max_items, next_token)
|
134
|
+
build_result("Query") do |doc|
|
135
|
+
doc.QueryResult do
|
136
|
+
if results
|
137
|
+
results.each do |res|
|
138
|
+
doc.ItemName res.name
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
if token
|
143
|
+
doc.NextToken token
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Build a result for the given action.
|
150
|
+
# The given block will add action-specific return fields.
|
151
|
+
def build_result(action)
|
152
|
+
xml = ''
|
153
|
+
doc = Builder::XmlMarkup.new(:target => xml)
|
154
|
+
doc.tag!("#{action}Response", :xmlns => "http://sdb.amazonaws.com/doc/2007-11-07") do
|
155
|
+
if block_given?
|
156
|
+
yield doc
|
157
|
+
end
|
158
|
+
|
159
|
+
doc.ResponseMetadata do
|
160
|
+
doc.RequestId "1234"
|
161
|
+
doc.BoxUsage "0"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
xml
|
166
|
+
end
|
167
|
+
|
168
|
+
# Return the error response for the given error code and message
|
169
|
+
def error_xml(code, msg)
|
170
|
+
xml = ''
|
171
|
+
doc = Builder::XmlMarkup.new(:target => xml)
|
172
|
+
|
173
|
+
doc.Response do
|
174
|
+
doc.Errors do
|
175
|
+
doc.Error do
|
176
|
+
doc.Code code.to_s
|
177
|
+
doc.Message msg
|
178
|
+
doc.BoxUsage "0"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
doc.RequestID "1234"
|
183
|
+
end
|
184
|
+
|
185
|
+
xml
|
186
|
+
end
|
187
|
+
|
188
|
+
def parse_attribute_args(req)
|
189
|
+
args = []
|
190
|
+
for i in (0...100)
|
191
|
+
name = req.query["Attribute.#{i}.Name"]
|
192
|
+
value = req.query["Attribute.#{i}.Value"]
|
193
|
+
replace = (req.query["Attribute.#{i}.Replace"] == "true")
|
194
|
+
if name && value
|
195
|
+
args << AttributeParam.new(name, value, replace)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
args
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
data/lib/simplerdb.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require 'rubygems'
|
3
|
+
require 'right_aws'
|
4
|
+
require 'simplerdb/server'
|
5
|
+
|
6
|
+
# Test the live, running service over HTTP
|
7
|
+
class FunctionalTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@server = SimplerDB::Server.new(8087)
|
11
|
+
@thread = Thread.new { @server.start }
|
12
|
+
@sdb = RightAws::SdbInterface.new('access','secret',
|
13
|
+
{:server => 'localhost', :port => 8087, :protocol => 'http'})
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
@server.shutdown
|
19
|
+
@thread.join
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_domains
|
23
|
+
# Create/list domains
|
24
|
+
@sdb.create_domain("d1")
|
25
|
+
@sdb.create_domain("d2")
|
26
|
+
result = @sdb.list_domains
|
27
|
+
assert result[:domains].index("d1")
|
28
|
+
assert result[:domains].index("d2")
|
29
|
+
|
30
|
+
# List domains with paging
|
31
|
+
domains = []
|
32
|
+
count = 0
|
33
|
+
@sdb.list_domains(1) do |result|
|
34
|
+
domains += result[:domains]
|
35
|
+
count += 1
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
assert count == 2
|
40
|
+
assert domains.index("d1")
|
41
|
+
assert domains.index("d2")
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_items
|
45
|
+
# Create some bands with albums + genre
|
46
|
+
@sdb.create_domain("bands")
|
47
|
+
|
48
|
+
attrs = {:albums => ['Being There', 'Summer Teeth'], :genre => "rock"}
|
49
|
+
@sdb.put_attributes("bands", "Wilco", attrs)
|
50
|
+
|
51
|
+
attrs = {:albums => ['OK Computer', 'Kid A'], :genre => "alternative"}
|
52
|
+
@sdb.put_attributes("bands", "Radiohead", attrs)
|
53
|
+
|
54
|
+
attrs = {:albums => ['The Soft Bulletin'], :genre => "alternative"}
|
55
|
+
@sdb.put_attributes("bands", "The Flaming Lips", attrs)
|
56
|
+
|
57
|
+
# Read the attributes back
|
58
|
+
results = @sdb.get_attributes("bands", "Wilco")
|
59
|
+
assert results[:attributes]["albums"].sort == ['Being There', 'Summer Teeth']
|
60
|
+
assert results[:attributes]["genre"] == ["rock"]
|
61
|
+
|
62
|
+
# Query the bands domain
|
63
|
+
results = @sdb.query("bands", "['genre' = 'alternative']")
|
64
|
+
assert results[:items].sort == ["Radiohead", "The Flaming Lips"]
|
65
|
+
|
66
|
+
results = @sdb.query("bands", "['albums' = 'Being There']")
|
67
|
+
assert results[:items] == ["Wilco"]
|
68
|
+
|
69
|
+
results = @sdb.query("bands", "['albums' starts-with 'OK' or 'albums' = 'The Soft Bulletin']")
|
70
|
+
assert results[:items].sort == ["Radiohead", "The Flaming Lips"]
|
71
|
+
|
72
|
+
# Modify and re-read attributes
|
73
|
+
@sdb.put_attributes("bands", "The Flaming Lips", {:albums => "Yoshimi Battles the Pink Robots"})
|
74
|
+
@sdb.put_attributes("bands", "The Flaming Lips", {:genre => "rock"}, :replace)
|
75
|
+
|
76
|
+
results = @sdb.get_attributes("bands", "The Flaming Lips")
|
77
|
+
assert results[:attributes]["albums"].sort == ["The Soft Bulletin", "Yoshimi Battles the Pink Robots"]
|
78
|
+
assert results[:attributes]["genre"] == ["rock"]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require 'simplerdb/query_language'
|
3
|
+
require 'simplerdb/db'
|
4
|
+
|
5
|
+
# Tests the query evaluator
|
6
|
+
class QueryEvaluatorTest < Test::Unit::TestCase
|
7
|
+
include SimplerDB
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@db = DB.instance
|
11
|
+
@db.create_domain("test")
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
@db.reset
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_single_predicate
|
19
|
+
bulk_insert('i1' => [["a1", "v1"], ["a2", "v2"], ["a3", "bcd"]],
|
20
|
+
'i2' => [["a1", "vv1"]])
|
21
|
+
|
22
|
+
assert_items 'i1', @db.query("test", "['a1' = 'v1']")
|
23
|
+
assert_items 'i1', @db.query("test", "['a1' != 'v2']")
|
24
|
+
assert_items 'i1', @db.query("test", "['a1' >= 'v1']")
|
25
|
+
assert_items 'i1', @db.query("test", "['a1' <= 'v1']")
|
26
|
+
assert_items 'i1', @db.query("test", "['a3' starts-with 'bc']")
|
27
|
+
assert_items 'i1', @db.query("test", "['a3' > 'cde']")
|
28
|
+
assert_items 'i1', @db.query("test", "['a3' < 'abc']")
|
29
|
+
assert_items ['i1', 'i2'], @db.query("test", "['a1' = 'v1' or 'a1' = 'vv1']")
|
30
|
+
assert_items ['i1', 'i2'], @db.query("test", "['a1' <= 'v1']")
|
31
|
+
|
32
|
+
assert_empty @db.query("test", "['a2' != 'v2']")
|
33
|
+
assert_empty @db.query("test", "['a2' > 'v2']")
|
34
|
+
assert_empty @db.query("test", "['a2' = 'v1']")
|
35
|
+
assert_empty @db.query("testxxxxx", "['a2' = 'v2']")
|
36
|
+
assert_empty @db.query("test", "['a2' = 'v1']")
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_ops
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def bulk_insert(items)
|
44
|
+
items.each do |name, attrs|
|
45
|
+
params = []
|
46
|
+
for attr in attrs
|
47
|
+
params << AttributeParam.new(attr[0], attr[1])
|
48
|
+
end
|
49
|
+
|
50
|
+
@db.put_attributes("test", name, params)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def assert_empty(results)
|
55
|
+
assert results[0].empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def assert_items(items, results)
|
59
|
+
results = results[0]
|
60
|
+
for item in items
|
61
|
+
found = false
|
62
|
+
for result in results
|
63
|
+
if result.name == item
|
64
|
+
found = true
|
65
|
+
break
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
assert found, "Missing item #{item}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require 'simplerdb/query_language'
|
3
|
+
|
4
|
+
# Tests the query parser
|
5
|
+
class QueryParserTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
# Taken from an AWS forum post
|
8
|
+
VALID = <<END
|
9
|
+
['attrR' = 'random']
|
10
|
+
['attrR' != 'random']
|
11
|
+
['attrR' < 'random']
|
12
|
+
['attrR' <= 'random']
|
13
|
+
['attrR' starts-with 'random']
|
14
|
+
['attrR' starts-with '0000009000']
|
15
|
+
['attrR' = 'random1' or 'attrR' = 'random2']
|
16
|
+
not ['attrR'='random' or 'attrR'='random']
|
17
|
+
not ['attrR'='random']
|
18
|
+
['attrR1' = 'random1'] union ['attrR2' = 'random2']
|
19
|
+
['attrR1' = 'random1'] union ['attrR2' < 'random2']
|
20
|
+
['attrR1' = 'random1'] union not ['attrR2' = 'random2']
|
21
|
+
['attrR1' = 'random1'] intersection ['attrR2' = 'random2']
|
22
|
+
['attrR1' = 'random1'] intersection not ['attrR2' = 'random2']
|
23
|
+
['attrR1' = 'random1'] intersection ['attrR2' < 'random2']
|
24
|
+
['attrR1' starts-with '0000009000'] intersection ['attrR2' >= 'random2']
|
25
|
+
['attrR1' > '0000000100'] intersection ['attrR2' < '0000200000'] intersection ['attrR3' > '0000000200']
|
26
|
+
['attrR1' = 'random1'] union ['attrR2' = 'random2'] intersection ['attrR3' = 'random3']
|
27
|
+
['attrR1' = 'random1'] intersection ['attrR2' = 'random2'] intersection ['attrR3' = 'random3']
|
28
|
+
['attrR1' = 'random1'] intersection ['attrR2' = 'random2'] intersection ['attrR3' = 'random3']
|
29
|
+
['attrR1' = 'random1'] union ['attrR2' = 'random2'] union ['attrR3' = 'random3']
|
30
|
+
['attrR1' > '0000000100'] intersection ['attrR2' < '0000200000'] intersection ['attrR3' > '0000000200']
|
31
|
+
END
|
32
|
+
|
33
|
+
INVALID = <<END
|
34
|
+
['attrR' == 'random']
|
35
|
+
['attrR' - 'random']
|
36
|
+
['attrR' < 'random]
|
37
|
+
['attrR' <= 'rand'om']
|
38
|
+
['attrR' starts with 'random']
|
39
|
+
['attrR' union '0000009000']
|
40
|
+
['attrR' = 'random1' or 'attrR' = 'random2'] foo ['foo' = 'bar']
|
41
|
+
[]
|
42
|
+
['bar']
|
43
|
+
['']
|
44
|
+
END
|
45
|
+
|
46
|
+
def setup
|
47
|
+
@lexer = Dhaka::Lexer.new(SimplerDB::QueryLexerSpec)
|
48
|
+
@parser = Dhaka::Parser.new(SimplerDB::QueryGrammar)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_valid_queries
|
52
|
+
VALID.each do |query|
|
53
|
+
result = @parser.parse(@lexer.lex(query.strip!))
|
54
|
+
assert result.is_a?(Dhaka::ParseSuccessResult), query
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_invalid_queries
|
59
|
+
INVALID.each do |query|
|
60
|
+
result = @parser.parse(@lexer.lex(query.strip!))
|
61
|
+
assert !result.is_a?(Dhaka::ParseSuccessResult), query
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require 'simplerdb/db'
|
3
|
+
|
4
|
+
class SimplerDBTest < Test::Unit::TestCase
|
5
|
+
include SimplerDB
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@db = DB.instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
@db.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_create_list_domains
|
16
|
+
%w| a b c c c |.each { |d| @db.create_domain(d) }
|
17
|
+
assert @db.list_domains[0].size == 3
|
18
|
+
|
19
|
+
domains,token = @db.list_domains(1)
|
20
|
+
assert domains.size == 1
|
21
|
+
assert token == 1
|
22
|
+
|
23
|
+
domains,token = @db.list_domains(1, 1)
|
24
|
+
assert domains.size == 1
|
25
|
+
assert token == 2
|
26
|
+
|
27
|
+
domains,token = @db.list_domains(10,2)
|
28
|
+
assert domains.size == 1
|
29
|
+
assert token.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_delete_domain
|
33
|
+
%w| a b c |.each { |d| @db.create_domain(d) }
|
34
|
+
@db.delete_domain('b')
|
35
|
+
domains,token = @db.list_domains
|
36
|
+
assert domains.size == 2
|
37
|
+
assert domains[0] == 'a' && domains[1] == 'c'
|
38
|
+
|
39
|
+
@db.delete_domain('xxxxx')
|
40
|
+
assert domains.size == 2
|
41
|
+
assert domains[0] == 'a' && domains[1] == 'c'
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_put_get_attributes
|
45
|
+
@db.create_domain("test")
|
46
|
+
|
47
|
+
attrs = [AttributeParam.new("a1", "v1"), AttributeParam.new("a1", "v2"), AttributeParam.new("a2", "v1")]
|
48
|
+
@db.put_attributes("test", "item1", attrs)
|
49
|
+
assert @db.get_attributes("test", "item1").size == 3
|
50
|
+
assert @db.get_attributes("test", "item1", "a2").size == 1
|
51
|
+
assert @db.get_attributes("test", "item1", "a1").size == 2
|
52
|
+
|
53
|
+
attrs = [AttributeParam.new("a1", "v3")]
|
54
|
+
@db.put_attributes("test", "item1", attrs)
|
55
|
+
assert @db.get_attributes("test", "item1", "a1").size == 3
|
56
|
+
|
57
|
+
assert @db.get_attributes("test", "itemXXXXX", "a2").size == 0
|
58
|
+
|
59
|
+
# Replacement
|
60
|
+
attrs = [AttributeParam.new("a1", "v1"), AttributeParam.new("a1", "v2"), AttributeParam.new("a1", "v3")]
|
61
|
+
@db.put_attributes("test", "item_repl", attrs)
|
62
|
+
assert @db.get_attributes("test", "item_repl", "a1").size == 3
|
63
|
+
attrs = [AttributeParam.new("a1", "v4", true)]
|
64
|
+
@db.put_attributes("test", "item_repl", attrs)
|
65
|
+
assert @db.get_attributes("test", "item_repl", "a1").size == 1
|
66
|
+
@db.get_attributes("test", "item_repl", "a1")[0].value == "v4"
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_delete_attributes
|
70
|
+
@db.create_domain("test")
|
71
|
+
|
72
|
+
attrs = [AttributeParam.new("a1", "v1"), AttributeParam.new("a1", "v2"), AttributeParam.new("a2", "v1")]
|
73
|
+
@db.put_attributes("test", "item1", attrs)
|
74
|
+
@db.delete_attributes("test", "item1", [AttributeParam.new("a1", "v1")])
|
75
|
+
assert @db.get_attributes("test", "item1").size == 2
|
76
|
+
@db.delete_attributes("test", "item1", [AttributeParam.new("a1", "v1")])
|
77
|
+
assert @db.get_attributes("test", "item1").size == 2
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: simplerdb
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.1"
|
7
|
+
date: 2008-03-15 00:00:00 -04:00
|
8
|
+
summary: Test your SimpleDB application offline
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: gary@tourb.us
|
12
|
+
homepage: " by Gary Elliott (gary@tourb.us)"
|
13
|
+
rubyforge_project: simplerdb
|
14
|
+
description: Test your SimpleDB application offline
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Gary Elliott
|
31
|
+
files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
- Rakefile
|
36
|
+
- bin/simplerdb
|
37
|
+
- lib/simplerdb.rb
|
38
|
+
- lib/simplerdb/Rakefile
|
39
|
+
- lib/simplerdb/client_exception.rb
|
40
|
+
- lib/simplerdb/db.rb
|
41
|
+
- lib/simplerdb/query_language.rb
|
42
|
+
- lib/simplerdb/servlet.rb
|
43
|
+
- lib/simplerdb/server.rb
|
44
|
+
- test/query_evaluator_test.rb
|
45
|
+
- test/query_parser_test.rb
|
46
|
+
- test/simplerdb_test.rb
|
47
|
+
- test/functional_test.rb
|
48
|
+
test_files: []
|
49
|
+
|
50
|
+
rdoc_options:
|
51
|
+
- --main
|
52
|
+
- README.txt
|
53
|
+
extra_rdoc_files:
|
54
|
+
- History.txt
|
55
|
+
- Manifest.txt
|
56
|
+
- README.txt
|
57
|
+
executables:
|
58
|
+
- simplerdb
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
dependencies:
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: dhaka
|
66
|
+
version_requirement:
|
67
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 2.2.1
|
72
|
+
version:
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: builder
|
75
|
+
version_requirement:
|
76
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "2.0"
|
81
|
+
version:
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: hoe
|
84
|
+
version_requirement:
|
85
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.4.0
|
90
|
+
version:
|