packages feed

sqel-0.0.1.0: lib/Sqel/PgType.hs

module Sqel.PgType where

import Data.List.NonEmpty ((<|))
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Exon
import Exon (exon)
import Lens.Micro (_1, _2, _3, _4, (^.))
import Lens.Micro.Extras (view)
import Sqel.Class.MatchView (MatchProjection)
import Sqel.Data.Codec (Codec (Codec), FullCodec)
import Sqel.Data.Dd (Dd, DdK, DdType)
import Sqel.Data.PgType (
  ColumnType (ColumnComp, ColumnPrim),
  PgColumn (PgColumn),
  PgColumnName (PgColumnName),
  PgColumns (PgColumns),
  PgComposite (PgComposite),
  PgStructure (PgStructure),
  PgTable (PgTable),
  PgTypeRef,
  StructureType (StructureComp, StructurePrim),
  TableSelectors (TableSelectors),
  TableValues (TableValues),
  pgCompRef,
  )
import Sqel.Data.PgTypeName (PgTableName, pgCompName, pgTableName)
import qualified Sqel.Data.Projection as Projection
import Sqel.Data.Projection (Projection (Projection))
import Sqel.Data.ProjectionWitness (ProjectionWitness (ProjectionWitness))
import Sqel.Data.Selector (Selector (Selector))
import Sqel.Data.Sql (Sql (Sql), sql)
import qualified Sqel.Data.TableSchema as TableSchema
import Sqel.Data.TableSchema (TableSchema (TableSchema))
import Sqel.Data.Term (Comp, CompInc (Merge), DdTerm (DdTerm), Struct (Comp, Prim))
import Sqel.ReifyCodec (ReifyCodec (reifyCodec))
import Sqel.ReifyDd (ReifyDd (reifyDd))
import Sqel.SOP.Error (Quoted)
import Sqel.Sql.Prepared (dollar)
import Sqel.Text.Quote (dquote)
import Type.Errors (ErrorMessage)

pgColumn ::
  DdTerm ->
  ([PgColumn], [(PgColumnName, StructureType)], Map PgTypeRef PgComposite, [NonEmpty PgColumnName])
pgColumn = \case
  DdTerm name _ unique constr (Prim t) ->
    ([PgColumn name (ColumnPrim t unique constr)], [(name, StructurePrim t unique constr)], mempty, [pure name])
  DdTerm name _ unique constr (Comp typeName c i sub) ->
    case comp typeName c i sub of
      (compType@(PgComposite cname _), struct, types, False, sels) ->
        (colType, structType, Map.insert ref compType types, (name <|) <$> sels)
        where
          colType = [PgColumn name (ColumnComp ref unique constr)]
          structType = [(name, StructureComp cname struct unique constr)]
          ref = pgCompRef cname
      (PgComposite _ (PgColumns columns), PgStructure struct, types, True, sels) ->
        (columns, struct, types, sels)

comp ::
  Text ->
  Comp ->
  CompInc ->
  [DdTerm] ->
  (PgComposite, PgStructure, Map PgTypeRef PgComposite, Bool, [NonEmpty PgColumnName])
comp typeName _ i sub =
  (compType, structType, Map.unions (view _3 <$> cols), i == Merge, view _4 =<< cols)
  where
    compType = PgComposite compName (PgColumns (view _1 =<< cols))
    structType = PgStructure (view _2 =<< cols)
    compName = pgCompName typeName
    cols = pgColumn <$> sub

