packages feed

sq-0.0.1: lib/Sq/Statement.hs

{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoFieldSelectors #-}

module Sq.Statement
   ( SQL
   , sql
   , Statement (..)
   , readStatement
   , writeStatement
   , BoundStatement (..)
   , bindStatement
   ) where

import Control.DeepSeq
import Control.Monad
import Data.Coerce
import Data.Functor.Contravariant
import Data.Profunctor
import Data.String
import Data.Text qualified as T
import Di.Df1 qualified as Di
import GHC.Records
import GHC.Show
import Language.Haskell.TH.Quote (QuasiQuoter (..))
import Prelude hiding (Read, log)

import Sq.Input
import Sq.Mode
import Sq.Output

--------------------------------------------------------------------------------

-- | Raw SQL string. Completely unchecked.
newtype SQL = SQL T.Text
   deriving newtype
      ( Eq
      , Ord
      , -- | Raw SQL string.
        Show
      , IsString
      , Semigroup
      , NFData
      )

instance Di.ToMessage SQL where
   message = Di.message . show

-- | Raw SQL string as 'T.Text'.
instance HasField "text" SQL T.Text where getField = coerce

-- | A 'QuasiQuoter' for raw SQL strings.
--
-- __WARNING:__ This doesn't check the validity of the SQL. It is offered simply
-- because writing multi-line strings in Haskell is otherwise very annoying.
sql :: QuasiQuoter
sql =
   QuasiQuoter
      { quoteExp = \s -> [|fromString @SQL s|]
      , quotePat = \_ -> fail "sql: No quotePat"
      , quoteType = \_ -> fail "sql: No quoteType"
      , quoteDec = \_ -> fail "sql: No quoteDec"
      }

--------------------------------------------------------------------------------

-- | * A SQL statement taking a value @i@ as input and producing rows of
-- @o@ values as output.
--
-- * @s@ indicates whether the statement is 'Read'-only or read-'Write'.
--
-- * Construct with 'readStatement' or 'writeStatement'.

-- Note: We don't export this constructor because 'readStatement' and
-- 'writeStatement' because it lead to better type inferrence, force users to
-- make a conscious choice about whether they are dealing with a 'Read' or
-- 'Write' statement, and prevent type system from accidentally inferring a
-- 'Write' mode for a 'Read' only statement, which would restrict its
-- usability.
data Statement (s :: Mode) i o = Statement
   { input :: Input i
   , output :: Output o
   , sql :: SQL
   }

instance Show (Statement s i o) where
   showsPrec n s =
      showParen (n >= appPrec1) $
         showString "Statement{sql = "
            . shows s.sql
            . showString ", input = .., output = ..}"

-- | Construct a 'Read'-only 'Statement'.
--
-- __WARNING__: This library doesn't __yet__ provide a safe way to construct
-- 'Statement's. You can potentially write anything in your 'SQL' string.
-- Don't do that.
--
-- * The 'SQL' must be read-only.
--
-- * The 'SQL' must contain a single statement.
--
-- * The 'SQL' must not contain any transaction nor savepoint management
-- statements.
readStatement :: Input i -> Output o -> SQL -> Statement 'Read i o
readStatement = Statement
{-# INLINE readStatement #-}

-- | Construct a 'Statement' that can only be executed as part of a 'Write'
-- 'Transaction'.
--
-- __WARNING__: This library doesn't __yet__ provide a safe way to construct
-- 'Statement's. You can potentially write anything in your 'SQL' string.
-- Don't do that.
--
-- * The 'SQL' must contain a single statement.
--
-- * The 'SQL' must not contain any transaction nor savepoint management
-- statements.
writeStatement :: Input i -> Output o -> SQL -> Statement 'Write i o
writeStatement = Statement
{-# INLINE writeStatement #-}

instance Functor (Statement s i) where
   fmap = rmap
   {-# INLINE fmap #-}

instance Profunctor (Statement s) where
   dimap f g (Statement i o s) = Statement (f >$< i) (g <$> o) s
   {-# INLINE dimap #-}

--------------------------------------------------------------------------------

data BoundStatement (s :: Mode) o = BoundStatement
   { input :: BoundInput
   , output :: Output o
   , sql :: SQL
   }

instance Show (BoundStatement s o) where
   showsPrec n s =
      showParen (n >= appPrec1) $
         showString "Statement{sql = "
            . shows s.sql
            . showString ", input = "
            . shows s.input
            . showString ", output = ..}"

bindStatement :: Statement s i o -> i -> Either ErrInput (BoundStatement s o)
bindStatement st i = do
   bi <- bindInput st.input i
   pure BoundStatement{input = bi, output = st.output, sql = st.sql}
{-# INLINE bindStatement #-}