{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- | This module provides an 'AnnotatedDatabaseSettings' type to be used as a drop-in replacement for the
-- standard 'DatabaseSettings'. Is it possible to \"downcast\" an 'AnnotatedDatabaseSettings' to a standard
-- 'DatabaseSettings' simply by calling 'deAnnotateDatabase'.
module Database.Beam.AutoMigrate.Annotated
  ( -- * User annotations
    Annotation (..),

    -- * Annotating a 'DatabaseSettings'
    AnnotatedDatabaseSettings,
    AnnotatedDatabaseEntity (..),
    IsAnnotatedDatabaseEntity (..),
    TableSchema,
    TableFieldSchema (..),
    FieldSchema (..),
    dbAnnotatedSchema,
    dbAnnotatedConstraints,
    annotatedDescriptor,
    defaultTableSchema,

    -- * Downcasting annotated types
    lowerEntityDescriptor,
    deannotate,

    -- * Specifying constraints
    -- $specifyingConstraints
    annotateTableFields,

    -- * Specifying Column constraints
    -- $specifyingColumnConstraints
    defaultsTo,

    -- * Specifying Table constraints
    -- $specifyingTableConstraints
    UniqueConstraint (..),

    -- ** Unique constraint
    uniqueConstraintOn,

    -- ** Foreign key constraint
    ForeignKeyConstraint (..),
    foreignKeyOnPk,
    foreignKeyOn,

    -- * Other types and functions
    TableKind,
    DatabaseKind,

    -- * Ports from Beam
    zipTables,
    GZipDatabase,

    -- * Internals
    pgDefaultConstraint,
  )
where

import Data.Kind
import Data.Monoid (Endo (..))
import Data.Proxy
import Data.Set (Set)
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Database.Beam as Beam
import Database.Beam.AutoMigrate.Compat
import Database.Beam.AutoMigrate.Types
import Database.Beam.AutoMigrate.Util
import Database.Beam.Backend.SQL (HasSqlValueSyntax (..), displaySyntax)
import Database.Beam.Postgres (Postgres)
import qualified Database.Beam.Postgres.Syntax as Pg
import Database.Beam.Query (QExpr)
import Database.Beam.Schema.Tables
  ( DatabaseEntity,
    DatabaseEntityDefaultRequirements,
    DatabaseEntityDescriptor,
    DatabaseEntityRegularRequirements,
    EntityModification (..),
    FieldModification (..),
    IsDatabaseEntity,
    PrimaryKey,
    TableEntity,
    dbEntityDescriptor,
    dbEntityName,
    dbTableSettings,
  )
import GHC.Generics as Generic
import Lens.Micro (SimpleGetter, (^.))
import qualified Lens.Micro as Lens

--
-- Annotating a 'DatabaseSettings' with meta information.
--

-- | To make kind signatures more readable.
type DatabaseKind = (Type -> Type) -> Type

-- | To make kind signatures more readable.
type TableKind = (Type -> Type) -> Type

-- | A user-defined annotation. Currently the only possible annotation is the ability to specify for which
-- tables the FK-discovery algorithm is \"turned\" off.
data Annotation where
  -- | Specifies that the given 'TableKind' (i.e. a table) has user-specified FK constraints. This is
  -- useful in case of ambiguity, i.e. when the automatic FK-discovery algorithm is not capable to
  -- infer the correct 'ForeignKey' constraints for a 'Table'. This can happen when the 'PrimaryKey' type
  -- family is not injective, which means there are multiple tables of table @FooT@ in the DB. Consider a
  -- situation where we have a table @BarT@ having a field of type @barField :: PrimaryKey FooT f@ but
  -- (crucially) there are two tables with type @f (TableEntity FooT)@ in the final database. In this
  -- circumstance the FK-discovery algorithm will bail out with a (static) error, and this is where this
  -- annotation comes into play: it allows us to selectively \"disable\" the discovery for the given
  -- table(s), and manually override the FKs.
  --
  -- /Caveat emptor/: Due to what we said earlier (namely that we cannot enforce that tables are not
  -- repeated multiple times within a DB) there might be situations where also the specified 'TableKind'
  -- is not unique. In this case the annotation would affect all the tables of the same type, but that is
  -- usually unavoidable, as the ambiguity was already present the minute we introduced in the DB two tables
  -- of the same type, and so it makes sense for the user to fully resolve the ambiguity manually.
  UserDefinedFk :: TableKind -> Annotation

-- | Zip tables together. Unfortunately we cannot reuse the stock 'zipTables' from 'beam-core', because it
-- works by supplying a rank-2 function with 'IsDatabaseEntity' and 'DatabaseEntityRegularRequirements' as
-- witnesses, we we need the annotated counterparts instead.
--
-- This function can be written without the need of a typeclass, but alas it requires the /unexported/
-- 'GZipDatabase' from 'beam-core', so we had to re-implement this ourselves for now.
zipTables ::
  ( Generic (db f),
    Generic (db g),
    Generic (db h),
    Monad m,
    GZipDatabase be f g h (Rep (db f)) (Rep (db g)) (Rep (db h))
  ) =>
  Proxy be ->
  (forall tbl. (IsAnnotatedDatabaseEntity be tbl, AnnotatedDatabaseEntityRegularRequirements be tbl) => f tbl -> g tbl -> m (h tbl)) ->
  db f ->
  db g ->
  m (db h)
-- We need the pattern type signature on 'combine' to get around a type checking bug in GHC 8.0.1.
-- In future releases, we will switch to the standard forall.
zipTables be combine (f :: db f) (g :: db g) =
  refl $ \h ->
    to <$> gZipDatabase (Proxy @f, Proxy @g, h, be) combine (from f) (from g)
  where
    -- For GHC 8.0.1 renamer bug
    refl :: (Proxy h -> m (db h)) -> m (db h)
    refl fn = fn Proxy

-- | See above on why this class has been re-implemented.
class GZipDatabase be f g h x y z where
  gZipDatabase ::
    Monad m =>
    (Proxy f, Proxy g, Proxy h, Proxy be) ->
    (forall tbl. (IsAnnotatedDatabaseEntity be tbl, AnnotatedDatabaseEntityRegularRequirements be tbl) => f tbl -> g tbl -> m (h tbl)) ->
    x () ->
    y () ->
    m (z ())

instance GZipDatabase be f g h x y z => GZipDatabase be f g h (M1 a b x) (M1 a b y) (M1 a b z) where
  gZipDatabase p combine ~(M1 f) ~(M1 g) = M1 <$> gZipDatabase p combine f g

instance
  ( GZipDatabase be f g h ax ay az,
    GZipDatabase be f g h bx by bz
  ) =>
  GZipDatabase be f g h (ax :*: bx) (ay :*: by) (az :*: bz)
  where
  gZipDatabase p combine ~(ax :*: bx) ~(ay :*: by) = do
    a <- gZipDatabase p combine ax ay
    b <- gZipDatabase p combine bx by
    pure (a :*: b)

instance
  ( IsAnnotatedDatabaseEntity be tbl,
    AnnotatedDatabaseEntityRegularRequirements be tbl
  ) =>
  GZipDatabase be f g h (K1 Generic.R (f tbl)) (K1 Generic.R (g tbl)) (K1 Generic.R (h tbl))
  where
  gZipDatabase _ combine ~(K1 x) ~(K1 y) =
    K1 <$> combine x y

instance
  ( Beam.Database be db,
    Generic (db f),
    Generic (db g),
    Generic (db h),
    GZipDatabase be f g h (Rep (db f)) (Rep (db g)) (Rep (db h))
  ) =>
  GZipDatabase be f g h (K1 Generic.R (db f)) (K1 Generic.R (db g)) (K1 Generic.R (db h))
  where
  gZipDatabase _ combine ~(K1 x) ~(K1 y) =
    K1 <$> zipTables (Proxy :: Proxy be) combine x y

--
-- An annotated Database settings.
--

-- | An 'AnnotatedDatabaseSettings' is similar in spirit to a @beam-core@ 'DatabaseSettings', but it
-- embellish the latter with extra metadata this library can use to derive more information about the input
-- DB, like table and column constraints.
type AnnotatedDatabaseSettings be db = db (AnnotatedDatabaseEntity be db)

-- | An 'AnnotatedDatabaseEntity' wraps the underlying 'DatabaseEntity' together with an annotated
-- description called 'AnnotatedDatabaseEntityDescriptor', which is once again similar to the standard
-- 'DatabaseEntityDescriptor' from Beam.
--
-- An 'AnnotatedDatabaseEntityDescriptor' is not a concrete type, but rather a data family provided by the
-- 'IsAnnotatedDatabaseEntity'.
data AnnotatedDatabaseEntity be (db :: (* -> *) -> *) entityType where
  AnnotatedDatabaseEntity ::
    (IsAnnotatedDatabaseEntity be entityType, IsDatabaseEntity be entityType) =>
    AnnotatedDatabaseEntityDescriptor be entityType ->
    DatabaseEntity be db entityType ->
    AnnotatedDatabaseEntity be db entityType

class IsDatabaseEntity be entityType => IsAnnotatedDatabaseEntity be entityType where
  data AnnotatedDatabaseEntityDescriptor be entityType :: *
  type AnnotatedDatabaseEntityDefaultRequirements be entityType :: Constraint
  type AnnotatedDatabaseEntityRegularRequirements be entityType :: Constraint

  dbAnnotatedEntityAuto ::
    AnnotatedDatabaseEntityRegularRequirements be entityType =>
    DatabaseEntityDescriptor be entityType ->
    AnnotatedDatabaseEntityDescriptor be entityType

instance
  IsDatabaseEntity be (TableEntity tbl) =>
  IsAnnotatedDatabaseEntity be (TableEntity tbl)
  where
  data AnnotatedDatabaseEntityDescriptor be (TableEntity tbl) where
    AnnotatedDatabaseTable ::
      Beam.Table tbl =>
      { dbAnnotatedSchema :: TableSchema tbl,
        dbAnnotatedConstraints :: Set TableConstraint
      } ->
      AnnotatedDatabaseEntityDescriptor be (TableEntity tbl)
  type
    AnnotatedDatabaseEntityDefaultRequirements be (TableEntity tbl) =
      (DatabaseEntityDefaultRequirements be (TableEntity tbl))
  type
    AnnotatedDatabaseEntityRegularRequirements be (TableEntity tbl) =
      ( DatabaseEntityRegularRequirements be (TableEntity tbl),
        GDefaultTableSchema (Rep (TableSchema tbl) ()) (Rep (Beam.TableSettings tbl) ()),
        Generic (TableSchema tbl),
        Generic (Beam.TableSettings tbl)
      )

  dbAnnotatedEntityAuto edesc = AnnotatedDatabaseTable (defaultTableSchema . dbTableSettings $ edesc) mempty

-- | A 'SimpleGetter' to get a plain 'DatabaseEntityDescriptor' from an 'AnnotatedDatabaseEntity'.
lowerEntityDescriptor :: SimpleGetter (AnnotatedDatabaseEntity be db entityType) (DatabaseEntityDescriptor be entityType)
lowerEntityDescriptor = Lens.to (\(AnnotatedDatabaseEntity _ e) -> e ^. dbEntityDescriptor)

annotatedDescriptor :: SimpleGetter (AnnotatedDatabaseEntity be db entityType) (AnnotatedDatabaseEntityDescriptor be entityType)
annotatedDescriptor = Lens.to (\(AnnotatedDatabaseEntity e _) -> e)

deannotate :: SimpleGetter (AnnotatedDatabaseEntity be db entityType) (DatabaseEntity be db entityType)
deannotate = Lens.to (\(AnnotatedDatabaseEntity _ e) -> e)

-- | A table schema.
type TableSchema tbl =
  tbl (TableFieldSchema tbl)

-- | A schema for a field within a given table
data TableFieldSchema (tbl :: (* -> *) -> *) ty where
  TableFieldSchema ::
    { tableFieldName :: ColumnName,
      tableFieldSchema :: FieldSchema ty
    } ->
    TableFieldSchema tbl ty

data FieldSchema ty where
  FieldSchema ::
    ColumnType ->
    Set ColumnConstraint ->
    FieldSchema ty

deriving instance Show (FieldSchema ty)

--
-- Deriving a 'TableSchema'.
--

class GDefaultTableSchema x y where
  gDefTblSchema :: Proxy x -> y -> x

instance GDefaultTableSchema (x p) (y p) => GDefaultTableSchema (D1 f x p) (D1 f y p) where
  gDefTblSchema (Proxy :: Proxy (D1 f x p)) (M1 y) =
    M1 $ gDefTblSchema (Proxy :: Proxy (x p)) y

instance GDefaultTableSchema (x p) (y p) => GDefaultTableSchema (C1 f x p) (C1 f y p) where
  gDefTblSchema (Proxy :: Proxy (C1 f x p)) (M1 y) =
    M1 $ gDefTblSchema (Proxy :: Proxy (x p)) y

instance
  (GDefaultTableSchema (a p) (c p), GDefaultTableSchema (b p) (d p)) =>
  GDefaultTableSchema ((a :*: b) p) ((c :*: d) p)
  where
  gDefTblSchema (Proxy :: Proxy ((a :*: b) p)) (c :*: d) =
    gDefTblSchema (Proxy :: Proxy (a p)) c
      :*: gDefTblSchema (Proxy :: Proxy (b p)) d

instance
  ( SchemaConstraint (Beam.TableField tbl ty) ~ ColumnConstraint,
    HasSchemaConstraints (Beam.TableField tbl ty),
    HasColumnType ty
  ) =>
  GDefaultTableSchema
    (S1 f (K1 Generic.R (TableFieldSchema tbl ty)) p)
    (S1 f (K1 Generic.R (Beam.TableField tbl ty)) p)
  where
  gDefTblSchema (_ :: Proxy (S1 f (K1 Generic.R (TableFieldSchema tbl ty)) p)) (M1 (K1 fName)) = M1 (K1 s)
    where
      s = TableFieldSchema (ColumnName $ fName ^. Beam.fieldName) defaultFieldSchema
      defaultFieldSchema =
        FieldSchema
          (defaultColumnType (Proxy @ty))
          (schemaConstraints (Proxy @(Beam.TableField tbl ty)))

-- | Instance where /g/ is things like a 'PrimaryKey' or a /mixin/.
instance
  ( Generic (g (Beam.TableField tbl2)),
    Generic (g (TableFieldSchema tbl2)),
    GDefaultTableSchema
      (Rep (g (TableFieldSchema tbl2)) ())
      (Rep (g (Beam.TableField tbl2)) ())
  ) =>
  GDefaultTableSchema
    (S1 f (K1 Generic.R (g (TableFieldSchema tbl2))) ())
    (S1 f (K1 Generic.R (g (Beam.TableField tbl2))) ())
  where
  gDefTblSchema (_ :: Proxy (S1 f (K1 Generic.R (g (TableFieldSchema tbl2))) ())) (M1 (K1 fName)) =
    M1 (K1 $ to' $ gDefTblSchema Proxy (from' fName))

