In the relational world, PL/SQL is used to store business/application logic inside the relational database. The same movement is currently happening in the NoSQL. Redis for example uses LUA script in its newest version (2.6), to allow developers to tweak Redis. There a lot of use-cases for a programming language in document-stores. Programming languages are used in various document stores like ArangoDB, CouchDB, MongoDB, or VoltDB. In my opinion in the following use-cases a suitable language like Ruby will be most handy.
Transactions: Quite a lot NoSQL databases, that implement transaction, use scripts to do so. While transaction in a RDBMS were long running processes, transactions in NoSQL database are short-running computations or modification. For example, Redis requires a complicated optimistic locking to simulate transactions. The same effect can much easier achieved with small script.
Business Logic: Document stores and also most graph databases store objects aka documents as JSON objects. One of the anti-patterns is to store an age field in these objects. It is very easy to compute the age from the birthday in the client. But even with such a simple example. there are pit-falls. What is the correct time-zone? How to handle a “is 18-year check” if the birthday is the 29.Feb? Instead of handling these corner-cases in each different client, it is much easier to implement them once and for all in the server.
Permission: With complex queries, permission handling can be a performance disaster. One easy solution is to wrap the query in the a script and handle permission there.
Graph Traversal: In order to implement complex graph queries, one either needs ascii arts or scripts to describe the traversal.
Different databases implement these use-cases using different languages like Lua, Erlang, or JavaScript. With this blog I would like to promote Ruby as a possible candidate for such tasks.
Using MRuby within ArangoDB
ArangoDB, like CouchDB, has a HTTP interface and uses JSON for document exchange. The internal Http-Server of ArangoDB uses data structures for request and responses that loosely resambles the WEBrick interface of Ruby.
I tried to implement the age example with ArangoDB using MRuby. The first step is to define the HttpResponse and HttpRequest – as @tisba pointed out, it will be much more Ruby-like when using attribute_reader, see here. I will rewrite this as soon as this issue with MRuby has been solved.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
module Arango class HttpRequest def body() return @body end def headers() return @headers end def parameters() return @parameters end def request_type() return @request_type end def suffix() return @suffix end end end |
and
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
module Arango class HttpResponse def content_type() return @content_type end def content_type=(type) @content_type = type end def body() return @body end def body=(text) @body = text.to_s end def status() return @status end def status=(code) @status = code.to_i end end end |
The AbstractServlet is also straight forward.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
module Arango class AbstractServlet @@HTTP_OK = 200 @@HTTP_CREATED = 201 def service(req, res) method = req.request_type if method == "GET" self.do_GET(req, res) elsif method == "PUT" self.do_PUT(req, res) elsif method == "POST" self.do_POST(req, res) elsif method == "DELETE" self.do_DELETE(req, res) elsif method == "HEAD" self.do_HEAD(req, res) else generate_unknown_method(req, res, method) end end def do_GET(req, res) res.status = @@HTTP_METHOD_NOT_ALLOWED end def do_PUT(req, res) res.status = @@HTTP_METHOD_NOT_ALLOWED end def do_POST(req, res) res.status = @@HTTP_METHOD_NOT_ALLOWED end def do_DELETE(req, res) res.status = @@HTTP_METHOD_NOT_ALLOWED end def do_HEAD(req, res) res.status = @@HTTP_METHOD_NOT_ALLOWED end def generate_unknown_method(req, res, method) res.status = @@HTTP_METHOD_NOT_ALLOWED end end end |
The missing piece is the C++/Ruby bridge. This bridge takes a C++ request objects, converts it into a Ruby object, calls the service method, takes the response object and converts it back to C++. The complete source code can be found at https://github.com/triAGENS/ArangoDB/tree/devel/arangod/MRServer.
Hallo World
The first script to try is always the “hallo world”:
1 2 3 4 5 6 7 |
class VersionHandler < Arango::AbstractServlet def do_GET request, response response.status = 200 response.content_type = 'text/plain' response.body = 'Hallo, World!' end |
Arango::HttpServer.mount “/_ruby/version”, VersionHandler
Using curl we now get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
> curl -v http://localhost:8529/_ruby/version > GET /_ruby/version HTTP/1.1 > User-Agent: curl/7.22.0 > Host: localhost:8529 > Accept: */* > < HTTP/1.1 200 OK < connection: Keep-Alive < content-type: text/plain < server: triagens GmbH High-Performance HTTP Server < content-length: 13 < * Connection #0 to host localhost left intact * Closing connection #0 Hallo, World! |
Age Example
The age example is almost as simple – no error handling :-). Assume that the collection containing the users is called “users” and the birthday is in attribute of the same name. The call to get the user with id 123456 is “/_ruby/user/123456”.
1 2 3 4 5 6 7 8 9 10 11 12 |
class UserHandler < Arango::AbstractServlet def do_GET request, response uid = request.suffix[0] user = db.users.document(uid) user.age = age_of_user(user) response.status = 200 response.content_type = 'application/json' response.body = to_json(user) end end Arango::HttpServer.mount "/_ruby/user", UserHandler |