Ogre.Query
Domain specific language for constructing queries.
Currently only a select query is supported.
Currently, the expression language permits construction arithmetic and logical expressions on the base types (int, float, str and bool).
type 'a t = 'a query
logical expression language, defined as
exp ::= str `string` | int `int64` | float `float` | bool `bool` | `'a attribute`.(`'b field`) | `'b field`.[`int`] | exp <bop> exp | <uop> exp bop ::= <aop> | <lop> | <cop> uop ::= not aop ::= + | - lop ::= || | && | ==> cop ::= < | > | = | <> | <= | >=
In the grammar above, the names delimited with backticks represent types of OCaml values, that should be passed at these syntactic locations (sort of an unquoting), for example, str "hello"
is an expression, as well as student.(gpa)
assuming that student
is a value of type 'a attribute
and gpa
is a field of type 'b field
.
Not all expressions are well-formed, as they also must obey to the typing rules. The typing rules are simple (informally):
0. x <bop> y
is wff if x
and y
are of the same type; 1. x.(y)
is wff if y
is a field of attribute x
; 2. x <aop> y
is wff if x
and y
are float or int; 3. not x
is wff if x
is bool 4. x <lop> y
is wff if x
and y
are bool 5. x <cop> y
has type bool.
join statement.
The join
statement is a list of equality classes. Each equality class defines a query constraint, requiring all elements of the class to be equal. The elements of the class are field variables, constructed with the field
function. There are two kinds of the field variables:
field y ~from:x
.field y
.A fully qualified variable matches only with the corresponding field expression, e.g., an equality class
[field teacher ~from:student; field id ~from:teacher]
emposes a constraint student.(teacher) = teacher.(id)
, and is roughly equivalent to the SQL's
INNER JOIN teacher ON student.id = teacher.id
Note: it is OK to use where
clause instead of the join
clause to join attributes, if it makes the query more readable. There is no performance penalty.
The unqualified variable matches with the same fields ignoring the attribute name, for example, an equality class field
classid
, will impose an equality constraint on values from all classid
fields of the selected attributes. Given a concrete select query:
select (from student $teacher) ~join:[[field classid]]
a constraint student.(classid) = teacher.(classid)
is constructed. Another way to construct the same selection is:
select (from student $teacher)
~where:student.(classid) = teacher.(classid)
a selection of attributes.
The tables clause can be constructed using the following grammar:
tables ::= from attr | <tables> $ attr
In other words, there are two constructors, a prefix from
attr
, and an infix attr1 $ attr2
, e.g.,
from students $ teachers $ classes
select ~where ~join (from t1 t2 ... tm)
selects attributes t1
, t2
, ..., tm
, join them by the fields specified in the join
clause, and filters those that satisfy the condition defined with the where
clause.
Examples:
Select all students that has the GPA rate greater than 3.8.
select
~where:(student.(gpa) > float 3.8)
(from students)
Select all students and their corresponding teachers, that have a GPA greater than 3.8 (assuming that teacher is a foreign key to the table of teachers).
select
~where:(student.(gpa) > float 3.8)
~join:[[field teacher ~from:student; field id ~from:teacher]]
(from students)
You may notice, that the select
query lacks the SQL's WHAT
clause, i.e., it is not possible or needed to specify columns. The reason for this, is that the query used as a value that is passed to some command constructor, (e.g.,foreach
), that can work with fields individually, e.g., the following is a complete correspondence of the SQL's:
SELECT name FROM students WHERE gpa > 3.5
foreach Query.(select
~where:(student.(gpa) > float 3.8)
(from students))
~f:(fun s -> return (Student.name s))
It is nearly three times as long, but in return it is type-safe, and composable.
from attr
adds an attribute attr
to the query. An attribute can be referenced in the query if it occurs in the from clause. Otherwise the query is not well-formed.
attrs $ attr
appends an attribute attr
to the sequence of chosen attributes attrs
.
field name
creates an unqualified join variable. field name ~from:attr
creates a qualified join variable.
See the join
type description, for the explanation of the join
expressions and joining.
module Array : sig ... end
Defines a subscripting syntax for creating field variables.
module String : sig ... end
Defines field subscripting syntax.
val str : string -> exp
str x
creates a string constant.
val int : int64 -> exp
int x
creates an integer constant
val bool : bool -> exp
bool x
creates a logic constant
val float : float -> exp
float x
creates a real number constant.
x ==> y
implication.
Be aware that the precedence of OCaml operator ==> is higher than a common precedence of the implication operator in mathematics.
That means, that an expression x && y ==> x && z
is parsed as x && (y ==> x) && z
.
The rule of the thumb is to always put parenthesis in an expression, that has an implication, as even if you're aware of the precedence issue, it is not known to a reader of your code, whether you were aware, or wrote this code by a mistake.