-- | Instance for things like 'Nullable (TableFieldSchema tbl)'.
instance
  ( Generic (PrimaryKey tbl1 (g (Beam.TableField tbl2))),
    Generic (PrimaryKey tbl1 (g (TableFieldSchema tbl2))),
    GDefaultTableSchema
      (Rep (PrimaryKey tbl1 (g (TableFieldSchema tbl2))) ())
      (Rep (PrimaryKey tbl1 (g (Beam.TableField tbl2))) ())
  ) =>
  GDefaultTableSchema
    (S1 f (K1 Generic.R (PrimaryKey tbl1 (g (TableFieldSchema tbl2)))) p)
    (S1 f (K1 Generic.R (PrimaryKey tbl1 (g (Beam.TableField tbl2)))) p)
  where
  gDefTblSchema (_ :: Proxy (S1 f (K1 Generic.R (PrimaryKey tbl1 (g (TableFieldSchema tbl2)))) p)) (M1 (K1 fName)) =
    M1 (K1 $ to' $ gDefTblSchema Proxy (from' fName))

defaultTableSchema ::
  forall tbl.
  ( GDefaultTableSchema (Rep (TableSchema tbl) ()) (Rep (Beam.TableSettings tbl) ()),
    Generic (TableSchema tbl),
    Generic (Beam.TableSettings tbl)
  ) =>
  Beam.TableSettings tbl ->
  TableSchema tbl