-- TODO this used to dquote the @names@ as well but it appears to fail for the sum index field
mkSelector :: NonEmpty PgColumnName -> Selector
mkSelector =
  Selector . Sql . \case
    [PgColumnName name] -> dquote name
    root :| names -> [exon|(##{dquote root}).##{Text.intercalate "." (coerce names)}|]

-- TODO use CommaSep
mkValues :: PgStructure -> [Sql]
mkValues (PgStructure base) =
  snd (mapAccumL mkCol (1 :: Int) base)
  where
    mkCol (n :: Int) = \case
      (_, StructurePrim _ _ _) -> (n + 1, [sql|##{dollar n}|])
      (_, StructureComp _ (PgStructure cols) _ _) ->
        (newN, [sql|row(#{Exon.intercalate ", " sub})|])
        where
          (newN, sub) =
            mapAccumL mkCol n cols

mkTable ::
  PgColumnName ->
  Maybe PgTableName ->
  PgColumns ->
  Map PgTypeRef PgComposite ->
  [NonEmpty PgColumnName] ->
  PgStructure ->
  PgTable a
mkTable (PgColumnName name) tableName cols types selectors struct =
  PgTable (fromMaybe (pgTableName name) tableName) cols types (TableSelectors (mkSelector <$> selectors)) values struct
  where
    values = TableValues (mkValues struct)

toTable :: DdTerm -> PgTable a
toTable = \case
  DdTerm name tableName unique constr (Prim t) ->
    mkTable name tableName cols [] [pure name] struct
    where
      cols = PgColumns [PgColumn name (ColumnPrim t unique constr)]
      struct = PgStructure [(name, StructurePrim t unique constr)]
  DdTerm name tableName _ _ (Comp typeName c i sub) ->
    mkTable name tableName cols types paths struct
    where
      (PgComposite _ cols, struct, types, _, paths) = comp typeName c i sub

pgTable ::
  ∀ s .
  ReifyDd s =>
  Dd s ->
  PgTable (DdType s)
pgTable dd =
  toTable (reifyDd dd)

type MkTableSchema :: DdK -> Constraint
class MkTableSchema table where
  tableSchema :: Dd table -> TableSchema (DdType table)

instance (
    ReifyDd table,
    ReifyCodec FullCodec table (DdType table)
  ) => MkTableSchema table where
  tableSchema tab =
    TableSchema (pgTable tab) (row ^. #decodeValue) (params ^. #encodeValue)
    where
      Codec params row = reifyCodec @FullCodec tab

class CheckedProjection' (check :: Maybe Void) (s :: DdK) where
  checkedProjection' :: Dd s -> ProjectionWitness (DdType s) table

instance CheckedProjection' 'Nothing s where
  checkedProjection' _ = ProjectionWitness

class CheckedProjection (proj :: DdK) (table :: DdK) where
  checkedProjection :: Dd proj -> ProjectionWitness (DdType proj) (DdType table)

type CheckProjectionStuck :: ErrorMessage
type CheckProjectionStuck =
  "Could not validate projection fields since there is not enough type information available." %
  "You are most likely missing a constraint for " <> Quoted "CheckedProjection" <> "."

instance (
    MatchProjection proj table match,
    CheckedProjection' match proj
  ) => CheckedProjection proj table where
    checkedProjection = checkedProjection' @match

-- TODO check that the table name matches, otherwise a query using the projection will use the wrong name.
-- also possible to automatically set it, but that might be incompatible with the db view interpreter feature, since
-- the name there can't be propagated here. but it would be possible to check only there and do it automatically here.
projectionWitness ::
  ∀ proj table .
  CheckedProjection proj table =>
  Dd proj ->
  Dd table ->
  ProjectionWitness (DdType proj) (DdType table)
projectionWitness proj _ =
  checkedProjection @proj @table proj

projection ::
  MkTableSchema proj =>
  MkTableSchema table =>
  CheckedProjection proj table =>
  Dd proj ->
  Dd table ->
  Projection (DdType proj) (DdType table)
projection ddProj ddTable =
  Projection {..}
  where
    table = tableSchema ddTable
    TableSchema {..} = tableSchema ddProj
    witness = projectionWitness ddProj ddTable

fullProjection ::
  MkTableSchema table =>
  CheckedProjection table table =>
  Dd table ->
  Projection (DdType table) (DdType table)
fullProjection dd =
  projection dd dd

toFullProjection :: TableSchema table -> Projection table table
toFullProjection table@TableSchema {..} =
  Projection {table, witness = ProjectionWitness, ..}