packages feed

sqel-0.0.1.0: lib/Sqel/Query.hs

module Sqel.Query where

import qualified Data.Map.Strict as Map
import Generics.SOP (NP (Nil))
import Sqel.Class.MatchView (MatchQuery)
import Sqel.Data.Codec (Encoder)
import Sqel.Data.Dd (
  Comp (Prod),
  CompInc (Nest),
  Dd (Dd),
  DdInc (DdNest),
  DdK (DdK),
  DdSort (DdProd),
  DdStruct (DdComp),
  DdType,
  ProdType (Reg),
  Struct (Comp, Prim),
  )
import Sqel.Data.FragType (FragType)
import Sqel.Data.Mods (pattern NoMods, NoMods)
import Sqel.Data.QuerySchema (QuerySchema (QuerySchema))
import Sqel.Data.Sel (MkTSel (mkTSel), Sel (SelSymbol), SelPrefix (DefaultPrefix), SelW (SelWSymbol), TSel (TSel))
import Sqel.Data.SelectExpr (
  SelectExpr (SelectExprAtom, SelectExprIgnore, SelectExprList, SelectExprNot, SelectExprSum),
  SelectFragment (SelectFragment),
  )
import Sqel.Data.Sql (Sql)
import Sqel.Prim (primAs)
import Sqel.ReifyCodec (ReifyCodec (reifyCodec))
import Sqel.SOP.Error (Quoted)
import Type.Errors (ErrorMessage)

import Sqel.Query.Fragments (ColumnPrefix (NoPrefix), joinFrag, joinSum)
import Sqel.Query.SelectExpr (ToSelectExpr (toSelectExpr))

class SelectExprUnlessError (check :: Maybe k) (s :: DdK) where
  selectExprUnlessError :: Dd s -> SelectExpr

instance ToSelectExpr s => SelectExprUnlessError 'Nothing s where
  selectExprUnlessError = toSelectExpr NoPrefix

class CheckedQuery (qs :: DdK) (ts :: DdK) where
  checkedQ :: Dd qs -> SelectExpr

-- TODO remove or move to MatchView
type CheckQueryStuck :: ErrorMessage
type CheckQueryStuck =
  "Could not validate query fields since there is not enough type information available." %
  "You are most likely missing a constraint for " <> Quoted "CheckedQuery" <> "."

instance (
    MatchQuery query table match,
    SelectExprUnlessError match query
  ) => CheckedQuery query table where
    checkedQ = selectExprUnlessError @_ @match

compileSelectExpr ::
  SelectExpr ->
  [SelectFragment]
compileSelectExpr expr =
  Map.elems (Map.mapWithKey SelectFragment (snd (spin 1 expr)))
  where
    spin :: Int -> SelectExpr -> (Int, Map FragType Sql)
    spin i = \case
      SelectExprAtom tpe code -> (i + 1, [(tpe, code i)])
      SelectExprList op sub -> second (Map.mapWithKey (joinFrag op)) (prod i sub)
      SelectExprSum sub -> second (Map.mapWithKey (joinSum i)) (prod (i + 1) sub)
      -- TODO
      SelectExprNot _ -> undefined
      SelectExprIgnore -> (i, mempty)
    prod i sub =
      second (Map.unionsWith (<>) . fmap (fmap pure)) (mapAccumL spin i sub)

querySchemaWith ::
  ∀ q query a .
  ReifyCodec Encoder query q =>
  Dd query ->
  SelectExpr ->
  QuerySchema q a
querySchemaWith query expr =
  QuerySchema (compileSelectExpr expr) (reifyCodec @Encoder query)

unsafeQuerySchema ::
  ∀ q query a .
  ToSelectExpr query =>
  ReifyCodec Encoder query q =>
  Dd query ->
  QuerySchema q a
unsafeQuerySchema query =
  querySchemaWith query (toSelectExpr NoPrefix query)

-- TODO CheckFields should be generated by classes with fundeps so that they can be built incrementally and provided
-- separately
-- try this out with QueryPending in bodhi
type CheckQuery :: DdK -> DdK -> Constraint
class CheckQuery query table where
  checkQuery ::
    Dd query ->
    Dd table ->
    QuerySchema (DdType query) (DdType table)

instance (
    CheckedQuery query table,
    ReifyCodec Encoder query (DdType query)
  ) => CheckQuery query table where
  checkQuery query _ =
    querySchemaWith query (checkedQ @query @table query)

type EmptyQuery =
  'DdK ('SelSymbol "") NoMods () ('Comp ('TSel 'DefaultPrefix "") ('Prod 'Reg) 'Nest '[])

emptyQuery :: Dd EmptyQuery
emptyQuery =
  Dd (SelWSymbol Proxy) NoMods (DdComp mkTSel DdProd DdNest Nil)

primIdQuery :: Dd ('DdK ('SelSymbol "id") NoMods a 'Prim)
primIdQuery =
  primAs @"id"