simplerdb 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: