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 ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1
2
+
3
+ * Initial release
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,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'simplerdb/server'
5
+
6
+ port = ARGV.size > 0 ? ARGV[0] : nil
7
+
8
+ SimplerDB::Server.new(port).start
@@ -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
@@ -0,0 +1,10 @@
1
+ # Exceptions that are caused by an invalid request from the client.
2
+ class ClientException < Exception
3
+ attr_accessor :code, :msg
4
+
5
+ def initialize(code, msg)
6
+ @code = code
7
+ @msg = msg
8
+ end
9
+
10
+ end
@@ -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,3 @@
1
+ class SimplerDB
2
+ VERSION = '0.1'
3
+ end
@@ -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: