| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Squeal.QuasiQuotes
Description
The quasi-quoter
ssql :: QuasiQuoter Source #
Splice in a squeal expression with the following type:
Statement db params <Concrete-Haskell-Row-Type>
Input parameters
The input parameters params is a polymorphic type that gets encoded into
SQL statement parameters. It is a direct pass-through to Squeal using Squeal's
genericParams. The upshot is that we don't monomorphize
this type the same way we do for the statement's resulting row type. See the
Polymorphic Params Discussion for the reason why not.
Resulting Row Type
<Canonical-Haskell-Row-Type> is going to be some concrete tuple of the
form:
(Field name1 type1, (Field name2 type2, (Field name3 type3, <continue nesting>, ()))...)
where the "name<N>" are phantom types of kind Symbol, which provide
the name of the corresponding column, and types "type<N>" are whatever
appropriate haskell type represents the postgres column type.
See the discussion section for why we monomorphize the
squeal Statement in this way.
Inline Haskell Values
If you don't want to use statement parameters, you can still get Haskell values into your statements is with special bulit-in sql functions:
inline(<ident>): Corresponds toinline(value being inlined must not be null)inline_param(<ident>): Corresponds toinlineParam(value can be null)
where <ident> is a haskell identifier in scope, whose type has an
Inline instance.
Examples
For the examples, let's assume you have a database like this:
type UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ] type UsersColumns = '[ "id" ::: 'Def :=> 'NotNull 'PGtext , "name" ::: 'NoDef :=> 'NotNull 'PGtext , "employee_id" ::: 'NoDef :=> 'NotNull 'PGuuid , "bio" ::: 'NoDef :=> 'Null 'PGtext ] type EmailsConstraints = '[ "pk_emails" ::: 'PrimaryKey '["id"] , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"] ] type EmailsColumns = '[ "id" ::: 'Def :=> 'NotNull 'PGint4 , "user_id" ::: 'NoDef :=> 'NotNull 'PGtext , "email" ::: 'NoDef :=> 'Null 'PGtext ] type Schema = '[ "users" ::: 'Table (UsersConstraints :=> UsersColumns) , "emails" ::: 'Table (EmailsConstraints :=> EmailsColumns) ]
Insert Example
mkStatement :: Int32 -> Text -> Maybe Text -> Statement DB () ()
mkStatement emailId uid email =
[ssql|
insert into
emails (id, user_id, email)
values (inline("emailId"), inline(uid), inline_param(email))
|]Notice the quotes around "emailId". This is because postgres SQL
parsing mandates the unquoted idents be converted to lower case
(as a way of being "case insensitive"), which would result in the
quasiquoter injecting the lower case emailid variable, which is not
in scope. The solution is to double quote the SQL ident so that its
casing is preserved.
Select Example
mkStatement
:: Text
-> Statement
DB
()
( Field "id" Text
, ( Field "name" Text
, ( Field "email" (Maybe Text)
, ()
)
)
)
mkStatement targetEmail =
[ssql|
select users.id, users.name, emails.email
from users
left outer join emails
on emails.user_id = users.id
where emails.email = inline("targetEmail")
|]These examples are more or less taken from the test suite. I strongly recommend reading the test code to see what is currently supported.
Discussion about monomorphized Statements
Monomorphized Output Rows
The reason we monomorphize the SQL statement using the nested tuple
structure (see ssql) is that Squeal by default allows for generic
based polymorphic input row types and output row types. This is
problematic for a number of reasons. It severely hinders type inference,
and it produces very bad error messages when the types are misaligned.
If you were to manually craft Squeal expressions, you would have the opportunity to add helpful type annotations for convenient and critical sub-expressions of the overall top-level Squeal expression. But because this quasi-quoter generates the Squeal expression for you, you have no opportunity to do something similar. The only place you can place a type annotation is at the entire top level expression generated by the quasi-quoter.
Therefore, the trade-offs between going with the polymorphic approach and a monomorphic approach don't have the same costs/benefits when using the quasi-quoter as they do when manually crafting Squeal.
To solve this problem (or rather to choose a different set of trade-offs), the quasi-quoter forces all input and output rows to be monomorphized into a "canonical" Haskell type which has this nested tuple of fields structure. Under the hood we do this via an injective type family, so that type inference can go both ways.
If you have a quasi-quoted query in hand, you can use a type hole to ask GHC what its inferred concrete type is. Conversely, if you know what the expected row type is, but your quasi-quoted SQL statement is failing to align with your expected types, you can put your expected row type in a type signature and get a much better error message about how and why the SQL statement is failing to type check.
Polymorphic Input Parameters
Type inferencing of statement parameters is not supported, unlike the type inferencing of statement output row types. That is to say you can write this (i.e. a type hole in the output slot) and expect to get a concrete type in your GHC error message.
statement :: Statement DB (Only Text) _ statement [ssql| select name from users where id = $1 |]
But GHC will not give you anything useful if you type this instead (i.e. a type hole in the input slot):
statement :: Statement DB _ (Field "name" Text, ()) statement [ssql| select name from users where id = $1 |]
The TLDR is that it can't easily be made to work without some pretty extensive machinery to cope with SQL's type system and even then it would come with some unpalatable trade-offs.
The basic problem is that SQL comparisons and other operators support
null polymorphic values as their parameters. So for instance, an SQL
statement can compare a non-null column with the literal value null,
and that is valid SQL. So if you compare the non-null column with a
parameter instead, what should the type of the parameter be?
E.g.:
select * from users where id = null -- compare with `null`. select * from users where id = $1 -- compare with a param. select * from users where id = id -- compare with non-nullable column.
Should the parameter be nullable or not nullable? It can be either! Therefore the corresponding Haskell type can be either! How do we choose?
We could say, by fiat, that input params always nullable (and make the
user type a bunch of Justs everywhere), but then consider this insert
statement where the parameter is being used to specify the value of
a non-null column.
insert into users (id) values ($1)
This instance of the parameter can't be nullable. It must be non-nullable.
So sometimes statement parameters can be null polymorphic and sometimes
they can't. Sometimes a Haskell type of either Something or
(Maybe Something) will work, and sometimes it won't.
I think the decision requires semantic knowledge of a non-trivial
subset of SQL. A design goal of this library is specifically to offload
semantic knowledge of SQL to Squeal, not to re-implement it. So unless
I can think of (or someone contributes) a really clever trick, I think
input params are going to stay polymorphic, with all the inscrutable
Squeal and Generics.SOP type errors you get when your types don't
quite align.