defaultTableSchema tSettings =
  to $ gDefTblSchema (Proxy :: Proxy (Rep (TableSchema tbl) ())) (from' tSettings)

from' :: Generic a => a -> Rep a ()
from' = from

to' :: Generic a => Rep a () -> a
to' = to

--
-- Annotating 'Table's and 'Field's after the default 'AnnotatedDatabaseSettings' has been instantiated.
--

-- $specifyingConstraints
-- Once an 'AnnotatedDatabaseSettings' has been acquired, the user is able to customise the default
-- medatata associated with it. In order to do so, one can reuse the existing machinery from Beam, in
-- particular the `withDbModification`. For example:
--
-- > annotatedDB :: AnnotatedDatabaseSettings Postgres FlowerDB
-- > annotatedDB = defaultAnnotatedDbSettings flowerDB `withDbModification` dbModification
-- >   { dbFlowers   = annotateTableFields tableModification { flowerDiscounted = defaultsTo (val_ $ Just True)
-- >                                                         , flowerPrice = defaultsTo (val_ $ Just 10.0)
-- >                                                         }
-- >                <> uniqueFields [U (addressPostalCode . addressRegion . flowerAddress)]
-- >   , dbLineItems = annotateTableFields tableModification { lineItemDiscount = defaultsTo (val_ $ Just False) }
-- >                <> uniqueFields [U lineItemFlowerID, U lineItemOrderID, U lineItemQuantity]
-- >   , dbOrders = annotateTableFields tableModification { orderTime = defaultsTo (cast_ currentTimestamp_ utctime) }
-- >              <> foreignKeyOnPk (dbFlowers flowerDB) orderFlowerIdRef Cascade Restrict
-- >              <> uniqueFields [U (addressPostalCode . addressRegion . orderAddress)]
-- >   }
--
-- Refer to the rest of the documentation for this module for more information about 'annotateTableFields',
-- 'uniqueFields' and 'foreignKeyOnPk'.

-- | Annotate the table fields for a given 'AnnotatedDatabaseEntity'. Refer to the $specifyingConstraints
-- section for an example.
annotateTableFields ::
  tbl (FieldModification (TableFieldSchema tbl)) ->
  EntityModification (AnnotatedDatabaseEntity be db) be (TableEntity tbl)
annotateTableFields modFields =
  EntityModification
    ( Endo
        ( \(AnnotatedDatabaseEntity tbl@(AnnotatedDatabaseTable {}) e) ->
            AnnotatedDatabaseEntity
              ( tbl
                  { dbAnnotatedSchema = Beam.withTableModification modFields (dbAnnotatedSchema tbl)
                  }
              )
              e
        )
    )

--
-- Specifying default values (Postgres-specific)
--

-- $specifyingColumnConstraints
-- Due to the fact most column constraints can span /multiple/ columns (think about @UNIQUE@ or
-- @FOREIGN KEY@) the only constraint associated to a 'TableFieldSchema' we allow to customise at the
-- \"single-column-granularity\" is @DEFAULT@.

-- | Specify a default value for an entity. The relevant migration will generate an associated SQL
-- @DEFAULT@. This function accepts any Beam's expression that also the standard 'field' machinery would
-- accept, for example:
--
-- > defaultsTo (val_ $ Just 10)
defaultsTo ::
  (HasColumnType ty, HasSqlValueSyntax Pg.PgValueSyntax ty) =>
  (forall ctx s. Beam.QGenExpr ctx Postgres s ty) ->
  FieldModification (TableFieldSchema tbl) ty
defaultsTo tyVal = FieldModification $ \old ->
  case tableFieldSchema old of
    FieldSchema ty c ->
      old
        { tableFieldSchema =
            FieldSchema ty $ S.singleton (pgDefaultConstraint tyVal) <> c
        }

-- | Postgres-specific function to convert any 'QGenExpr' into a meaningful 'PgExpressionSyntax', so
-- that it can be rendered inside a 'Default' column constraint.
pgDefaultConstraint ::
  forall ty.
  (HasColumnType ty, HasSqlValueSyntax Pg.PgValueSyntax ty) =>
  (forall ctx s. Beam.QGenExpr ctx Postgres s ty) ->
  ColumnConstraint
pgDefaultConstraint tyVal =
  let syntaxFragment = T.pack . displaySyntax . Pg.fromPgExpression $ defaultTo_ tyVal
      dVal = case defaultTypeCast (Proxy @ty) of
        Nothing -> syntaxFragment
        Just tc | T.head syntaxFragment == '\'' -> syntaxFragment <> "::" <> tc
        -- NOTE(and) Special-case handling for CURRENT_TIMESTAMP. See issue #31.
        Just tc | syntaxFragment == "CURRENT_TIMESTAMP" -> "(" <> syntaxFragment <> ")::" <> tc
        Just tc -> "'" <> syntaxFragment <> "'::" <> tc
   in Default dVal
  where
    -- NOTE(adn) We are unfortunately once again forced to copy and paste some code from beam-migrate.
    -- In particular, `beam-migrate` wraps the returning 'QExpr' into a 'DefaultValue' newtype wrapper,
    -- which only purpose is to define an instance for 'FieldReturnType' (cfr.
    -- /Database.Beam.AutoMigrate.SQL.Tables/) and the underlying 'BeamSqlBackendExpressionSyntax' is used to
    -- call 'columnSchemaSyntax', which is then used in /their own/ definition of `FieldSchema`, which we
    -- don't follow.
    -- NOTE(adn) It's unclear what \"t\" stands for here, probably \"TablePrefix\". Not documented in
    -- `beam-migrate` itself.
    defaultTo_ :: (forall s. QExpr Postgres s a) -> Pg.PgExpressionSyntax
    defaultTo_ (Beam.QExpr e) = e "t"

--
-- Specifying uniqueness constraints
--

-- $specifyingTableConstraints
-- Is it possible to annotate an 'AnnotatedDatabaseEntity' with @UNIQUE@ and @FOREIGN KEY@ constraints.

data UniqueConstraint (tbl :: ((* -> *) -> *)) where
  -- | Use this to \"tag\" a standard Beam 'TableField' selector or 'PrimaryKey'.
  U :: HasColumnNames entity tbl => (tbl (Beam.TableField tbl) -> entity) -> UniqueConstraint tbl

-- | Given a list of 'TableField' selectors wrapped in a 'UniqueConstraint' type constructor, it adds
-- to the relevant 'AnnotatedDatabaseEntity' a new @UNIQUE@ 'TableConstraint' composed by /all/ the
-- fields specified. To put it differently, every call to 'uniqueConstraintOn' generates a /separate/
-- @UNIQUE@ constraint composed by the listed fields.
-- If a 'PrimaryKey' is passed as input, it will desugar under the hood into as many columns as
-- the primary key refers to.
uniqueConstraintOn ::
  [UniqueConstraint tbl] ->
  EntityModification (AnnotatedDatabaseEntity be db) be (TableEntity tbl)
uniqueConstraintOn us =
  EntityModification
    ( Endo
        ( \(AnnotatedDatabaseEntity tbl@(AnnotatedDatabaseTable {}) e) ->
            AnnotatedDatabaseEntity
              ( tbl
                  { dbAnnotatedConstraints =
                      let cols = concatMap (\case (U f) -> colNames (tableSettings e) f) us
                          tName = e ^. dbEntityDescriptor . dbEntityName
                          conname = T.intercalate "_" (tName : map columnName cols) <> "_ukey"
                       in S.insert (Unique conname (S.fromList cols)) (dbAnnotatedConstraints tbl)
                  }
              )
              e
        )
    )

--
-- Specifying FK constrainst
--

data ForeignKeyConstraint (tbl :: ((* -> *) -> *)) (tbl' :: ((* -> *) -> *)) where
  References ::
    Beam.Beamable (PrimaryKey tbl') =>
    (tbl (Beam.TableField tbl) -> PrimaryKey tbl' (Beam.TableField tbl)) ->
    (tbl' (Beam.TableField tbl') -> Beam.Columnar Beam.Identity (Beam.TableField tbl' ty)) ->
    ForeignKeyConstraint tbl tbl'

-- | Special-case combinator to use when defining FK constraints referencing the /primary key/ of the
-- target table.
foreignKeyOnPk ::
  ( Beam.Beamable (PrimaryKey tbl'),
    Beam.Beamable tbl',
    Beam.Table tbl',
    PrimaryKey tbl' f ~ PrimaryKey tbl' g
  ) =>
  -- | The 'DatabaseEntity' of the /referenced/ table.
  DatabaseEntity be db (TableEntity tbl') ->
  -- | A function yielding a 'PrimaryKey'. This is usually a record field of the table
  -- you want to define the FK /for/, and it must have /PrimaryKey externalTable f/ as
  -- its column-tag.
  (tbl (Beam.TableField tbl) -> PrimaryKey tbl' (Beam.TableField tbl)) ->
  -- | What do to \"on delete\"
  ReferenceAction ->
  -- | What do to \"on update\"
  ReferenceAction ->
  EntityModification (AnnotatedDatabaseEntity be db) be (TableEntity tbl)
foreignKeyOnPk externalEntity ourColumn onDelete onUpdate =
  EntityModification
    ( Endo
        ( \(AnnotatedDatabaseEntity tbl@(AnnotatedDatabaseTable {}) e) ->
            AnnotatedDatabaseEntity
              ( tbl
                  { dbAnnotatedConstraints =
                      let colPairs =
                            zipWith
                              (,)
                              (fieldAsColumnNames (ourColumn (tableSettings e)))
                              (fieldAsColumnNames (Beam.pk (tableSettings externalEntity)))
                          tName = externalEntity ^. dbEntityDescriptor . dbEntityName
                          conname = T.intercalate "_" (tName : map (columnName . snd) colPairs) <> "_fkey"
                       in S.insert
                            (ForeignKey conname (TableName tName) (S.fromList colPairs) onDelete onUpdate)
                            (dbAnnotatedConstraints tbl)
                  }
              )
              e
        )
    )

foreignKeyOn ::
  Beam.Beamable tbl' =>
  DatabaseEntity be db (TableEntity tbl') ->
  [ForeignKeyConstraint tbl tbl'] ->
  -- | On Delete
  ReferenceAction ->
  -- | On Update
  ReferenceAction ->
  EntityModification (AnnotatedDatabaseEntity be db) be (TableEntity tbl)
foreignKeyOn externalEntity us onDelete onUpdate =
  EntityModification
    ( Endo
        ( \(AnnotatedDatabaseEntity tbl@(AnnotatedDatabaseTable {}) e) ->
            AnnotatedDatabaseEntity
              ( tbl
                  { dbAnnotatedConstraints =
                      let colPairs =
                            concatMap
                              ( \case
                                  (References ours theirs) ->
                                    zipWith
                                      (,)
                                      (fieldAsColumnNames (ours (tableSettings e)))
                                      [ColumnName (theirs (tableSettings externalEntity) ^. Beam.fieldName)]
                              )
                              us
                          tName = externalEntity ^. dbEntityDescriptor . dbEntityName
                          conname = T.intercalate "_" (tName : map (columnName . snd) colPairs) <> "_fkey"
                       in S.insert
                            (ForeignKey conname (TableName tName) (S.fromList colPairs) onDelete onUpdate)
                            (dbAnnotatedConstraints tbl)
                  }
              )
              e
        )
    )