beam-postgres-0.6.3.0: test/Database/Beam/Postgres/Test/CTENegative.hs
{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE StandaloneDeriving #-}
-- This module deliberately contains expressions which must not type-check.
-- Deferred type errors are isolated here so the positive CTE tests retain
-- normal, strict type checking.
module Database.Beam.Postgres.Test.CTENegative
( invalidNestedDelete
, invalidNestedInsert
, invalidNestedUpdate
, invalidNestedSelectThenDelete
, invalidNestedDeleteThenSelect
, invalidNestedEmptyInsert
, invalidNestedIdentityUpdate
, invalidNestedSideEffectDelete
, invalidCoercedPlacement
, invalidRecursiveInsert
, invalidReuseSideEffect
) where
import qualified Data.Coerce as Coerce
import Data.Int (Int32)
import Data.Text (Text)
import Database.Beam
import Database.Beam.Postgres
import qualified Database.Beam.Postgres.Full as Pg
import qualified Database.Beam.Query.CTE as CTE
data NegativeCteRowT f = NegativeCteRow
{ negativeCteId :: C f Int32
, negativeCteValue :: C f Text
} deriving (Generic, Beamable)
deriving instance Show (NegativeCteRowT Identity)
deriving instance Eq (NegativeCteRowT Identity)
instance Table NegativeCteRowT where
data PrimaryKey NegativeCteRowT f = NegativeCteRowKey (C f Int32)
deriving (Generic, Beamable)
primaryKey = NegativeCteRowKey . negativeCteId
newtype NegativeCteDb entity = NegativeCteDb
{ negativeCteRows :: entity (TableEntity NegativeCteRowT)
} deriving (Generic, Database Postgres)
negativeCteDb :: DatabaseSettings Postgres NegativeCteDb
negativeCteDb = defaultDbSettings
-- Each of the following three expressions attempts to put a modifying CTE in
-- pgSelectWithNested. They must fail with PgCteTopLevelOnly versus
-- PgCteNestedAllowed,
-- independently of which data-modifying command produced the CTE.
invalidNestedDelete :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedDelete = select $ Pg.pgSelectWithNested $ do
deleted <- topLevelDeleteCte
pure (reuse deleted)
invalidNestedInsert :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedInsert = select $ Pg.pgSelectWithNested $ do
inserted <- Pg.cteInsertReturning
(negativeCteRows negativeCteDb)
(insertValues [NegativeCteRow 2 "inserted"])
Pg.onConflictDefault
id
case inserted of
Nothing -> pure $ all_ (negativeCteRows negativeCteDb)
Just inserted' -> pure (reuse inserted')
invalidNestedUpdate :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedUpdate = select $ Pg.pgSelectWithNested $ do
updated <- Pg.cteUpdateReturning
(negativeCteRows negativeCteDb)
(\row -> negativeCteValue row <-. val_ "updated")
(\row -> negativeCteId row ==. val_ 1)
id
case updated of
Nothing -> pure $ all_ (negativeCteRows negativeCteDb)
Just updated' -> pure (reuse updated')
-- Placement is a property of the whole With block. Reordering a normal SELECT
-- CTE around the DELETE must not weaken the top-level-only requirement.
invalidNestedSelectThenDelete :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedSelectThenDelete = select $ Pg.pgSelectWithNested $ do
_ <- nestedSelectCte
deleted <- topLevelDeleteCte
pure (reuse deleted)
invalidNestedDeleteThenSelect :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedDeleteThenSelect = select $ Pg.pgSelectWithNested $ do
deleted <- topLevelDeleteCte
_ <- nestedSelectCte
pure (reuse deleted)
-- The result is conservatively top-level-only even when the supplied values or
-- assignments make the INSERT or UPDATE a no-op. The placement index cannot
-- vary with that value-level outcome.
invalidNestedEmptyInsert :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedEmptyInsert = select $ Pg.pgSelectWithNested $ do
inserted <- Pg.cteInsertReturning
(negativeCteRows negativeCteDb)
SqlInsertValuesEmpty
Pg.onConflictDefault
id
case inserted of
Nothing -> pure $ all_ (negativeCteRows negativeCteDb)
Just inserted' -> pure (reuse inserted')
invalidNestedIdentityUpdate :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedIdentityUpdate = select $ Pg.pgSelectWithNested $ do
updated <- Pg.cteUpdateReturning
(negativeCteRows negativeCteDb)
(const mempty)
(const (val_ True))
id
case updated of
Nothing -> pure $ all_ (negativeCteRows negativeCteDb)
Just updated' -> pure (reuse updated')
-- A no-RETURNING modifying CTE has the same top-level placement requirement as
-- its returning counterpart, even though it exposes no relation.
invalidNestedSideEffectDelete :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidNestedSideEffectDelete = select $ Pg.pgSelectWithNested $ do
Pg.cteDelete
(negativeCteRows negativeCteDb)
(\row -> negativeCteId row ==. val_ 1)
pure $ all_ (negativeCteRows negativeCteDb)
-- PgWith has nominal roles and an abstract constructor, so Data.Coerce cannot
-- be used to relabel a top-level-only block as nested-safe.
invalidCoercedPlacement :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidCoercedPlacement = select $ Pg.pgSelectWithNested $ coercePlacement $ do
deleted <- topLevelDeleteCte
pure (reuse deleted)
-- MonadFix exists only for PgCteNestedAllowed. This prevents an INSERT CTE from
-- reading its own RETURNING rows recursively, which PostgreSQL rejects.
invalidRecursiveInsert :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidRecursiveInsert = Pg.pgSelectWithTopLevel $ mdo
~(Just inserted) <- Pg.cteInsertReturning
(negativeCteRows negativeCteDb)
(insertFrom (reuse inserted))
Pg.onConflictDefault
id
pure (reuse inserted)
-- Side-effect-only CTEs deliberately return unit because a DML statement
-- without RETURNING forms no temporary relation in PostgreSQL.
invalidReuseSideEffect :: SqlSelect Postgres (NegativeCteRowT Identity)
invalidReuseSideEffect = Pg.pgSelectWithTopLevel $ do
deleted <- Pg.cteDelete
(negativeCteRows negativeCteDb)
(\row -> negativeCteId row ==. val_ 1)
let impossible
:: ReusableQ Postgres NegativeCteDb
(NegativeCteRowT (QExpr Postgres CTE.QAnyScope))
impossible = deleted
pure (reuse impossible)
coercePlacement
:: Pg.PgWith NegativeCteDb 'Pg.PgCteTopLevelOnly a
-> Pg.PgWith NegativeCteDb 'Pg.PgCteNestedAllowed a
coercePlacement = Coerce.coerce
nestedSelectCte
:: Pg.PgWith NegativeCteDb placement
(ReusableQ Postgres NegativeCteDb
(NegativeCteRowT (QExpr Postgres CTE.QAnyScope)))
nestedSelectCte = Pg.pgSelecting $ all_ (negativeCteRows negativeCteDb)
topLevelDeleteCte
:: Pg.PgWith NegativeCteDb 'Pg.PgCteTopLevelOnly
(ReusableQ Postgres NegativeCteDb
(NegativeCteRowT (QExpr Postgres CTE.QAnyScope)))
topLevelDeleteCte = Pg.cteDeleteReturning
(negativeCteRows negativeCteDb)
(\row -> negativeCteId row ==. val_ 1)
id