Some familiarity with ML-like languages (e.g. Ocaml, Haskell) will help a lot.
Queries are built out of the following core functions which are described below:
map : ({α} → {β}) → [{α}] → [{β}]
filter : ({α} → ~bool) → [{α}] → [{α}]
order : (Comparable β) ⇒ ({α} → β) → [{α}] → [{α}]
natjoin : ({γ} = {α} ⋈ {β}) ⇒ [{α}] → [{β}] → [{γ}]
join : ({α} → {β} → ~bool) → ({α} → {β} → {γ}) → [{α}] → [{β}] → [{γ}]
aggregate : ({β} = Agg {γ}) ⇒ ({α} → {β}) → [{α}] → [{γ}]
Database types are prefixed by a tilde (~
), e.g.
~int4
~text
~bool
Table rows are surrounded by curly brackets, e.g.
{a: ~int4, b: ~text}
is a table row containing fields a
and b
with types ~int4
and ~text
respectively.
Type variables are represented by Greek letters, e.g.
α
and β
can represent any type.
Row polymorphic rows are rows with a type variable at the end e.g.
{a: ~int4, b: ~text, ..α}
is a table row containing fields a
and b
with types ~int4
and ~text
respectively, but can also contain additional fields.
Query results (containing multiple rows) are surrounded by square brackets, e.g.
[{a: ~int4, b: ~text}]
is a query result containing rows of {a: ~int4, b: ~text}
Functions are represented by an arrow (→
) e.g.
α → α
is the type of the identity function that takes any value and returns the same value.~int4 → ~int4 → ~bool
can be considered a function that takes two integers and returns a boolean. Because the arrow is right associative (~int4 → (~int4 → ~bool)
), it is technically a function that accepts an~int4
and returns a new function~int4 → ~bool
i.e. it can be partially applied.
Type constraints come before a double arrow (⇒
)
(Num α)
requires that the typeα
is a number (~int2/4/8
,~numeric
or~float4/8
).(Comparable α)
requires that the typeα
is a comparable. Most primitive types are valid.(DB α)
requires that the typeα
can be represented in the underlying database. Any type starting with a tilde (~
) is valid.({γ} = {α} ⋈ {β})
requires that{γ}
is the natural join of{α}
and{β}
.{α}
and{β}
must have at least one field in common.({β} = Agg {γ})
requires that{β}
contains the aggregated types of{γ}
e.g.({x: Agg ~int4, y: Agg ~text} = Agg {x: ~int4, y: ~text})
Each table in your database is represented by its name. This is a complete query that will return the contents of the table (equivalent to SELECT * FROM ...
) and its type is a set of rows containing the fields of the table.
Our example database will have the expressions:
example : [{a: ~int4, b: ~text}]
join_example : [{a: ~int4, c: ~text}]
As in Haskell, lambdas are written:
\arg -> expression
For example, \x -> x
is the identity function with the type α → α
.
Use dot notation to access fields, e.g.
\t -> t.x
is a function that takes a row and returns the value of its x
field. It has type {x: α, ..β} → α
. Note that the input row is row-polymorphic, i.e. this function can accept any row that has an x
field.
Rows can be constructed using curly brackets, e.g.
{a: 1, b: 'Hello'}
constructs a row with type {a: ~int4, b: ~text}
.
The filter function has type ({α} → ~bool) → [{α}] → [{α}]
SQUEE> filter (\t -> t.a = 1) example
: [{a: ~int4, b: ~text}]
| a | b |
+---+----------+
| 1 | example1 |
The order function has type (Comparable β) ⇒ ({α} → β) → [{α}] → [{α}]
SQUEE> order (\t -> 0 - t.a) example
: [{a: ~int4, b: ~text}]
| a | b |
+---+----------+
| 2 | example2 |
| 1 | example1 |
The map function has type ({α} → {β}) → [{α}] → [{β}]
SQUEE> map (\t -> {a: t.a + 1, b: t.b}) example
: [{a: ~int4, b: ~text}]
| a | b |
+---+----------+
| 2 | example1 |
| 3 | example2 |
The natural join function has the type ({γ} = {α} ⋈ {β}) ⇒ [{α}] → [{β}] → [{γ}]
SQUEE> natjoin example join_example
: [{a: ~int4, b: ~text, c: ~text}]
| a | b | c |
+---+----------+---------------+
| 1 | example1 | join_example1 |
| 2 | example2 | join_example2 |
The generalised inner join function has the type ({α} → {β} → ~bool) → ({α} → {β} → {γ}) → [{α}] → [{β}] → [{γ}]
.
The first argument is the condition on which to join, and the second argument merges the two rows in to one.
SQUEE> join (\a b -> a.a < b.a + 1) (\a b -> {a: a.a, b: a.b, c: b.c}) example join_example
: [{a: ~int4, b: ~text, c: ~text}]
| a | b | c |
+---+----------+---------------+
| 1 | example1 | join_example1 |
| 1 | example1 | join_example2 |
| 2 | example2 | join_example2 |
The aggregate function has type ({β} = Agg {γ}) ⇒ ({α} → {β}) → [{α}] → [{γ}]
.
The first argument is the mapping from a row to an aggregate result.
Functions that return aggregate results include:
sum : (Num α) ⇒ α → Agg α
count : Agg ~int4
SQUEE> aggregate (\t -> {sum: sum t.a, count: count}) example
: [{count: ~int4, sum: ~int4}]
| count | sum |
+-------+-----+
| 2 | 3 |
The pipe (|
) is an infix operator with type α → (α → β) → β
:
SQUEE> example | filter (\t -> t.a = 1) | natjoin join_example
: [{a: ~int4, b: ~text, c: ~text}]
| a | b | c |
+---+----------+---------------+
| 1 | example1 | join_example1 |
Assign values and functions with the syntax:
def [name] := [expression]
For example,
SQUEE> def filteredExample := example | filter (\t -> t.a = 1)
filteredExample : [{a: ~int4, b: ~text}]
SQUEE> def identity := \x -> x
identity : α → α
SQUEE> identity filteredExample
: [{a: ~int4, b: ~text}]
| a | b |
+---+----------+
| 1 | example1 |
export
has similar syntax to def
. It signifies what queries should be included when generating code.
For example, if we have the file example.squee
:
def notExported := example
export exportedExample := example
export filteredExportedExample a := example | filter (\t -> t.a = a)
Then the command squee generate sql-prepare example.squee
will generate:
PREPARE exportedExample AS
SELECT "a","b" FROM "example" AS _t;
PREPARE filteredExportedExample AS
SELECT "a","b" FROM "example" AS _t WHERE ("a") = ($1);