sq-0.1: lib/Sq/Output.hs
{-# LANGUAGE StrictData #-}
{-# LANGUAGE UndecidableInstances #-}
module Sq.Output
( Output
, ErrOutput (..)
, decode
, runOutput
, output
, OutputDefault (..)
, goutputDefault
, GOutputDefault
, houtput
, HOutput
) where
import Control.Applicative
import Control.Exception.Safe qualified as Ex
import Control.Monad
import Control.Monad.Trans.Resource qualified as R hiding (runResourceT)
import Data.Coerce
import Data.Kind
import Data.List.NonEmpty qualified as NEL
import Data.Proxy
import Data.String
import Database.SQLite3 qualified as S
import GHC.Generics qualified as G
import Generics.SOP qualified as SOP
import Generics.SOP.GGP qualified as SOP
import Sq.Decoders
import Sq.Names
import Sq.Support
--------------------------------------------------------------------------------
-- | How to decode an output row from a single 'Sq.Statement'.
--
-- * Construct with 'decode', 'IsString'.
--
-- * Nest with 'output'.
--
-- * Compose with 'Monoid', 'Functor', 'Applicative', 'Alternative', 'Monad',
-- 'MonadPlus', 'MonadFail' and 'Ex.MonadThrow' tools.
data Output o
= Output_Pure o
| Output_Fail Ex.SomeException
| Output_Decode BindingName (Decode (Output o))
data ErrOutput
= -- | Error from v'Decode'.
ErrOutput_ColumnValue BindingName ErrDecode
| -- | Missing column name in the raw 'SQL'.
ErrOutput_ColumnMissing BindingName
| -- | Error from 'Ex.MonadThrow'.
ErrOutput_Fail Ex.SomeException
deriving stock (Show)
deriving anyclass (Ex.Exception)
-- | Decode the column with the given 'Name'.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('decode' \"foo\" 'decodeDefault')
-- \"SELECT foo FROM t\"
-- :: ('DecodeDefault' x)
-- => 'Sq.Statement' 'Sq.Read' () x
-- @
--
-- Note that by design, this library doesn't support positional 'Output'
-- parameters. You must always pick a 'Name'. In the raw SQL, you can use @AS@
-- to rename your output columns as necessary.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('decode' \"abc\" 'decodeDefault')
-- \"SELECT foo AS abc FROM t\"
-- :: ('DecodeDefault' x)
-- => 'Sq.Statement' 'Sq.Read' () x
-- @
--
-- Multiple 'Outputs's can be composed with 'Monoid', 'Functor', 'Applicative',
-- 'Alternative', 'Monad', 'MonadPlus', 'MonadFail' and 'Ex.MonadThrow' tools.
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- (do foo <- 'decode' \"foo\" 'decodeDefault'
-- 'when' (foo > 10) do
-- 'fail' \"Oh no!"
-- bar <- 'decode' \"bar\" 'decodeDefault'
-- 'pure' (foo, bar))
-- \"SELECT foo, bar FROM t\"
-- :: ('DecodeDefault' y)
-- => 'Sq.Statement' 'Sq.Read' () ('Int', y)
-- @
--
-- Pro-tip: Consider using the 'IsString' instance for 'Output',
-- where for example @\"foo\"@ means @'decode' \"foo\" 'decodeDefault'@:
--
-- @
-- 'Sq.readStatement'
-- ('liftA2' (,) \"foo\" \"bar\")
-- 'mempty'
-- \"SELECT foo, bar FROM t\"
-- :: ('DecodeDefault' x, 'DecodeDefault' y)
-- => 'Sq.Statement' 'Sq.Read' () (x, y)
-- @
decode :: Name -> Decode o -> Output o
decode n vda = Output_Decode (BindingName (pure n)) (Output_Pure <$> vda)
{-# INLINE decode #-}
-- | Add a prefix 'Name' to column names in the given 'Output',
-- separated by @\__@
--
-- This is useful for making reusable 'Output's. For example,
-- consider the following.
--
-- @
-- data Point = Point { x :: 'Int', y :: 'Int' }
--
-- pointOutput :: 'Output' Point
-- pointOutput = Point '<$>' \"x\" '<*>' \"y\"
-- @
--
-- After using 'output':
--
-- @
-- 'Sq.readStatement'
-- 'mempty'
-- ('liftA2' (,) ('output' \"p1\" pointOutput)
-- ('output' \"p2\" pointOutput))
-- ['Sq.sql'|
-- SELECT ax AS p1\__x, ay AS p1\__y,
-- bx AS p2\__x, by AS p2\__y
-- FROM vectors|]
-- :: 'Sq.Statement' 'Sq.Read' () (Point, Point)
-- @
output :: Name -> Output o -> Output o
output n = \case
Output_Decode bn d ->
Output_Decode (coerce (NEL.cons n) bn) (output n <$> d)
o -> o
-- | TODO cache names after lookup. Important for Alternative.
runOutput
:: (Monad m)
=> (BindingName -> m (Maybe S.SQLData))
-> Output o
-> m (Either ErrOutput o)
runOutput f = \case
Output_Decode bn (Decode vda) -> do
f bn >>= \case
Just s -> case vda s of
Right d -> runOutput f d
Left e -> pure $ Left $ ErrOutput_ColumnValue bn e
Nothing -> pure $ Left $ ErrOutput_ColumnMissing bn
Output_Pure a -> pure $ Right a
Output_Fail e -> pure $ Left $ ErrOutput_Fail e
instance Functor Output where
fmap = liftA
{-# INLINE fmap #-}
instance Applicative Output where
pure = Output_Pure
{-# INLINE pure #-}
liftA2 = liftM2
{-# INLINE liftA2 #-}
instance Alternative Output where
empty = fail "empty"
{-# INLINE empty #-}
l <|> r = case l of
Output_Decode n vda ->
Output_Decode n (fmap (<|> r) vda)
Output_Pure _ -> l
Output_Fail _ -> r
instance MonadPlus Output where
mzero = fail "mzero"
{-# INLINE mzero #-}
mplus = (<|>)
{-# INLINE mplus #-}
instance Monad Output where
l >>= k = case l of
Output_Decode n vda ->
Output_Decode n (fmap (>>= k) vda)
Output_Pure a -> k a
Output_Fail e -> Output_Fail e
instance Ex.MonadThrow Output where
throwM = Output_Fail . Ex.toException
instance MonadFail Output where
fail = Ex.throwString
instance (Semigroup o) => Semigroup (Output o) where
(<>) = liftA2 (<>)
{-# INLINE (<>) #-}
instance (Monoid o) => Monoid (Output o) where
mempty = pure mempty
{-# INLINE mempty #-}
instance (DecodeDefault i) => IsString (Output i) where
fromString s = decode (fromString s) decodeDefault
{-# INLINE fromString #-}
--------------------------------------------------------------------------------
-- | 'Constraint' to be satisfied for using 'houtput'.
type HOutput :: ((Type -> Type) -> k -> Type) -> k -> Constraint
type HOutput = HAsum
-- | Given a "Data.SOP".'SOP.Prod' containing all the possible 'Output'
-- decoders, obtain an 'Output' for any 'SOP.NS', 'SOP.NP', 'SOP.SOP' or
-- 'SOP.POP' having that same @xs@.
--
-- Composes products 'SOP.NP' and 'SOP.POP' using 'Applicative',
-- and sums 'SOP.NS' and 'SOP.SOP' using 'Alternative'.
houtput :: (HOutput h xs) => SOP.Prod h Output xs -> Output (h SOP.I xs)
houtput = hasum
{-# INLINE houtput #-}
--------------------------------------------------------------------------------
-- | 'Constraint' to be satisfied for using 'goutputDefault'.
type GOutputDefault :: Type -> Constraint
type GOutputDefault o =
( OnlyRecords (SOP.GDatatypeInfoOf o)
, SOP.All2 DecodeDefault (SOP.GCode o)
, G.Generic o
, SOP.GTo o
, SOP.GDatatypeInfo o
, HOutput SOP.SOP (SOP.GCode o)
)
-- | Generic 'Output' implementation for types with GHC 'G.Generic' instance
-- where all the constructors are records with named fields, to be used as
-- 'Name's, and each field type has a 'DecodeDefault' instance.
--
-- If the datatype has more than one constructor, each one is tried in order
-- until one of them matches.
goutputDefault :: forall o. (GOutputDefault o) => Output o
goutputDefault =
fmap SOP.gto $ houtput $ SOP.POP do
case SOP.gdatatypeInfo (Proxy @o) of
SOP.Newtype _ _ ci -> f ci SOP.:* SOP.Nil
SOP.ADT _ _ cis _ ->
SOP.hcmap (Proxy @(SOP.All DecodeDefault)) f cis
where
f
:: forall b
. (SOP.All DecodeDefault b)
=> SOP.ConstructorInfo b
-> SOP.NP Output b
f (SOP.Record _ fis) =
SOP.hcmap
(Proxy @DecodeDefault)
(\(SOP.FieldInfo s) -> decode (fromString s) decodeDefault)
fis
f _ = undefined -- impossible due to OnlyRecords
--------------------------------------------------------------------------------
-- | Default way to decode the 'Output' from a 'Sq.Statement' as a Haskell
-- value of type @a@.
--
-- If there there exist also a 'Sq.InputDefault' instance for @o@, then it
-- must roundtrip with the 'Sq.OutputDefault' instance for @o@.
class OutputDefault o where
outputDefault :: Output o
-- | 'goutputDefault' is used as default implementation.
default outputDefault :: (GOutputDefault o) => Output o
outputDefault = goutputDefault
-- | We don't export this, because we export 'OutputDefault' instances for
-- 'SOP.NS', 'SOP.NP', 'SOP.SOP' and 'SOP.POP'.
houtputDefault
:: ( HOutput h xs
, SOP.AllN (SOP.Prod h) OutputDefault xs
, SOP.HPure (SOP.Prod h)
)
=> Output (h SOP.I xs)
houtputDefault = houtput (SOP.hcpure (Proxy @OutputDefault) outputDefault)
-- | Read "Data.SOP".
--
-- __WARNING__ This may lead to unexpected results if the underlying
-- 'OutputDefault's don't check for any /tag/ for discriminating between the
-- various @xss@.
instance
(SOP.SListI2 xss, SOP.All2 OutputDefault xss)
=> OutputDefault (SOP.SOP SOP.I xss)
where
outputDefault = houtputDefault
{-# INLINE outputDefault #-}
-- | Read "Data.SOP".
instance
(SOP.SListI2 xss, SOP.All2 OutputDefault xss)
=> OutputDefault (SOP.POP SOP.I xss)
where
outputDefault = houtputDefault
{-# INLINE outputDefault #-}
-- | Read "Data.SOP".
instance
(SOP.SListI xs, SOP.All OutputDefault xs)
=> OutputDefault (SOP.NP SOP.I xs)
where
outputDefault = houtputDefault
{-# INLINE outputDefault #-}
-- | Read "Data.SOP".
--
-- __WARNING__ This may lead to unexpected results if the underlying
-- 'OutputDefault's don't check for any /tag/ for discriminating between
-- the various @xs@.
instance
(SOP.SListI xs, SOP.All OutputDefault xs)
=> OutputDefault (SOP.NS SOP.I xs)
where
outputDefault = houtputDefault
{-# INLINE outputDefault #-}
instance (OutputDefault a, OutputDefault b) => OutputDefault (a, b) where
outputDefault = (,) <$> outputDefault <*> outputDefault
instance
(OutputDefault a, OutputDefault b, OutputDefault c)
=> OutputDefault (a, b, c)
where
outputDefault = (,,) <$> outputDefault <*> outputDefault <*> outputDefault
instance
(OutputDefault a, OutputDefault b, OutputDefault c, OutputDefault d)
=> OutputDefault (a, b, c, d)
where
outputDefault =
(,,,)
<$> outputDefault
<*> outputDefault
<*> outputDefault
<*> outputDefault
-- | __WARNING__ This may lead to unexpected results if the underlying
-- 'OutputDefault's don't check for any /tag/ for discriminating between @a@
-- and @b@.
instance (OutputDefault a, OutputDefault b) => OutputDefault (Either a b) where
outputDefault = fmap Left outputDefault <|> fmap Right outputDefault