pg-schema-0.8.0.0: src/PgSchema/DML/Select.hs
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE DerivingVia #-}
module PgSchema.DML.Select where
import Control.Monad.RWS
import Control.Monad
import Data.Bifunctor
import Data.Foldable as F
import Data.Functor
import Data.List as L
import Data.List.NonEmpty as NE
import Data.Maybe
import Data.Monoid
import Data.Singletons
import Data.String
import Data.Text as T
import Data.Tuple
import PgSchema.DML.Select.Types
import PgSchema.Ann
import Database.PostgreSQL.Simple hiding(In(..))
import Database.PostgreSQL.Simple.Types(PGArray(..))
import PgSchema.Schema
import GHC.Generics
import GHC.TypeLits
import PgSchema.Types
import PgSchema.Utils.Internal
import Prelude as P
data QueryRead ren sch t = QueryRead
{ qrCurrTabNum :: Int
, qrPath :: [PathElem]
, qrParam :: QueryParam ren sch t }
data ParentInfo = ParentInfo
{ piRelDbName :: Text
, piFromNum :: Int
, piToNum :: Int
, piParentTab :: NameNS
, piRefs :: [Ref' Text]
, piPath :: [PathElem] }
deriving Show
data QueryState = QueryState
{ qsLastTabNum :: Int
, qsParents :: [ParentInfo] }
deriving Show
type MonadQuery ren sch t m = (MonadRWS (QueryRead ren sch t) [SomeToField] QueryState m)
-- | Checks that @r@ describes a decodable @SELECT@ row for @ann@:
--
-- * 'CRecInfo': every field of @r@ maps to a plain column or relation edge of the
-- root table in the schema (field names, kinds, and nesting match 'Cols').
-- * 'FromRow': each mapped field can be read from a PostgreSQL result column.
type Selectable ann r = (CRecInfo ann r, FromRow (PgTag ann r))
-- | Run a single @SELECT@ for root table @tab@ (see annotation @ann@ with schema
-- @sch@ and 'Renamer' @ren@) and decode rows into @[r]@, also returning the SQL
-- text and bind parameters (for tracing or debugging).
--
-- The desired result type @r@ /fixes the shape/ of each row: typically a record
-- with columns of the root table and nested Haskell values for relations along the
-- schema graph. There may be several /child/-side fields (often lists of nested
-- records) and several /parent/-side fields (nested records; wrapped in 'Maybe'
-- when the foreign-key side you join from allows NULL).
--
-- What actually appears in the generated SQL beyond that shape is controlled by
-- the 'QueryParam': filters, which paths are traversed, ordering on a branch
-- (e.g. child rows sorted by their position number), limits, and similar options.
-- You only configure in 'QueryParam' what you need for this query—not every
-- possible relation field in @r@.
--
-- Build 'QueryParam' with the 'MonadQP' API.
--
selectSch :: forall ann -> forall r. Selectable ann r
=> Connection -> QueryParamAnn ann -> IO ([r], (Text,[SomeToField]))
selectSch ann @r conn (selectText ann @r -> (sql,fs)) =
trace' ("\n\n" <> T.unpack sql <> "\n\n" <> P.show fs <> "\n\n")
$ (,(sql,fs)) . fmap (unPgTag @ann @r) <$> query conn (fromString $ T.unpack sql) fs
-- | Return the generated @SELECT@ SQL text (and bind parameters), e.g. for debugging.
selectText :: forall ann -> forall r. (CRecInfo ann r)
=> QueryParamAnn ann -> (Text,[SomeToField])
selectText ann @r qp = evalRWS (selectM "" (getRecordInfo @ann @r)) (qr0 qp) qs0
qr0 :: QueryParam ren sch tab -> QueryRead ren sch tab
qr0 qrParam = QueryRead
{ qrCurrTabNum = 0 , qrPath = [] , qrParam }
qs0 :: QueryState
qs0 = QueryState { qsLastTabNum = 0, qsParents = [] }
two :: (a,b,c) -> (a,b)
two (a,b,_) = (a,b)
third :: (a,b,c) -> c
third (_,_,c) = c
jsonPairing :: [(Text, Text)] -> Text
jsonPairing fs = "jsonb_build_object(" <> T.intercalate "," pairs <> ")"
where
pairs = mapMaybe (\(a,b) -> Just $ "'" <> b <> "'," <> a) fs
newtype TextI (s::Symbol) = TextI { unTextI :: Text}
instance KnownSymbol s => Semigroup (TextI s) where
TextI a <> TextI b =
TextI $ T.intercalate (demote @s) $ L.filter (not . T.null) [a,b]
instance KnownSymbol s => Monoid (TextI s) where mempty = TextI mempty
selectM :: forall ren sch t m. MonadQuery ren sch t m => Text -> RecordInfo Text -> m Text
selectM refTxt ri = do
QueryRead {..} <- ask
(fmap two -> flds) <- traverse fieldM ri.fields
-- qsParents are collected "in depth" (it is ok for joins etc) but in reverse order
parents <- gets
$ fmap (\p -> p { piPath = L.reverse p.piPath}) . L.reverse . qsParents
let
basePath = L.reverse qrPath
(unTextI -> condText, condPars) = F.fold $ L.reverse
$ mapMaybe (\(CondWithPath p cond) ->
if
| not (pathIsPrefixOf basePath p) -> Nothing
| pathsEqual p basePath -> Just $ first TextI $ pgCond qrCurrTabNum cond
| otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents
<&> \pari -> first (TextI @" and ") $ pgCond pari.piToNum cond
) qrParam.qpConds
(unTextI -> ordText, ordPars) = F.fold $ L.reverse
$ mapMaybe (\(OrdWithPath p ord) ->
if
| not (pathIsPrefixOf basePath p) -> Nothing
| pathsEqual p basePath -> Just $ pgOrd qrCurrTabNum ord
| otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents
<&> \pari -> pgOrd pari.piToNum ord
) qrParam.qpOrds
(distTexts, distPars) = F.fold $ L.reverse
$ mapMaybe (\(DistWithPath p dist) ->
if
| not (pathIsPrefixOf basePath p) -> Nothing
| pathsEqual p basePath -> Just $ pgDist qrCurrTabNum dist
| otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents
<&> \pari -> pgDist pari.piToNum dist
) qrParam.qpDistinct
sel
| L.null basePath =
T.intercalate "," $ L.map (\(a,b) -> a <> " \"" <> b <> "\"") flds
| otherwise = jsonPairing flds
whereText = let conds = L.filter (not . T.null) [refTxt, condText] in
if L.null conds then mempty else " where " <> T.intercalate " and " conds
orderText
| T.null ordText && T.null distTexts.orderBy.unTextI = ""
| otherwise = " order by " <> T.intercalate ","
(L.filter (not . T.null) [distTexts.orderBy.unTextI, ordText])
distinctText
| distTexts.distinct.getAny && T.null distTexts.distinctOn.unTextI = " distinct "
| T.null distTexts.distinctOn.unTextI = ""
| otherwise = " distinct on (" <> distTexts.distinctOn.unTextI <> ") "
qsLimOff = loByPath (L.reverse qrPath) $ qpLOs qrParam
groupByText
| P.null aggrs || P.null others = ""
| otherwise = " group by " <> T.intercalate ","
(L.nub $ others >>= \fi -> case fi.fieldKind of
RFPlain{} -> [fi.fieldDbName]
RFToHere _ refs -> refs <&> \ref -> ref.toName
RFFromHere _ refs -> refs <&> \ref -> ref.fromName
_ -> [])
where
(aggrs, others) = L.partition
(\fld -> case fld.fieldKind of { RFAggr{} -> True; _ -> False })
ri.fields
tell $ distPars <> condPars <> distPars <> ordPars
pure $ "select "
<> distinctText
<> sel
<> " from " <> qualName ri.tabName <> " t" <> show' qrCurrTabNum
<> " " <> T.unwords (joinText <$> trace' (show' parents) parents)
<> whereText
<> groupByText
<> orderText
<> qsLimOff
renderUnsafeExpr :: Int -> [Text] -> Text -> Text
renderUnsafeExpr n ps = T.concat
. P.zipWith (<>) (mempty : ((("t" <> show' n <> ".") <> ) <$> ps))
. T.splitOn "?"
-- >>> renderUnsafeExpr 5 ["a", "b"] "foo ? bar ? baz"
-- "foo t5.a bar t5.b baz"
-- | SQL text for the column expression, result alias, and emptiness-test expression
-- (non-obvious for nested relation fields).
fieldM :: MonadQuery ren sch tab m => FieldInfo Text -> m (Text, Text, Text)
fieldM fi = case fi.fieldKind of
RFEmpty s -> pure ("null", s, "true")
RFSelfRef{} -> error "Impossible: RFSelfRef should be changed to RFFromHere or RFToHere"
RFPlain {} -> do
n <- asks qrCurrTabNum
let val = "t" <> show' n <> "." <> fi.fieldDbName
pure (val, fi.fieldName, val <> " is null")
RFAggr _ (T.drop 1 . T.toLower . T.show -> fname) _ -> do
n <- asks qrCurrTabNum
let val = fname <> "(" <> "t" <> show' n <> "." <> fi.fieldDbName <> ")"
pure case fname of
"count" -> ("count(*)", fi.fieldName, " false")
_ -> (val, fi.fieldName, val <> " is null")
RFUnsafe ps expr -> do
n <- asks qrCurrTabNum
let val = renderUnsafeExpr n ps expr
pure (val, fi.fieldName, val <> " is null")
RFFromHere ri refs -> do
QueryRead {..} <- ask
modify \QueryState{qsLastTabNum = (+1) -> n2, qsParents} -> QueryState
{ qsLastTabNum = n2
-- , qsJoins = joinText qrCurrTabNum n2 : qsJoins
, qsParents = ParentInfo
{ piRelDbName = fi.fieldDbName
, piFromNum = qrCurrTabNum
, piToNum = n2
, piParentTab = ri.tabName
, piRefs = refs
, piPath = mkPathStep fi.fieldName fi.fieldDbName FromHere : qrPath }
: qsParents }
n2 <- gets qsLastTabNum
(flds, pars) <- listen $ local
(\qr -> qr{ qrCurrTabNum = n2
, qrPath = mkPathStep fi.fieldName fi.fieldDbName FromHere : qrPath })
$ traverse fieldM ri.fields
val <- if L.any (fdNullable . fromDef) refs
then do
tell pars
pure $ "case when " <> T.intercalate " and " (third <$> flds)
<> " then null else " <> jsonPairing (two <$> flds) <> " end"
else pure $ jsonPairing $ two <$> flds
pure (val, fi.fieldName, val <> " is null")
RFToHere ri refs -> do
QueryRead{..} <- ask
QueryState {qsLastTabNum = (+1) -> tabNum, qsParents} <- get
modify (const $ QueryState tabNum [])
selText <- local
(\qr -> qr { qrCurrTabNum = tabNum
, qrPath = mkPathStep fi.fieldName fi.fieldDbName ToHere : qrPath })
$ selectM (refCond tabNum qrCurrTabNum refs) ri
modify (\qs -> qs { qsParents = qsParents })
let
val = "array(" <> selText <> ")"
pure ("array_to_json(" <> val <> ")", fi.fieldName, val <> " = '{}'")
joinText :: ParentInfo -> Text
joinText ParentInfo{..} =
outer <> "join " <> qualName piParentTab <> " t" <> show' piToNum
<> " on " <> refCond piFromNum piToNum piRefs
where
outer
| hasNullableRefs piRefs = "left outer "
| otherwise = ""
refCond :: Int -> Int -> [Ref] -> Text
refCond nFrom nTo = T.intercalate " and " . fmap compFlds
where
compFlds Ref {fromName, toName} =
fldt nFrom fromName <> "=" <> fldt nTo toName
where
fldt n = (("t" <> show' n <> ".") <>)
withLOWithPath
:: forall ren sch t r. (LO -> r) -> [PathElem] -> LimOffWithPath ren sch t -> Maybe r
withLOWithPath f p (LimOffWithPath p' lo) =
guard (pathsEqual p p') >> pure (f lo)
withLOsWithPath
:: forall ren sch t r. (LO -> r) -> [PathElem] -> [LimOffWithPath ren sch t] -> Maybe r
withLOsWithPath f p = join . L.find isJust . L.map (withLOWithPath f p)
lowp :: forall ren sch t. forall (path :: [PathElemK]) ->
( PathCtx ren sch t path, PathEndsMany sch t path ) =>
LO -> LimOffWithPath ren sch t
lowp p = LimOffWithPath @p (demote @p)
rootLO :: forall ren sch t. LO -> LimOffWithPath ren sch t
rootLO = lowp []
convLO :: LO -> Text
convLO (LO ml mo) =
maybe "" ((" limit " <>) . show') ml
<> maybe "" ((" offset " <>) . show') mo
loByPath :: forall ren sch t. [PathElem] -> [LimOffWithPath ren sch t] -> Text
loByPath p = fromMaybe mempty . withLOsWithPath convLO p
runCond :: Int -> CondMonad a -> (a,[SomeToField])
runCond n x = evalRWS x ("q", pure n) 0
tabPref :: CondMonad Text
tabPref = asks \case
(_, n :| []) -> "t" <> show' n
(p, n :| (np : _)) -> "t" <> show' np <> p <> show' n
qual :: forall (fld :: Symbol). ToStar fld => CondMonad Text
qual = tabPref <&> (<> "." <> (demote @fld))
--
convCond :: forall ren sch t . Cond ren sch t -> CondMonad Text
convCond = \case
EmptyCond -> pure mempty
Cmp @n cmp v -> do
tell [SomeToField v]
qual @(ApplyRenamer ren n) <&> (<> " " <> showCmp cmp <> " ?")
In @n (NE.toList -> vs) -> do
tell [SomeToField $ PGArray vs]
qual @(ApplyRenamer ren n) <&> (<> " = any(?::" <> qualName (getFldDef @sch @t @(ApplyRenamer ren n)).fdType <> "[])")
InArr @n vs -> do
tell [SomeToField $ PGArray vs]
qual @(ApplyRenamer ren n) <&> (<> " = any(?::" <> qualName (getFldDef @sch @t @(ApplyRenamer ren n)).fdType <> "[])")
Null @n -> qual @(ApplyRenamer ren n) <&> (<> " is null")
Not c -> getNot <$> convCond c
BoolOp bo c1 c2 -> getBoolOp bo <$> convCond c1 <*> convCond c2
Child @_ @_ @ref tabParam cond ->
getRef @(RdFrom (TRelDef sch (ApplyRenamerNS ren ref))) True (demote @(TRelDef sch (ApplyRenamerNS ren ref))).rdCols
tabParam cond
Parent @_ @_ @ref cond ->
getRef @(RdTo (TRelDef sch (ApplyRenamerNS ren ref))) False (demote @(TRelDef sch (ApplyRenamerNS ren ref))).rdCols
defTabParam cond
UnsafeCond m -> m
where
getNot c
| c == mempty = mempty
| otherwise = "not (" <> c <> ")"
getBoolOp bo cc1 cc2
| cc1 == mempty = cc2 -- so EmptyCond works both with &&& and |||
| cc2 == mempty = cc1
| otherwise = case bo of
And -> cc1 <> " and " <> cc2
Or -> "(" <> cc1 <> " or " <> cc2 <> ")"
getRef
:: forall tab. CTabDef sch tab
=> Bool -> [(Text, Text)] -> TabParam ren sch tab -> Cond ren sch tab
-> CondMonad Text
getRef isChild cols tabParam cond = do
tpp <- tabPref
modify (+1)
cnum <- get
(tpc, condInt, TextI ordInt, condExt) <- local (second (cnum <|))
$ (,,,) <$> tabPref <*> convCond tabParam.cond
<*> convOrd tabParam.order <*> convCond cond
pure $ mkExists tpp tpc condInt ordInt condExt
where
mkExists tpp tpc cin oint cout
= "exists (select 1 from (select * from " <> tn <> " " <> tpc
<> " where "
<> T.intercalate " and " (
(\(ch,pr) -> tpc <> "." <> ch <> " = "
<> tpp <> "." <> pr)
. (if isChild then id else swap)
<$> cols)
<> (if T.null cin then "" else " and " <> cin)
<> case tabParam of
TabParam EmptyCond [] (LO Nothing Nothing) -> ""
TabParam _ _ (convLO -> loTxt) ->
(if T.null oint then "" else " order by " <> oint) <> loTxt
<> ") " <> tpc
<> (if T.null cout then "" else " where " <> cout)
<> ")"
where
tn = qualName $ demote @tab
pgCond :: forall ren sch t . Int -> Cond ren sch t -> (Text, [SomeToField])
pgCond n cond = evalRWS (convCond cond) ("q", pure n) 0
pgOrd :: forall ren sch t. Int -> [OrdFld ren sch t] -> (TextI ",", [SomeToField])
pgOrd n ord = evalRWS (convOrd ord) ("o", pure n) 0
pgDist :: forall ren sch t. Int -> Dist ren sch t -> (DistTexts, [SomeToField])
pgDist n dist = evalRWS (convDist dist) ("o", pure n) 0
withCondWithPath :: forall ren sch t r. (forall t'. Cond ren sch t' -> r) ->
[PathElem] -> CondWithPath ren sch t -> Maybe r
withCondWithPath f p (CondWithPath p' cond) =
f cond <$ guard (pathsEqual p p')
withCondsWithPath :: forall ren sch t r. (forall t'. Cond ren sch t' -> r) ->
[PathElem] -> [CondWithPath ren sch t] -> Maybe r
withCondsWithPath f p = join . L.find isJust . L.map (withCondWithPath f p)
cwp :: forall path -> forall ren sch t.
PathCtx ren sch t path =>
Cond ren sch (TabOnDPathRen ren sch t path) -> CondWithPath ren sch t
cwp p = CondWithPath @p (demote @p)
rootCond :: Cond ren sch t -> CondWithPath ren sch t
rootCond = cwp []
condByPath :: Int -> [PathElem] -> [CondWithPath ren sch t] -> (Text, [SomeToField])
condByPath num p = F.fold . withCondsWithPath (pgCond num) p
ordByPath :: Int -> [PathElem] -> [OrdWithPath ren sch t] -> (TextI ",", [SomeToField])
ordByPath num p = F.fold . withOrdsWithPath (pgOrd num) p
distByPath :: Int -> [PathElem] -> [DistWithPath ren sch t] -> (DistTexts, [SomeToField])
distByPath num p = F.fold . withDistsWithPath (pgDist num) p
withOrdWithPath :: forall ren sch t r. (forall t'. [OrdFld ren sch t'] -> r) ->
[PathElem] -> OrdWithPath ren sch t -> Maybe r
withOrdWithPath f p (OrdWithPath p' ord) =
f ord <$ guard (pathsEqual p p')
withDistWithPath :: forall ren sch t r. (forall t'. Dist ren sch t' -> r) ->
[PathElem] -> DistWithPath ren sch t -> Maybe r
withDistWithPath f p (DistWithPath p' dist) =
f dist <$ guard (pathsEqual p p')
--
withOrdsWithPath :: forall ren sch t r . (forall t'. [OrdFld ren sch t'] -> r) ->
[PathElem] -> [OrdWithPath ren sch t] -> Maybe r
withOrdsWithPath f p = join . L.find isJust . L.map (withOrdWithPath f p)
withDistsWithPath :: forall ren sch t r . (forall t'. Dist ren sch t' -> r) ->
[PathElem] -> [DistWithPath ren sch t] -> Maybe r
withDistsWithPath f p = join . L.find isJust . L.map (withDistWithPath f p)
owp :: forall path -> forall sch t t'.
( PathCtx ren sch t path, TabOnDPathRen ren sch t path ~ t' ) =>
[OrdFld ren sch t'] -> OrdWithPath ren sch t
owp p = OrdWithPath @p (demote @p)
rootOrd :: forall ren sch t. [OrdFld ren sch t] -> OrdWithPath ren sch t
rootOrd = owp []
dwp :: forall path -> forall ren sch t.
( PathCtx ren sch t path, PathEndsMany sch t path ) =>
Dist ren sch (TabOnDPathRen ren sch t path) -> DistWithPath ren sch t
dwp p = DistWithPath @p (demote @p)
rootDist :: forall ren sch t. Dist ren sch t -> DistWithPath ren sch t
rootDist = dwp []
convPreOrd :: forall ren sch tab. [OrdFld ren sch tab] -> CondMonad [(Text, OrdDirection)]
convPreOrd = traverse processFld
where
processFld = \case
OrdFld @fld od -> (, od) <$> qual @(ApplyRenamer ren fld)
UnsafeOrd m -> m
renderOrd :: [(Text, OrdDirection)] -> TextI ","
renderOrd = foldMap (TextI . render)
where
render (t, show' -> od) = t <> " " <> od <> " nulls last"
convOrd :: forall ren sch tab. [OrdFld ren sch tab] -> CondMonad (TextI ",")
convOrd = fmap renderOrd . convPreOrd
data DistTexts = DistTexts
{ distinct :: Any
, distinctOn :: TextI ","
, orderBy :: TextI "," }
deriving Generic
deriving (Semigroup, Monoid) via (Generically DistTexts)
convDist :: forall ren sch tab. Dist ren sch tab -> CondMonad DistTexts
convDist = \case
Distinct -> pure $ mempty { distinct = Any True }
DistinctOn ofs -> convPreOrd ofs <&> \xs -> mempty
{ distinctOn = foldMap (TextI . fst) xs
, orderBy = renderOrd xs }