Marco Polo is a binary OrientDB driver for Elixir.
Documentation is available at http://hexdocs.pm/marco_polo.
Add MarcoPolo as a dependency of your application inside your mix.exs
file:
def deps do
[{:marco_polo, "~> 0.1"}]
end
Now run mix deps.get
in your shell to fetch and compile MarcoPolo. To play with MarcoPolo, run iex -S mix
in your project:
{:ok, conn} = MarcoPolo.start_link(user: "admin",
password: "admin",
connection: {:db, "GratefulDeadConcerts", :document})
{:ok, %{response: cluster_id}} = MarcoPolo.command(conn, "CREATE CLASS ProgrammingLanguage")
cluster_id #=> 15
query = "INSERT INTO ProgrammingLanguage(name) VALUES (?)"
{:ok, %{response: doc}} = MarcoPolo.command(conn, query, params: ["Elixir"])
doc.rid #=> #MarcoPolo.RID<#15:0>
doc.version #=> 1
doc.fields #=> %{"name" => "Elixir"}
query = "SELECT FROM ProgrammingLanguage WHERE name = :name"
{:ok, %{response: [language]}} = MarcoPolo.command(conn, query, params: %{name: "Elixir"})
language == doc #=> true
Some OrientDB types are "more specific" than their Elixir counterparts. For example, OrientDB can represent integers as ints (4 bytes), longs (8 bytes), or shorts (2 bytes). Elixir only knows about integers. For this reason, the OrientDB type can be forced (similarly to a type cast) from Elixir. For example, Elixir integers are encoded as ints (4 bytes) by default, but they can be forced to be encoded as shorts or longs by using a tagged tuple:
332 #=> gets encoded with 4 bytes as the int 332
{:long, 332} #=> gets encoded with 8 bytes as the long 332
The same can be done for other types as well.
The following table shows how Elixir types map to OrientDB types and viceversa. The t
Elixir | Encoded as OrientDB type | Decoded in Elixir as |
---|---|---|
true , false |
boolean | same as original |
83 or {:int, 83} |
integer | 83 |
{:short, 21} |
short | 21 |
{:long, 944} |
long | 944 |
{:float, 3.14} |
float | 3.14 |
2.71 or {:double, 2.71} |
double | 2.71 |
Decimal.new(3.14) (using Decimal) |
decimal | same as original |
"foo" , <<1, 2, 3>> |
string | "foo" , <<1, 2, 3>> |
{:binary, <<7, 2>>} |
binary | <<7, 2>> |
%MarcoPolo.Date{year: 2015 month: 7, day: 1} |
date | same as original |
%MarcoPolo.DateTime{year: 2015 month: 7, day: 1, hour: 0, min: 37, sec: 14, msec: 0} |
datetime | same as original |
%MarcoPolo.Document{} |
embedded | same as original |
[1, "foo", {:float, 3.14}] |
embedded list | [1, "foo", 3.14] |
#HashSet<[2, 1]> |
embedded set | #HashSet<[2, 1]> |
%{"foo" => true} |
embedded map | %{"foo" => true} |
%MarcoPolo.RID{cluster_id: 21, position: 3} |
link | %MarcoPolo.RID{cluster_id: 21, position: 3} |
{:link_list, [%MarcoPolo.RID{}, ...]} |
link list | {:link_list, [%MarcoPolo.RID{}, ...]} |
{:link_set, #HashSet<%MarcoPolo.RID{}, ...>} |
link set | {:link_set, #HashSet<%MarcoPolo.RID{}, ...>} |
{:link_map, %{"foo" => %MarcoPolo.RID{}, ...}} |
link set | {:link_map, %{"foo" => %MarcoPolo.RID{}, ...}} |
Caveats:
- embedded maps and link maps only support strings as keys. During encoding,
MarcoPolo tries to convert all keys to strings using the
to_string/1
function and the information about the original type is lost (so that at decoding, all map keys are strings). - encoding and decoding of RidBags is described in the "RidBags" section below.
As of version 0.1, MarcoPolo doesn't support tree RidBags. It only supports embedded RidBags.
Embedded RidBags are represented in Elixir like this:
{:link_bag, [%MarcoPolo.RID, ...]}
Tree-based RidBags will likely be supported in the upcoming versions. In the
meantime, if you need to, you can configure the OrientDB server so that it uses
only embedded RidBags (up to a number of links). To do this, set the value of
the ridBag.embeddedToSbtreeBonsaiThreshold
option in the server's XML config
to a very high value (e.g. 1 billion), so that embedded RidBags will be used up
to that number of links. For example:
<properties>
...
<entry name="ridBag.embeddedToSbtreeBonsaiThreshold" value="1000000000" />
</properties>
MarcoPolo supports OrientDB fetch plans. Starting with these data:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :document})
{:ok, %{response: country}} = MarcoPolo.command(conn, "INSERT INTO Country(name) VALUES ('USA')")
query = "INSERT INTO City(name, country) VALUES ('New York', ?)"
{:ok, %{response: city}} = MarcoPolo.command(conn, query, params: [country.rid])
query = "INSERT INTO Street(name, city) VALUES ('5th avenue', ?)"
{:ok, %{response: street}} = MarcoPolo.command(conn, query, params: [city.rid])
we can fetch the city and the country when we fetch the street:
query = "SELECT FROM Street WHERE name = '5th avenue'"
{:ok, %{response: [street], linked_records: linked}} = MarcoPolo.command(conn, query, fetch_plan: "*:-1")
ny = MarcoPolo.FetchPlan.resolve_links!(street.fields["city"], linked)
usa = MarcoPolo.FetchPlan.resolve_links!(ny.fields["country"], linked)
MarcoPolo supports working with graphs using the same MarcoPolo.command/3
function showed above:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :graph})
query = "CREATE VERTEX V SET name = 'Pizza place'"
{:ok, %{response: pizza_place}} = MarcoPolo.command(conn, query)
query = "CREATE VERTEX V SET name = 'Jane'"
{:ok, %{response: jane}} = MarcoPolo.command(conn, query)
query = "CREATE EDGE HasEatenIn FROM ? to ?"
params = [jane.rid, pizza_place.rid]
{:ok, %{response: edge}} = MarcoPolo.command(conn, query, params: params)
edge.fields["in"] #=> {:link_list, [pizza_place.rid]}
edge.fields["out"] #=> {:link_list, [jane.rid]}
The :graph
atom in the MarcoPolo.start_link/1
function shuld reflect how the
database was created. If it was created as a graph database, then we use
:graph
, otherwise we use :document
. The differences between graph and
document databases are differences in the implementation on the server side; the
API is exactly the same between the two types.
OrientDB supports server-side scripting (for example,
JavaScript and SQL-batch). MarcoPolo supports
this feature through the MarcoPolo.script/4
function:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :document})
{:ok, _} = MarcoPolo.script(conn, "Javascript", """
db.command('CREATE CLASS Number);
for (var i = 1; i <= 10; i++) {
db.command('INSERT INTO Number(value) VALUES (' + i + ')');
}
""")
OrientDB supports server-side transactions, meaning transactions that happen only on the server. The clients sends all the operations it wants to perform in the transactions, and the server either performs them all atomically or reverts all of them if there's an error in one of them. To perform a transaction in MarcoPolo:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :document})
{:ok, resp} = MarcoPolo.transaction(conn, [
{:create, %MarcoPolo.Document{class: "Foo", fields: %{"foo" => "bar"}}},
{:delete, %MarcoPolo.Document{rid: %MarcoPolo.RID{cluster_id: 10, position: 39}}},
])
resp.created
#=> %MarcoPolo.Document{class: "Foo", fields: %{"foo" => "bar"}, rid: %MarcoPolo.RID{...}}
resp.updated
#=> []
To perform transactions with manual rollback (similar to the ones in most relational databases), you have to use server-side scripting. For example, you can perform a transaction by using a SQL script:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :document})
script = """
begin
let account = create vertex Account set name = 'Luke'
let city = select from City where name = 'London' lock record
let edge = create edge Lives from $account to $city
commit
return $edge
"""
MarcoPolo.script(conn, "SQL", script)
Note: this is an experimental feature in OrientDB, and thus subject to frequent changes. It should be considered experimental in MarcoPolo as well.
Live Queries are OrientDB's take on PubSub. A client starts
"watching" a given query, and OrientDB sends messages to that client every time
something happens that changes the result of the query. For example, a LIVE SELECT FROM Person
query subscribes to the SELECT FROM Person
query. If a
client adds record to Person
, then all clients subscribed to that query get a
message that says a new Person
has been created. Subscriptions are identified
by tokens.
All of this is pretty straightforward in MarcoPolo:
{:ok, conn} = MarcoPolo.start_link(user: "root",
password: "root",
connection: {:db, "GratefulDeadConcerts", :document})
# Let's keep the token around so that we can unsubscribe later
{:ok, token} = MarcoPolo.live_query(conn, "LIVE SELECT FROM Person", self())
# The third argument to live_query/3 is the pid that will receive messages from
# the live query
MarcoPolo.command(conn, "INSERT INTO Person(name) VALUES ('Olivia Dunham')")
receive do msg -> msg end
#=> {:orientdb_live_query, token, {:create, %MarcoPolo.Document{class: "Person", ...}}}
MarcoPolo.command(conn, "UPDATE Person SET name = 'Fauxlivia Dunham' WHERE name = 'Olivia Dunham'")
receive do msg -> msg end
#=> {:orientdb_live_query, token, {:update, %MarcoPolo.Document{class: "Person", ...}}}
# Ok, enough with Fringe references
:ok = MarcoPolo.live_query_unsubscribe(conn, token)
For more information on how to contribute to MarcoPolo (including how to clone the repository and run tests), have a look at the CONTRIBUTING file.
See the LICENSE file.