Basic Queries
Given our database definition and database descriptor, we can query database entities and retrieve data. Before we discuss writing queries, we will take a look at some of the important query types.
Data types
The Q data type
Beam queries are built using the Q data type. Q's signature is as follows
data Q be db s a
In this definition
beis the particular Beam backend thisQmonad is written for. Each beam backend defines a custom tag type. For example,beam-sqliteprovides theSqlitetag, andbeam-postgresprovides thePostgrestag. You can see what SQL backends are available in GHCi by asking for info on theBeamSqlBackendclass.
Prelude Database.Beam Database.Beam.Sqlite Data.Text Database.SQLite.Simple Lens.Micro Data.Time Database.Beam.Backend.SQL T> :info BeamSqlBackend
ass (Database.Beam.Backend.Types.BeamBackend be,
IsSql92Syntax (BeamSqlBackendSyntax be),
Sql92SanityCheck (BeamSqlBackendSyntax be),
HasSqlValueSyntax (BeamSqlBackendValueSyntax be) Bool,
HasSqlValueSyntax (BeamSqlBackendValueSyntax be) SqlNull,
Eq (BeamSqlBackendExpressionSyntax be)) =>
BeamSqlBackend be
-- Defined at /Users/travis/Projects/beam/beam-core/Database/Beam/Backend/SQL.hs:212:1
stance BeamSqlBackend Sqlite
-- Defined at /Users/travis/Projects/beam/beam-sqlite/Database/Beam/Sqlite/Connection.hs:157:10
stance (IsSql92Syntax syntax, Sql92SanityCheck syntax,
HasSqlValueSyntax (Sql92ValueSyntax syntax) Bool,
HasSqlValueSyntax (Sql92ValueSyntax syntax) SqlNull,
Eq (Sql92ExpressionSyntax syntax)) =>
BeamSqlBackend (MockSqlBackend syntax)
-- Defined at /Users/travis/Projects/beam/beam-core/Database/Beam/Backend/SQL.hs:238:10
-
dbis the type of the database (as we defined above). This is used to ensure you only query database entities that are in scope in this database. -
sis the scope parameter. For the most part, you'll write your queries so that they work over alls. Beam manipulates this parameter internally to ensure that the fields in your expressions are always in scope at run-time. -
ais the type of the result of the query.
Q is a monad, which means you can use your favorite Monad, Applicative,
and Functor functions. The Functor instance can be used to create
projections
as explained in the next section.
The Monad and Applicative instances can be used
to create JOINs.
The QGenExpr type
While Q represents the result of whole queries (entire SELECTs for example),
QGenExpr represents the type of SQL expressions. QGenExpr also takes some
type parameters:
data QGenExpr context be s a
-
contextis the particular way in which this expression is being used. For example, expressions containing aggregates havecontext ~ QAggregateContext. Expressions returning scalar values havecontext ~ QValueContext. -
beis the backend for which this expression is written. For example, expressions destined for execution in PostgreSQL, will substitute this forPostgres(fromDatabase.Beam.Postgresin thebeam-postgrespackage). You can also leave this polymorphic if you want your expression to be useable across multiple backends.
Note
In previous versions of beam be indicated the 'syntax' rather
than the backend. This was confusing because the syntax and backend
types were not obviously related. Beam >=0.8.0.0 uses the backend
type consistently to indicate where an expression is being used.
-
sis a scoping parameter, which will match thesinQ. -
ais the type of this expression. For example, expressions returning SQLintvalues, will have Haskell typeInt32. This ensures that your SQL query won't fail at run-time with a type error.
Beam defines some specializations of QGenExpr for common uses.
type QExpr = QGenExpr QValueContext
type QAgg = QGenExpr QAggregateContext
type QOrd = QGenExpr QOrderingContext
type QWindowExpr = QGenExpr QWindowingContext
type QWindowFrame = QGenExpr QWindowFrameContext
type QGroupExpr = QGenExpr QGroupingContext
Thus, value expressions can be given the simpler type of QExpr be s a.
Expressions containing aggregates are typed as QAgg be s a.
A note on type inference
These types may seem incredibly complicated. Indeed, the safety that beam tries to provide requires these scary-looking types.
But alas, do not fear! Beam is also designed to assist type inference. For the
most part, you will rarely need to annotate these types in your code.
Occassionally you will need to provide a type for the result of an expression.
For example, SELECTing just the literal 1 may cause an ambiguity, because
the compiler won't know which Integral type to use. Beam provides an easy
utility function as_ for this. With -XTypeApplications enabled,
as_ @Int32 (ambiguous expression)
ensures that ambiguous expression has the type QGenExpr ctxt be s Int32
with the ctxt, be, and s types appropriately inferred.
Simple queries
The easiest query is simply getting all the rows in a specific table. If you
have a database object (something with type DatabaseSettings be db) with some
table or view entities, you can use the all_ function to retrieve all rows in
a specific table or view.
For example, to retrieve all PersonT entries in the exampleDb we defined in
the last section, we can say
all_ (persons exampleDb) :: Q be ExampleDb s (PersonT (QExpr s))
Note
We give the full type of the query here for illustrative purposes only. There is no need to do so in your own code
Two things to note. Firstly, here PersonT is parameterized over the QExpr s
higher-kinded type. This means that each field in PersonT now contains a SQL
expression instead of a Haskell value. This is the magic that our parameterized
types allow.
Thus,
personFirstName (all_ (persons exampleDb)) :: QExpr be s Text
and
personFirstName (Person "John" "Smith" 23 "john.smith@example.com" "8888888888" :: Person) :: Text
Secondly, the field type has the same scope variable as the entire query. This
means, it can only be used in the scope of this query. You will never be able to
inspect the type of s from outside Q.
Once we have a query in terms of Q, we can use the select function from
Database.Beam.Query to turn it into a select statement that can be run against
the backend. select takes an expression of type Q, and converts it into a
SQL statement, ready to be executed against the database.
The output of the query passed to select must follow some conventions, so that
beam knows how to serialize, deserialize, and project the appropriate values
from the query. In particular, the return type of your query must be either
- a plain expression (i.e., type
QExpr), - a
Beamabletype (i.e., a table or primary key, defined as above), or - any combination of tuples of the above (Beam supports up to 8-tuples by default). Higher-order tuples can be formed by nested tuples. For example, for 16 return values, you can return a 2-tuple of 8-tuples or an 8-tuple of 2-tuples or a 4-tuple of 4-tuples, etc.
With this in mind, we can use select to get a query statement against our
database. The return type of all_ is just the table we ask for. In this case,
we're interested in the persons table. The persons table has the Beamable
type PersonT. As expected, the SqlSelect will return us concrete Person
values (recall that Person is equivalent to PersonT Identity).
select (all_ (persons exampleDb)) :: HasQBuilder be => SqlSelect be Person
Normally, you'd ship this select statement off to a backend to run, but for the purposes of this tutorial, we can also ask beam to dump what the standard SQL expression this query encodes.
dumpSqlSelect (all_ (persons exampleDb))
SELECT `t0`.`email` AS "res0", `t0`.`first_name` AS "res1", `t0`.`last_name` AS "res2", `t0`.`password` AS "res3" FROM "cart_users" AS "t0"
Internally, dumpSqlSelect uses a beam-core provided syntax to generate
standard ANSI SQL expressions. Note that these expressions should not be shipped
to a backend directly, as they may not be escaped properly. Still, it is useful
to see what would run.
Tip
all_ only works for TableEntitys. Use allFromView_ for ViewEntitys.
A note on composability
All beam queries are composable. This means that you can freely mix values of
type Q in whichever way typechecks and expect a reasonable SQL query. This
differs from the behavior of SQL, where the syntax for composing queries depends
on the structure of that query.
For example, suppose you wanted to fetch all rows of a table, filter them by a condition, limit the amount of rows returned and then join these rows with another table. In SQL, you'd have to write explicit subselects, take care of handling projections, etc. This is because this query doesn't fit into the 'standard' SQL query structure.
However, in beam, you can simply write this query. Beam will take care of generating explicit subselects and handling projections. Scoping rules enforced by the Haskell type system ensure that the query is constructed correctly.
For example, we can write the following (meaningless) query, and things will work as expected.
do tbl1 <-
limit_ 10 $
filter_ (\customer -> ((customerFirstName customer `like_` "Jo%") &&. (customerLastName customer `like_` "S%")) &&.
(addressState (customerAddress customer) ==. just_ "CA" ||. addressState (customerAddress customer) ==. just_ "WA")) $
all_ (customer chinookDb)
tbl2 <- all_ (track chinookDb)
pure (tbl1, tbl2)
SELECT "t0"."res0" AS "res0",
"t0"."res1" AS "res1",
"t0"."res2" AS "res2",
"t0"."res3" AS "res3",
"t0"."res4" AS "res4",
"t0"."res5" AS "res5",
"t0"."res6" AS "res6",
"t0"."res7" AS "res7",
"t0"."res8" AS "res8",
"t0"."res9" AS "res9",
"t0"."res10" AS "res10",
"t0"."res11" AS "res11",
"t0"."res12" AS "res12",
"t1"."TrackId" AS "res13",
"t1"."Name" AS "res14",
"t1"."AlbumId" AS "res15",
"t1"."MediaTypeId" AS "res16",
"t1"."GenreId" AS "res17",
"t1"."Composer" AS "res18",
"t1"."Milliseconds" AS "res19",
"t1"."Bytes" AS "res20",
"t1"."UnitPrice" AS "res21"
FROM
(SELECT "t0"."CustomerId" AS "res0",
"t0"."FirstName" AS "res1",
"t0"."LastName" AS "res2",
"t0"."Company" AS "res3",
"t0"."Address" AS "res4",
"t0"."City" AS "res5",
"t0"."State" AS "res6",
"t0"."Country" AS "res7",
"t0"."PostalCode" AS "res8",
"t0"."Phone" AS "res9",
"t0"."Fax" AS "res10",
"t0"."Email" AS "res11",
"t0"."SupportRepId" AS "res12"
FROM "Customer" AS "t0"
WHERE ((("t0"."FirstName") LIKE ('Jo%'))
AND (("t0"."LastName") LIKE ('S%')))
AND ((("t0"."State") IS NOT DISTINCT
FROM ('CA'))
OR (("t0"."State") IS NOT DISTINCT
FROM ('WA')))
LIMIT 10) AS "t0"
CROSS JOIN "Track" AS "t1"
SELECT "t0"."res0" AS "res0",
"t0"."res1" AS "res1",
"t0"."res2" AS "res2",
"t0"."res3" AS "res3",
"t0"."res4" AS "res4",
"t0"."res5" AS "res5",
"t0"."res6" AS "res6",
"t0"."res7" AS "res7",
"t0"."res8" AS "res8",
"t0"."res9" AS "res9",
"t0"."res10" AS "res10",
"t0"."res11" AS "res11",
"t0"."res12" AS "res12",
"t1"."TrackId" AS "res13",
"t1"."Name" AS "res14",
"t1"."AlbumId" AS "res15",
"t1"."MediaTypeId" AS "res16",
"t1"."GenreId" AS "res17",
"t1"."Composer" AS "res18",
"t1"."Milliseconds" AS "res19",
"t1"."Bytes" AS "res20",
"t1"."UnitPrice" AS "res21"
FROM
(SELECT "t0"."CustomerId" AS "res0",
"t0"."FirstName" AS "res1",
"t0"."LastName" AS "res2",
"t0"."Company" AS "res3",
"t0"."Address" AS "res4",
"t0"."City" AS "res5",
"t0"."State" AS "res6",
"t0"."Country" AS "res7",
"t0"."PostalCode" AS "res8",
"t0"."Phone" AS "res9",
"t0"."Fax" AS "res10",
"t0"."Email" AS "res11",
"t0"."SupportRepId" AS "res12"
FROM "Customer" AS "t0"
WHERE ((("t0"."FirstName") LIKE (?))
AND (("t0"."LastName") LIKE (?)))
AND ((CASE
WHEN (("t0"."State") IS NULL)
AND ((?) IS NULL) THEN ?
WHEN (("t0"."State") IS NULL)
OR ((?) IS NULL) THEN ?
ELSE ("t0"."State")=(?)
END)
OR (CASE
WHEN (("t0"."State") IS NULL)
AND ((?) IS NULL) THEN ?
WHEN (("t0"."State") IS NULL)
OR ((?) IS NULL) THEN ?
ELSE ("t0"."State")=(?)
END))
LIMIT 10) AS "t0"
INNER JOIN "Track" AS "t1";
-- With values: [SQLText "Jo%",SQLText "S%",SQLText "CA",SQLInteger 1,SQLText "CA",SQLInteger 0,SQLText "CA",SQLText "WA",SQLInteger 1,SQLText "WA",SQLInteger 0,SQLText "WA"]
This allows you to easily factor out queries. This means you can build a query library in your application and then freely mix and match these queries as necessary. This allows you to offload as much processing to the database as possible, rather than shipping data to your application pre-processing.
-- 'complicatedQuery' could be declared and imported from an external module here. The generated query is the same regardless
let complicatedQuery =
filter_ (\customer -> ((customerFirstName customer `like_` "Jo%") &&. (customerLastName customer `like_` "S%")) &&.
(addressState (customerAddress customer) ==. just_ "CA" ||. addressState (customerAddress customer) ==. just_ "WA")) $
all_ (customer chinookDb)
in do tbl1 <- limit_ 10 $ complicatedQuery
tbl2 <- all_ (track chinookDb)
pure (tbl1, tbl2)
SELECT "t0"."res0" AS "res0",
"t0"."res1" AS "res1",
"t0"."res2" AS "res2",
"t0"."res3" AS "res3",
"t0"."res4" AS "res4",
"t0"."res5" AS "res5",
"t0"."res6" AS "res6",
"t0"."res7" AS "res7",
"t0"."res8" AS "res8",
"t0"."res9" AS "res9",
"t0"."res10" AS "res10",
"t0"."res11" AS "res11",
"t0"."res12" AS "res12",
"t1"."TrackId" AS "res13",
"t1"."Name" AS "res14",
"t1"."AlbumId" AS "res15",
"t1"."MediaTypeId" AS "res16",
"t1"."GenreId" AS "res17",
"t1"."Composer" AS "res18",
"t1"."Milliseconds" AS "res19",
"t1"."Bytes" AS "res20",
"t1"."UnitPrice" AS "res21"
FROM
(SELECT "t0"."CustomerId" AS "res0",
"t0"."FirstName" AS "res1",
"t0"."LastName" AS "res2",
"t0"."Company" AS "res3",
"t0"."Address" AS "res4",
"t0"."City" AS "res5",
"t0"."State" AS "res6",
"t0"."Country" AS "res7",
"t0"."PostalCode" AS "res8",
"t0"."Phone" AS "res9",
"t0"."Fax" AS "res10",
"t0"."Email" AS "res11",
"t0"."SupportRepId" AS "res12"
FROM "Customer" AS "t0"
WHERE ((("t0"."FirstName") LIKE ('Jo%'))
AND (("t0"."LastName") LIKE ('S%')))
AND ((("t0"."State") IS NOT DISTINCT
FROM ('CA'))
OR (("t0"."State") IS NOT DISTINCT
FROM ('WA')))
LIMIT 10) AS "t0"
CROSS JOIN "Track" AS "t1"
SELECT "t0"."res0" AS "res0",
"t0"."res1" AS "res1",
"t0"."res2" AS "res2",
"t0"."res3" AS "res3",
"t0"."res4" AS "res4",
"t0"."res5" AS "res5",
"t0"."res6" AS "res6",
"t0"."res7" AS "res7",
"t0"."res8" AS "res8",
"t0"."res9" AS "res9",
"t0"."res10" AS "res10",
"t0"."res11" AS "res11",
"t0"."res12" AS "res12",
"t1"."TrackId" AS "res13",
"t1"."Name" AS "res14",
"t1"."AlbumId" AS "res15",
"t1"."MediaTypeId" AS "res16",
"t1"."GenreId" AS "res17",
"t1"."Composer" AS "res18",
"t1"."Milliseconds" AS "res19",
"t1"."Bytes" AS "res20",
"t1"."UnitPrice" AS "res21"
FROM
(SELECT "t0"."CustomerId" AS "res0",
"t0"."FirstName" AS "res1",
"t0"."LastName" AS "res2",
"t0"."Company" AS "res3",
"t0"."Address" AS "res4",
"t0"."City" AS "res5",
"t0"."State" AS "res6",
"t0"."Country" AS "res7",
"t0"."PostalCode" AS "res8",
"t0"."Phone" AS "res9",
"t0"."Fax" AS "res10",
"t0"."Email" AS "res11",
"t0"."SupportRepId" AS "res12"
FROM "Customer" AS "t0"
WHERE ((("t0"."FirstName") LIKE (?))
AND (("t0"."LastName") LIKE (?)))
AND ((CASE
WHEN (("t0"."State") IS NULL)
AND ((?) IS NULL) THEN ?
WHEN (("t0"."State") IS NULL)
OR ((?) IS NULL) THEN ?
ELSE ("t0"."State")=(?)
END)
OR (CASE
WHEN (("t0"."State") IS NULL)
AND ((?) IS NULL) THEN ?
WHEN (("t0"."State") IS NULL)
OR ((?) IS NULL) THEN ?
ELSE ("t0"."State")=(?)
END))
LIMIT 10) AS "t0"
INNER JOIN "Track" AS "t1";
-- With values: [SQLText "Jo%",SQLText "S%",SQLText "CA",SQLInteger 1,SQLText "CA",SQLInteger 0,SQLText "CA",SQLText "WA",SQLInteger 1,SQLText "WA",SQLInteger 0,SQLText "WA"]