Last week, we have launched Datomisca, our opensource Scala API trying to enhance Datomic experience for Scala developers.
Datomic is great in Clojure because it is was made for it. Yet, we believe Scala can provide a very good platform for Datomic too because the functional concepts found in Clojure are also in Scala except that Scala is a compiled and statically typed language whereas Clojure is dynamic. Scala could also bring a few features on top of Clojure based on its features such as static typing, typeclasses, macros…
This article is the first of a serie of articles aiming at describing as shortly as possible specific features provided by Datomisca. Today, let’s present how Datomisca enhances Datomic queries.
Query in Datomic?
Let’s take the same old example of a Person having :
- a name
String
- a age
Long
- a birth
Date
So, how do you write a query in Datomic searching a person by its name? Like that…
1 2 3 4 |
|
As you can see, this is Clojure using Datalog rules.
In a summary, this query:
- accepts 2 inputs parameters:
- a datasource
$
- a name parameter
?name
- a datasource
- searches facts respecting datalog rule
[ ?e :person/name ?name ]
: a fact having attribute:person/name
with value?name
- returns the ID of the found facts
?e
Reminders about Datomic queries
Query is a static data structure
An important aspect of queries to understand in Datomic is that a query is purely a static data structure and not something functional. We could compare it to a prepared statement in SQL: build it once and reuse it as much as you need.
Query has input/ouput parameters
In previous example:
:in
enumerates input parameters:find
enumerates output parameters
When executing this query, you must provide the right number of input parameters and you will retrieve the given number of output parameters.
Query in Datomisca?
So now, how do you write the same query in Datomisca?
1 2 3 4 5 |
|
I see you’re a bit disappointed: a query as a string whereas in Clojure, it’s a real data structure…
This is actually the way the Java API sends query for now. Moreover, using strings like implies potential bad practices such as building queries by concatenating strings which are often the origin of risks of code injection in SQL for example…
But in Scala we can do a bit better using new Scala 2.10 features : Scala macros.
So, using Datomisca, when you write this code, in fact, the query string is parsed by a Scala macro:
- If there are any error, the compilation breaks showing where the error was detected.
- If the query seems valid (with respect to our parser), the String is actually replaced by a AST representing this query as a data structure.
- The input/output parameters are infered to determine their numbers.
Please note that the compiled query is a simple immutable AST which could be manipulated as a Clojure query and re-used as many times as you want.
Example OK with single output
1 2 3 4 5 6 7 8 9 |
|
Without going in deep details, here you can see that the compiled version of q
isn’t a Query[String]
but a TypedQueryAuto2[DatomicData, DatomicData, DatomicData]
being an AST representing the query.
TypedQueryAuto2[DatomicData, DatomicData, DatomicData]
means you have:
- 2 input parameters
$ ?name
of typeDatomicData
andDatomicData
- Last type parameter represents output parameter
?e
of typeDatomicData
Note : DatomicData
is explained in next paragraph.
Example OK with several outputs
1 2 3 4 5 6 7 8 9 10 |
|
TypedQueryAuto2[DatomicData,DatomicData,(DatomicData, DatomicData)]
means you have:
- 2 input parameters
$ ?name
of typeDatomicData
- last tupled type parameter represents the 2 output parameters
?e ?age
of typeDatomicData
Examples with syntax-error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Here is see that the compiler will tell you where it detects syntax errors.
The query compiler is not yet complete so don’t hesitate to report us when you discover issues.
What’s DatomicData
?
Datomisca wraps completely Datomic API and types. So Datomisca doesn’t let any Datomic/Clojure types perspirating into its domain and wraps them all in the so-called DatomicData
which is the abstract parent trait of all Datomic types seen from Datomisca. For each Datomic type, you have the corresponding specific DatomicData
:
- DString for String
- DLong for Long
- DatomicFloat for Float
- DSet for Set
- DInstant for Instant
- …
Why not using Pure Scala types directly?
Firstly, because type correspondence is not exact between Datomic types and Scala. The best sample is Instant
: is it a java.util.Date
or a jodatime.DateTime
?
Secondly, we wanted to keep the possibility of converting Datomic types into different Scala types depending on our needs so we have abstracted those types.
This abstraction also isolates us and we can decide exactly how we want to map Datomic types to Scala. The trade-off is naturally that, if new types appear in Datomic, we must wrap them.
Keep in mind that Datomisca queries accept and return DatomicData
All query data used as input and output paremeters shall be DatomicData
. When getting results, you can convert those generic DatomicData
into one of the previous specific types (DString
, DLong
, … ).
From DatomicData
, you can also convert to Scala pure types based on implicit typeclasses:
1 2 3 4 5 6 7 8 |
|
Note 1 : that current Scala query compiler is a bit restricted to the specific domain of Datomic queries and doesn’t support all Clojure syntax which might create a few limitations when calling Clojure functions in queries. Anyway, a full Clojure syntax Scala compiler is in the TODO list so these limitations will disappear once it’s implemented…
Note 2 : Current macro just infers the number of input/output parameters but, using Schema typed attributes that we will present in a future article, we will provide some deeper features such as parameter type inference.
Execute the query
You can create queries independently of any connection to Datomic.
But you need an implicit DatomicConnection
in your scope to execute it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Please note we made the database
input parameter mandatory even if it’s implicit in when importing Datomic._
because in Clojure, it’s also required and we wanted to stick to it.
Compile-error if wrong number of inputs
If you don’t provide 2 input parameters, you will get a compile error because the query expects 2 input parameters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The compile error seems a bit long as the compiler tries a few different version of Datomic.q
but just remind that when you see cannot be applied to (datomisca.TypedQueryAuto2[…
, it means you provided the wrong number of input parameters.
Use query results
Query results are List[DatomicData…]
depending on the output parameters inferred by the Scala macros.
In our case, we have 2 output parameters so we expect a List[(DatomicData, DatomicData)]
.
Using List.map
(or headOption
to get the first one only), you can then use pattern matching to specialize your (DatomicData, DatomicData)
to (DLong, DInstant)
as you expect.
1 2 3 4 5 6 |
|
Note 1: that when you want to convert your DatomicData
, you can use our converters based on implicit typeclasses as following
Note 2: The Scala macro has not way just based on query to infer the real types of output parameters but ther is a TODO in the roadmap: using typed schema attributes presented in a future article, we will be able to do better certainly… Be patient ;)
More complex queries
As Datomisca parses the queries, you may wonder what is the level of completeness of the query parser for now?
Here are a few examples showing what can be executed already:
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 49 50 51 52 53 |
|
Note that currently Datomisca reserializes queries to string when executing because Java API requires it but once Datomic Java API accepts that we pass List[List[Object]] instead of strings for query, the interaction will be more direct…
Have datomiscafun!