{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskellQuotes #-} {- | Description: quasiquoter understanding SQL and producing [squeal-postgresql](https://hackage.haskell.org/package/squeal-postgresql) expressions. -} module Squeal.QuasiQuotes ( -- * The quasi-quoter ssql, Field (..), -- * Discussion about monomorphized 'Statements' -- -- $discussion ) where import Language.Haskell.TH.Quote ( QuasiQuoter(QuasiQuoter, quoteDec, quoteExp, quotePat, quoteType) ) import Language.Haskell.TH.Syntax (Exp(AppE, VarE), Q, runIO) import Prelude ( Applicative(pure), Either(Left, Right), Maybe(Nothing), MonadFail(fail) , Semigroup((<>)), Show(show), ($), (.), String, error, print ) import Squeal.QuasiQuotes.Delete (toSquealDelete) import Squeal.QuasiQuotes.Insert (toSquealInsert) import Squeal.QuasiQuotes.MonoRow ( Field(Field, unField), monoManipulation, monoQuery ) import Squeal.QuasiQuotes.Query (toSquealQuery) import Squeal.QuasiQuotes.Update (toSquealUpdate) import qualified Data.Text as Text import qualified PostgresqlSyntax.Ast as PGT_AST import qualified PostgresqlSyntax.Parsing as PGT_Parse {- | 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 `Squeal.PostgreSQL.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](#polymorphic-params) 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](#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 to 'Squeal.PostgreSQL.Expression.Inline.inline' (value being inlined must not be null) * @inline_param(\<ident\>)@: Corresponds to 'Squeal.PostgreSQL.Expression.Inline.inlineParam' (value can be null) where @\<ident\>@ is a haskell identifier in scope, whose type has an 'Squeal.PostgreSQL.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](https://github.com/owensmurray/squeal-postgresql-qq/blob/master/test/test.hs) to see what is currently supported. -} ssql :: QuasiQuoter ssql :: QuasiQuoter ssql = QuasiQuoter { quoteExp :: [Char] -> Q Exp quoteExp = Either [Char] PreparableStmt -> Q Exp toSqueal (Either [Char] PreparableStmt -> Q Exp) -> ([Char] -> Either [Char] PreparableStmt) -> [Char] -> Q Exp forall b c a. (b -> c) -> (a -> b) -> a -> c . Parser PreparableStmt -> Text -> Either [Char] PreparableStmt forall a. Parser a -> Text -> Either [Char] a PGT_Parse.run Parser PreparableStmt PGT_Parse.preparableStmt (Text -> Either [Char] PreparableStmt) -> ([Char] -> Text) -> [Char] -> Either [Char] PreparableStmt forall b c a. (b -> c) -> (a -> b) -> a -> c . Text -> Text Text.strip (Text -> Text) -> ([Char] -> Text) -> [Char] -> Text forall b c a. (b -> c) -> (a -> b) -> a -> c . [Char] -> Text Text.pack , quotePat :: [Char] -> Q Pat quotePat = [Char] -> [Char] -> Q Pat forall a. HasCallStack => [Char] -> a error [Char] "pattern quotes not supported" , quoteType :: [Char] -> Q Type quoteType = [Char] -> [Char] -> Q Type forall a. HasCallStack => [Char] -> a error [Char] "type quotes not supported" , quoteDec :: [Char] -> Q [Dec] quoteDec = [Char] -> [Char] -> Q [Dec] forall a. HasCallStack => [Char] -> a error [Char] "declaration quotes not supported" } toSqueal :: Either String PGT_AST.PreparableStmt -> Q Exp toSqueal :: Either [Char] PreparableStmt -> Q Exp toSqueal = \case Left [Char] err -> [Char] -> Q Exp forall a. [Char] -> Q a forall (m :: * -> *) a. MonadFail m => [Char] -> m a fail [Char] err Right PreparableStmt statement -> do IO () -> Q () forall a. IO a -> Q a runIO (PreparableStmt -> IO () forall a. Show a => a -> IO () print PreparableStmt statement) PreparableStmt -> Q Exp toSquealStatement PreparableStmt statement toSquealStatement :: PGT_AST.PreparableStmt -> Q Exp toSquealStatement :: PreparableStmt -> Q Exp toSquealStatement = \case PGT_AST.SelectPreparableStmt SelectStmt theQuery -> do queryExp <- [Text] -> Maybe (NonEmpty Ident) -> SelectStmt -> Q Exp toSquealQuery [] Maybe (NonEmpty Ident) forall a. Maybe a Nothing SelectStmt theQuery pure $ VarE 'monoQuery `AppE` queryExp PGT_AST.InsertPreparableStmt InsertStmt stmt -> do manipExp <- InsertStmt -> Q Exp toSquealInsert InsertStmt stmt pure $ VarE 'monoManipulation `AppE` manipExp PGT_AST.UpdatePreparableStmt UpdateStmt stmt -> do manipExp <- UpdateStmt -> Q Exp toSquealUpdate UpdateStmt stmt pure $ VarE 'monoManipulation `AppE` manipExp PGT_AST.DeletePreparableStmt DeleteStmt stmt -> do manipExp <- DeleteStmt -> Q Exp toSquealDelete DeleteStmt stmt pure $ VarE 'monoManipulation `AppE` manipExp PreparableStmt unsupported -> [Char] -> Q Exp forall a. HasCallStack => [Char] -> a error ([Char] -> Q Exp) -> [Char] -> Q Exp forall a b. (a -> b) -> a -> b $ [Char] "Unsupported statement: " [Char] -> [Char] -> [Char] forall a. Semigroup a => a -> a -> a <> PreparableStmt -> [Char] forall a. Show a => a -> [Char] show PreparableStmt unsupported {- $discussion #discussion# #monomorphized-output-rows# == 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-params# == 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 'Just's 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. -}