packages feed

hpqtypes-extras 1.16.4.4 → 1.17.0.0

raw patch · 26 files changed

+5695/−3990 lines, 26 filessetup-changedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Database.PostgreSQL.PQTypes.Model.Trigger: [triggerDeferrable] :: Trigger -> Bool
- Database.PostgreSQL.PQTypes.Model.Trigger: [triggerInitiallyDeferred] :: Trigger -> Bool
- Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhere Database.PostgreSQL.PQTypes.SQL.Builder.SqlAll
- Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.Sqlable Database.PostgreSQL.PQTypes.SQL.Builder.SqlAll
- Database.PostgreSQL.PQTypes.SQL.Builder: instance GHC.Show.Show Database.PostgreSQL.PQTypes.SQL.Builder.SqlAll
+ Database.PostgreSQL.PQTypes.Checks: DatabaseDefinitions :: [Extension] -> [CompositeType] -> [EnumType] -> [Domain] -> [Table] -> DatabaseDefinitions
+ Database.PostgreSQL.PQTypes.Checks: [dbComposites] :: DatabaseDefinitions -> [CompositeType]
+ Database.PostgreSQL.PQTypes.Checks: [dbDomains] :: DatabaseDefinitions -> [Domain]
+ Database.PostgreSQL.PQTypes.Checks: [dbEnums] :: DatabaseDefinitions -> [EnumType]
+ Database.PostgreSQL.PQTypes.Checks: [dbExtensions] :: DatabaseDefinitions -> [Extension]
+ Database.PostgreSQL.PQTypes.Checks: [dbTables] :: DatabaseDefinitions -> [Table]
+ Database.PostgreSQL.PQTypes.Checks: [eoCheckForeignKeysIndexes] :: ExtrasOptions -> !Bool
+ Database.PostgreSQL.PQTypes.Checks: [eoCheckOverlappingIndexes] :: ExtrasOptions -> !Bool
+ Database.PostgreSQL.PQTypes.Checks: checkDatabaseWithReport :: forall m. (MonadDB m, MonadLog m, MonadThrow m) => ExtrasOptions -> DatabaseDefinitions -> m ValidationResult
+ Database.PostgreSQL.PQTypes.Checks: data DatabaseDefinitions
+ Database.PostgreSQL.PQTypes.Checks: data ValidationResult
+ Database.PostgreSQL.PQTypes.Checks: emptyDbDefinitions :: DatabaseDefinitions
+ Database.PostgreSQL.PQTypes.Checks: validationError :: Text -> ValidationResult
+ Database.PostgreSQL.PQTypes.Checks: validationInfo :: Text -> ValidationResult
+ Database.PostgreSQL.PQTypes.ExtrasOptions: [eoCheckForeignKeysIndexes] :: ExtrasOptions -> !Bool
+ Database.PostgreSQL.PQTypes.ExtrasOptions: [eoCheckOverlappingIndexes] :: ExtrasOptions -> !Bool
+ Database.PostgreSQL.PQTypes.Model.EnumType: EnumType :: !RawSQL () -> ![RawSQL ()] -> EnumType
+ Database.PostgreSQL.PQTypes.Model.EnumType: [etName] :: EnumType -> !RawSQL ()
+ Database.PostgreSQL.PQTypes.Model.EnumType: [etValues] :: EnumType -> ![RawSQL ()]
+ Database.PostgreSQL.PQTypes.Model.EnumType: data EnumType
+ Database.PostgreSQL.PQTypes.Model.EnumType: instance GHC.Classes.Eq Database.PostgreSQL.PQTypes.Model.EnumType.EnumType
+ Database.PostgreSQL.PQTypes.Model.EnumType: instance GHC.Classes.Ord Database.PostgreSQL.PQTypes.Model.EnumType.EnumType
+ Database.PostgreSQL.PQTypes.Model.EnumType: instance GHC.Show.Show Database.PostgreSQL.PQTypes.Model.EnumType.EnumType
+ Database.PostgreSQL.PQTypes.Model.EnumType: sqlCreateEnum :: EnumType -> RawSQL ()
+ Database.PostgreSQL.PQTypes.Model.EnumType: sqlDropEnum :: RawSQL () -> RawSQL ()
+ Database.PostgreSQL.PQTypes.Model.Index: [idxNotDistinctNulls] :: TableIndex -> Bool
+ Database.PostgreSQL.PQTypes.Model.Index: indexColumnName :: IndexColumn -> RawSQL ()
+ Database.PostgreSQL.PQTypes.Model.PrimaryKey: pkColumns :: PrimaryKey -> [RawSQL ()]
+ Database.PostgreSQL.PQTypes.Model.Trigger: After :: TriggerActionTime
+ Database.PostgreSQL.PQTypes.Model.Trigger: Before :: TriggerActionTime
+ Database.PostgreSQL.PQTypes.Model.Trigger: Deferrable :: ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: DeferrableInitiallyDeferred :: ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: NotDeferrable :: ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: TriggerConstraint :: ConstraintAttributes -> TriggerKind
+ Database.PostgreSQL.PQTypes.Model.Trigger: TriggerRegular :: TriggerActionTime -> TriggerKind
+ Database.PostgreSQL.PQTypes.Model.Trigger: [triggerKind] :: Trigger -> TriggerKind
+ Database.PostgreSQL.PQTypes.Model.Trigger: data ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: data TriggerActionTime
+ Database.PostgreSQL.PQTypes.Model.Trigger: data TriggerKind
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Classes.Eq Database.PostgreSQL.PQTypes.Model.Trigger.ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Classes.Eq Database.PostgreSQL.PQTypes.Model.Trigger.TriggerActionTime
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Classes.Eq Database.PostgreSQL.PQTypes.Model.Trigger.TriggerKind
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Show.Show Database.PostgreSQL.PQTypes.Model.Trigger.ConstraintAttributes
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Show.Show Database.PostgreSQL.PQTypes.Model.Trigger.TriggerActionTime
+ Database.PostgreSQL.PQTypes.Model.Trigger: instance GHC.Show.Show Database.PostgreSQL.PQTypes.Model.Trigger.TriggerKind
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlWhereAll :: [SqlCondition] -> SqlWhereAll
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlWhereAny :: [SqlCondition] -> SqlWhereAny
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlDeleteRecursiveWith] :: SqlDelete -> Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlInsertRecursiveWith] :: SqlInsert -> Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlInsertSelectRecursiveWith] :: SqlInsertSelect -> Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlSelectRecursiveWith] :: SqlSelect -> Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlUpdateRecursiveWith] :: SqlUpdate -> Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlWhereAllWhere] :: SqlWhereAll -> [SqlCondition]
+ Database.PostgreSQL.PQTypes.SQL.Builder: [sqlWhereAnyWhere] :: SqlWhereAny -> [SqlCondition]
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhere Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAll
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhere Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAny
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.Sqlable Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAll
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance Database.PostgreSQL.PQTypes.SQL.Builder.Sqlable Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAny
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance GHC.Base.Semigroup Database.PostgreSQL.PQTypes.SQL.Builder.Recursive
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance GHC.Show.Show Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAll
+ Database.PostgreSQL.PQTypes.SQL.Builder: instance GHC.Show.Show Database.PostgreSQL.PQTypes.SQL.Builder.SqlWhereAny
+ Database.PostgreSQL.PQTypes.SQL.Builder: newtype SqlWhereAll
+ Database.PostgreSQL.PQTypes.SQL.Builder: newtype SqlWhereAny
+ Database.PostgreSQL.PQTypes.SQL.Builder: sqlAll :: State SqlWhereAll () -> SQL
+ Database.PostgreSQL.PQTypes.SQL.Builder: sqlAny :: State SqlWhereAny () -> SQL
+ Database.PostgreSQL.PQTypes.SQL.Builder: sqlWithRecursive :: (MonadState v m, SqlWith v, Sqlable s) => SQL -> s -> m ()
- Database.PostgreSQL.PQTypes.Checks: ExtrasOptions :: !Maybe Int -> !Bool -> !ObjectsValidationMode -> !Bool -> ExtrasOptions
+ Database.PostgreSQL.PQTypes.Checks: ExtrasOptions :: !Maybe Int -> !Bool -> !ObjectsValidationMode -> !Bool -> !Bool -> !Bool -> ExtrasOptions
- Database.PostgreSQL.PQTypes.Checks: checkDatabase :: forall m. (MonadDB m, MonadLog m, MonadThrow m) => ExtrasOptions -> [CompositeType] -> [Domain] -> [Table] -> m ()
+ Database.PostgreSQL.PQTypes.Checks: checkDatabase :: forall m. (MonadDB m, MonadLog m, MonadThrow m) => ExtrasOptions -> DatabaseDefinitions -> m ()
- Database.PostgreSQL.PQTypes.Checks: migrateDatabase :: (MonadIO m, MonadDB m, MonadLog m, MonadMask m) => ExtrasOptions -> [Extension] -> [CompositeType] -> [Domain] -> [Table] -> [Migration m] -> m ()
+ Database.PostgreSQL.PQTypes.Checks: migrateDatabase :: (MonadIO m, MonadDB m, MonadLog m, MonadMask m) => ExtrasOptions -> DatabaseDefinitions -> [Migration m] -> m ()
- Database.PostgreSQL.PQTypes.ExtrasOptions: ExtrasOptions :: !Maybe Int -> !Bool -> !ObjectsValidationMode -> !Bool -> ExtrasOptions
+ Database.PostgreSQL.PQTypes.ExtrasOptions: ExtrasOptions :: !Maybe Int -> !Bool -> !ObjectsValidationMode -> !Bool -> !Bool -> !Bool -> ExtrasOptions
- Database.PostgreSQL.PQTypes.Model.Index: TableIndex :: [IndexColumn] -> [RawSQL ()] -> IndexMethod -> Bool -> Bool -> Maybe (RawSQL ()) -> TableIndex
+ Database.PostgreSQL.PQTypes.Model.Index: TableIndex :: [IndexColumn] -> [RawSQL ()] -> IndexMethod -> Bool -> Bool -> Maybe (RawSQL ()) -> Bool -> TableIndex
- Database.PostgreSQL.PQTypes.Model.Trigger: Trigger :: RawSQL () -> RawSQL () -> Set TriggerEvent -> Bool -> Bool -> Maybe (RawSQL ()) -> RawSQL () -> Trigger
+ Database.PostgreSQL.PQTypes.Model.Trigger: Trigger :: RawSQL () -> RawSQL () -> TriggerKind -> Set TriggerEvent -> Maybe (RawSQL ()) -> RawSQL () -> Trigger
- Database.PostgreSQL.PQTypes.SQL.Builder: SqlDelete :: SQL -> SQL -> [SqlCondition] -> [SQL] -> [(SQL, SQL, Materialized)] -> SqlDelete
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlDelete :: SQL -> SQL -> [SqlCondition] -> [SQL] -> [(SQL, SQL, Materialized)] -> Recursive -> SqlDelete
- Database.PostgreSQL.PQTypes.SQL.Builder: SqlInsert :: SQL -> Maybe (SQL, Maybe SQL) -> [(SQL, Multiplicity SQL)] -> [SQL] -> [(SQL, SQL, Materialized)] -> SqlInsert
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlInsert :: SQL -> Maybe (SQL, Maybe SQL) -> [(SQL, Multiplicity SQL)] -> [SQL] -> [(SQL, SQL, Materialized)] -> Recursive -> SqlInsert
- Database.PostgreSQL.PQTypes.SQL.Builder: SqlInsertSelect :: SQL -> Maybe (SQL, Maybe SQL) -> Bool -> [(SQL, SQL)] -> [SQL] -> SQL -> [SqlCondition] -> [SQL] -> [SQL] -> [SQL] -> Integer -> Integer -> [(SQL, SQL, Materialized)] -> SqlInsertSelect
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlInsertSelect :: SQL -> Maybe (SQL, Maybe SQL) -> Bool -> [(SQL, SQL)] -> [SQL] -> SQL -> [SqlCondition] -> [SQL] -> [SQL] -> [SQL] -> Integer -> Integer -> [(SQL, SQL, Materialized)] -> Recursive -> SqlInsertSelect
- Database.PostgreSQL.PQTypes.SQL.Builder: SqlSelect :: SQL -> [SQL] -> [SQL] -> Bool -> [SQL] -> [SqlCondition] -> [SQL] -> [SQL] -> [SQL] -> Integer -> Integer -> [(SQL, SQL, Materialized)] -> SqlSelect
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlSelect :: SQL -> [SQL] -> [SQL] -> Bool -> [SQL] -> [SqlCondition] -> [SQL] -> [SQL] -> [SQL] -> Integer -> Integer -> [(SQL, SQL, Materialized)] -> Recursive -> SqlSelect
- Database.PostgreSQL.PQTypes.SQL.Builder: SqlUpdate :: SQL -> SQL -> [SqlCondition] -> [(SQL, SQL)] -> [SQL] -> [(SQL, SQL, Materialized)] -> SqlUpdate
+ Database.PostgreSQL.PQTypes.SQL.Builder: SqlUpdate :: SQL -> SQL -> [SqlCondition] -> [(SQL, SQL)] -> [SQL] -> [(SQL, SQL, Materialized)] -> Recursive -> SqlUpdate
- Database.PostgreSQL.PQTypes.SQL.Builder: sqlWhereAny :: (MonadState v m, SqlWhere v) => [State SqlAll ()] -> m ()
+ Database.PostgreSQL.PQTypes.SQL.Builder: sqlWhereAny :: (MonadState v m, SqlWhere v) => [State SqlWhereAll ()] -> m ()

Files

CHANGELOG.md view
@@ -1,3 +1,15 @@+# hpqtypes-extras-1.17.0.0 (2025-03-12)+* Grouped some parameters of `migrateDatabase` and `checkDatabase` into a+  `DatabaseDefinitions` record type.+* Add an optional check that all foreign keys have an index.+* Add support for NULLS NOT DISTINCT in unique indexes.+* Add `sqlAll` and `sqlAny` to allow creating `SQL` expressions with+  nested `AND` and `OR` conditions.+* Add `SqlWhereAll` and `SqlWhereAny` so they can be used in signatures.+* Add rudimentary support for enum types.+* Add support for some regular triggers, ie: `AFTER` triggers without constraints+  and `BEFORE` triggers.+ # hpqtypes-extras-1.16.4.4 (2023-08-23) * Switch from `cryptonite` to `crypton`. * Make `sqlWhereEqualsAny`, `sqlWhereIn` and `sqlWhereNotIn` prepared-query
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
benchmark/Main.hs view
@@ -7,22 +7,71 @@ import Database.PostgreSQL.PQTypes.Deriving  main :: IO ()-main = defaultMain-  [ bgroup "enum"-    [ bench "encode" $ nf (encodeEnum @T) T42-    , bench "decode" $ nf (decodeEnum @T) 42-    ]-  , bgroup "enum-text"-    [ bench "encode" $ nf (encodeEnumAsText @S) S42-    , bench "decode" $ nf (decodeEnumAsText @S) "text_42"+main =+  defaultMain+    [ bgroup+        "enum"+        [ bench "encode" $ nf (encodeEnum @T) T42+        , bench "decode" $ nf (decodeEnum @T) 42+        ]+    , bgroup+        "enum-text"+        [ bench "encode" $ nf (encodeEnumAsText @S) S42+        , bench "decode" $ nf (decodeEnumAsText @S) "text_42"+        ]     ]-  ] -data T = T01 | T02 | T03 | T04 | T05 | T06 | T07 | T08 | T09 | T10-       | T11 | T12 | T13 | T14 | T15 | T16 | T17 | T18 | T19 | T20-       | T21 | T22 | T23 | T24 | T25 | T26 | T27 | T28 | T29 | T30-       | T31 | T32 | T33 | T34 | T35 | T36 | T37 | T38 | T39 | T40-       | T41 | T42 | T43 | T44 | T45 | T46 | T47 | T48 | T49 | T50+data T+  = T01+  | T02+  | T03+  | T04+  | T05+  | T06+  | T07+  | T08+  | T09+  | T10+  | T11+  | T12+  | T13+  | T14+  | T15+  | T16+  | T17+  | T18+  | T19+  | T20+  | T21+  | T22+  | T23+  | T24+  | T25+  | T26+  | T27+  | T28+  | T29+  | T30+  | T31+  | T32+  | T33+  | T34+  | T35+  | T36+  | T37+  | T38+  | T39+  | T40+  | T41+  | T42+  | T43+  | T44+  | T45+  | T46+  | T47+  | T48+  | T49+  | T50   deriving (Eq, Show, Enum, Bounded)  instance NFData T where@@ -33,26 +82,112 @@ -- >>> isInjective (encodeEnum @T) -- True instance EnumEncoding T where-   type EnumBase T = Int16-   encodeEnum = \case-     T01 -> 1;  T02 -> 2;  T03 -> 3;  T04 -> 4;  T05 -> 5-     T06 -> 6;  T07 -> 7;  T08 -> 8;  T09 -> 9;  T10 -> 10-     T11 -> 11; T12 -> 12; T13 -> 13; T14 -> 14; T15 -> 15-     T16 -> 16; T17 -> 17; T18 -> 18; T19 -> 19; T20 -> 20-     T21 -> 21; T22 -> 22; T23 -> 23; T24 -> 24; T25 -> 25-     T26 -> 26; T27 -> 27; T28 -> 28; T29 -> 29; T30 -> 30-     T31 -> 31; T32 -> 32; T33 -> 33; T34 -> 34; T35 -> 35-     T36 -> 36; T37 -> 37; T38 -> 38; T39 -> 39; T40 -> 40-     T41 -> 41; T42 -> 42; T43 -> 43; T44 -> 44; T45 -> 45-     T46 -> 46; T47 -> 47; T48 -> 48; T49 -> 49; T50 -> 50+  type EnumBase T = Int16+  encodeEnum = \case+    T01 -> 1+    T02 -> 2+    T03 -> 3+    T04 -> 4+    T05 -> 5+    T06 -> 6+    T07 -> 7+    T08 -> 8+    T09 -> 9+    T10 -> 10+    T11 -> 11+    T12 -> 12+    T13 -> 13+    T14 -> 14+    T15 -> 15+    T16 -> 16+    T17 -> 17+    T18 -> 18+    T19 -> 19+    T20 -> 20+    T21 -> 21+    T22 -> 22+    T23 -> 23+    T24 -> 24+    T25 -> 25+    T26 -> 26+    T27 -> 27+    T28 -> 28+    T29 -> 29+    T30 -> 30+    T31 -> 31+    T32 -> 32+    T33 -> 33+    T34 -> 34+    T35 -> 35+    T36 -> 36+    T37 -> 37+    T38 -> 38+    T39 -> 39+    T40 -> 40+    T41 -> 41+    T42 -> 42+    T43 -> 43+    T44 -> 44+    T45 -> 45+    T46 -> 46+    T47 -> 47+    T48 -> 48+    T49 -> 49+    T50 -> 50  ---------------------------------------- -data S = S01 | S02 | S03 | S04 | S05 | S06 | S07 | S08 | S09 | S10-       | S11 | S12 | S13 | S14 | S15 | S16 | S17 | S18 | S19 | S20-       | S21 | S22 | S23 | S24 | S25 | S26 | S27 | S28 | S29 | S30-       | S31 | S32 | S33 | S34 | S35 | S36 | S37 | S38 | S39 | S40-       | S41 | S42 | S43 | S44 | S45 | S46 | S47 | S48 | S49 | S50+data S+  = S01+  | S02+  | S03+  | S04+  | S05+  | S06+  | S07+  | S08+  | S09+  | S10+  | S11+  | S12+  | S13+  | S14+  | S15+  | S16+  | S17+  | S18+  | S19+  | S20+  | S21+  | S22+  | S23+  | S24+  | S25+  | S26+  | S27+  | S28+  | S29+  | S30+  | S31+  | S32+  | S33+  | S34+  | S35+  | S36+  | S37+  | S38+  | S39+  | S40+  | S41+  | S42+  | S43+  | S44+  | S45+  | S46+  | S47+  | S48+  | S49+  | S50   deriving (Eq, Show, Enum, Bounded)  instance NFData S where@@ -64,16 +199,53 @@ -- True instance EnumAsTextEncoding S where   encodeEnumAsText = \case-    S01 -> "text_01"; S02 -> "text_02"; S03 -> "text_03"; S04 -> "text_04"-    S05 -> "text_05"; S06 -> "text_06"; S07 -> "text_07"; S08 -> "text_08"-    S09 -> "text_09"; S10 -> "text_10"; S11 -> "text_11"; S12 -> "text_12"-    S13 -> "text_13"; S14 -> "text_14"; S15 -> "text_15"; S16 -> "text_16"-    S17 -> "text_17"; S18 -> "text_18"; S19 -> "text_19"; S20 -> "text_20"-    S21 -> "text_21"; S22 -> "text_22"; S23 -> "text_23"; S24 -> "text_24"-    S25 -> "text_25"; S26 -> "text_26"; S27 -> "text_27"; S28 -> "text_28"-    S29 -> "text_29"; S30 -> "text_30"; S31 -> "text_31"; S32 -> "text_32"-    S33 -> "text_33"; S34 -> "text_34"; S35 -> "text_35"; S36 -> "text_36"-    S37 -> "text_37"; S38 -> "text_38"; S39 -> "text_39"; S40 -> "text_40"-    S41 -> "text_41"; S42 -> "text_42"; S43 -> "text_43"; S44 -> "text_44"-    S45 -> "text_45"; S46 -> "text_46"; S47 -> "text_47"; S48 -> "text_48"-    S49 -> "text_49"; S50 -> "text_50";+    S01 -> "text_01"+    S02 -> "text_02"+    S03 -> "text_03"+    S04 -> "text_04"+    S05 -> "text_05"+    S06 -> "text_06"+    S07 -> "text_07"+    S08 -> "text_08"+    S09 -> "text_09"+    S10 -> "text_10"+    S11 -> "text_11"+    S12 -> "text_12"+    S13 -> "text_13"+    S14 -> "text_14"+    S15 -> "text_15"+    S16 -> "text_16"+    S17 -> "text_17"+    S18 -> "text_18"+    S19 -> "text_19"+    S20 -> "text_20"+    S21 -> "text_21"+    S22 -> "text_22"+    S23 -> "text_23"+    S24 -> "text_24"+    S25 -> "text_25"+    S26 -> "text_26"+    S27 -> "text_27"+    S28 -> "text_28"+    S29 -> "text_29"+    S30 -> "text_30"+    S31 -> "text_31"+    S32 -> "text_32"+    S33 -> "text_33"+    S34 -> "text_34"+    S35 -> "text_35"+    S36 -> "text_36"+    S37 -> "text_37"+    S38 -> "text_38"+    S39 -> "text_39"+    S40 -> "text_40"+    S41 -> "text_41"+    S42 -> "text_42"+    S43 -> "text_43"+    S44 -> "text_44"+    S45 -> "text_45"+    S46 -> "text_46"+    S47 -> "text_47"+    S48 -> "text_48"+    S49 -> "text_49"+    S50 -> "text_50"
hpqtypes-extras.cabal view
@@ -1,6 +1,6 @@-cabal-version:       2.2+cabal-version:       3.0 name:                hpqtypes-extras-version:             1.16.4.4+version:             1.17.0.0 synopsis:            Extra utilities for hpqtypes library description:         The following extras for hpqtypes library:                      .@@ -20,7 +20,7 @@ copyright:           Scrive AB category:            Database build-type:          Simple-tested-with:         GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.8 || ==9.4.6 || ==9.6.2+tested-with:         GHC == { 8.10.7, 9.0.2, 9.2.8, 9.4.8, 9.6.6, 9.8.4, 9.10.1, 9.12.1 }  Source-repository head   Type:     git@@ -33,6 +33,7 @@                     , ExistentialQuantification                     , FlexibleContexts                     , GeneralizedNewtypeDeriving+                    , ImportQualifiedPost                     , LambdaCase                     , MultiWayIf                     , OverloadedStrings@@ -45,6 +46,7 @@                     , TypeFamilies                     , UndecidableInstances                     , ViewPatterns+  ghc-options: -Werror=prepositive-qualified-module  library   import: common-stanza@@ -62,6 +64,7 @@                  , Database.PostgreSQL.PQTypes.Model.ColumnType                  , Database.PostgreSQL.PQTypes.Model.CompositeType                  , Database.PostgreSQL.PQTypes.Model.Domain+                 , Database.PostgreSQL.PQTypes.Model.EnumType                  , Database.PostgreSQL.PQTypes.Model.Extension                  , Database.PostgreSQL.PQTypes.Model.ForeignKey                  , Database.PostgreSQL.PQTypes.Model.Index
src/Database/PostgreSQL/PQTypes/Checks.hs view
@@ -1,1253 +1,1611 @@-module Database.PostgreSQL.PQTypes.Checks (-  -- * Checks-    checkDatabase-  , createTable-  , createDomain--  -- * Options-  , ExtrasOptions(..)-  , defaultExtrasOptions-  , ObjectsValidationMode(..)--  -- * Migrations-  , migrateDatabase-  ) where--import Control.Arrow ((&&&))-import Control.Concurrent (threadDelay)-import Control.Monad-import Control.Monad.Catch-import Control.Monad.Reader-import Data.Int-import Data.Function-import Data.List (partition)-import Data.Maybe-import Data.Monoid.Utils-import Data.Ord (comparing)-import Data.Typeable (cast)-import qualified Data.String-import Data.Text (Text)-import Database.PostgreSQL.PQTypes-import GHC.Stack (HasCallStack)-import Log-import TextShow-import qualified Data.List as L-import qualified Data.Map as M-import qualified Data.Set as S-import qualified Data.Text as T--import Database.PostgreSQL.PQTypes.ExtrasOptions-import Database.PostgreSQL.PQTypes.Checks.Util-import Database.PostgreSQL.PQTypes.Migrate-import Database.PostgreSQL.PQTypes.Model-import Database.PostgreSQL.PQTypes.SQL.Builder-import Database.PostgreSQL.PQTypes.Versions--headExc :: String -> [a] -> a-headExc s []    = error s-headExc _ (x:_) = x---------------------------------------------- | Run migrations and check the database structure.-migrateDatabase-  :: (MonadIO m, MonadDB m, MonadLog m, MonadMask m)-  => ExtrasOptions-  -> [Extension]-  -> [CompositeType]-  -> [Domain]-  -> [Table]-  -> [Migration m]-  -> m ()-migrateDatabase options-  extensions composites domains tables migrations = do-  setDBTimeZoneToUTC-  mapM_ checkExtension extensions-  tablesWithVersions <- getTableVersions (tableVersions : tables)-  -- 'checkDBConsistency' also performs migrations.-  checkDBConsistency options domains tablesWithVersions migrations-  resultCheck =<< checkCompositesStructure tablesWithVersions-                                           CreateCompositesIfDatabaseEmpty-                                           (eoObjectsValidationMode options)-                                           composites-  resultCheck =<< checkDomainsStructure domains-  resultCheck =<< checkDBStructure options tablesWithVersions-  resultCheck =<< checkTablesWereDropped migrations--  when (eoObjectsValidationMode options == DontAllowUnknownObjects) $ do-    resultCheck =<< checkUnknownTables tables-    resultCheck =<< checkExistenceOfVersionsForTables (tableVersions : tables)--  -- After migrations are done make sure the table versions are correct.-  resultCheck . checkVersions options =<< getTableVersions (tableVersions : tables)--  -- everything is OK, commit changes-  commit---- | Run checks on the database structure and whether the database needs to be--- migrated. Will do a full check of DB structure.-checkDatabase-  :: forall m . (MonadDB m, MonadLog m, MonadThrow m)-  => ExtrasOptions-  -> [CompositeType]-  -> [Domain]-  -> [Table]-  -> m ()-checkDatabase options composites domains tables = do-  tablesWithVersions <- getTableVersions (tableVersions : tables)-  resultCheck $ checkVersions options tablesWithVersions-  resultCheck =<< checkCompositesStructure tablesWithVersions-                                           DontCreateComposites-                                           (eoObjectsValidationMode options)-                                           composites-  resultCheck =<< checkDomainsStructure domains-  resultCheck =<< checkDBStructure options tablesWithVersions-  when (eoObjectsValidationMode options == DontAllowUnknownObjects) $ do-    resultCheck =<< checkUnknownTables tables-    resultCheck =<< checkExistenceOfVersionsForTables (tableVersions : tables)--  -- Check initial setups only after database structure is considered-  -- consistent as before that some of the checks may fail internally.-  resultCheck =<< checkInitialSetups tables--  where-    checkInitialSetups :: [Table] -> m ValidationResult-    checkInitialSetups tbls =-      liftM mconcat . mapM checkInitialSetup' $ tbls--    checkInitialSetup' :: Table -> m ValidationResult-    checkInitialSetup' t@Table{..} = case tblInitialSetup of-      Nothing -> return mempty-      Just is -> checkInitialSetup is >>= \case-        True  -> return mempty-        False -> return . validationError $ "Initial setup for table '"-                 <> tblNameText t <> "' is not valid"---- | Return SQL fragment of current catalog within quotes-currentCatalog :: (MonadDB m, MonadThrow m) => m (RawSQL ())-currentCatalog = do-  runSQL_ "SELECT current_catalog::text"-  dbname <- fetchOne runIdentity-  return $ unsafeSQL $ "\"" ++ dbname ++ "\""---- | Check for a given extension. We need to read from 'pg_extension'--- table as Amazon RDS limits usage of 'CREATE EXTENSION IF NOT EXISTS'.-checkExtension :: (MonadDB m, MonadLog m, MonadThrow m) => Extension -> m ()-checkExtension (Extension extension) = do-  logInfo_ $ "Checking for extension '" <> txtExtension <> "'"-  extensionExists <- runQuery01 . sqlSelect "pg_extension" $ do-    sqlResult "TRUE"-    sqlWhereEq "extname" $ unRawSQL extension-  if not extensionExists-    then do-      logInfo_ $ "Creating extension '" <> txtExtension <> "'"-      runSQL_ $ "CREATE EXTENSION IF NOT EXISTS" <+> raw extension-    else logInfo_ $ "Extension '" <> txtExtension <> "' exists"-  where-    txtExtension = unRawSQL extension---- | Check whether the database returns timestamps in UTC, and set the--- timezone to UTC if it doesn't.-setDBTimeZoneToUTC :: (MonadDB m, MonadLog m, MonadThrow m) => m ()-setDBTimeZoneToUTC = do-  runSQL_ "SHOW timezone"-  timezone :: String <- fetchOne runIdentity-  when (timezone /= "UTC") $ do-    dbname <- currentCatalog-    logInfo_ $ "Setting '" <> unRawSQL dbname-      <> "' database to return timestamps in UTC"-    runQuery_ $ "ALTER DATABASE" <+> dbname <+> "SET TIMEZONE = 'UTC'"-    -- Setting the database timezone doesn't change the session timezone.-    runSQL_ $ "SET timezone = 'UTC'"---- | Get the names of all user-defined tables that actually exist in--- the DB.-getDBTableNames :: (MonadDB m) => m [Text]-getDBTableNames = do-  runQuery_ $ sqlSelect "information_schema.tables" $ do-    sqlResult "table_name::text"-    sqlWhere "table_name <> 'table_versions'"-    sqlWhere "table_type = 'BASE TABLE'"-    sqlWhereExists $ sqlSelect "unnest(current_schemas(false)) as cs" $ do-      sqlResult "TRUE"-      sqlWhere "cs = table_schema"--  dbTableNames <- fetchMany runIdentity-  return dbTableNames--checkVersions :: ExtrasOptions -> TablesWithVersions -> ValidationResult-checkVersions options = mconcat . map checkVersion-  where-    checkVersion :: (Table, Int32) -> ValidationResult-    checkVersion (t@Table{..}, v)-      | if eoAllowHigherTableVersions options-        then tblVersion <= v-        else tblVersion == v = mempty-      | v == 0    = validationError $-                    "Table '" <> tblNameText t <> "' must be created"-      | otherwise = validationError $-                    "Table '" <> tblNameText t-                    <> "' must be migrated" <+> showt v <+> "->"-                    <+> showt tblVersion---- | Check that there's a 1-to-1 correspondence between the list of--- 'Table's and what's actually in the database.-checkUnknownTables :: (MonadDB m, MonadLog m) => [Table] -> m ValidationResult-checkUnknownTables tables = do-  dbTableNames  <- getDBTableNames-  let tableNames = map (unRawSQL . tblName) tables-      absent     = dbTableNames L.\\ tableNames-      notPresent = tableNames   L.\\ dbTableNames--  if (not . null $ absent) || (not . null $ notPresent)-    then do-    mapM_ (logInfo_ . (<+>) "Unknown table:") absent-    mapM_ (logInfo_ . (<+>) "Table not present in the database:") notPresent-    return $-      (validateIsNull "Unknown tables:" absent) <>-      (validateIsNull "Tables not present in the database:" notPresent)-    else return mempty--validateIsNull :: Text -> [Text] -> ValidationResult-validateIsNull _   [] = mempty-validateIsNull msg ts = validationError $ msg <+> T.intercalate ", " ts---- | Check that there's a 1-to-1 correspondence between the list of--- 'Table's and what's actually in the table 'table_versions'.-checkExistenceOfVersionsForTables-  :: (MonadDB m, MonadLog m)-  => [Table] -> m ValidationResult-checkExistenceOfVersionsForTables tables = do-  runQuery_ $ sqlSelect "table_versions" $ do-    sqlResult "name::text"-  (existingTableNames :: [Text]) <- fetchMany runIdentity--  let tableNames = map (unRawSQL . tblName) tables-      absent     = existingTableNames L.\\ tableNames-      notPresent = tableNames   L.\\ existingTableNames--  if (not . null $ absent) || (not . null $ notPresent)-    then do-    mapM_ (logInfo_ . (<+>) "Unknown entry in 'table_versions':") absent-    mapM_ (logInfo_ . (<+>) "Table not present in the 'table_versions':")-      notPresent-    return $-      (validateIsNull "Unknown entry in table_versions':"  absent ) <>-      (validateIsNull "Tables not present in the 'table_versions':" notPresent)-    else return mempty---checkDomainsStructure :: (MonadDB m, MonadThrow m)-                      => [Domain] -> m ValidationResult-checkDomainsStructure defs = fmap mconcat . forM defs $ \def -> do-  runQuery_ . sqlSelect "pg_catalog.pg_type t1" $ do-    sqlResult "t1.typname::text" -- name-    sqlResult "(SELECT pg_catalog.format_type(t2.oid, t2.typtypmod) \-              \FROM pg_catalog.pg_type t2 \-              \WHERE t2.oid = t1.typbasetype)" -- type-    sqlResult "NOT t1.typnotnull" -- nullable-    sqlResult "t1.typdefault" -- default value-    sqlResult "ARRAY(SELECT c.conname::text FROM pg_catalog.pg_constraint c \-              \WHERE c.contypid = t1.oid ORDER by c.oid)" -- constraint names-    sqlResult "ARRAY(SELECT regexp_replace(pg_get_constraintdef(c.oid, true), '\-              \CHECK \\((.*)\\)', '\\1') FROM pg_catalog.pg_constraint c \-              \WHERE c.contypid = t1.oid \-              \ORDER by c.oid)" -- constraint definitions-    sqlResult "ARRAY(SELECT c.convalidated FROM pg_catalog.pg_constraint c \-              \WHERE c.contypid = t1.oid \-              \ORDER by c.oid)" -- are constraints validated?-    sqlWhereEq "t1.typname" $ unRawSQL $ domName def-  mdom <- fetchMaybe $-    \(dname, dtype, nullable, defval, cnames, conds, valids) ->-      Domain-      { domName = unsafeSQL dname-      , domType = dtype-      , domNullable = nullable-      , domDefault = unsafeSQL <$> defval-      , domChecks =-          mkChecks $ zipWith3-          (\cname cond validated ->-             Check-             { chkName = unsafeSQL cname-             , chkCondition = unsafeSQL cond-             , chkValidated = validated-             }) (unArray1 cnames) (unArray1 conds) (unArray1 valids)-      }-  return $ case mdom of-    Just dom-      | dom /= def -> topMessage "domain" (unRawSQL $ domName dom) $ mconcat [-          compareAttr dom def "name" domName-        , compareAttr dom def "type" domType-        , compareAttr dom def "nullable" domNullable-        , compareAttr dom def "default" domDefault-        , compareAttr dom def "checks" domChecks-        ]-      | otherwise -> mempty-    Nothing -> validationError $ "Domain '" <> unRawSQL (domName def)-               <> "' doesn't exist in the database"-  where-    compareAttr :: (Eq a, Show a)-                => Domain -> Domain -> Text -> (Domain -> a) -> ValidationResult-    compareAttr dom def attrname attr-      | attr dom == attr def = mempty-      | otherwise            = validationError $-        "Attribute '" <> attrname-        <> "' does not match (database:" <+> T.pack (show $ attr dom)-        <> ", definition:" <+> T.pack (show $ attr def) <> ")"---- | Check that the tables that must have been dropped are actually--- missing from the DB.-checkTablesWereDropped :: (MonadDB m, MonadThrow m) =>-                          [Migration m] -> m ValidationResult-checkTablesWereDropped mgrs = do-  let droppedTableNames = [ mgrTableName mgr-                          | mgr <- mgrs, isDropTableMigration mgr ]-  fmap mconcat . forM droppedTableNames $-    \tblName -> do-      mver <- checkTableVersion (T.unpack . unRawSQL $ tblName)-      return $ if isNothing mver-               then mempty-               else validationError $ "The table '" <> unRawSQL tblName-                    <> "' that must have been dropped"-                    <> " is still present in the database."--data CompositesCreationMode-  = CreateCompositesIfDatabaseEmpty-  | DontCreateComposites-  deriving Eq---- | Check that there is 1 to 1 correspondence between composite types in the--- database and the list of their code definitions.-checkCompositesStructure-  :: MonadDB m-  => TablesWithVersions-  -> CompositesCreationMode-  -> ObjectsValidationMode-  -> [CompositeType]-  -> m ValidationResult-checkCompositesStructure tablesWithVersions ccm ovm compositeList = getDBCompositeTypes >>= \case-  [] | noTablesPresent tablesWithVersions && ccm == CreateCompositesIfDatabaseEmpty -> do-         -- DB is not initialized, create composites if there are any defined.-         mapM_ (runQuery_ . sqlCreateComposite) compositeList-         return mempty-  dbCompositeTypes -> pure $ mconcat-    [ checkNotPresentComposites-    , checkDatabaseComposites-    ]-    where-      compositeMap = M.fromList $-        map ((unRawSQL . ctName) &&& ctColumns) compositeList--      checkNotPresentComposites =-        let notPresent = S.toList $ M.keysSet compositeMap-              S.\\ S.fromList (map (unRawSQL . ctName) dbCompositeTypes)-        in validateIsNull "Composite types not present in the database:" notPresent--      checkDatabaseComposites = mconcat . (`map` dbCompositeTypes) $ \dbComposite ->-        let cname = unRawSQL $ ctName dbComposite-        in case cname `M.lookup` compositeMap of-          Just columns -> topMessage "composite type" cname $-            checkColumns 1 columns (ctColumns dbComposite)-          Nothing -> case ovm of-            AllowUnknownObjects     -> mempty-            DontAllowUnknownObjects -> validationError $ mconcat-              [ "Composite type '"-              , T.pack $ show dbComposite-              , "' from the database doesn't have a corresponding code definition"-              ]-        where-          checkColumns-            :: Int -> [CompositeColumn] -> [CompositeColumn] -> ValidationResult-          checkColumns _ [] [] = mempty-          checkColumns _ rest [] = validationError $-            objectHasLess "Composite type" "columns" rest-          checkColumns _ [] rest = validationError $-            objectHasMore "Composite type" "columns" rest-          checkColumns !n (d:defs) (c:cols) = mconcat [-              validateNames $ ccName d == ccName c-            , validateTypes $ ccType d == ccType c-            , checkColumns (n+1) defs cols-            ]-            where-              validateNames True  = mempty-              validateNames False = validationError $-                errorMsg ("no. " <> showt n) "names" (unRawSQL . ccName)--              validateTypes True  = mempty-              validateTypes False = validationError $-                errorMsg (unRawSQL $ ccName d) "types" (T.pack . show . ccType)--              errorMsg ident attr f =-                "Column '" <> ident <> "' differs in"-                <+> attr <+> "(database:" <+> f c <> ", definition:" <+> f d <> ")."---- | Checks whether the database is consistent.-checkDBStructure-  :: forall m. (MonadDB m, MonadThrow m)-  => ExtrasOptions-  -> TablesWithVersions-  -> m ValidationResult-checkDBStructure options tables = fmap mconcat . forM tables $ \(table, version) -> do-  result <- topMessage "table" (tblNameText table) <$> checkTableStructure table-  -- If we allow higher table versions in the database, show inconsistencies as-  -- info messages only.-  return $ if eoAllowHigherTableVersions options && tblVersion table < version-           then validationErrorsToInfos result-           else result-  where-    checkTableStructure :: Table -> m ValidationResult-    checkTableStructure table@Table{..} = do-      -- get table description from pg_catalog as describeTable-      -- mechanism from HDBC doesn't give accurate results-      runQuery_ $ sqlSelect "pg_catalog.pg_attribute a" $ do-        sqlResult "a.attname::text"-        sqlResult "pg_catalog.format_type(a.atttypid, a.atttypmod)"-        sqlResult . parenthesize . toSQLCommand $-          sqlSelect "pg_catalog.pg_collation c, pg_catalog.pg_type t" $ do-            sqlResult "c.collname::text"-            -- `typcollation` specifies the default collation of the type (if-            -- any), and `attcollation` is the collation of the column.-            sqlWhere "c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation"-        sqlResult "NOT a.attnotnull"-        sqlResult . parenthesize . toSQLCommand $-          sqlSelect "pg_catalog.pg_attrdef d" $ do-            sqlResult "pg_catalog.pg_get_expr(d.adbin, d.adrelid)"-            sqlWhere "d.adrelid = a.attrelid"-            sqlWhere "d.adnum = a.attnum"-            sqlWhere "a.atthasdef"-        sqlWhere "a.attnum > 0"-        sqlWhere "NOT a.attisdropped"-        sqlWhereEqSql "a.attrelid" $ sqlGetTableID table-        sqlOrderBy "a.attnum"-      desc <- fetchMany fetchTableColumn-      -- get info about constraints from pg_catalog-      pk <- sqlGetPrimaryKey table-      runQuery_ $ sqlGetChecks table-      checks <- fetchMany fetchTableCheck-      runQuery_ $ sqlGetIndexes table-      indexes <- fetchMany fetchTableIndex-      runQuery_ $ sqlGetForeignKeys table-      fkeys <- fetchMany fetchForeignKey-      triggers <- getDBTriggers tblName-      return $ mconcat [-          checkColumns 1 tblColumns desc-        , checkPrimaryKey tblPrimaryKey pk-        , checkChecks tblChecks checks-        , checkIndexes tblIndexes indexes-        , checkForeignKeys tblForeignKeys fkeys-        , checkTriggers tblTriggers triggers-        ]-      where-        fetchTableColumn-          :: (String, ColumnType, Maybe Text, Bool, Maybe String) -> TableColumn-        fetchTableColumn (name, ctype, collation, nullable, mdefault) = TableColumn {-            colName = unsafeSQL name-          , colType = ctype-          , colCollation = flip rawSQL () <$> collation-          , colNullable = nullable-          , colDefault = unsafeSQL `liftM` mdefault-          }--        checkColumns-          :: Int -> [TableColumn] -> [TableColumn] -> ValidationResult-        checkColumns _ [] [] = mempty-        checkColumns _ rest [] = validationError $-          objectHasLess "Table" "columns" rest-        checkColumns _ [] rest = validationError $-          objectHasMore "Table" "columns" rest-        checkColumns !n (d:defs) (c:cols) = mconcat [-            validateNames $ colName d == colName c-          -- bigserial == bigint + autoincrement and there is no-          -- distinction between them after table is created.-          , validateTypes $ colType d == colType c ||-            (colType d == BigSerialT && colType c == BigIntT)-          -- There is a problem with default values determined by-          -- sequences as they're implicitly specified by db, so-          -- let's omit them in such case.-          , validateDefaults $ colDefault d == colDefault c ||-            (colDefault d == Nothing-             && ((T.isPrefixOf "nextval('" . unRawSQL) `liftM` colDefault c)-                == Just True)-          , validateNullables $ colNullable d == colNullable c-          , checkColumns (n+1) defs cols-          ]-          where-            validateNames True  = mempty-            validateNames False = validationError $-              errorMsg ("no. " <> showt n) "names" (unRawSQL . colName)--            validateTypes True  = mempty-            validateTypes False = validationError $-              errorMsg cname "types" (T.pack . show . colType)-              <+> sqlHint ("TYPE" <+> columnTypeToSQL (colType d))--            validateNullables True  = mempty-            validateNullables False = validationError $-              errorMsg cname "nullables" (showt . colNullable)-              <+> sqlHint ((if colNullable d then "DROP" else "SET")-                            <+> "NOT NULL")--            validateDefaults True  = mempty-            validateDefaults False = validationError $-              (errorMsg cname "defaults" (showt . fmap unRawSQL . colDefault))-              <+> sqlHint set_default-              where-                set_default = case colDefault d of-                  Just v  -> "SET DEFAULT" <+> v-                  Nothing -> "DROP DEFAULT"--            cname = unRawSQL $ colName d-            errorMsg ident attr f =-              "Column '" <> ident <> "' differs in"-              <+> attr <+> "(table:" <+> f c <> ", definition:" <+> f d <> ")."-            sqlHint sql =-              "(HINT: SQL for making the change is: ALTER TABLE"-              <+> tblNameText table <+> "ALTER COLUMN" <+> unRawSQL (colName d)-              <+> unRawSQL sql <> ")"--        checkPrimaryKey :: Maybe PrimaryKey -> Maybe (PrimaryKey, RawSQL ())-                        -> ValidationResult-        checkPrimaryKey mdef mpk = mconcat [-            checkEquality "PRIMARY KEY" def (map fst pk)-          , checkNames (const (pkName tblName)) pk-          , if (eoEnforcePKs options)-            then checkPKPresence tblName mdef mpk-            else mempty-          ]-          where-            def = maybeToList mdef-            pk = maybeToList mpk--        checkChecks :: [Check] -> [Check] -> ValidationResult-        checkChecks defs checks =-          mapValidationResult id mapErrs (checkEquality "CHECKs" defs checks)-          where-            mapErrs []      = []-            mapErrs errmsgs = errmsgs <>-              [ " (HINT: If checks are equal modulo number of \-                \ parentheses/whitespaces used in conditions, \-                \ just copy and paste expected output into source code)"-              ]--        checkIndexes :: [TableIndex] -> [(TableIndex, RawSQL ())]-                     -> ValidationResult-        checkIndexes defs allIndexes = mconcat-          $ checkEquality "INDEXes" defs (map fst indexes)-          : checkNames (indexName tblName) indexes-          : map localIndexInfo localIndexes-          where-            localIndexInfo (index, name) = validationInfo $ T.concat-              [ "Found a local index '"-              , unRawSQL name-              , "': "-              , T.pack (show index)-              ]--            (localIndexes, indexes) = (`partition` allIndexes) $ \(_, name) ->-              -- Manually created indexes for ad-hoc improvements.-                 "local_" `T.isPrefixOf` unRawSQL name-              -- Indexes related to the REINDEX operation, see-              -- https://www.postgresql.org/docs/15/sql-reindex.html-              || "_ccnew" `T.isSuffixOf` unRawSQL name-              || "_ccold" `T.isSuffixOf` unRawSQL name--        checkForeignKeys :: [ForeignKey] -> [(ForeignKey, RawSQL ())]-                         -> ValidationResult-        checkForeignKeys defs fkeys = mconcat [-            checkEquality "FOREIGN KEYs" defs (map fst fkeys)-          , checkNames (fkName tblName) fkeys-          ]--        checkTriggers :: [Trigger] -> [(Trigger, RawSQL ())] -> ValidationResult-        checkTriggers defs triggers =-          mapValidationResult id mapErrs $ checkEquality "TRIGGERs" defs' triggers-          where-            defs' = map (\t -> (t, triggerFunctionMakeName $ triggerName t)) defs-            mapErrs []      = []-            mapErrs errmsgs = errmsgs <>-              [ "(HINT: If WHEN clauses are equal modulo number of parentheses, whitespace, \-                \case of variables or type casts used in conditions, just copy and paste \-                \expected output into source code.)"-              ]---- | Checks whether database is consistent, performing migrations if--- necessary. Requires all table names to be in lower case.------ The migrations list must have the following properties:---   * consecutive 'mgrFrom' numbers---   * no duplicates---   * all 'mgrFrom' are less than table version number of the table in---     the 'tables' list-checkDBConsistency-  :: forall m. (MonadIO m, MonadDB m, MonadLog m, MonadMask m)-  => ExtrasOptions -> [Domain] -> TablesWithVersions -> [Migration m]-  -> m ()-checkDBConsistency options domains tablesWithVersions migrations = do-  autoTransaction <- tsAutoTransaction <$> getTransactionSettings-  unless autoTransaction $ do-    error "checkDBConsistency: tsAutoTransaction setting needs to be True"-  -- Check the validity of the migrations list.-  validateMigrations-  validateDropTableMigrations--  -- Load version numbers of the tables that actually exist in the DB.-  dbTablesWithVersions <- getDBTableVersions--  if noTablesPresent tablesWithVersions--    -- No tables are present, create everything from scratch.-    then do-      createDBSchema-      initializeDB--    -- Migration mode.-    else do-      -- Additional validity checks for the migrations list.-      validateMigrationsAgainstDB [ (tblName table, tblVersion table, actualVer)-                                  | (table, actualVer) <- tablesWithVersions ]-      validateDropTableMigrationsAgainstDB dbTablesWithVersions-      -- Run migrations, if necessary.-      runMigrations dbTablesWithVersions--  where-    tables = map fst tablesWithVersions--    errorInvalidMigrations :: HasCallStack => [RawSQL ()] -> a-    errorInvalidMigrations tblNames =-      error $ "checkDBConsistency: invalid migrations for tables"-              <+> (L.intercalate ", " $ map (T.unpack . unRawSQL) tblNames)--    checkMigrationsListValidity :: Table -> [Int32] -> [Int32] -> m ()-    checkMigrationsListValidity table presentMigrationVersions-      expectedMigrationVersions = do-      when (presentMigrationVersions /= expectedMigrationVersions) $ do-        logAttention "Migrations are invalid" $ object [-            "table"                       .= tblNameText table-          , "migration_versions"          .= presentMigrationVersions-          , "expected_migration_versions" .= expectedMigrationVersions-          ]-        errorInvalidMigrations [tblName $ table]--    validateMigrations :: m ()-    validateMigrations = forM_ tables $ \table -> do-      -- FIXME: https://github.com/scrive/hpqtypes-extras/issues/73-      let presentMigrationVersions-            = [ mgrFrom | Migration{..} <- migrations-                        , mgrTableName == tblName table ]-          expectedMigrationVersions-            = reverse $ take (length presentMigrationVersions) $-              reverse  [0 .. tblVersion table - 1]-      checkMigrationsListValidity table presentMigrationVersions-        expectedMigrationVersions--    validateDropTableMigrations :: m ()-    validateDropTableMigrations = do-      let droppedTableNames =-            [ mgrTableName $ mgr | mgr <- migrations-                                 , isDropTableMigration mgr ]-          tableNames =-            [ tblName tbl | tbl <- tables ]--      -- Check that the intersection between the 'tables' list and-      -- dropped tables is empty.-      let intersection = L.intersect droppedTableNames tableNames-      when (not . null $ intersection) $ do-          logAttention ("The intersection between tables "-                        <> "and dropped tables is not empty")-            $ object-            [ "intersection" .= map unRawSQL intersection ]-          errorInvalidMigrations [ tblName tbl-                                 | tbl <- tables-                                 , tblName tbl `elem` intersection ]--      -- Check that if a list of migrations for a given table has a-      -- drop table migration, it is unique and is the last migration-      -- in the list.-      let migrationsByTable     = L.groupBy ((==) `on` mgrTableName)-                                  migrations-          dropMigrationLists    = [ mgrs | mgrs <- migrationsByTable-                                         , any isDropTableMigration mgrs ]-          invalidMigrationLists =-            [ mgrs | mgrs <- dropMigrationLists-                   , (not . isDropTableMigration . last $ mgrs) ||-                     (length . filter isDropTableMigration $ mgrs) > 1 ]--      when (not . null $ invalidMigrationLists) $ do-        let tablesWithInvalidMigrationLists =-              [ mgrTableName mgr | mgrs <- invalidMigrationLists-                                 , let mgr = head mgrs ]-        logAttention ("Migration lists for some tables contain "-                      <> "either multiple drop table migrations or "-                      <> "a drop table migration in non-tail position.")-          $ object [ "tables" .=-                     [ unRawSQL tblName-                     | tblName <- tablesWithInvalidMigrationLists ] ]-        errorInvalidMigrations tablesWithInvalidMigrationLists--    createDBSchema :: m ()-    createDBSchema = do-      logInfo_ "Creating domains..."-      mapM_ createDomain domains-      -- Create all tables with no constraints first to allow cyclic references.-      logInfo_ "Creating tables..."-      mapM_ (createTable False) tables-      logInfo_ "Creating table constraints..."-      mapM_ createTableConstraints tables-      logInfo_ "Done."--    initializeDB :: m ()-    initializeDB = do-      logInfo_ "Running initial setup for tables..."-      forM_ tables $ \t -> case tblInitialSetup t of-        Nothing -> return ()-        Just tis -> do-          logInfo_ $ "Initializing" <+> tblNameText t <> "..."-          initialSetup tis-      logInfo_ "Done."--    -- | Input is a list of (table name, expected version, actual-    -- version) triples.-    validateMigrationsAgainstDB :: [(RawSQL (), Int32, Int32)] -> m ()-    validateMigrationsAgainstDB tablesWithVersions_-      = forM_ tablesWithVersions_ $ \(tableName, expectedVer, actualVer) ->-        when (expectedVer /= actualVer) $-        case [ m | m@Migration{..} <- migrations-                 , mgrTableName == tableName ] of-          [] ->-            error $ "checkDBConsistency: no migrations found for table '"-              ++ (T.unpack . unRawSQL $ tableName) ++ "', cannot migrate "-              ++ show actualVer ++ " -> " ++ show expectedVer-          (m:_) | mgrFrom m > actualVer ->-                  error $ "checkDBConsistency: earliest migration for table '"-                    ++ (T.unpack . unRawSQL $ tableName) ++ "' is from version "-                    ++ show (mgrFrom m) ++ ", cannot migrate "-                    ++ show actualVer ++ " -> " ++ show expectedVer-                | otherwise -> return ()--    validateDropTableMigrationsAgainstDB :: [(Text, Int32)] -> m ()-    validateDropTableMigrationsAgainstDB dbTablesWithVersions = do-      let dbTablesToDropWithVersions =-            [ (tblName, mgrFrom mgr, fromJust mver)-            | mgr <- migrations-            , isDropTableMigration mgr-            , let tblName = mgrTableName mgr-            , let mver = lookup (unRawSQL tblName) $ dbTablesWithVersions-            , isJust mver ]-      forM_ dbTablesToDropWithVersions $ \(tblName, fromVer, ver) ->-        when (fromVer /= ver) $-          -- In case when the table we're going to drop is an old-          -- version, check that there are migrations that bring it to-          -- a new one.-          validateMigrationsAgainstDB [(tblName, fromVer, ver)]--    findMigrationsToRun :: [(Text, Int32)] -> [Migration m]-    findMigrationsToRun dbTablesWithVersions =-      let tableNamesToDrop = [ mgrTableName mgr | mgr <- migrations-                                                , isDropTableMigration mgr ]-          droppedEventually :: Migration m -> Bool-          droppedEventually mgr = mgrTableName mgr `elem` tableNamesToDrop--          lookupVer :: Migration m -> Maybe Int32-          lookupVer mgr = lookup (unRawSQL $ mgrTableName mgr)-                          dbTablesWithVersions--          tableDoesNotExist = isNothing . lookupVer--          -- The idea here is that we find the first migration we need-          -- to run and then just run all migrations in order after-          -- that one.-          migrationsToRun' = dropWhile-            (\mgr ->-               case lookupVer mgr of-                 -- Table doesn't exist in the DB. If it's a create-                 -- table migration and we're not going to drop the-                 -- table afterwards, this is our starting point.-                 Nothing -> not $-                            (mgrFrom mgr == 0) &&-                            (not . droppedEventually $ mgr)-                 -- Table exists in the DB. Run only those migrations-                 -- that have mgrFrom >= table version in the DB.-                 Just ver -> not $-                             mgrFrom mgr >= ver)-            migrations--          -- Special case: also include migrations for tables that do-          -- not exist in the DB and ARE going to be dropped if they-          -- come as a consecutive list before the starting point that-          -- we've found.-          ---          -- Case in point: createTable t, doSomethingTo t,-          -- doSomethingTo t1, dropTable t. If our starting point is-          -- 'doSomethingTo t1', and that step depends on 't',-          -- 'doSomethingTo t1' will fail. So we include 'createTable-          -- t' and 'doSomethingTo t' as well.-          l                     = length migrationsToRun'-          initialMigrations     = drop l $ reverse migrations-          additionalMigrations' = takeWhile-            (\mgr -> droppedEventually mgr && tableDoesNotExist mgr)-            initialMigrations-          -- Check that all extra migration chains we've chosen begin-          -- with 'createTable', otherwise skip adding them (to-          -- prevent raising an exception during the validation step).-          additionalMigrations  =-            let ret  = reverse additionalMigrations'-                grps = L.groupBy ((==) `on` mgrTableName) ret-            in if any ((/=) 0 . mgrFrom . head) grps-               then []-               else ret-          -- Also there's no point in adding these extra migrations if-          -- we're not running any migrations to begin with.-          migrationsToRun       = if not . null $ migrationsToRun'-                                  then additionalMigrations ++ migrationsToRun'-                                  else []-      in migrationsToRun--    runMigration :: (Migration m) -> m ()-    runMigration Migration{..} = do-      case mgrAction of-        StandardMigration mgrDo -> do-          logMigration-          mgrDo-          updateTableVersion--        DropTableMigration mgrDropTableMode -> do-          logInfo_ $ arrListTable mgrTableName <> "drop table"-          runQuery_ $ sqlDropTable mgrTableName-            mgrDropTableMode-          runQuery_ $ sqlDelete "table_versions" $ do-            sqlWhereEq "name" (T.unpack . unRawSQL $ mgrTableName)--        CreateIndexConcurrentlyMigration tname idx -> do-          logMigration-          -- We're in auto transaction mode (as ensured at the beginning of-          -- 'checkDBConsistency'), so we need to issue explicit SQL commit,-          -- because using 'commit' function automatically starts another-          -- transaction. We don't want that as concurrent creation of index-          -- won't run inside a transaction.-          bracket_ (runSQL_ "COMMIT") (runSQL_ "BEGIN") $ do-            -- If migration was run before but creation of an index failed, index-            -- will be left in the database in an inactive state, so when we-            -- rerun, we need to remove it first (see-            -- https://www.postgresql.org/docs/9.6/sql-createindex.html for more-            -- information).-            runQuery_ $ "DROP INDEX CONCURRENTLY IF EXISTS" <+> indexName tname idx-            runQuery_ (sqlCreateIndexConcurrently tname idx)-          updateTableVersion--        DropIndexConcurrentlyMigration tname idx -> do-          logMigration-          -- We're in auto transaction mode (as ensured at the beginning of-          -- 'checkDBConsistency'), so we need to issue explicit SQL commit,-          -- because using 'commit' function automatically starts another-          -- transaction. We don't want that as concurrent dropping of index-          -- won't run inside a transaction.-          bracket_ (runSQL_ "COMMIT") (runSQL_ "BEGIN") $ do-            runQuery_ (sqlDropIndexConcurrently tname idx)-          updateTableVersion--        ModifyColumnMigration tableName cursorSql updateSql batchSize -> do-          logMigration-          when (batchSize < 1000) $ do-            error "Batch size cannot be less than 1000"-          withCursorSQL "migration_cursor" NoScroll Hold cursorSql $ \cursor -> do-            -- Vacuum should be done approximately once every 5% of the table-            -- has been updated, or every 1000 rows as a minimum.-            ---            -- In PostgreSQL, when a record is updated, a new version of this-            -- record is created. The old one is destroyed by the "vacuum"-            -- command when no transaction needs it anymore. So there's an-            -- autovacuum daemon whose purpose is to do this cleanup, and that-            -- is sufficient most of the time. We assume that it's tuned to try-            -- to keep the "bloat" (dead records) at around 10% of the table-            -- size in the environment, and it's also tuned to not saturate the-            -- server with IO operations while doing the vacuum - vacuuming is-            -- IO intensive as there are a lot of reads and rewrites, which-            -- makes it slow and costly. So, autovacuum wouldn't be able to keep-            -- up with the aggressive batch update. Therefore we need to run-            -- vacuum ourselves, to keep things in check. The 5% limit is-            -- arbitrary, but a reasonable ballpark estimate: it more or less-            -- makes sure we keep dead records in the 10% envelope and the table-            -- doesn't grow too much during the operation.-            vacuumThreshold <- max 1000 . fromIntegral . (`div` 20) <$> getRowEstimate tableName-            let cursorLoop processed = do-                  cursorFetch_ cursor (CD_Forward batchSize)-                  primaryKeys <- fetchMany id-                  unless (null primaryKeys) $ do-                    updateSql primaryKeys-                    if processed + batchSize >= vacuumThreshold-                    then do-                      bracket_ (runSQL_ "COMMIT")-                               (runSQL_ "BEGIN")-                               (runQuery_ $ "VACUUM" <+> tableName)-                      cursorLoop 0-                    else do-                      commit-                      cursorLoop (processed + batchSize)-            cursorLoop 0-          updateTableVersion--      where-        logMigration = do-          logInfo_ $ arrListTable mgrTableName-            <> showt mgrFrom <+> "->" <+> showt (succ mgrFrom)--        updateTableVersion = do-          runQuery_ $ sqlUpdate "table_versions" $ do-            sqlSet "version"  (succ mgrFrom)-            sqlWhereEq "name" (T.unpack . unRawSQL $ mgrTableName)--        -- Get the estimated number of rows of the given table. It might not-        -- work properly if the table is present in multiple database schemas.-        -- See https://wiki.postgresql.org/wiki/Count_estimate.-        getRowEstimate :: MonadDB m => RawSQL () -> m Int32-        getRowEstimate tableName = do-          runQuery_ . sqlSelect "pg_class" $ do-            sqlResult "reltuples::integer"-            sqlWhereEq "relname" $ unRawSQL tableName-          fetchOne runIdentity--    runMigrations :: [(Text, Int32)] -> m ()-    runMigrations dbTablesWithVersions = do-      let migrationsToRun = findMigrationsToRun dbTablesWithVersions-      validateMigrationsToRun migrationsToRun dbTablesWithVersions-      when (not . null $ migrationsToRun) $ do-        logInfo_ "Running migrations..."-        forM_ migrationsToRun $ \mgr -> fix $ \loop -> do-          let restartMigration query = do-                logAttention "Failed to acquire a lock" $ object ["query" .= query]-                logInfo_ "Restarting the migration shortly..."-                liftIO $ threadDelay 1000000-                loop-          handleJust lockNotAvailable restartMigration $ do-            forM_ (eoLockTimeoutMs options) $ \lockTimeout -> do-              runSQL_ $ "SET LOCAL lock_timeout TO" <+> intToSQL lockTimeout-            runMigration mgr `onException` rollback-            logInfo_ $ "Committing migration changes..."-            commit-        logInfo_ "Running migrations... done."-      where-        intToSQL :: Int -> SQL-        intToSQL = unsafeSQL . show--        lockNotAvailable :: DBException -> Maybe String-        lockNotAvailable DBException{..}-          | Just DetailedQueryError{..} <- cast dbeError-          , qeErrorCode == LockNotAvailable = Just $ show dbeQueryContext-          | otherwise                       = Nothing--    validateMigrationsToRun :: [Migration m] -> [(Text, Int32)] -> m ()-    validateMigrationsToRun migrationsToRun dbTablesWithVersions = do--      let migrationsToRunGrouped :: [[Migration m]]-          migrationsToRunGrouped =-            L.groupBy ((==) `on` mgrTableName) .-            L.sortBy (comparing mgrTableName) $ -- NB: stable sort-            migrationsToRun--          loc_common = "Database.PostgreSQL.PQTypes.Checks."-            ++ "checkDBConsistency.validateMigrationsToRun"--          lookupDBTableVer :: [Migration m] -> Maybe Int32-          lookupDBTableVer mgrGroup =-            lookup (unRawSQL . mgrTableName . headExc head_err-                    $ mgrGroup) dbTablesWithVersions-            where-              head_err = loc_common ++ ".lookupDBTableVer: broken invariant"--          groupsWithWrongDBTableVersions :: [([Migration m], Int32)]-          groupsWithWrongDBTableVersions =-            [ (mgrGroup, dbTableVer)-            | mgrGroup <- migrationsToRunGrouped-            , let dbTableVer = fromMaybe 0 $ lookupDBTableVer mgrGroup-            , dbTableVer /= (mgrFrom . headExc head_err $ mgrGroup)-            ]-            where-              head_err = loc_common-                ++ ".groupsWithWrongDBTableVersions: broken invariant"--          mgrGroupsNotInDB :: [[Migration m]]-          mgrGroupsNotInDB =-            [ mgrGroup-            | mgrGroup <- migrationsToRunGrouped-            , isNothing $ lookupDBTableVer mgrGroup-            ]--          groupsStartingWithDropTable :: [[Migration m]]-          groupsStartingWithDropTable =-            [ mgrGroup-            | mgrGroup <- mgrGroupsNotInDB-            , isDropTableMigration . headExc head_err $ mgrGroup-            ]-            where-              head_err = loc_common-                ++ ".groupsStartingWithDropTable: broken invariant"--          groupsNotStartingWithCreateTable :: [[Migration m]]-          groupsNotStartingWithCreateTable =-            [ mgrGroup-            | mgrGroup <- mgrGroupsNotInDB-            , mgrFrom (headExc head_err mgrGroup) /= 0-            ]-            where-              head_err = loc_common-                ++ ".groupsNotStartingWithCreateTable: broken invariant"--          tblNames :: [[Migration m]] -> [RawSQL ()]-          tblNames grps =-            [ mgrTableName . headExc head_err $ grp | grp <- grps ]-            where-              head_err = loc_common ++ ".tblNames: broken invariant"--      when (not . null $ groupsWithWrongDBTableVersions) $ do-        let tnms = tblNames . map fst $ groupsWithWrongDBTableVersions-        logAttention-          ("There are migration chains selected for execution "-            <> "that expect a different starting table version number "-            <> "from the one in the database. "-            <> "This likely means that the order of migrations is wrong.")-          $ object [ "tables" .= map unRawSQL tnms ]-        errorInvalidMigrations tnms--      when (not . null $ groupsStartingWithDropTable) $ do-        let tnms = tblNames groupsStartingWithDropTable-        logAttention "There are drop table migrations for non-existing tables."-          $ object [ "tables" .= map unRawSQL tnms ]-        errorInvalidMigrations tnms--      -- NB: the following check can break if we allow renaming tables.-      when (not . null $ groupsNotStartingWithCreateTable) $ do-        let tnms = tblNames groupsNotStartingWithCreateTable-        logAttention-          ("Some tables haven't been created yet, but" <>-            "their migration lists don't start with a create table migration.")-          $ object [ "tables" .=  map unRawSQL tnms ]-        errorInvalidMigrations tnms---- | Type synonym for a list of tables along with their database versions.-type TablesWithVersions = [(Table, Int32)]---- | Associate each table in the list with its version as it exists in--- the DB, or 0 if it's missing from the DB.-getTableVersions :: (MonadDB m, MonadThrow m) => [Table] -> m TablesWithVersions-getTableVersions tbls =-  sequence-  [ (\mver -> (tbl, fromMaybe 0 mver)) <$> checkTableVersion (tblNameString tbl)-  | tbl <- tbls ]---- | Given a result of 'getTableVersions' check if no tables are present in the--- database.-noTablesPresent :: TablesWithVersions -> Bool-noTablesPresent = all ((==) 0 . snd)---- | Like 'getTableVersions', but for all user-defined tables that--- actually exist in the DB.-getDBTableVersions :: (MonadDB m, MonadThrow m) => m [(Text, Int32)]-getDBTableVersions = do-  dbTableNames <- getDBTableNames-  sequence-    [ (\mver -> (name, fromMaybe 0 mver)) <$> checkTableVersion (T.unpack name)-    | name <- dbTableNames ]---- | Check whether the table exists in the DB, and return 'Just' its--- version if it does, or 'Nothing' if it doesn't.-checkTableVersion :: (MonadDB m, MonadThrow m) => String -> m (Maybe Int32)-checkTableVersion tblName = do-  doesExist <- runQuery01 . sqlSelect "pg_catalog.pg_class c" $ do-    sqlResult "TRUE"-    sqlLeftJoinOn "pg_catalog.pg_namespace n" "n.oid = c.relnamespace"-    sqlWhereEq "c.relname" $ tblName-    sqlWhere "pg_catalog.pg_table_is_visible(c.oid)"-  if doesExist-    then do-      runQuery_ $ "SELECT version FROM table_versions WHERE name ="-        <?> tblName-      mver <- fetchMaybe runIdentity-      case mver of-        Just ver -> return $ Just ver-        Nothing  -> error $ "checkTableVersion: table '"-          ++ tblName-          ++ "' is present in the database, "-          ++ "but there is no corresponding version info in 'table_versions'."-    else do-      return Nothing---- *** TABLE STRUCTURE ***--sqlGetTableID :: Table -> SQL-sqlGetTableID table = parenthesize . toSQLCommand $-  sqlSelect "pg_catalog.pg_class c" $ do-    sqlResult "c.oid"-    sqlLeftJoinOn "pg_catalog.pg_namespace n" "n.oid = c.relnamespace"-    sqlWhereEq "c.relname" $ tblNameString table-    sqlWhere "pg_catalog.pg_table_is_visible(c.oid)"---- *** PRIMARY KEY ***--sqlGetPrimaryKey-  :: (MonadDB m, MonadThrow m)-  => Table -> m (Maybe (PrimaryKey, RawSQL ()))-sqlGetPrimaryKey table = do--  (mColumnNumbers :: Maybe [Int16]) <- do-    runQuery_ . sqlSelect "pg_catalog.pg_constraint" $ do-      sqlResult "conkey"-      sqlWhereEqSql "conrelid" (sqlGetTableID table)-      sqlWhereEq "contype" 'p'-    fetchMaybe $ unArray1 . runIdentity--  case mColumnNumbers of-    Nothing -> do return Nothing-    Just columnNumbers -> do-      columnNames <- do-        forM columnNumbers $ \k -> do-          runQuery_ . sqlSelect "pk_columns" $ do--            sqlWith "key_series" . sqlSelect "pg_constraint as c2" $ do-              sqlResult "unnest(c2.conkey) as k"-              sqlWhereEqSql "c2.conrelid" $ sqlGetTableID table-              sqlWhereEq "c2.contype" 'p'--            sqlWith "pk_columns" . sqlSelect "key_series" $ do-              sqlJoinOn  "pg_catalog.pg_attribute as a" "a.attnum = key_series.k"-              sqlResult "a.attname::text as column_name"-              sqlResult "key_series.k as column_order"-              sqlWhereEqSql "a.attrelid" $ sqlGetTableID table--            sqlResult "pk_columns.column_name"-            sqlWhereEq "pk_columns.column_order" k--          fetchOne (\(Identity t) -> t :: String)--      runQuery_ . sqlSelect "pg_catalog.pg_constraint as c" $ do-        sqlWhereEq "c.contype" 'p'-        sqlWhereEqSql "c.conrelid" $ sqlGetTableID table-        sqlResult "c.conname::text"-        sqlResult $ Data.String.fromString-          ("array['" <> (mintercalate "', '" columnNames) <> "']::text[]")--      join <$> fetchMaybe fetchPrimaryKey--fetchPrimaryKey :: (String, Array1 String) -> Maybe (PrimaryKey, RawSQL ())-fetchPrimaryKey (name, Array1 columns) = (, unsafeSQL name)-  <$> (pkOnColumns $ map unsafeSQL columns)---- *** CHECKS ***--sqlGetChecks :: Table -> SQL-sqlGetChecks table = toSQLCommand . sqlSelect "pg_catalog.pg_constraint c" $ do-  sqlResult "c.conname::text"-  sqlResult "regexp_replace(pg_get_constraintdef(c.oid, true), \-            \'CHECK \\((.*)\\)', '\\1') AS body" -- check body-  sqlResult "c.convalidated" -- validated?-  sqlWhereEq "c.contype" 'c'-  sqlWhereEqSql "c.conrelid" $ sqlGetTableID table--fetchTableCheck :: (String, String, Bool) -> Check-fetchTableCheck (name, condition, validated) = Check {-  chkName = unsafeSQL name-, chkCondition = unsafeSQL condition-, chkValidated = validated-}---- *** INDEXES ***--sqlGetIndexes :: Table -> SQL-sqlGetIndexes table = toSQLCommand . sqlSelect "pg_catalog.pg_class c" $ do-  sqlResult "c.relname::text" -- index name-  sqlResult $ "ARRAY(" <> selectCoordinates "0" "i.indnkeyatts" <> ")" -- array of key columns in the index-  sqlResult $ "ARRAY(" <> selectCoordinates "i.indnkeyatts" "i.indnatts" <> ")" -- array of included columns in the index-  sqlResult "am.amname::text" -- the method used (btree, gin etc)-  sqlResult "i.indisunique" -- is it unique?-  sqlResult "i.indisvalid"  -- is it valid?-  -- if partial, get constraint def-  sqlResult "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)"-  sqlJoinOn "pg_catalog.pg_index i" "c.oid = i.indexrelid"-  sqlJoinOn "pg_catalog.pg_am am" "c.relam = am.oid"-  sqlLeftJoinOn "pg_catalog.pg_constraint r"-    "r.conrelid = i.indrelid AND r.conindid = i.indexrelid"-  sqlWhereEqSql "i.indrelid" $ sqlGetTableID table-  sqlWhereIsNULL "r.contype" -- fetch only "pure" indexes-  where-    -- Get all coordinates of the index.-    selectCoordinates start end = smconcat [-        "WITH RECURSIVE coordinates(k, name) AS ("-      , "  VALUES (" <> start <> "::integer, NULL)"-      , "  UNION ALL"-      , "    SELECT k+1, pg_catalog.pg_get_indexdef(i.indexrelid, k+1, true)"-      , "      FROM coordinates"-      , "     WHERE k < " <> end-      , ")"-      , "SELECT name FROM coordinates WHERE name IS NOT NULL"-      ]--fetchTableIndex-  :: (String, Array1 String, Array1 String, String, Bool, Bool, Maybe String)-  -> (TableIndex, RawSQL ())-fetchTableIndex (name, Array1 keyColumns, Array1 includeColumns, method, unique, valid, mconstraint) =-  (TableIndex-   { idxColumns = map (indexColumn . unsafeSQL) keyColumns-   , idxInclude = map unsafeSQL includeColumns-   , idxMethod = read method-   , idxUnique = unique-   , idxValid = valid-   , idxWhere = unsafeSQL `liftM` mconstraint-   }-  , unsafeSQL name)---- *** FOREIGN KEYS ***--sqlGetForeignKeys :: Table -> SQL-sqlGetForeignKeys table = toSQLCommand-                          . sqlSelect "pg_catalog.pg_constraint r" $ do-  sqlResult "r.conname::text" -- fk name-  sqlResult $-    "ARRAY(SELECT a.attname::text FROM pg_catalog.pg_attribute a JOIN ("-    <> unnestWithOrdinality "r.conkey"-    <> ") conkeys ON (a.attnum = conkeys.item) \-       \WHERE a.attrelid = r.conrelid \-       \ORDER BY conkeys.n)" -- constrained columns-  sqlResult "c.relname::text" -- referenced table-  sqlResult $ "ARRAY(SELECT a.attname::text \-              \FROM pg_catalog.pg_attribute a JOIN ("-    <> unnestWithOrdinality "r.confkey"-    <> ") confkeys ON (a.attnum = confkeys.item) \-       \WHERE a.attrelid = r.confrelid \-       \ORDER BY confkeys.n)" -- referenced columns-  sqlResult "r.confupdtype" -- on update-  sqlResult "r.confdeltype" -- on delete-  sqlResult "r.condeferrable" -- deferrable?-  sqlResult "r.condeferred" -- initially deferred?-  sqlResult "r.convalidated" -- validated?-  sqlJoinOn "pg_catalog.pg_class c" "c.oid = r.confrelid"-  sqlWhereEqSql "r.conrelid" $ sqlGetTableID table-  sqlWhereEq "r.contype" 'f'-  where-    unnestWithOrdinality :: RawSQL () -> SQL-    unnestWithOrdinality arr =-      "SELECT n, " <> raw arr-      <> "[n] AS item FROM generate_subscripts(" <> raw arr <> ", 1) AS n"--fetchForeignKey ::-  (String, Array1 String, String, Array1 String, Char, Char, Bool, Bool, Bool)-  -> (ForeignKey, RawSQL ())-fetchForeignKey-  ( name, Array1 columns, reftable, Array1 refcolumns-  , on_update, on_delete, deferrable, deferred, validated ) = (ForeignKey {-  fkColumns = map unsafeSQL columns-, fkRefTable = unsafeSQL reftable-, fkRefColumns = map unsafeSQL refcolumns-, fkOnUpdate = charToForeignKeyAction on_update-, fkOnDelete = charToForeignKeyAction on_delete-, fkDeferrable = deferrable-, fkDeferred = deferred-, fkValidated = validated-}, unsafeSQL name)-  where-    charToForeignKeyAction c = case c of-      'a' -> ForeignKeyNoAction-      'r' -> ForeignKeyRestrict-      'c' -> ForeignKeyCascade-      'n' -> ForeignKeySetNull-      'd' -> ForeignKeySetDefault-      _   -> error $ "fetchForeignKey: invalid foreign key action code: "-                     ++ show c+module Database.PostgreSQL.PQTypes.Checks+  ( -- * Definitions+    DatabaseDefinitions (..)+  , emptyDbDefinitions++    -- * Checks+  , checkDatabase+  , checkDatabaseWithReport+  , createTable+  , createDomain++    -- * Options+  , ExtrasOptions (..)+  , defaultExtrasOptions+  , ObjectsValidationMode (..)++    -- * Migrations+  , migrateDatabase++    -- * Internals for tests+  , ValidationResult+  , validationError+  , validationInfo+  ) where++import Control.Arrow ((&&&))+import Control.Concurrent (threadDelay)+import Control.Monad+import Control.Monad.Catch+import Control.Monad.Extra (mconcatMapM)+import Control.Monad.Writer as W+import Data.Foldable (foldMap')+import Data.Function+import Data.Int+import Data.List (partition)+import Data.List qualified as L+import Data.Map qualified as M+import Data.Maybe+import Data.Monoid.Utils+import Data.Set qualified as S+import Data.String qualified+import Data.Text (Text)+import Data.Text qualified as T+import Data.Typeable (cast)+import Database.PostgreSQL.PQTypes+import GHC.Stack (HasCallStack)+import Log+import TextShow++import Database.PostgreSQL.PQTypes.Checks.Util+import Database.PostgreSQL.PQTypes.ExtrasOptions+import Database.PostgreSQL.PQTypes.Migrate+import Database.PostgreSQL.PQTypes.Model+import Database.PostgreSQL.PQTypes.SQL.Builder+import Database.PostgreSQL.PQTypes.Versions++headExc :: String -> [a] -> a+headExc s [] = error s+headExc _ (x : _) = x++data DatabaseDefinitions = DatabaseDefinitions+  { dbExtensions :: [Extension]+  , dbComposites :: [CompositeType]+  , dbEnums :: [EnumType]+  , dbDomains :: [Domain]+  , dbTables :: [Table]+  }++emptyDbDefinitions :: DatabaseDefinitions+emptyDbDefinitions = DatabaseDefinitions [] [] [] [] []++----------------------------------------++-- | Run migrations and check the database structure.+migrateDatabase+  :: (MonadIO m, MonadDB m, MonadLog m, MonadMask m)+  => ExtrasOptions+  -> DatabaseDefinitions+  -> [Migration m]+  -> m ()+migrateDatabase+  options+  DatabaseDefinitions+    { dbExtensions = extensions+    , dbComposites = composites+    , dbEnums = enums+    , dbDomains = domains+    , dbTables = tables+    }+  migrations = do+    setDBTimeZoneToUTC+    mapM_ checkExtension extensions+    tablesWithVersions <- getTableVersions (tableVersions : tables)+    -- 'checkDBConsistency' also performs migrations.+    checkDBConsistency options domains enums tablesWithVersions migrations+    resultCheck+      =<< checkCompositesStructure+        tablesWithVersions+        CreateCompositesIfDatabaseEmpty+        (eoObjectsValidationMode options)+        composites+    resultCheck =<< checkEnumTypes enums+    resultCheck =<< checkDomainsStructure domains+    resultCheck =<< checkDBStructure options tablesWithVersions+    resultCheck =<< checkTablesWereDropped migrations++    when (eoObjectsValidationMode options == DontAllowUnknownObjects) $ do+      resultCheck =<< checkUnknownTables tables+      resultCheck =<< checkExistenceOfVersionsForTables (tableVersions : tables)++    -- After migrations are done make sure the table versions are correct.+    resultCheck . checkVersions options =<< getTableVersions (tableVersions : tables)++    -- everything is OK, commit changes+    commit++-- | Run checks on the database structure and whether the database needs to be+-- migrated. Will do a full check of DB structure.+checkDatabaseWithReport+  :: forall m+   . (MonadDB m, MonadLog m, MonadThrow m)+  => ExtrasOptions+  -> DatabaseDefinitions+  -> m ValidationResult+checkDatabaseWithReport+  options+  DatabaseDefinitions+    { dbExtensions = _+    , dbComposites = composites+    , dbEnums = enums+    , dbDomains = domains+    , dbTables = tables+    } = execWriterT $ do+    (_, report) <- W.listen $ do+      tablesWithVersions <- getTableVersions (tableVersions : tables)+      tell $ checkVersions options tablesWithVersions+      tell+        =<< checkCompositesStructure+          tablesWithVersions+          DontCreateComposites+          (eoObjectsValidationMode options)+          composites+      tell =<< checkEnumTypes enums+      tell =<< checkDomainsStructure domains+      tell =<< checkDBStructure options tablesWithVersions+      when (eoObjectsValidationMode options == DontAllowUnknownObjects) $ do+        tell =<< checkUnknownTables tables+        tell =<< checkExistenceOfVersionsForTables (tableVersions : tables)++    -- Check initial setups only after database structure is considered+    -- consistent as before that some of the checks may fail internally.+    unless (resultHasErrors report) $+      tell =<< lift (checkInitialSetups tables)+    where+      checkInitialSetups :: [Table] -> m ValidationResult+      checkInitialSetups = mconcatMapM checkInitialSetupForTable++      checkInitialSetupForTable table = case tblInitialSetup table of+        Nothing -> return mempty+        Just setup ->+          checkInitialSetup setup >>= \case+            True -> return mempty+            False ->+              return . validationError $+                "Initial setup for table '"+                  <> tblNameText table+                  <> "' is not valid"++-- | An equivalent to `checkDatabaseWithReport opts dbDefs >>= resultCheck`.+checkDatabase+  :: forall m+   . (MonadDB m, MonadLog m, MonadThrow m)+  => ExtrasOptions+  -> DatabaseDefinitions+  -> m ()+checkDatabase options dbDefinitions =+  checkDatabaseWithReport options dbDefinitions >>= resultCheck++-- | Return SQL fragment of current catalog within quotes+currentCatalog :: (MonadDB m, MonadThrow m) => m (RawSQL ())+currentCatalog = do+  runSQL_ "SELECT current_catalog::text"+  dbname <- fetchOne runIdentity+  return $ unsafeSQL $ "\"" ++ dbname ++ "\""++-- | Check for a given extension. We need to read from 'pg_extension'+-- table as Amazon RDS limits usage of 'CREATE EXTENSION IF NOT EXISTS'.+checkExtension :: (MonadDB m, MonadLog m, MonadThrow m) => Extension -> m ()+checkExtension (Extension extension) = do+  logInfo_ $ "Checking for extension '" <> txtExtension <> "'"+  extensionExists <- runQuery01 . sqlSelect "pg_extension" $ do+    sqlResult "TRUE"+    sqlWhereEq "extname" $ unRawSQL extension+  if not extensionExists+    then do+      logInfo_ $ "Creating extension '" <> txtExtension <> "'"+      runSQL_ $ "CREATE EXTENSION IF NOT EXISTS" <+> raw extension+    else logInfo_ $ "Extension '" <> txtExtension <> "' exists"+  where+    txtExtension = unRawSQL extension++-- | Check whether the database returns timestamps in UTC, and set the+-- timezone to UTC if it doesn't.+setDBTimeZoneToUTC :: (MonadDB m, MonadLog m, MonadThrow m) => m ()+setDBTimeZoneToUTC = do+  runSQL_ "SHOW timezone"+  timezone :: String <- fetchOne runIdentity+  when (timezone /= "UTC") $ do+    dbname <- currentCatalog+    logInfo_ $+      "Setting '"+        <> unRawSQL dbname+        <> "' database to return timestamps in UTC"+    runQuery_ $ "ALTER DATABASE" <+> dbname <+> "SET TIMEZONE = 'UTC'"+    -- Setting the database timezone doesn't change the session timezone.+    runSQL_ "SET timezone = 'UTC'"++-- | Get the names of all user-defined tables that actually exist in+-- the DB.+getDBTableNames :: MonadDB m => m [Text]+getDBTableNames = do+  runQuery_ $ sqlSelect "information_schema.tables" $ do+    sqlResult "table_name::text"+    sqlWhere "table_name <> 'table_versions'"+    sqlWhere "table_type = 'BASE TABLE'"+    sqlWhereExists $ sqlSelect "unnest(current_schemas(false)) as cs" $ do+      sqlResult "TRUE"+      sqlWhere "cs = table_schema"+  fetchMany runIdentity++checkVersions :: ExtrasOptions -> TablesWithVersions -> ValidationResult+checkVersions options = mconcat . map checkVersion+  where+    checkVersion :: (Table, Int32) -> ValidationResult+    checkVersion (t@Table {..}, v)+      | if eoAllowHigherTableVersions options+          then tblVersion <= v+          else tblVersion == v =+          mempty+      | v == 0 =+          validationError $+            "Table '" <> tblNameText t <> "' must be created"+      | otherwise =+          validationError $+            "Table '"+              <> tblNameText t+              <> "' must be migrated"+              <+> showt v+              <+> "->"+              <+> showt tblVersion++-- | Check that there's a 1-to-1 correspondence between the list of+-- 'Table's and what's actually in the database.+checkUnknownTables :: (MonadDB m, MonadLog m) => [Table] -> m ValidationResult+checkUnknownTables tables = do+  dbTableNames <- getDBTableNames+  let tableNames = map (unRawSQL . tblName) tables+      absent = dbTableNames L.\\ tableNames+      notPresent = tableNames L.\\ dbTableNames++  if (not . null $ absent) || (not . null $ notPresent)+    then do+      mapM_ (logInfo_ . (<+>) "Unknown table:") absent+      mapM_ (logInfo_ . (<+>) "Table not present in the database:") notPresent+      return $+        validateIsNull "Unknown tables:" absent+          <> validateIsNull "Tables not present in the database:" notPresent+    else return mempty++validateIsNull :: Text -> [Text] -> ValidationResult+validateIsNull _ [] = mempty+validateIsNull msg ts = validationError $ msg <+> T.intercalate ", " ts++-- | Check that there's a 1-to-1 correspondence between the list of+-- 'Table's and what's actually in the table 'table_versions'.+checkExistenceOfVersionsForTables+  :: (MonadDB m, MonadLog m)+  => [Table]+  -> m ValidationResult+checkExistenceOfVersionsForTables tables = do+  runQuery_ $ sqlSelect "table_versions" $ do+    sqlResult "name::text"+  (existingTableNames :: [Text]) <- fetchMany runIdentity++  let tableNames = map (unRawSQL . tblName) tables+      absent = existingTableNames L.\\ tableNames+      notPresent = tableNames L.\\ existingTableNames++  if (not . null $ absent) || (not . null $ notPresent)+    then do+      mapM_ (logInfo_ . (<+>) "Unknown entry in 'table_versions':") absent+      mapM_+        (logInfo_ . (<+>) "Table not present in the 'table_versions':")+        notPresent+      return $+        validateIsNull "Unknown entry in table_versions':" absent+          <> validateIsNull "Tables not present in the 'table_versions':" notPresent+    else return mempty++checkDomainsStructure+  :: (MonadDB m, MonadThrow m)+  => [Domain]+  -> m ValidationResult+checkDomainsStructure defs = fmap mconcat . forM defs $ \def -> do+  runQuery_ . sqlSelect "pg_catalog.pg_type t1" $ do+    sqlResult "t1.typname::text" -- name+    sqlResult+      "(SELECT pg_catalog.format_type(t2.oid, t2.typtypmod) \+      \FROM pg_catalog.pg_type t2 \+      \WHERE t2.oid = t1.typbasetype)" -- type+    sqlResult "NOT t1.typnotnull" -- nullable+    sqlResult "t1.typdefault" -- default value+    sqlResult+      "ARRAY(SELECT c.conname::text FROM pg_catalog.pg_constraint c \+      \WHERE c.contypid = t1.oid ORDER by c.oid)" -- constraint names+    sqlResult+      "ARRAY(SELECT regexp_replace(pg_get_constraintdef(c.oid, true), '\+      \CHECK \\((.*)\\)', '\\1') FROM pg_catalog.pg_constraint c \+      \WHERE c.contypid = t1.oid \+      \ORDER by c.oid)" -- constraint definitions+    sqlResult+      "ARRAY(SELECT c.convalidated FROM pg_catalog.pg_constraint c \+      \WHERE c.contypid = t1.oid \+      \ORDER by c.oid)" -- are constraints validated?+    sqlWhereEq "t1.typname" $ unRawSQL $ domName def+  mdom <- fetchMaybe $+    \(dname, dtype, nullable, defval, cnames, conds, valids) ->+      Domain+        { domName = unsafeSQL dname+        , domType = dtype+        , domNullable = nullable+        , domDefault = unsafeSQL <$> defval+        , domChecks =+            mkChecks $+              zipWith3+                ( \cname cond validated ->+                    Check+                      { chkName = unsafeSQL cname+                      , chkCondition = unsafeSQL cond+                      , chkValidated = validated+                      }+                )+                (unArray1 cnames)+                (unArray1 conds)+                (unArray1 valids)+        }+  return $ case mdom of+    Just dom+      | dom /= def ->+          topMessage "domain" (unRawSQL $ domName dom) $+            mconcat+              [ compareAttr dom def "name" domName+              , compareAttr dom def "type" domType+              , compareAttr dom def "nullable" domNullable+              , compareAttr dom def "default" domDefault+              , compareAttr dom def "checks" domChecks+              ]+      | otherwise -> mempty+    Nothing ->+      validationError $+        "Domain '"+          <> unRawSQL (domName def)+          <> "' doesn't exist in the database"+  where+    compareAttr+      :: (Eq a, Show a)+      => Domain+      -> Domain+      -> Text+      -> (Domain -> a)+      -> ValidationResult+    compareAttr dom def attrname attr+      | attr dom == attr def = mempty+      | otherwise =+          validationError $+            "Attribute '"+              <> attrname+              <> "' does not match (database:"+              <+> T.pack (show $ attr dom)+              <> ", definition:"+              <+> T.pack (show $ attr def)+              <> ")"++checkEnumTypes+  :: (MonadDB m, MonadThrow m)+  => [EnumType]+  -> m ValidationResult+checkEnumTypes defs = fmap mconcat . forM defs $ \defEnum -> do+  runQuery_ . sqlSelect "pg_catalog.pg_type t" $ do+    sqlResult "t.typname::text" -- name+    sqlResult+      "ARRAY(SELECT e.enumlabel::text FROM pg_catalog.pg_enum e WHERE e.enumtypid = t.oid ORDER BY e.enumsortorder)" -- values+    sqlWhereEq "t.typname" $ unRawSQL $ etName defEnum+  enum <- fetchMaybe $+    \(enumName, enumValues) ->+      EnumType+        { etName = unsafeSQL enumName+        , etValues = map unsafeSQL $ unArray1 enumValues+        }+  pure $ case enum of+    Just dbEnum -> do+      let enumName = unRawSQL $ etName defEnum+          dbValues = map unRawSQL $ etValues dbEnum+          defValues = map unRawSQL $ etValues defEnum+          dbSet = S.fromList dbValues+          defSet = S.fromList defValues+      if+        | dbValues == defValues -> mempty+        | L.sort dbValues == L.sort defValues ->+            validationInfo $+              "Enum '"+                <> enumName+                <> "' has same values, but differs in order (database: "+                <> T.pack (show dbValues)+                <> ", definition: "+                <> T.pack (show defValues)+                <> "). "+                <> "This isn't usually a problem, unless the enum is used for ordering."+        | S.isSubsetOf defSet dbSet ->+            validationInfo $+              "Enum '"+                <> enumName+                <> "' has all necessary values, but the database has additional ones "+                <> "(database: "+                <> T.pack (show dbValues)+                <> ", definition: "+                <> T.pack (show defValues)+                <> ")"+        | otherwise ->+            validationError $+              "Enum '"+                <> enumName+                <> "' does not match (database: "+                <> T.pack (show dbValues)+                <> ", definition: "+                <> T.pack (show defValues)+                <> ")"+    Nothing ->+      validationError $+        "Enum '"+          <> unRawSQL (etName defEnum)+          <> "' doesn't exist in the database"++-- | Check that the tables that must have been dropped are actually+-- missing from the DB.+checkTablesWereDropped+  :: (MonadDB m, MonadThrow m)+  => [Migration m]+  -> m ValidationResult+checkTablesWereDropped mgrs = do+  let droppedTableNames =+        [ mgrTableName mgr+        | mgr <- mgrs+        , isDropTableMigration mgr+        ]+  fmap mconcat . forM droppedTableNames $+    \tblName -> do+      mver <- checkTableVersion (T.unpack . unRawSQL $ tblName)+      return $+        if isNothing mver+          then mempty+          else+            validationError $+              "The table '"+                <> unRawSQL tblName+                <> "' that must have been dropped"+                <> " is still present in the database."++data CompositesCreationMode+  = CreateCompositesIfDatabaseEmpty+  | DontCreateComposites+  deriving (Eq)++-- | Check that there is 1 to 1 correspondence between composite types in the+-- database and the list of their code definitions.+checkCompositesStructure+  :: MonadDB m+  => TablesWithVersions+  -> CompositesCreationMode+  -> ObjectsValidationMode+  -> [CompositeType]+  -> m ValidationResult+checkCompositesStructure tablesWithVersions ccm ovm compositeList =+  getDBCompositeTypes >>= \case+    [] | noTablesPresent tablesWithVersions && ccm == CreateCompositesIfDatabaseEmpty -> do+      -- DB is not initialized, create composites if there are any defined.+      mapM_ (runQuery_ . sqlCreateComposite) compositeList+      return mempty+    dbCompositeTypes ->+      pure $+        mconcat+          [ checkNotPresentComposites+          , checkDatabaseComposites+          ]+      where+        compositeMap =+          M.fromList $+            map ((unRawSQL . ctName) &&& ctColumns) compositeList++        checkNotPresentComposites =+          let notPresent =+                S.toList $+                  M.keysSet compositeMap+                    S.\\ S.fromList (map (unRawSQL . ctName) dbCompositeTypes)+          in validateIsNull "Composite types not present in the database:" notPresent++        checkDatabaseComposites = mconcat . (`map` dbCompositeTypes) $ \dbComposite ->+          let cname = unRawSQL $ ctName dbComposite+          in case cname `M.lookup` compositeMap of+              Just columns ->+                topMessage "composite type" cname $+                  checkColumns 1 columns (ctColumns dbComposite)+              Nothing -> case ovm of+                AllowUnknownObjects -> mempty+                DontAllowUnknownObjects ->+                  validationError $+                    mconcat+                      [ "Composite type '"+                      , T.pack $ show dbComposite+                      , "' from the database doesn't have a corresponding code definition"+                      ]+          where+            checkColumns+              :: Int -> [CompositeColumn] -> [CompositeColumn] -> ValidationResult+            checkColumns _ [] [] = mempty+            checkColumns _ rest [] =+              validationError $+                objectHasLess "Composite type" "columns" rest+            checkColumns _ [] rest =+              validationError $+                objectHasMore "Composite type" "columns" rest+            checkColumns !n (d : defs) (c : cols) =+              mconcat+                [ validateNames $ ccName d == ccName c+                , validateTypes $ ccType d == ccType c+                , checkColumns (n + 1) defs cols+                ]+              where+                validateNames True = mempty+                validateNames False =+                  validationError $+                    errorMsg ("no. " <> showt n) "names" (unRawSQL . ccName)++                validateTypes True = mempty+                validateTypes False =+                  validationError $+                    errorMsg (unRawSQL $ ccName d) "types" (T.pack . show . ccType)++                errorMsg ident attr f =+                  "Column '"+                    <> ident+                    <> "' differs in"+                    <+> attr+                    <+> "(database:"+                    <+> f c+                    <> ", definition:"+                    <+> f d+                    <> ")."++-- | Checks whether the database is consistent.+checkDBStructure+  :: forall m+   . (MonadDB m, MonadThrow m)+  => ExtrasOptions+  -> TablesWithVersions+  -> m ValidationResult+checkDBStructure options tables = fmap mconcat . forM tables $ \(table, version) -> do+  result <- topMessage "table" (tblNameText table) <$> checkTableStructure table+  -- If we allow higher table versions in the database, show inconsistencies as+  -- info messages only.+  return $+    if eoAllowHigherTableVersions options && tblVersion table < version+      then validationErrorsToInfos result+      else result+  where+    checkTableStructure :: Table -> m ValidationResult+    checkTableStructure table@Table {..} = do+      -- get table description from pg_catalog as describeTable+      -- mechanism from HDBC doesn't give accurate results+      runQuery_ $ sqlSelect "pg_catalog.pg_attribute a" $ do+        sqlResult "a.attname::text"+        sqlResult "pg_catalog.format_type(a.atttypid, a.atttypmod)"+        sqlResult . parenthesize . toSQLCommand $+          sqlSelect "pg_catalog.pg_collation c, pg_catalog.pg_type t" $ do+            sqlResult "c.collname::text"+            -- `typcollation` specifies the default collation of the type (if+            -- any), and `attcollation` is the collation of the column.+            sqlWhere "c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation"+        sqlResult "NOT a.attnotnull"+        sqlResult . parenthesize . toSQLCommand $+          sqlSelect "pg_catalog.pg_attrdef d" $ do+            sqlResult "pg_catalog.pg_get_expr(d.adbin, d.adrelid)"+            sqlWhere "d.adrelid = a.attrelid"+            sqlWhere "d.adnum = a.attnum"+            sqlWhere "a.atthasdef"+        sqlWhere "a.attnum > 0"+        sqlWhere "NOT a.attisdropped"+        sqlWhereEqSql "a.attrelid" $ sqlGetTableID table+        sqlOrderBy "a.attnum"+      desc <- fetchMany fetchTableColumn++      isAbove15 <- checkVersionIsAtLeast15+      -- get info about constraints from pg_catalog+      pk <- sqlGetPrimaryKey table+      runQuery_ $ sqlGetChecks table+      checks <- fetchMany fetchTableCheck+      runQuery_ $ sqlGetIndexes isAbove15 table+      indexes <- fetchMany fetchTableIndex+      runQuery_ $ sqlGetForeignKeys table+      fkeys <- fetchMany fetchForeignKey+      triggers <- getDBTriggers tblName+      checkedOverlaps <- checkOverlappingIndexes tblName+      return $+        mconcat+          [ checkColumns 1 tblColumns desc+          , checkPrimaryKey tblPrimaryKey pk+          , checkChecks tblChecks checks+          , checkIndexes tblIndexes indexes+          , checkForeignKeys tblForeignKeys fkeys+          , checkForeignKeyIndexes tblPrimaryKey tblForeignKeys tblIndexes+          , checkTriggers tblTriggers triggers+          , checkedOverlaps+          ]+      where+        fetchTableColumn+          :: (String, ColumnType, Maybe Text, Bool, Maybe String) -> TableColumn+        fetchTableColumn (name, ctype, collation, nullable, mdefault) =+          TableColumn+            { colName = unsafeSQL name+            , colType = ctype+            , colCollation = flip rawSQL () <$> collation+            , colNullable = nullable+            , colDefault = unsafeSQL <$> mdefault+            }++        checkColumns+          :: Int -> [TableColumn] -> [TableColumn] -> ValidationResult+        checkColumns _ [] [] = mempty+        checkColumns _ rest [] =+          validationError $+            objectHasLess "Table" "columns" rest+        checkColumns _ [] rest =+          validationError $+            objectHasMore "Table" "columns" rest+        checkColumns !n (d : defs) (c : cols) =+          mconcat+            [ validateNames $ colName d == colName c+            , -- bigserial == bigint + autoincrement and there is no+              -- distinction between them after table is created.+              validateTypes $+                colType d == colType c+                  || (colType d == BigSerialT && colType c == BigIntT)+            , -- There is a problem with default values determined by+              -- sequences as they're implicitly specified by db, so+              -- let's omit them in such case.+              validateDefaults $+                colDefault d == colDefault c+                  || ( isNothing (colDefault d)+                        && (T.isPrefixOf "nextval('" . unRawSQL <$> colDefault c)+                          == Just True+                     )+            , validateNullables $ colNullable d == colNullable c+            , checkColumns (n + 1) defs cols+            ]+          where+            validateNames True = mempty+            validateNames False =+              validationError $+                errorMsg ("no. " <> showt n) "names" (unRawSQL . colName)++            validateTypes True = mempty+            validateTypes False =+              validationError $+                errorMsg cname "types" (T.pack . show . colType)+                  <+> sqlHint ("TYPE" <+> columnTypeToSQL (colType d))++            validateNullables True = mempty+            validateNullables False =+              validationError $+                errorMsg cname "nullables" (showt . colNullable)+                  <+> sqlHint+                    ( (if colNullable d then "DROP" else "SET")+                        <+> "NOT NULL"+                    )++            validateDefaults True = mempty+            validateDefaults False =+              validationError $+                errorMsg cname "defaults" (showt . fmap unRawSQL . colDefault)+                  <+> sqlHint set_default+              where+                set_default = case colDefault d of+                  Just v -> "SET DEFAULT" <+> v+                  Nothing -> "DROP DEFAULT"++            cname = unRawSQL $ colName d+            errorMsg ident attr f =+              "Column '"+                <> ident+                <> "' differs in"+                <+> attr+                <+> "(table:"+                <+> f c+                <> ", definition:"+                <+> f d+                <> ")."+            sqlHint sql =+              "(HINT: SQL for making the change is: ALTER TABLE"+                <+> tblNameText table+                <+> "ALTER COLUMN"+                <+> unRawSQL (colName d)+                <+> unRawSQL sql+                <> ")"++        checkPrimaryKey+          :: Maybe PrimaryKey+          -> Maybe (PrimaryKey, RawSQL ())+          -> ValidationResult+        checkPrimaryKey mdef mpk =+          mconcat+            [ checkEquality "PRIMARY KEY" def (map fst pk)+            , checkNames (const (pkName tblName)) pk+            , if eoEnforcePKs options+                then checkPKPresence tblName mdef mpk+                else mempty+            ]+          where+            def = maybeToList mdef+            pk = maybeToList mpk++        checkChecks :: [Check] -> [Check] -> ValidationResult+        checkChecks defs checks =+          mapValidationResult id mapErrs (checkEquality "CHECKs" defs checks)+          where+            mapErrs [] = []+            mapErrs errmsgs =+              errmsgs+                <> [ " (HINT: If checks are equal modulo number of \+                     \ parentheses/whitespaces used in conditions, \+                     \ just copy and paste expected output into source code)"+                   ]++        checkIndexes+          :: [TableIndex]+          -> [(TableIndex, RawSQL ())]+          -> ValidationResult+        checkIndexes defs allIndexes =+          mconcat $+            checkEquality "INDEXes" defs (map fst indexes)+              : checkNames (indexName tblName) indexes+              : map localIndexInfo localIndexes+          where+            localIndexInfo (index, name) =+              validationInfo $+                T.concat+                  [ "Found a local index '"+                  , unRawSQL name+                  , "': "+                  , T.pack (show index)+                  ]++            (localIndexes, indexes) = (`partition` allIndexes) $ \(_, name) ->+              -- Manually created indexes for ad-hoc improvements.+              "local_" `T.isPrefixOf` unRawSQL name+                -- Indexes related to the REINDEX operation, see+                -- https://www.postgresql.org/docs/15/sql-reindex.html+                || "_ccnew" `T.isSuffixOf` unRawSQL name+                || "_ccold" `T.isSuffixOf` unRawSQL name++        checkForeignKeys+          :: [ForeignKey]+          -> [(ForeignKey, RawSQL ())]+          -> ValidationResult+        checkForeignKeys defs fkeys =+          mconcat+            [ checkEquality "FOREIGN KEYs" defs (map fst fkeys)+            , checkNames (fkName tblName) fkeys+            ]++        checkForeignKeyIndexes :: Maybe PrimaryKey -> [ForeignKey] -> [TableIndex] -> ValidationResult+        checkForeignKeyIndexes pkey foreignKeys indexes =+          if eoCheckForeignKeysIndexes options+            then foldMap' go foreignKeys+            else mempty+          where+            -- Map index on the given table name to a list of list of names+            -- so that index on a and index on (b, c) becomes [[a], [b, c,]].+            allIndexes :: [[RawSQL ()]]+            allIndexes = fmap (fmap indexColumnName . idxColumns) . filter (isNothing . idxWhere) $ indexes++            allCoverage :: [[RawSQL ()]]+            allCoverage = maybe [] pkColumns pkey : allIndexes+            -- A foreign key is covered if it is a prefix of a list of indices.+            -- So a FK on a is covered by an index on (a, b) but not an index on (b, a).+            coveredFK :: ForeignKey -> [[RawSQL ()]] -> Bool+            coveredFK fk = any (\idx -> fkColumns fk `L.isPrefixOf` idx)++            go :: ForeignKey -> ValidationResult+            go fk =+              let columns = map unRawSQL (fkColumns fk)+              in if coveredFK fk allCoverage+                  then mempty+                  else validationError $ mconcat ["\n  ● Foreign key '(", T.intercalate "," columns, ")' is missing an index"]++        checkTriggers :: [Trigger] -> [(Trigger, RawSQL ())] -> ValidationResult+        checkTriggers defs triggers =+          mapValidationResult id mapErrs $ checkEquality "TRIGGERs" defs' triggers+          where+            defs' = map (\t -> (t, triggerFunctionMakeName $ triggerName t)) defs+            mapErrs [] = []+            mapErrs errmsgs =+              errmsgs+                <> [ "(HINT: If WHEN clauses are equal modulo number of parentheses, whitespace, \+                     \case of variables or type casts used in conditions, just copy and paste \+                     \expected output into source code.)"+                   ]++        checkOverlappingIndexes :: MonadDB m => RawSQL () -> m ValidationResult+        checkOverlappingIndexes tableName =+          if eoCheckOverlappingIndexes options+            then go+            else pure mempty+          where+            go = do+              let handleOverlap (contained, contains) =+                    mconcat+                      [ "\n  ●  Index "+                      , contains+                      , " contains index "+                      , contained+                      ]+              runSQL_ $ checkOverlappingIndexesQuery tableName+              overlaps <- fetchMany handleOverlap+              pure $+                if null overlaps+                  then mempty+                  else validationError . T.unlines $ "Some indexes are overlapping" : overlaps++-- | Checks whether database is consistent, performing migrations if+-- necessary. Requires all table names to be in lower case.+--+-- The migrations list must have the following properties:+--   * consecutive 'mgrFrom' numbers+--   * no duplicates+--   * all 'mgrFrom' are less than table version number of the table in+--     the 'tables' list+checkDBConsistency+  :: forall m+   . (MonadIO m, MonadDB m, MonadLog m, MonadMask m)+  => ExtrasOptions+  -> [Domain]+  -> [EnumType]+  -> TablesWithVersions+  -> [Migration m]+  -> m ()+checkDBConsistency options domains enums tablesWithVersions migrations = do+  autoTransaction <- tsAutoTransaction <$> getTransactionSettings+  unless autoTransaction $ do+    error "checkDBConsistency: tsAutoTransaction setting needs to be True"+  -- Check the validity of the migrations list.+  validateMigrations+  validateDropTableMigrations++  -- Load version numbers of the tables that actually exist in the DB.+  dbTablesWithVersions <- getDBTableVersions++  if noTablesPresent tablesWithVersions+    -- No tables are present, create everything from scratch.+    then do+      createDBSchema+      initializeDB++    -- Migration mode.+    else do+      -- Additional validity checks for the migrations list.+      validateMigrationsAgainstDB+        [ (tblName table, tblVersion table, actualVer)+        | (table, actualVer) <- tablesWithVersions+        ]+      validateDropTableMigrationsAgainstDB dbTablesWithVersions+      -- Run migrations, if necessary.+      runMigrations dbTablesWithVersions+  where+    tables = map fst tablesWithVersions++    errorInvalidMigrations :: HasCallStack => [RawSQL ()] -> a+    errorInvalidMigrations tblNames =+      error $+        "checkDBConsistency: invalid migrations for tables"+          <+> L.intercalate ", " (map (T.unpack . unRawSQL) tblNames)++    checkMigrationsListValidity :: Table -> [Int32] -> [Int32] -> m ()+    checkMigrationsListValidity+      table+      presentMigrationVersions+      expectedMigrationVersions = do+        when (presentMigrationVersions /= expectedMigrationVersions) $ do+          logAttention "Migrations are invalid" $+            object+              [ "table" .= tblNameText table+              , "migration_versions" .= presentMigrationVersions+              , "expected_migration_versions" .= expectedMigrationVersions+              ]+          errorInvalidMigrations [tblName table]++    validateMigrations :: m ()+    validateMigrations = forM_ tables $ \table -> do+      -- FIXME: https://github.com/scrive/hpqtypes-extras/issues/73+      let presentMigrationVersions =+            [ mgrFrom | Migration {..} <- migrations, mgrTableName == tblName table+            ]+          expectedMigrationVersions =+            reverse $+              take (length presentMigrationVersions) $+                reverse [0 .. tblVersion table - 1]+      checkMigrationsListValidity+        table+        presentMigrationVersions+        expectedMigrationVersions++    validateDropTableMigrations :: m ()+    validateDropTableMigrations = do+      let droppedTableNames =+            [ mgrTableName mgr | mgr <- migrations, isDropTableMigration mgr+            ]+          tableNames =+            [tblName tbl | tbl <- tables]++      -- Check that the intersection between the 'tables' list and+      -- dropped tables is empty.+      let intersection = L.intersect droppedTableNames tableNames+      unless (null intersection) $ do+        logAttention+          ( "The intersection between tables "+              <> "and dropped tables is not empty"+          )+          $ object+            ["intersection" .= map unRawSQL intersection]+        errorInvalidMigrations+          [ tblName tbl+          | tbl <- tables+          , tblName tbl `elem` intersection+          ]++      -- Check that if a list of migrations for a given table has a+      -- drop table migration, it is unique and is the last migration+      -- in the list.+      let migrationsByTable =+            L.groupBy+              ((==) `on` mgrTableName)+              migrations+          dropMigrationLists =+            [ mgrs | mgrs <- migrationsByTable, any isDropTableMigration mgrs+            ]+          invalidMigrationLists =+            [ mgrs | mgrs <- dropMigrationLists, (not . isDropTableMigration . last $ mgrs)+                                                  || (length . filter isDropTableMigration $ mgrs) > 1+            ]++      unless (null invalidMigrationLists) $ do+        let tablesWithInvalidMigrationLists =+              [ mgrTableName mgr | mgrs <- invalidMigrationLists, let mgr = head mgrs+              ]+        logAttention+          ( "Migration lists for some tables contain "+              <> "either multiple drop table migrations or "+              <> "a drop table migration in non-tail position."+          )+          $ object+            [ "tables"+                .= [ unRawSQL tblName+                   | tblName <- tablesWithInvalidMigrationLists+                   ]+            ]+        errorInvalidMigrations tablesWithInvalidMigrationLists++    createDBSchema :: m ()+    createDBSchema = do+      logInfo_ "Creating domains..."+      mapM_ createDomain domains+      logInfo_ "Creating enums..."+      mapM_ (runQuery_ . sqlCreateEnum) enums+      -- Create all tables with no constraints first to allow cyclic references.+      logInfo_ "Creating tables..."+      mapM_ (createTable False) tables+      logInfo_ "Creating table constraints..."+      mapM_ createTableConstraints tables+      logInfo_ "Done."++    initializeDB :: m ()+    initializeDB = do+      logInfo_ "Running initial setup for tables..."+      forM_ tables $ \t -> case tblInitialSetup t of+        Nothing -> return ()+        Just tis -> do+          logInfo_ $ "Initializing" <+> tblNameText t <> "..."+          initialSetup tis+      logInfo_ "Done."++    -- \| Input is a list of (table name, expected version, actual+    -- version) triples.+    validateMigrationsAgainstDB :: [(RawSQL (), Int32, Int32)] -> m ()+    validateMigrationsAgainstDB tablesWithVersions_ =+      forM_ tablesWithVersions_ $ \(tableName, expectedVer, actualVer) ->+        when (expectedVer /= actualVer) $+          case [ m | m@Migration {..} <- migrations, mgrTableName == tableName+               ] of+            [] ->+              error $+                "checkDBConsistency: no migrations found for table '"+                  ++ (T.unpack . unRawSQL $ tableName)+                  ++ "', cannot migrate "+                  ++ show actualVer+                  ++ " -> "+                  ++ show expectedVer+            (m : _)+              | mgrFrom m > actualVer ->+                  error $+                    "checkDBConsistency: earliest migration for table '"+                      ++ (T.unpack . unRawSQL $ tableName)+                      ++ "' is from version "+                      ++ show (mgrFrom m)+                      ++ ", cannot migrate "+                      ++ show actualVer+                      ++ " -> "+                      ++ show expectedVer+              | otherwise -> return ()++    validateDropTableMigrationsAgainstDB :: [(Text, Int32)] -> m ()+    validateDropTableMigrationsAgainstDB dbTablesWithVersions = do+      let dbTablesToDropWithVersions =+            [ (tblName, mgrFrom mgr, fromJust mver)+            | mgr <- migrations+            , isDropTableMigration mgr+            , let tblName = mgrTableName mgr+            , let mver = lookup (unRawSQL tblName) dbTablesWithVersions+            , isJust mver+            ]+      forM_ dbTablesToDropWithVersions $ \(tblName, fromVer, ver) ->+        when (fromVer /= ver) $+          -- In case when the table we're going to drop is an old+          -- version, check that there are migrations that bring it to+          -- a new one.+          validateMigrationsAgainstDB [(tblName, fromVer, ver)]++    findMigrationsToRun :: [(Text, Int32)] -> [Migration m]+    findMigrationsToRun dbTablesWithVersions =+      let tableNamesToDrop =+            [ mgrTableName mgr | mgr <- migrations, isDropTableMigration mgr+            ]+          droppedEventually :: Migration m -> Bool+          droppedEventually mgr = mgrTableName mgr `elem` tableNamesToDrop++          lookupVer :: Migration m -> Maybe Int32+          lookupVer mgr =+            lookup+              (unRawSQL $ mgrTableName mgr)+              dbTablesWithVersions++          tableDoesNotExist = isNothing . lookupVer++          -- The idea here is that we find the first migration we need+          -- to run and then just run all migrations in order after+          -- that one.+          migrationsToRun' =+            dropWhile+              ( \mgr ->+                  case lookupVer mgr of+                    -- Table doesn't exist in the DB. If it's a create+                    -- table migration and we're not going to drop the+                    -- table afterwards, this is our starting point.+                    Nothing ->+                      not $+                        (mgrFrom mgr == 0)+                          && (not . droppedEventually $ mgr)+                    -- Table exists in the DB. Run only those migrations+                    -- that have mgrFrom >= table version in the DB.+                    Just ver -> mgrFrom mgr < ver+              )+              migrations++          -- Special case: also include migrations for tables that do+          -- not exist in the DB and ARE going to be dropped if they+          -- come as a consecutive list before the starting point that+          -- we've found.+          --+          -- Case in point: createTable t, doSomethingTo t,+          -- doSomethingTo t1, dropTable t. If our starting point is+          -- 'doSomethingTo t1', and that step depends on 't',+          -- 'doSomethingTo t1' will fail. So we include 'createTable+          -- t' and 'doSomethingTo t' as well.+          l = length migrationsToRun'+          initialMigrations = drop l $ reverse migrations+          additionalMigrations' =+            takeWhile+              (\mgr -> droppedEventually mgr && tableDoesNotExist mgr)+              initialMigrations+          -- Check that all extra migration chains we've chosen begin+          -- with 'createTable', otherwise skip adding them (to+          -- prevent raising an exception during the validation step).+          additionalMigrations =+            let ret = reverse additionalMigrations'+                grps = L.groupBy ((==) `on` mgrTableName) ret+            in if any ((/=) 0 . mgrFrom . head) grps+                then []+                else ret+          -- Also there's no point in adding these extra migrations if+          -- we're not running any migrations to begin with.+          migrationsToRun =+            if not . null $ migrationsToRun'+              then additionalMigrations ++ migrationsToRun'+              else []+      in migrationsToRun++    runMigration :: Migration m -> m ()+    runMigration Migration {..} = do+      case mgrAction of+        StandardMigration mgrDo -> do+          logMigration+          mgrDo+          updateTableVersion+        DropTableMigration mgrDropTableMode -> do+          logInfo_ $ arrListTable mgrTableName <> "drop table"+          runQuery_ $+            sqlDropTable+              mgrTableName+              mgrDropTableMode+          runQuery_ $ sqlDelete "table_versions" $ do+            sqlWhereEq "name" (T.unpack . unRawSQL $ mgrTableName)+        CreateIndexConcurrentlyMigration tname idx -> do+          logMigration+          -- We're in auto transaction mode (as ensured at the beginning of+          -- 'checkDBConsistency'), so we need to issue explicit SQL commit,+          -- because using 'commit' function automatically starts another+          -- transaction. We don't want that as concurrent creation of index+          -- won't run inside a transaction.+          bracket_ (runSQL_ "COMMIT") (runSQL_ "BEGIN") $ do+            -- If migration was run before but creation of an index failed, index+            -- will be left in the database in an inactive state, so when we+            -- rerun, we need to remove it first (see+            -- https://www.postgresql.org/docs/9.6/sql-createindex.html for more+            -- information).+            runQuery_ $ "DROP INDEX CONCURRENTLY IF EXISTS" <+> indexName tname idx+            runQuery_ (sqlCreateIndexConcurrently tname idx)+          updateTableVersion+        DropIndexConcurrentlyMigration tname idx -> do+          logMigration+          -- We're in auto transaction mode (as ensured at the beginning of+          -- 'checkDBConsistency'), so we need to issue explicit SQL commit,+          -- because using 'commit' function automatically starts another+          -- transaction. We don't want that as concurrent dropping of index+          -- won't run inside a transaction.+          bracket_ (runSQL_ "COMMIT") (runSQL_ "BEGIN") $ do+            runQuery_ (sqlDropIndexConcurrently tname idx)+          updateTableVersion+        ModifyColumnMigration tableName cursorSql updateSql batchSize -> do+          logMigration+          when (batchSize < 1000) $ do+            error "Batch size cannot be less than 1000"+          withCursorSQL "migration_cursor" NoScroll Hold cursorSql $ \cursor -> do+            -- Vacuum should be done approximately once every 5% of the table+            -- has been updated, or every 1000 rows as a minimum.+            --+            -- In PostgreSQL, when a record is updated, a new version of this+            -- record is created. The old one is destroyed by the "vacuum"+            -- command when no transaction needs it anymore. So there's an+            -- autovacuum daemon whose purpose is to do this cleanup, and that+            -- is sufficient most of the time. We assume that it's tuned to try+            -- to keep the "bloat" (dead records) at around 10% of the table+            -- size in the environment, and it's also tuned to not saturate the+            -- server with IO operations while doing the vacuum - vacuuming is+            -- IO intensive as there are a lot of reads and rewrites, which+            -- makes it slow and costly. So, autovacuum wouldn't be able to keep+            -- up with the aggressive batch update. Therefore we need to run+            -- vacuum ourselves, to keep things in check. The 5% limit is+            -- arbitrary, but a reasonable ballpark estimate: it more or less+            -- makes sure we keep dead records in the 10% envelope and the table+            -- doesn't grow too much during the operation.+            vacuumThreshold <- max 1000 . fromIntegral . (`div` 20) <$> getRowEstimate tableName+            let cursorLoop processed = do+                  cursorFetch_ cursor (CD_Forward batchSize)+                  primaryKeys <- fetchMany id+                  unless (null primaryKeys) $ do+                    updateSql primaryKeys+                    if processed + batchSize >= vacuumThreshold+                      then do+                        bracket_+                          (runSQL_ "COMMIT")+                          (runSQL_ "BEGIN")+                          (runQuery_ $ "VACUUM" <+> tableName)+                        cursorLoop 0+                      else do+                        commit+                        cursorLoop (processed + batchSize)+            cursorLoop 0+          updateTableVersion+      where+        logMigration = do+          logInfo_ $+            arrListTable mgrTableName+              <> showt mgrFrom+              <+> "->"+              <+> showt (succ mgrFrom)++        updateTableVersion = do+          runQuery_ $ sqlUpdate "table_versions" $ do+            sqlSet "version" (succ mgrFrom)+            sqlWhereEq "name" (T.unpack . unRawSQL $ mgrTableName)++        -- Get the estimated number of rows of the given table. It might not+        -- work properly if the table is present in multiple database schemas.+        -- See https://wiki.postgresql.org/wiki/Count_estimate.+        getRowEstimate :: MonadDB m => RawSQL () -> m Int32+        getRowEstimate tableName = do+          runQuery_ . sqlSelect "pg_class" $ do+            sqlResult "reltuples::integer"+            sqlWhereEq "relname" $ unRawSQL tableName+          fetchOne runIdentity++    runMigrations :: [(Text, Int32)] -> m ()+    runMigrations dbTablesWithVersions = do+      let migrationsToRun = findMigrationsToRun dbTablesWithVersions+      validateMigrationsToRun migrationsToRun dbTablesWithVersions+      unless (null migrationsToRun) $ do+        logInfo_ "Running migrations..."+        forM_ migrationsToRun $ \mgr -> fix $ \loop -> do+          let restartMigration query = do+                logAttention "Failed to acquire a lock" $ object ["query" .= query]+                logInfo_ "Restarting the migration shortly..."+                liftIO $ threadDelay 1000000+                loop+          handleJust lockNotAvailable restartMigration $ do+            forM_ (eoLockTimeoutMs options) $ \lockTimeout -> do+              runSQL_ $ "SET LOCAL lock_timeout TO" <+> intToSQL lockTimeout+            runMigration mgr `onException` rollback+            logInfo_ "Committing migration changes..."+            commit+        logInfo_ "Running migrations... done."+      where+        intToSQL :: Int -> SQL+        intToSQL = unsafeSQL . show++        lockNotAvailable :: DBException -> Maybe String+        lockNotAvailable DBException {..}+          | Just DetailedQueryError {..} <- cast dbeError+          , qeErrorCode == LockNotAvailable =+              Just $ show dbeQueryContext+          | otherwise = Nothing++    validateMigrationsToRun :: [Migration m] -> [(Text, Int32)] -> m ()+    validateMigrationsToRun migrationsToRun dbTablesWithVersions = do+      let migrationsToRunGrouped :: [[Migration m]]+          migrationsToRunGrouped =+            L.groupBy ((==) `on` mgrTableName)+              . L.sortOn mgrTableName+              $ migrationsToRun -- NB: stable sort+          loc_common =+            "Database.PostgreSQL.PQTypes.Checks."+              ++ "checkDBConsistency.validateMigrationsToRun"++          lookupDBTableVer :: [Migration m] -> Maybe Int32+          lookupDBTableVer mgrGroup =+            lookup+              ( unRawSQL . mgrTableName . headExc head_err $+                  mgrGroup+              )+              dbTablesWithVersions+            where+              head_err = loc_common ++ ".lookupDBTableVer: broken invariant"++          groupsWithWrongDBTableVersions :: [([Migration m], Int32)]+          groupsWithWrongDBTableVersions =+            [ (mgrGroup, dbTableVer)+            | mgrGroup <- migrationsToRunGrouped+            , let dbTableVer = fromMaybe 0 $ lookupDBTableVer mgrGroup+            , dbTableVer /= (mgrFrom . headExc head_err $ mgrGroup)+            ]+            where+              head_err =+                loc_common+                  ++ ".groupsWithWrongDBTableVersions: broken invariant"++          mgrGroupsNotInDB :: [[Migration m]]+          mgrGroupsNotInDB =+            [ mgrGroup+            | mgrGroup <- migrationsToRunGrouped+            , isNothing $ lookupDBTableVer mgrGroup+            ]++          groupsStartingWithDropTable :: [[Migration m]]+          groupsStartingWithDropTable =+            [ mgrGroup+            | mgrGroup <- mgrGroupsNotInDB+            , isDropTableMigration . headExc head_err $ mgrGroup+            ]+            where+              head_err =+                loc_common+                  ++ ".groupsStartingWithDropTable: broken invariant"++          groupsNotStartingWithCreateTable :: [[Migration m]]+          groupsNotStartingWithCreateTable =+            [ mgrGroup+            | mgrGroup <- mgrGroupsNotInDB+            , mgrFrom (headExc head_err mgrGroup) /= 0+            ]+            where+              head_err =+                loc_common+                  ++ ".groupsNotStartingWithCreateTable: broken invariant"++          tblNames :: [[Migration m]] -> [RawSQL ()]+          tblNames grps =+            [mgrTableName . headExc head_err $ grp | grp <- grps]+            where+              head_err = loc_common ++ ".tblNames: broken invariant"++      unless (null groupsWithWrongDBTableVersions) $ do+        let tnms = tblNames . map fst $ groupsWithWrongDBTableVersions+        logAttention+          ( "There are migration chains selected for execution "+              <> "that expect a different starting table version number "+              <> "from the one in the database. "+              <> "This likely means that the order of migrations is wrong."+          )+          $ object ["tables" .= map unRawSQL tnms]+        errorInvalidMigrations tnms++      unless (null groupsStartingWithDropTable) $ do+        let tnms = tblNames groupsStartingWithDropTable+        logAttention "There are drop table migrations for non-existing tables." $+          object ["tables" .= map unRawSQL tnms]+        errorInvalidMigrations tnms++      -- NB: the following check can break if we allow renaming tables.+      unless (null groupsNotStartingWithCreateTable) $ do+        let tnms = tblNames groupsNotStartingWithCreateTable+        logAttention+          ( "Some tables haven't been created yet, but"+              <> "their migration lists don't start with a create table migration."+          )+          $ object ["tables" .= map unRawSQL tnms]+        errorInvalidMigrations tnms++-- | Type synonym for a list of tables along with their database versions.+type TablesWithVersions = [(Table, Int32)]++-- The server_version_num has been there since 8.2+checkVersionIsAtLeast15 :: (MonadDB m, MonadThrow m) => m Bool+checkVersionIsAtLeast15 = do+  runSQL01_ "select current_setting('server_version_num',true)::int >= 150000;"+  fetchOne runIdentity++-- | Associate each table in the list with its version as it exists in+-- the DB, or 0 if it's missing from the DB.+getTableVersions :: (MonadDB m, MonadThrow m) => [Table] -> m TablesWithVersions+getTableVersions tbls =+  sequence+    [ (\mver -> (tbl, fromMaybe 0 mver)) <$> checkTableVersion (tblNameString tbl)+    | tbl <- tbls+    ]++-- | Given a result of 'getTableVersions' check if no tables are present in the+-- database.+noTablesPresent :: TablesWithVersions -> Bool+noTablesPresent = all ((==) 0 . snd)++-- | Like 'getTableVersions', but for all user-defined tables that+-- actually exist in the DB.+getDBTableVersions :: (MonadDB m, MonadThrow m) => m [(Text, Int32)]+getDBTableVersions = do+  dbTableNames <- getDBTableNames+  sequence+    [ (\mver -> (name, fromMaybe 0 mver)) <$> checkTableVersion (T.unpack name)+    | name <- dbTableNames+    ]++-- | Check whether the table exists in the DB, and return 'Just' its+-- version if it does, or 'Nothing' if it doesn't.+checkTableVersion :: (MonadDB m, MonadThrow m) => String -> m (Maybe Int32)+checkTableVersion tblName = do+  doesExist <- runQuery01 . sqlSelect "pg_catalog.pg_class c" $ do+    sqlResult "TRUE"+    sqlLeftJoinOn "pg_catalog.pg_namespace n" "n.oid = c.relnamespace"+    sqlWhereEq "c.relname" tblName+    sqlWhere "pg_catalog.pg_table_is_visible(c.oid)"+  if doesExist+    then do+      runQuery_ $+        "SELECT version FROM table_versions WHERE name ="+          <?> tblName+      mver <- fetchMaybe runIdentity+      case mver of+        Just ver -> return $ Just ver+        Nothing ->+          error $+            "checkTableVersion: table '"+              ++ tblName+              ++ "' is present in the database, "+              ++ "but there is no corresponding version info in 'table_versions'."+    else do+      return Nothing++-- *** TABLE STRUCTURE ***++sqlGetTableID :: Table -> SQL+sqlGetTableID table = parenthesize . toSQLCommand $+  sqlSelect "pg_catalog.pg_class c" $ do+    sqlResult "c.oid"+    sqlLeftJoinOn "pg_catalog.pg_namespace n" "n.oid = c.relnamespace"+    sqlWhereEq "c.relname" $ tblNameString table+    sqlWhere "pg_catalog.pg_table_is_visible(c.oid)"++-- *** PRIMARY KEY ***++sqlGetPrimaryKey+  :: (MonadDB m, MonadThrow m)+  => Table+  -> m (Maybe (PrimaryKey, RawSQL ()))+sqlGetPrimaryKey table = do+  (mColumnNumbers :: Maybe [Int16]) <- do+    runQuery_ . sqlSelect "pg_catalog.pg_constraint" $ do+      sqlResult "conkey"+      sqlWhereEqSql "conrelid" (sqlGetTableID table)+      sqlWhereEq "contype" 'p'+    fetchMaybe $ unArray1 . runIdentity++  case mColumnNumbers of+    Nothing -> do return Nothing+    Just columnNumbers -> do+      columnNames <- do+        forM columnNumbers $ \k -> do+          runQuery_ . sqlSelect "pk_columns" $ do+            sqlWith "key_series" . sqlSelect "pg_constraint as c2" $ do+              sqlResult "unnest(c2.conkey) as k"+              sqlWhereEqSql "c2.conrelid" $ sqlGetTableID table+              sqlWhereEq "c2.contype" 'p'++            sqlWith "pk_columns" . sqlSelect "key_series" $ do+              sqlJoinOn "pg_catalog.pg_attribute as a" "a.attnum = key_series.k"+              sqlResult "a.attname::text as column_name"+              sqlResult "key_series.k as column_order"+              sqlWhereEqSql "a.attrelid" $ sqlGetTableID table++            sqlResult "pk_columns.column_name"+            sqlWhereEq "pk_columns.column_order" k++          fetchOne (\(Identity t) -> t :: String)++      runQuery_ . sqlSelect "pg_catalog.pg_constraint as c" $ do+        sqlWhereEq "c.contype" 'p'+        sqlWhereEqSql "c.conrelid" $ sqlGetTableID table+        sqlResult "c.conname::text"+        sqlResult $+          Data.String.fromString+            ("array['" <> mintercalate "', '" columnNames <> "']::text[]")++      join <$> fetchMaybe fetchPrimaryKey++fetchPrimaryKey :: (String, Array1 String) -> Maybe (PrimaryKey, RawSQL ())+fetchPrimaryKey (name, Array1 columns) =+  (,unsafeSQL name)+    <$> pkOnColumns (map unsafeSQL columns)++-- *** CHECKS ***++sqlGetChecks :: Table -> SQL+sqlGetChecks table = toSQLCommand . sqlSelect "pg_catalog.pg_constraint c" $ do+  sqlResult "c.conname::text"+  sqlResult+    "regexp_replace(pg_get_constraintdef(c.oid, true), \+    \'CHECK \\((.*)\\)', '\\1') AS body" -- check body+  sqlResult "c.convalidated" -- validated?+  sqlWhereEq "c.contype" 'c'+  sqlWhereEqSql "c.conrelid" $ sqlGetTableID table++fetchTableCheck :: (String, String, Bool) -> Check+fetchTableCheck (name, condition, validated) =+  Check+    { chkName = unsafeSQL name+    , chkCondition = unsafeSQL condition+    , chkValidated = validated+    }++-- *** INDEXES ***+sqlGetIndexes :: Bool -> Table -> SQL+sqlGetIndexes nullsNotDistinctSupported table = toSQLCommand . sqlSelect "pg_catalog.pg_class c" $ do+  sqlResult "c.relname::text" -- index name+  sqlResult $ "ARRAY(" <> selectCoordinates "0" "i.indnkeyatts" <> ")" -- array of key columns in the index+  sqlResult $ "ARRAY(" <> selectCoordinates "i.indnkeyatts" "i.indnatts" <> ")" -- array of included columns in the index+  sqlResult "am.amname::text" -- the method used (btree, gin etc)+  sqlResult "i.indisunique" -- is it unique?+  sqlResult "i.indisvalid" -- is it valid?+  -- does it have NULLS NOT DISTINCT ?+  if nullsNotDistinctSupported+    then sqlResult "i.indnullsnotdistinct"+    else sqlResult "false"+  -- if partial, get constraint def+  sqlResult "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)"+  sqlJoinOn "pg_catalog.pg_index i" "c.oid = i.indexrelid"+  sqlJoinOn "pg_catalog.pg_am am" "c.relam = am.oid"+  sqlLeftJoinOn+    "pg_catalog.pg_constraint r"+    "r.conrelid = i.indrelid AND r.conindid = i.indexrelid"+  sqlWhereEqSql "i.indrelid" $ sqlGetTableID table+  sqlWhereIsNULL "r.contype" -- fetch only "pure" indexes+  where+    -- Get all coordinates of the index.+    selectCoordinates start end =+      smconcat+        [ "WITH RECURSIVE coordinates(k, name) AS ("+        , "  VALUES (" <> start <> "::integer, NULL)"+        , "  UNION ALL"+        , "    SELECT k+1, pg_catalog.pg_get_indexdef(i.indexrelid, k+1, true)"+        , "      FROM coordinates"+        , "     WHERE k < " <> end+        , ")"+        , "SELECT name FROM coordinates WHERE name IS NOT NULL"+        ]++fetchTableIndex+  :: (String, Array1 String, Array1 String, String, Bool, Bool, Bool, Maybe String)+  -> (TableIndex, RawSQL ())+fetchTableIndex (name, Array1 keyColumns, Array1 includeColumns, method, unique, valid, nullsNotDistinct, mconstraint) =+  ( TableIndex+      { idxColumns = map (indexColumn . unsafeSQL) keyColumns+      , idxInclude = map unsafeSQL includeColumns+      , idxMethod = read method+      , idxUnique = unique+      , idxValid = valid+      , idxWhere = unsafeSQL <$> mconstraint+      , idxNotDistinctNulls = nullsNotDistinct+      }+  , unsafeSQL name+  )++-- *** FOREIGN KEYS ***++sqlGetForeignKeys :: Table -> SQL+sqlGetForeignKeys table = toSQLCommand+  . sqlSelect "pg_catalog.pg_constraint r"+  $ do+    sqlResult "r.conname::text" -- fk name+    sqlResult $+      "ARRAY(SELECT a.attname::text FROM pg_catalog.pg_attribute a JOIN ("+        <> unnestWithOrdinality "r.conkey"+        <> ") conkeys ON (a.attnum = conkeys.item) \+           \WHERE a.attrelid = r.conrelid \+           \ORDER BY conkeys.n)" -- constrained columns+    sqlResult "c.relname::text" -- referenced table+    sqlResult $+      "ARRAY(SELECT a.attname::text \+      \FROM pg_catalog.pg_attribute a JOIN ("+        <> unnestWithOrdinality "r.confkey"+        <> ") confkeys ON (a.attnum = confkeys.item) \+           \WHERE a.attrelid = r.confrelid \+           \ORDER BY confkeys.n)" -- referenced columns+    sqlResult "r.confupdtype" -- on update+    sqlResult "r.confdeltype" -- on delete+    sqlResult "r.condeferrable" -- deferrable?+    sqlResult "r.condeferred" -- initially deferred?+    sqlResult "r.convalidated" -- validated?+    sqlJoinOn "pg_catalog.pg_class c" "c.oid = r.confrelid"+    sqlWhereEqSql "r.conrelid" $ sqlGetTableID table+    sqlWhereEq "r.contype" 'f'+  where+    unnestWithOrdinality :: RawSQL () -> SQL+    unnestWithOrdinality arr =+      "SELECT n, "+        <> raw arr+        <> "[n] AS item FROM generate_subscripts("+        <> raw arr+        <> ", 1) AS n"++fetchForeignKey+  :: (String, Array1 String, String, Array1 String, Char, Char, Bool, Bool, Bool)+  -> (ForeignKey, RawSQL ())+fetchForeignKey+  ( name+    , Array1 columns+    , reftable+    , Array1 refcolumns+    , on_update+    , on_delete+    , deferrable+    , deferred+    , validated+    ) =+    ( ForeignKey+        { fkColumns = map unsafeSQL columns+        , fkRefTable = unsafeSQL reftable+        , fkRefColumns = map unsafeSQL refcolumns+        , fkOnUpdate = charToForeignKeyAction on_update+        , fkOnDelete = charToForeignKeyAction on_delete+        , fkDeferrable = deferrable+        , fkDeferred = deferred+        , fkValidated = validated+        }+    , unsafeSQL name+    )+    where+      charToForeignKeyAction c = case c of+        'a' -> ForeignKeyNoAction+        'r' -> ForeignKeyRestrict+        'c' -> ForeignKeyCascade+        'n' -> ForeignKeySetNull+        'd' -> ForeignKeySetDefault+        _ ->+          error $+            "fetchForeignKey: invalid foreign key action code: "+              ++ show c
src/Database/PostgreSQL/PQTypes/Checks/Util.hs view
@@ -1,85 +1,99 @@ {-# LANGUAGE CPP #-}-module Database.PostgreSQL.PQTypes.Checks.Util (-  ValidationResult,-  validationError,-  validationInfo,-  mapValidationResult,-  validationErrorsToInfos,-  resultCheck,-  topMessage,-  tblNameText,-  tblNameString,-  checkEquality,-  checkNames,-  checkPKPresence,-  objectHasLess,-  objectHasMore,-  arrListTable++module Database.PostgreSQL.PQTypes.Checks.Util+  ( ValidationResult+  , validationError+  , validationInfo+  , mapValidationResult+  , validationErrorsToInfos+  , resultCheck+  , resultHasErrors+  , topMessage+  , tblNameText+  , tblNameString+  , checkEquality+  , checkNames+  , checkPKPresence+  , objectHasLess+  , objectHasMore+  , arrListTable+  , checkOverlappingIndexesQuery   ) where  import Control.Monad.Catch #if !MIN_VERSION_base(4,11,0) import Data.Monoid #endif+import Data.List qualified as L import Data.Monoid.Utils+import Data.Semigroup qualified as SG import Data.Text (Text)+import Data.Text qualified as T import Log import TextShow-import qualified Data.List as L-import qualified Data.Text as T-import qualified Data.Semigroup as SG -import Database.PostgreSQL.PQTypes.Model import Database.PostgreSQL.PQTypes+import Database.PostgreSQL.PQTypes.Model  -- | A (potentially empty) list of info/error messages. data ValidationResult = ValidationResult-  { vrInfos  :: [Text]+  { vrInfos :: [Text]   , vrErrors :: [Text]   }+  deriving (Show, Eq)  validationError :: Text -> ValidationResult-validationError err = mempty { vrErrors = [err] }+validationError err = mempty {vrErrors = [err]}  validationInfo :: Text -> ValidationResult-validationInfo msg  = mempty { vrInfos = [msg] }+validationInfo msg = mempty {vrInfos = [msg]}  -- | Downgrade all error messages in a ValidationResult to info messages. validationErrorsToInfos :: ValidationResult -> ValidationResult-validationErrorsToInfos ValidationResult{..} =-  mempty { vrInfos = vrInfos <> vrErrors }+validationErrorsToInfos ValidationResult {..} =+  mempty {vrInfos = vrInfos <> vrErrors} -mapValidationResult ::-  ([Text] -> [Text]) -> ([Text] -> [Text]) -> ValidationResult -> ValidationResult-mapValidationResult mapInfos mapErrs ValidationResult{..} =-  mempty { vrInfos = mapInfos vrInfos, vrErrors = mapErrs vrErrors }+mapValidationResult+  :: ([Text] -> [Text]) -> ([Text] -> [Text]) -> ValidationResult -> ValidationResult+mapValidationResult mapInfos mapErrs ValidationResult {..} =+  mempty {vrInfos = mapInfos vrInfos, vrErrors = mapErrs vrErrors}  instance SG.Semigroup ValidationResult where-  (ValidationResult infos0 errs0) <> (ValidationResult infos1 errs1)-    = ValidationResult (infos0 <> infos1) (errs0 <> errs1)+  (ValidationResult infos0 errs0) <> (ValidationResult infos1 errs1) =+    ValidationResult (infos0 <> infos1) (errs0 <> errs1)  instance Monoid ValidationResult where-  mempty  = ValidationResult [] []+  mempty = ValidationResult [] []   mappend = (SG.<>)  topMessage :: Text -> Text -> ValidationResult -> ValidationResult-topMessage objtype objname vr@ValidationResult{..} =+topMessage objtype objname vr@ValidationResult {..} =   case vrErrors of     [] -> vr-    es -> ValidationResult vrInfos-          ("There are problems with the" <+>-            objtype <+> "'" <> objname <> "'" : es)+    es ->+      ValidationResult+        vrInfos+        ( "There are problems with the"+            <+> objtype+            <+> "'"+            <> objname+            <> "'"+            : es+        ) +resultHasErrors :: ValidationResult -> Bool+resultHasErrors ValidationResult {..} = not $ null vrErrors+ -- | Log all messages in a 'ValidationResult', and fail if any of them -- were errors. resultCheck   :: (MonadLog m, MonadThrow m)   => ValidationResult   -> m ()-resultCheck ValidationResult{..} = do+resultCheck ValidationResult {..} = do   mapM_ logInfo_ vrInfos   case vrErrors of-    []   -> return ()+    [] -> return ()     msgs -> do       mapM_ logAttention_ msgs       error "resultCheck: validation failed"@@ -95,19 +109,21 @@ checkEquality :: (Eq t, Show t) => Text -> [t] -> [t] -> ValidationResult checkEquality pname defs props = case (defs L.\\ props, props L.\\ defs) of   ([], []) -> mempty-  (def_diff, db_diff) -> validationError $ mconcat [-      "Table and its definition have diverged and have "-    , showt $ length db_diff-    , " and "-    , showt $ length def_diff-    , " different "-    , pname-    , " each, respectively:\n"-    , "  ● table:"-    , showDiff db_diff-    , "\n  ● definition:"-    , showDiff def_diff-    ]+  (def_diff, db_diff) ->+    validationError $+      mconcat+        [ "Table and its definition have diverged and have "+        , showt $ length db_diff+        , " and "+        , showt $ length def_diff+        , " different "+        , pname+        , " each, respectively:\n"+        , "  ● table:"+        , showDiff db_diff+        , "\n  ● definition:"+        , showDiff def_diff+        ]   where     showDiff = mconcat . map (("\n    ○ " <>) . T.pack . show) @@ -117,52 +133,110 @@     check (prop, name) = case prop_name prop of       pname         | pname == name -> mempty-        | otherwise     -> validationError . mconcat $ [-            "Property "-          , T.pack $ show prop-          , " has invalid name (expected: "-          , unRawSQL pname-          , ", given: "-          , unRawSQL name-          , ")."-          ]+        | otherwise ->+            validationError . mconcat $+              [ "Property "+              , T.pack $ show prop+              , " has invalid name (expected: "+              , unRawSQL pname+              , ", given: "+              , unRawSQL name+              , ")."+              ]  -- | Check presence of primary key on the named table. We cover all the cases so -- this could be used standalone, but note that the those where the table source -- definition and the table in the database differ in this respect is also -- covered by @checkEquality@.-checkPKPresence :: RawSQL ()-                -- ^ The name of the table to check for presence of primary key-              -> Maybe PrimaryKey-                -- ^ A possible primary key gotten from the table data structure-              -> Maybe (PrimaryKey, RawSQL ())-                -- ^ A possible primary key as retrieved from database along-                -- with its name-              -> ValidationResult+checkPKPresence+  :: RawSQL ()+  -- ^ The name of the table to check for presence of primary key+  -> Maybe PrimaryKey+  -- ^ A possible primary key gotten from the table data structure+  -> Maybe (PrimaryKey, RawSQL ())+  -- ^ A possible primary key as retrieved from database along+  -- with its name+  -> ValidationResult checkPKPresence tableName mdef mpk =   case (mdef, mpk) of     (Nothing, Nothing) -> valRes [noSrc, noTbl]-    (Nothing, Just _)  -> valRes [noSrc]-    (Just _, Nothing)  -> valRes [noTbl]-    _                  -> mempty+    (Nothing, Just _) -> valRes [noSrc]+    (Just _, Nothing) -> valRes [noTbl]+    _ -> mempty   where     noSrc = "no source definition"     noTbl = "no table definition"     valRes msgs =-        validationError . mconcat $-        [ "Table ", unRawSQL tableName+      validationError . mconcat $+        [ "Table "+        , unRawSQL tableName         , " has no primary key defined "-        , " (" <> (mintercalate ", " msgs) <> ")"]+        , " (" <> mintercalate ", " msgs <> ")"+        ]  objectHasLess :: Show t => Text -> Text -> t -> Text objectHasLess otype ptype missing =-  otype <+> "in the database has *less*" <+> ptype <+>-  "than its definition (missing:" <+> T.pack (show missing) <> ")"+  otype+    <+> "in the database has *less*"+    <+> ptype+    <+> "than its definition (missing:"+    <+> T.pack (show missing)+    <> ")"  objectHasMore :: Show t => Text -> Text -> t -> Text objectHasMore otype ptype extra =-  otype <+> "in the database has *more*" <+> ptype <+>-  "than its definition (extra:" <+> T.pack (show extra) <> ")"+  otype+    <+> "in the database has *more*"+    <+> ptype+    <+> "than its definition (extra:"+    <+> T.pack (show extra)+    <> ")"  arrListTable :: RawSQL () -> Text arrListTable tableName = " ->" <+> unRawSQL tableName <> ": "++checkOverlappingIndexesQuery :: RawSQL () -> SQL+checkOverlappingIndexesQuery tableName =+  smconcat+    [ "WITH"+    , -- get predicates (WHERE clause) definition in text format (ugly but the parsed version+      -- can differ even if the predicate is the same), ignore functional indexes at the same time+      -- as that would make this query very ugly+      "     indexdata1 AS (SELECT *"+    , "                         , ((regexp_match(pg_get_indexdef(indexrelid)"+    , "                                        , 'WHERE (.*)$')))[1] AS preddef"+    , "                    FROM pg_index"+    , "                    WHERE indexprs IS NULL"+    , "                    AND indrelid = '" <> raw tableName <> "'::regclass)"+    , -- add the rest of metadata and do the join+      "   , indexdata2 AS (SELECT t1.*"+    , "                         , pg_get_indexdef(t1.indexrelid) AS contained"+    , "                         , pg_get_indexdef(t2.indexrelid) AS contains"+    , "                         , array_to_string(t1.indkey, '+') AS colindex"+    , "                         , array_to_string(t2.indkey, '+') AS colotherindex"+    , "                         , t2.indexrelid AS other_index"+    , "                         , t2.indisunique AS other_indisunique"+    , "                         , t2.preddef AS other_preddef"+    , -- cross join all indexes on the same table to try all combination (except oneself)+      "                    FROM indexdata1 AS t1"+    , "                         INNER JOIN indexdata1 AS t2 ON t1.indrelid = t2.indrelid"+    , "                                                    AND t1.indexrelid <> t2.indexrelid)"+    , "  SELECT contained"+    , "       , contains"+    , "  FROM indexdata2"+    , " JOIN pg_class c ON (c.oid = indexdata2.indexrelid)"+    , -- The indexes are the same or the "other" is larger than us+      "  WHERE (colotherindex = colindex"+    , "      OR colotherindex LIKE colindex || '+%')"+    , -- and this is not a local index+      "    AND NOT c.relname ILIKE 'local_%'"+    , -- and we have the same predicate+      "    AND other_preddef IS NOT DISTINCT FROM preddef"+    , -- and either the other is unique (so better than us) or none of us is unique+      "    AND (NOT indisunique)"+    , "    OR ("+    , "             indisunique"+    , "         AND other_indisunique"+    , "         AND colindex = colotherindex"+    , ");"+    ]
src/Database/PostgreSQL/PQTypes/Deriving.hs view
@@ -1,22 +1,24 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-module Database.PostgreSQL.PQTypes.Deriving (-  -- * Helpers, to be used with @deriving via@ (@-XDerivingVia@).-    SQLEnum(..)-  , EnumEncoding(..)-  , SQLEnumAsText(..)-  , EnumAsTextEncoding(..)++module Database.PostgreSQL.PQTypes.Deriving+  ( -- * Helpers, to be used with @deriving via@ (@-XDerivingVia@).+    SQLEnum (..)+  , EnumEncoding (..)+  , SQLEnumAsText (..)+  , EnumAsTextEncoding (..)+     -- * For use in doctests.   , isInjective   ) where -import Control.Exception (SomeException(..), throwIO)+import Control.Exception (SomeException (..), throwIO) import Data.List.Extra (enumerate, nubSort) import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Typeable import Database.PostgreSQL.PQTypes import Foreign.Storable-import qualified Data.Map.Strict as Map  -- | Helper newtype to be used with @deriving via@ to derive @(PQFormat, ToSQL, -- FromSQL)@ instances for enums, given an instance of 'EnumEncoding'.@@ -59,11 +61,14 @@   ( -- The semantic type needs to be finitely enumerable.     Enum a   , Bounded a-    -- The base type needs to be enumerable and ordered.-  , Enum (EnumBase a)+  , -- The base type needs to be enumerable and ordered.+    Enum (EnumBase a)   , Ord (EnumBase a)-  ) => EnumEncoding a where+  ) =>+  EnumEncoding a+  where   type EnumBase a+   -- | Encode @a@ as a base type.   encodeEnum :: a -> EnumBase a @@ -73,13 +78,14 @@   -- /Note:/ The default implementation looks up values in 'decodeEnumMap' and   -- can be overwritten for performance if necessary.   decodeEnum :: EnumBase a -> Either [(EnumBase a, EnumBase a)] a-  decodeEnum b = maybe (Left . intervals $ Map.keys (decodeEnumMap @a)) Right-               $ Map.lookup b (decodeEnumMap @a)+  decodeEnum b =+    maybe (Left . intervals $ Map.keys (decodeEnumMap @a)) Right $+      Map.lookup b (decodeEnumMap @a)    -- | Include the inverse map as a top-level part of the 'EnumEncoding'   -- instance to ensure it is only computed once by GHC.   decodeEnumMap :: Map (EnumBase a) a-  decodeEnumMap = Map.fromList [ (encodeEnum a, a) | a <- enumerate ]+  decodeEnumMap = Map.fromList [(encodeEnum a, a) | a <- enumerate]  instance PQFormat (EnumBase a) => PQFormat (SQLEnum a) where   pqFormat = pqFormat @(EnumBase a)@@ -88,7 +94,9 @@   ( EnumEncoding a   , PQFormat (EnumBase a)   , ToSQL (EnumBase a)-  ) => ToSQL (SQLEnum a) where+  )+  => ToSQL (SQLEnum a)+  where   type PQDest (SQLEnum a) = PQDest (EnumBase a)   toSQL (SQLEnum a) = toSQL $ encodeEnum a @@ -99,15 +107,20 @@   , FromSQL (EnumBase a)   , Show (EnumBase a)   , Typeable (EnumBase a)-  ) => FromSQL (SQLEnum a) where+  )+  => FromSQL (SQLEnum a)+  where   type PQBase (SQLEnum a) = PQBase (EnumBase a)   fromSQL base = do     b <- fromSQL base     case decodeEnum b of-      Left validRange -> throwIO $ SomeException RangeError-        { reRange = validRange-        , reValue = b-        }+      Left validRange ->+        throwIO $+          SomeException+            RangeError+              { reRange = validRange+              , reValue = b+              }       Right a -> return $ SQLEnum a  -- | A special case of 'SQLEnum', where the enum is to be encoded as text@@ -152,13 +165,14 @@   -- /Note:/ The default implementation looks up values in 'decodeEnumAsTextMap'   -- and can be overwritten for performance if necessary.   decodeEnumAsText :: Text -> Either [Text] a-  decodeEnumAsText text = maybe (Left $ Map.keys (decodeEnumAsTextMap @a)) Right-                        $ Map.lookup text (decodeEnumAsTextMap @a)+  decodeEnumAsText text =+    maybe (Left $ Map.keys (decodeEnumAsTextMap @a)) Right $+      Map.lookup text (decodeEnumAsTextMap @a)    -- | Include the inverse map as a top-level part of the 'SQLEnumTextEncoding'   -- instance to ensure it is only computed once by GHC.   decodeEnumAsTextMap :: Map Text a-  decodeEnumAsTextMap = Map.fromList [ (encodeEnumAsText a, a) | a <- enumerate ]+  decodeEnumAsTextMap = Map.fromList [(encodeEnumAsText a, a) | a <- enumerate]  instance EnumAsTextEncoding a => PQFormat (SQLEnumAsText a) where   pqFormat = pqFormat @Text@@ -172,10 +186,13 @@   fromSQL base = do     text <- fromSQL base     case decodeEnumAsText text of-      Left validValues -> throwIO $ SomeException InvalidValue-        { ivValue       = text-        , ivValidValues = Just validValues-        }+      Left validValues ->+        throwIO $+          SomeException+            InvalidValue+              { ivValue = text+              , ivValidValues = Just validValues+              }       Right a -> return $ SQLEnumAsText a  -- | To be used in doctests to prove injectivity of encoding functions.@@ -186,7 +203,7 @@ -- >>> isInjective (\(_ :: Bool) -> False) -- False isInjective :: (Enum a, Bounded a, Eq a, Eq b) => (a -> b) -> Bool-isInjective f = null [ (a, b) | a <- enumerate, b <- enumerate, a /= b, f a == f b ]+isInjective f = null [(a, b) | a <- enumerate, b <- enumerate, a /= b, f a == f b]  -- | Internal helper: given a list of values, decompose it into a list of -- intervals.@@ -195,16 +212,17 @@ -- [(-1,3),(42,43),(88,88)] -- -- prop> nubSort xs == concatMap (\(l,r) -> [l .. r]) (intervals xs)-intervals :: forall  a . (Enum a, Ord a) => [a] -> [(a, a)]+intervals :: forall a. (Enum a, Ord a) => [a] -> [(a, a)] intervals as = case nubSort as of   [] -> []   (first : ascendingRest) -> accumIntervals (first, first) ascendingRest   where     accumIntervals :: (a, a) -> [a] -> [(a, a)]     accumIntervals (lower, upper) [] = [(lower, upper)]-    accumIntervals (lower, upper) (first' : ascendingRest') = if succ upper == first'-      then accumIntervals (lower, first') ascendingRest'-      else (lower, upper) : accumIntervals (first', first') ascendingRest'+    accumIntervals (lower, upper) (first' : ascendingRest') =+      if succ upper == first'+        then accumIntervals (lower, first') ascendingRest'+        else (lower, upper) : accumIntervals (first', first') ascendingRest'  -- $setup -- >>> import Data.Int
src/Database/PostgreSQL/PQTypes/ExtrasOptions.hs view
@@ -1,28 +1,36 @@ module Database.PostgreSQL.PQTypes.ExtrasOptions-  ( ExtrasOptions(..)+  ( ExtrasOptions (..)   , defaultExtrasOptions-  , ObjectsValidationMode(..)+  , ObjectsValidationMode (..)   ) where -data ExtrasOptions =-    ExtrasOptions-    { eoLockTimeoutMs            :: !(Maybe Int)-    , eoEnforcePKs               :: !Bool-      -- ^ Validate that every handled table has a primary key-    , eoObjectsValidationMode    :: !ObjectsValidationMode-      -- ^ Validation mode for unknown tables and composite types.-    , eoAllowHigherTableVersions :: !Bool-      -- ^ Whether to allow tables in the database to have higher versions than-      -- the one in the code definition.-    } deriving Eq+data ExtrasOptions+  = ExtrasOptions+  { eoLockTimeoutMs :: !(Maybe Int)+  , eoEnforcePKs :: !Bool+  -- ^ Validate that every handled table has a primary key+  , eoObjectsValidationMode :: !ObjectsValidationMode+  -- ^ Validation mode for unknown tables and composite types.+  , eoAllowHigherTableVersions :: !Bool+  -- ^ Whether to allow tables in the database to have higher versions than+  -- the one in the code definition.+  , eoCheckForeignKeysIndexes :: !Bool+  -- ^ Check if all foreign keys have indexes.+  , eoCheckOverlappingIndexes :: !Bool+  -- ^ Check if some indexes are redundant+  }+  deriving (Eq)  defaultExtrasOptions :: ExtrasOptions-defaultExtrasOptions = ExtrasOptions-  { eoLockTimeoutMs            = Nothing-  , eoEnforcePKs               = False-  , eoObjectsValidationMode    = DontAllowUnknownObjects-  , eoAllowHigherTableVersions = False-  }+defaultExtrasOptions =+  ExtrasOptions+    { eoLockTimeoutMs = Nothing+    , eoEnforcePKs = False+    , eoObjectsValidationMode = DontAllowUnknownObjects+    , eoAllowHigherTableVersions = False+    , eoCheckForeignKeysIndexes = False+    , eoCheckOverlappingIndexes = False+    }  data ObjectsValidationMode = AllowUnknownObjects | DontAllowUnknownObjects-  deriving Eq+  deriving (Eq)
src/Database/PostgreSQL/PQTypes/Migrate.hs view
@@ -1,12 +1,12 @@-module Database.PostgreSQL.PQTypes.Migrate (-  createDomain,-  createTable,-  createTableConstraints,-  createTableTriggers+module Database.PostgreSQL.PQTypes.Migrate+  ( createDomain+  , createTable+  , createTableConstraints+  , createTableTriggers   ) where  import Control.Monad-import qualified Data.Foldable as F+import Data.Foldable qualified as F  import Database.PostgreSQL.PQTypes import Database.PostgreSQL.PQTypes.Checks.Util@@ -14,14 +14,14 @@ import Database.PostgreSQL.PQTypes.SQL.Builder  createDomain :: MonadDB m => Domain -> m ()-createDomain dom@Domain{..} = do+createDomain dom@Domain {..} = do   -- create the domain   runQuery_ $ sqlCreateDomain dom   -- add constraint checks to the domain   F.forM_ domChecks $ runQuery_ . sqlAlterDomain domName . sqlAddValidCheckMaybeDowntime  createTable :: MonadDB m => Bool -> Table -> m ()-createTable withConstraints table@Table{..} = do+createTable withConstraints table@Table {..} = do   -- Create empty table and add the columns.   runQuery_ $ sqlCreateTable tblName   runQuery_ $ sqlAlterTable tblName $ map sqlAddColumn tblColumns@@ -39,13 +39,12 @@     sqlSet "version" tblVersion  createTableConstraints :: MonadDB m => Table -> m ()-createTableConstraints Table{..} = when (not $ null addConstraints) $ do+createTableConstraints Table {..} = unless (null addConstraints) $ do   runQuery_ $ sqlAlterTable tblName addConstraints   where-    addConstraints = concat-      [ map sqlAddValidCheckMaybeDowntime tblChecks-      , map (sqlAddValidFKMaybeDowntime tblName) tblForeignKeys-      ]+    addConstraints =+      map sqlAddValidCheckMaybeDowntime tblChecks+        ++ map (sqlAddValidFKMaybeDowntime tblName) tblForeignKeys  createTableTriggers :: MonadDB m => Table -> m () createTableTriggers = mapM_ createTrigger . tblTriggers
src/Database/PostgreSQL/PQTypes/Model.hs view
@@ -1,8 +1,9 @@-module Database.PostgreSQL.PQTypes.Model (-    module Database.PostgreSQL.PQTypes.Model.Check+module Database.PostgreSQL.PQTypes.Model+  ( module Database.PostgreSQL.PQTypes.Model.Check   , module Database.PostgreSQL.PQTypes.Model.ColumnType   , module Database.PostgreSQL.PQTypes.Model.CompositeType   , module Database.PostgreSQL.PQTypes.Model.Domain+  , module Database.PostgreSQL.PQTypes.Model.EnumType   , module Database.PostgreSQL.PQTypes.Model.Extension   , module Database.PostgreSQL.PQTypes.Model.ForeignKey   , module Database.PostgreSQL.PQTypes.Model.Index@@ -16,6 +17,7 @@ import Database.PostgreSQL.PQTypes.Model.ColumnType import Database.PostgreSQL.PQTypes.Model.CompositeType import Database.PostgreSQL.PQTypes.Model.Domain+import Database.PostgreSQL.PQTypes.Model.EnumType import Database.PostgreSQL.PQTypes.Model.Extension import Database.PostgreSQL.PQTypes.Model.ForeignKey import Database.PostgreSQL.PQTypes.Model.Index
src/Database/PostgreSQL/PQTypes/Model/Check.hs view
@@ -1,5 +1,5 @@-module Database.PostgreSQL.PQTypes.Model.Check (-    Check(..)+module Database.PostgreSQL.PQTypes.Model.Check+  ( Check (..)   , tblCheck   , sqlAddValidCheckMaybeDowntime   , sqlAddNotValidCheck@@ -10,19 +10,22 @@ import Data.Monoid.Utils import Database.PostgreSQL.PQTypes -data Check = Check {-  chkName      :: RawSQL ()-, chkCondition :: RawSQL ()-, chkValidated :: Bool -- ^ Set to 'False' if check is created as NOT VALID and-                       -- left in such state (for whatever reason).-} deriving (Eq, Ord, Show)+data Check = Check+  { chkName :: RawSQL ()+  , chkCondition :: RawSQL ()+  , chkValidated :: Bool+  -- ^ Set to 'False' if check is created as NOT VALID and+  -- left in such state (for whatever reason).+  }+  deriving (Eq, Ord, Show)  tblCheck :: Check-tblCheck = Check-  { chkName      = ""-  , chkCondition = ""-  , chkValidated = True-  }+tblCheck =+  Check+    { chkName = ""+    , chkCondition = ""+    , chkValidated = True+    }  -- | Add valid check constraint. Warning: PostgreSQL acquires SHARE ROW -- EXCLUSIVE lock (that prevents updates) on modified table for the duration of@@ -42,14 +45,15 @@ sqlValidateCheck checkName = "VALIDATE CONSTRAINT" <+> checkName  sqlAddCheck_ :: Bool -> Check -> RawSQL ()-sqlAddCheck_ valid Check{..} = smconcat [-    "ADD CONSTRAINT"-  , chkName-  , "CHECK ("-  , chkCondition-  , ")"-  , if valid then "" else " NOT VALID"-  ]+sqlAddCheck_ valid Check {..} =+  smconcat+    [ "ADD CONSTRAINT"+    , chkName+    , "CHECK ("+    , chkCondition+    , ")"+    , if valid then "" else " NOT VALID"+    ]  sqlDropCheck :: RawSQL () -> RawSQL () sqlDropCheck name = "DROP CONSTRAINT" <+> name
src/Database/PostgreSQL/PQTypes/Model/ColumnType.hs view
@@ -1,10 +1,10 @@-module Database.PostgreSQL.PQTypes.Model.ColumnType (-    ColumnType(..)+module Database.PostgreSQL.PQTypes.Model.ColumnType+  ( ColumnType (..)   , columnTypeToSQL   ) where +import Data.Text qualified as T import Database.PostgreSQL.PQTypes-import qualified Data.Text as T  data ColumnType   = BigIntT@@ -25,7 +25,7 @@   | XmlT   | ArrayT !ColumnType   | CustomT !(RawSQL ())-    deriving (Eq, Ord, Show)+  deriving (Eq, Ord, Show)  instance PQFormat ColumnType where   pqFormat = pqFormat @T.Text@@ -55,21 +55,21 @@           | otherwise -> CustomT $ rawSQL tname ()  columnTypeToSQL :: ColumnType -> RawSQL ()-columnTypeToSQL BigIntT            = "BIGINT"-columnTypeToSQL BigSerialT         = "BIGSERIAL"-columnTypeToSQL BinaryT            = "BYTEA"-columnTypeToSQL BoolT              = "BOOLEAN"-columnTypeToSQL DateT              = "DATE"-columnTypeToSQL DoubleT            = "DOUBLE PRECISION"-columnTypeToSQL IntegerT           = "INTEGER"-columnTypeToSQL UuidT              = "UUID"-columnTypeToSQL IntervalT          = "INTERVAL"-columnTypeToSQL JsonT              = "JSON"-columnTypeToSQL JsonbT             = "JSONB"-columnTypeToSQL SmallIntT          = "SMALLINT"-columnTypeToSQL TextT              = "TEXT"-columnTypeToSQL TSVectorT          = "TSVECTOR"+columnTypeToSQL BigIntT = "BIGINT"+columnTypeToSQL BigSerialT = "BIGSERIAL"+columnTypeToSQL BinaryT = "BYTEA"+columnTypeToSQL BoolT = "BOOLEAN"+columnTypeToSQL DateT = "DATE"+columnTypeToSQL DoubleT = "DOUBLE PRECISION"+columnTypeToSQL IntegerT = "INTEGER"+columnTypeToSQL UuidT = "UUID"+columnTypeToSQL IntervalT = "INTERVAL"+columnTypeToSQL JsonT = "JSON"+columnTypeToSQL JsonbT = "JSONB"+columnTypeToSQL SmallIntT = "SMALLINT"+columnTypeToSQL TextT = "TEXT"+columnTypeToSQL TSVectorT = "TSVECTOR" columnTypeToSQL TimestampWithZoneT = "TIMESTAMPTZ"-columnTypeToSQL XmlT               = "XML"-columnTypeToSQL (ArrayT t)         = columnTypeToSQL t <> "[]"-columnTypeToSQL (CustomT tname)    = tname+columnTypeToSQL XmlT = "XML"+columnTypeToSQL (ArrayT t) = columnTypeToSQL t <> "[]"+columnTypeToSQL (CustomT tname) = tname
src/Database/PostgreSQL/PQTypes/Model/CompositeType.hs view
@@ -1,30 +1,32 @@-module Database.PostgreSQL.PQTypes.Model.CompositeType (-    CompositeType(..)-  , CompositeColumn(..)+module Database.PostgreSQL.PQTypes.Model.CompositeType+  ( CompositeType (..)+  , CompositeColumn (..)   , compositeTypePqFormat   , sqlCreateComposite   , sqlDropComposite   , getDBCompositeTypes   ) where +import Data.ByteString qualified as BS import Data.Int import Data.Monoid.Utils+import Data.Text.Encoding qualified as T import Database.PostgreSQL.PQTypes-import qualified Data.ByteString as BS-import qualified Data.Text.Encoding as T  import Database.PostgreSQL.PQTypes.Model.ColumnType import Database.PostgreSQL.PQTypes.SQL.Builder -data CompositeType = CompositeType {-  ctName    :: !(RawSQL ())-, ctColumns :: ![CompositeColumn]-} deriving (Eq, Ord, Show)+data CompositeType = CompositeType+  { ctName :: !(RawSQL ())+  , ctColumns :: ![CompositeColumn]+  }+  deriving (Eq, Ord, Show) -data CompositeColumn = CompositeColumn {-  ccName :: !(RawSQL ())-, ccType :: ColumnType-} deriving (Eq, Ord, Show)+data CompositeColumn = CompositeColumn+  { ccName :: !(RawSQL ())+  , ccType :: ColumnType+  }+  deriving (Eq, Ord, Show)  -- | Convenience function for converting CompositeType definition to -- corresponding 'pqFormat' definition.@@ -33,15 +35,16 @@  -- | Make SQL query that creates a composite type. sqlCreateComposite :: CompositeType -> RawSQL ()-sqlCreateComposite CompositeType{..} = smconcat [-    "CREATE TYPE"-  , ctName-  , "AS ("-  , mintercalate ", " $ map columnToSQL ctColumns-  , ")"-  ]+sqlCreateComposite CompositeType {..} =+  smconcat+    [ "CREATE TYPE"+    , ctName+    , "AS ("+    , mintercalate ", " $ map columnToSQL ctColumns+    , ")"+    ]   where-    columnToSQL CompositeColumn{..} = ccName <+> columnTypeToSQL ccType+    columnToSQL CompositeColumn {..} = ccName <+> columnTypeToSQL ccType  -- | Make SQL query that drops a composite type. sqlDropComposite :: RawSQL () -> RawSQL ()@@ -68,8 +71,8 @@         sqlWhereEq "a.attrelid" oid         sqlOrderBy "a.attnum"       columns <- fetchMany fetch-      return CompositeType { ctName = unsafeSQL name, ctColumns = columns }+      return CompositeType {ctName = unsafeSQL name, ctColumns = columns}       where         fetch :: (String, ColumnType) -> CompositeColumn         fetch (cname, ctype) =-          CompositeColumn { ccName = unsafeSQL cname, ccType = ctype }+          CompositeColumn {ccName = unsafeSQL cname, ccType = ctype}
src/Database/PostgreSQL/PQTypes/Model/Domain.hs view
@@ -1,5 +1,5 @@-module Database.PostgreSQL.PQTypes.Model.Domain (-    Domain(..)+module Database.PostgreSQL.PQTypes.Model.Domain+  ( Domain (..)   , mkChecks   , sqlCreateDomain   , sqlAlterDomain@@ -49,31 +49,33 @@ -- and edit old migrations), whereas the current solution makes the -- transition trivial. -data Domain = Domain {-  -- | Name of the domain.-  domName     :: RawSQL ()-  -- | Type of the domain.-, domType     :: ColumnType-  -- | Defines whether the domain value can be NULL.+data Domain = Domain+  { domName :: RawSQL ()+  -- ^ Name of the domain.+  , domType :: ColumnType+  -- ^ Type of the domain.+  , domNullable :: Bool+  -- ^ Defines whether the domain value can be NULL.   -- *Cannot* be superseded by a table column definition.-, domNullable :: Bool-  -- Default value for the domain. *Can* be-  -- superseded by a table column definition.-, domDefault  :: Maybe (RawSQL ())-  -- Set of constraint checks on the domain.-, domChecks   :: Set Check-} deriving (Eq, Ord, Show)+  , -- Default value for the domain. *Can* be+    -- superseded by a table column definition.+    domDefault :: Maybe (RawSQL ())+  , -- Set of constraint checks on the domain.+    domChecks :: Set Check+  }+  deriving (Eq, Ord, Show)  mkChecks :: [Check] -> Set Check mkChecks = fromList  sqlCreateDomain :: Domain -> RawSQL ()-sqlCreateDomain Domain{..} = smconcat [-    "CREATE DOMAIN" <+> domName <+> "AS"-  , columnTypeToSQL domType-  , if domNullable then "NULL" else "NOT NULL"-  , maybe "" ("DEFAULT" <+>) domDefault-  ]+sqlCreateDomain Domain {..} =+  smconcat+    [ "CREATE DOMAIN" <+> domName <+> "AS"+    , columnTypeToSQL domType+    , if domNullable then "NULL" else "NOT NULL"+    , maybe "" ("DEFAULT" <+>) domDefault+    ]  sqlAlterDomain :: RawSQL () -> RawSQL () -> RawSQL () sqlAlterDomain dname alter = "ALTER DOMAIN" <+> dname <+> alter
+ src/Database/PostgreSQL/PQTypes/Model/EnumType.hs view
@@ -0,0 +1,32 @@+module Database.PostgreSQL.PQTypes.Model.EnumType+  ( EnumType (..)+  , sqlCreateEnum+  , sqlDropEnum+  ) where++import Data.Monoid.Utils+import Data.Text qualified as T+import Database.PostgreSQL.PQTypes++data EnumType = EnumType+  { etName :: !(RawSQL ())+  , etValues :: ![RawSQL ()]+  }+  deriving (Eq, Ord, Show)++-- | Make SQL query that creates an enum type.+sqlCreateEnum :: EnumType -> RawSQL ()+sqlCreateEnum EnumType {..} =+  smconcat+    [ "CREATE TYPE"+    , etName+    , "AS ENUM ("+    , mintercalate ", " $ map quotedValue etValues+    , ")"+    ]+  where+    quotedValue v = rawSQL ("'" <> T.replace "'" "''" (unRawSQL v) <> "'" :: T.Text) ()++-- | Make SQL query that drops a composite type.+sqlDropEnum :: RawSQL () -> RawSQL ()+sqlDropEnum = ("DROP TYPE" <+>)
src/Database/PostgreSQL/PQTypes/Model/Extension.hs view
@@ -1,5 +1,5 @@-module Database.PostgreSQL.PQTypes.Model.Extension (-    Extension(..)+module Database.PostgreSQL.PQTypes.Model.Extension+  ( Extension (..)   , ununExtension   ) where @@ -7,7 +7,7 @@ import Data.Text (Text) import Database.PostgreSQL.PQTypes -newtype Extension = Extension { unExtension :: RawSQL () }+newtype Extension = Extension {unExtension :: RawSQL ()}   deriving (Eq, Ord, Show, IsString)  ununExtension :: Extension -> Text
src/Database/PostgreSQL/PQTypes/Model/ForeignKey.hs view
@@ -1,6 +1,6 @@-module Database.PostgreSQL.PQTypes.Model.ForeignKey (-    ForeignKey(..)-  , ForeignKeyAction(..)+module Database.PostgreSQL.PQTypes.Model.ForeignKey+  ( ForeignKey (..)+  , ForeignKeyAction (..)   , fkOnColumn   , fkOnColumns   , fkName@@ -11,20 +11,22 @@   ) where  import Data.Monoid.Utils+import Data.Text qualified as T import Database.PostgreSQL.PQTypes-import qualified Data.Text as T -data ForeignKey = ForeignKey {-  fkColumns    :: [RawSQL ()]-, fkRefTable   :: RawSQL ()-, fkRefColumns :: [RawSQL ()]-, fkOnUpdate   :: ForeignKeyAction-, fkOnDelete   :: ForeignKeyAction-, fkDeferrable :: Bool-, fkDeferred   :: Bool-, fkValidated  :: Bool -- ^ Set to 'False' if foreign key is created as NOT-                       -- VALID and left in such state (for whatever reason).-} deriving (Eq, Ord, Show)+data ForeignKey = ForeignKey+  { fkColumns :: [RawSQL ()]+  , fkRefTable :: RawSQL ()+  , fkRefColumns :: [RawSQL ()]+  , fkOnUpdate :: ForeignKeyAction+  , fkOnDelete :: ForeignKeyAction+  , fkDeferrable :: Bool+  , fkDeferred :: Bool+  , fkValidated :: Bool+  -- ^ Set to 'False' if foreign key is created as NOT+  -- VALID and left in such state (for whatever reason).+  }+  deriving (Eq, Ord, Show)  data ForeignKeyAction   = ForeignKeyNoAction@@ -39,26 +41,29 @@   fkOnColumns [column] reftable [refcolumn]  fkOnColumns :: [RawSQL ()] -> RawSQL () -> [RawSQL ()] -> ForeignKey-fkOnColumns columns reftable refcolumns = ForeignKey {-  fkColumns    = columns-, fkRefTable   = reftable-, fkRefColumns = refcolumns-, fkOnUpdate   = ForeignKeyCascade-, fkOnDelete   = ForeignKeyNoAction-, fkDeferrable = True-, fkDeferred   = False-, fkValidated  = True-}+fkOnColumns columns reftable refcolumns =+  ForeignKey+    { fkColumns = columns+    , fkRefTable = reftable+    , fkRefColumns = refcolumns+    , fkOnUpdate = ForeignKeyCascade+    , fkOnDelete = ForeignKeyNoAction+    , fkDeferrable = True+    , fkDeferred = False+    , fkValidated = True+    }  fkName :: RawSQL () -> ForeignKey -> RawSQL ()-fkName tname ForeignKey{..} = shorten $ mconcat [-    "fk__"-  , tname-  , "__"-  , mintercalate "__" fkColumns-  , "__"-  , fkRefTable-  ]+fkName tname ForeignKey {..} =+  shorten $+    mconcat+      [ "fk__"+      , tname+      , "__"+      , mintercalate "__" fkColumns+      , "__"+      , fkRefTable+      ]   where     -- PostgreSQL's limit for identifier is 63 characters     shorten = flip rawSQL () . T.take 63 . unRawSQL@@ -82,22 +87,23 @@ sqlValidateFK tname fk = "VALIDATE CONSTRAINT" <+> fkName tname fk  sqlAddFK_ :: Bool -> RawSQL () -> ForeignKey -> RawSQL ()-sqlAddFK_ valid tname fk@ForeignKey{..} = mconcat [-    "ADD CONSTRAINT" <+> fkName tname fk <+> "FOREIGN KEY ("-  , mintercalate ", " fkColumns-  , ") REFERENCES" <+> fkRefTable <+> "("-  , mintercalate ", " fkRefColumns-  , ") ON UPDATE" <+> foreignKeyActionToSQL fkOnUpdate-  , "  ON DELETE" <+> foreignKeyActionToSQL fkOnDelete-  , " " <> if fkDeferrable then "DEFERRABLE" else "NOT DEFERRABLE"-  , " INITIALLY" <+> if fkDeferred then "DEFERRED" else "IMMEDIATE"-  , if valid then "" else " NOT VALID"-  ]+sqlAddFK_ valid tname fk@ForeignKey {..} =+  mconcat+    [ "ADD CONSTRAINT" <+> fkName tname fk <+> "FOREIGN KEY ("+    , mintercalate ", " fkColumns+    , ") REFERENCES" <+> fkRefTable <+> "("+    , mintercalate ", " fkRefColumns+    , ") ON UPDATE" <+> foreignKeyActionToSQL fkOnUpdate+    , "  ON DELETE" <+> foreignKeyActionToSQL fkOnDelete+    , " " <> if fkDeferrable then "DEFERRABLE" else "NOT DEFERRABLE"+    , " INITIALLY" <+> if fkDeferred then "DEFERRED" else "IMMEDIATE"+    , if valid then "" else " NOT VALID"+    ]   where-    foreignKeyActionToSQL ForeignKeyNoAction   = "NO ACTION"-    foreignKeyActionToSQL ForeignKeyRestrict   = "RESTRICT"-    foreignKeyActionToSQL ForeignKeyCascade    = "CASCADE"-    foreignKeyActionToSQL ForeignKeySetNull    = "SET NULL"+    foreignKeyActionToSQL ForeignKeyNoAction = "NO ACTION"+    foreignKeyActionToSQL ForeignKeyRestrict = "RESTRICT"+    foreignKeyActionToSQL ForeignKeyCascade = "CASCADE"+    foreignKeyActionToSQL ForeignKeySetNull = "SET NULL"     foreignKeyActionToSQL ForeignKeySetDefault = "SET DEFAULT"  sqlDropFK :: RawSQL () -> ForeignKey -> RawSQL ()
src/Database/PostgreSQL/PQTypes/Model/Index.hs view
@@ -1,14 +1,15 @@-module Database.PostgreSQL.PQTypes.Model.Index (-    TableIndex(..)-  , IndexColumn(..)+module Database.PostgreSQL.PQTypes.Model.Index+  ( TableIndex (..)+  , IndexColumn (..)   , indexColumn   , indexColumnWithOperatorClass-  , IndexMethod(..)+  , IndexMethod (..)   , tblIndex   , indexOnColumn   , indexOnColumns   , indexOnColumnWithMethod   , indexOnColumnsWithMethod+  , indexColumnName   , uniqueIndexOnColumn   , uniqueIndexOnColumnWithCondition   , uniqueIndexOnColumns@@ -19,32 +20,39 @@   , sqlDropIndexConcurrently   ) where +import Crypto.Hash qualified as H+import Data.ByteArray qualified as BA+import Data.ByteString.Base16 qualified as B16+import Data.ByteString.Char8 qualified as BS import Data.Char import Data.Function-import Data.String import Data.Monoid.Utils+import Data.String+import Data.Text qualified as T+import Data.Text.Encoding qualified as T import Database.PostgreSQL.PQTypes-import qualified Crypto.Hash as H-import qualified Data.ByteArray as BA-import qualified Data.ByteString.Base16 as B16-import qualified Data.ByteString.Char8 as BS-import qualified Data.Text as T-import qualified Data.Text.Encoding as T -data TableIndex = TableIndex {-  idxColumns :: [IndexColumn]-, idxInclude :: [RawSQL ()]-, idxMethod  :: IndexMethod-, idxUnique  :: Bool-, idxValid   :: Bool -- ^ If creation of index with CONCURRENTLY fails, index-                     -- will be marked as invalid. Set it to 'False' if such-                     -- situation is expected.-, idxWhere   :: Maybe (RawSQL ())-} deriving (Eq, Ord, Show)+data TableIndex = TableIndex+  { idxColumns :: [IndexColumn]+  , idxInclude :: [RawSQL ()]+  , idxMethod :: IndexMethod+  , idxUnique :: Bool+  , idxValid :: Bool+  , -- \^ If creation of index with CONCURRENTLY fails, index+    -- will be marked as invalid. Set it to 'False' if such+    -- situation is expected.+    idxWhere :: Maybe (RawSQL ())+  , idxNotDistinctNulls :: Bool+  }+  -- \^ Adds NULL NOT DISTINCT on the index, meaning that+  -- \^ only one NULL value will be accepted; other NULLs+  -- \^ will be perceived as a violation of the constraint.+  -- \^ NB: will only be used if idxUnique is set to True+  deriving (Eq, Ord, Show)  data IndexColumn   = IndexColumn (RawSQL ()) (Maybe (RawSQL ()))-  deriving Show+  deriving (Show)  -- If one of the two columns doesn't specify the operator class, we just ignore -- it and still treat them as equivalent.@@ -68,107 +76,129 @@ indexColumnName :: IndexColumn -> RawSQL () indexColumnName (IndexColumn col _) = col -data IndexMethod =-    BTree+data IndexMethod+  = BTree   | GIN   deriving (Eq, Ord)  instance Show IndexMethod where-    show BTree = "btree"-    show GIN   = "gin"+  show BTree = "btree"+  show GIN = "gin"  instance Read IndexMethod where-    readsPrec _ (map toLower -> "btree") = [(BTree,"")]-    readsPrec _ (map toLower -> "gin")   = [(GIN,"")]-    readsPrec _ _       = []+  readsPrec _ (map toLower -> "btree") = [(BTree, "")]+  readsPrec _ (map toLower -> "gin") = [(GIN, "")]+  readsPrec _ _ = []  tblIndex :: TableIndex-tblIndex = TableIndex {-  idxColumns = []-, idxInclude = []-, idxMethod = BTree-, idxUnique = False-, idxValid = True-, idxWhere = Nothing-}+tblIndex =+  TableIndex+    { idxColumns = []+    , idxInclude = []+    , idxMethod = BTree+    , idxUnique = False+    , idxValid = True+    , idxWhere = Nothing+    , idxNotDistinctNulls = False+    }  indexOnColumn :: IndexColumn -> TableIndex-indexOnColumn column = tblIndex { idxColumns = [column] }+indexOnColumn column = tblIndex {idxColumns = [column]}  -- | Create an index on the given column with the specified method.  No checks -- are made that the method is appropriate for the type of the column. indexOnColumnWithMethod :: IndexColumn -> IndexMethod -> TableIndex indexOnColumnWithMethod column method =-    tblIndex { idxColumns = [column]-             , idxMethod = method }+  tblIndex+    { idxColumns = [column]+    , idxMethod = method+    }  indexOnColumns :: [IndexColumn] -> TableIndex-indexOnColumns columns = tblIndex { idxColumns = columns }+indexOnColumns columns = tblIndex {idxColumns = columns}  -- | Create an index on the given columns with the specified method.  No checks -- are made that the method is appropriate for the type of the column; -- cf. [the PostgreSQL manual](https://www.postgresql.org/docs/current/static/indexes-multicolumn.html). indexOnColumnsWithMethod :: [IndexColumn] -> IndexMethod -> TableIndex indexOnColumnsWithMethod columns method =-    tblIndex { idxColumns = columns-             , idxMethod = method }+  tblIndex+    { idxColumns = columns+    , idxMethod = method+    }  uniqueIndexOnColumn :: IndexColumn -> TableIndex-uniqueIndexOnColumn column = TableIndex {-  idxColumns = [column]-, idxInclude = []-, idxMethod = BTree-, idxUnique = True-, idxValid = True-, idxWhere = Nothing-}+uniqueIndexOnColumn column =+  TableIndex+    { idxColumns = [column]+    , idxInclude = []+    , idxMethod = BTree+    , idxUnique = True+    , idxValid = True+    , idxWhere = Nothing+    , idxNotDistinctNulls = False+    }  uniqueIndexOnColumns :: [IndexColumn] -> TableIndex-uniqueIndexOnColumns columns = TableIndex {-  idxColumns = columns-, idxInclude = []-, idxMethod = BTree-, idxUnique = True-, idxValid = True-, idxWhere = Nothing-}+uniqueIndexOnColumns columns =+  TableIndex+    { idxColumns = columns+    , idxInclude = []+    , idxMethod = BTree+    , idxUnique = True+    , idxValid = True+    , idxWhere = Nothing+    , idxNotDistinctNulls = False+    }  uniqueIndexOnColumnWithCondition :: IndexColumn -> RawSQL () -> TableIndex-uniqueIndexOnColumnWithCondition column whereC = TableIndex {-  idxColumns = [column]-, idxInclude = []-, idxMethod = BTree-, idxUnique = True-, idxValid = True-, idxWhere = Just whereC-}+uniqueIndexOnColumnWithCondition column whereC =+  TableIndex+    { idxColumns = [column]+    , idxInclude = []+    , idxMethod = BTree+    , idxUnique = True+    , idxValid = True+    , idxWhere = Just whereC+    , idxNotDistinctNulls = False+    }  indexName :: RawSQL () -> TableIndex -> RawSQL ()-indexName tname TableIndex{..} = flip rawSQL () $ T.take 63 . unRawSQL $ mconcat [-    if idxUnique then "unique_idx__" else "idx__"-  , tname-  , "__"-  , mintercalate "__" $ map (asText sanitize . indexColumnName) idxColumns-  , if null idxInclude-    then ""-    else "$$" <> mintercalate "__" (map (asText sanitize) idxInclude)-  , maybe "" (("__" <>) . hashWhere) idxWhere-  ]+indexName tname TableIndex {..} =+  flip rawSQL () $+    T.take 63 . unRawSQL $+      mconcat+        [ if idxUnique then "unique_idx__" else "idx__"+        , tname+        , "__"+        , mintercalate "__" $ map (asText sanitize . indexColumnName) idxColumns+        , if null idxInclude+            then ""+            else "$$" <> mintercalate "__" (map (asText sanitize) idxInclude)+        , maybe "" (("__" <>) . hashWhere) idxWhere+        ]   where     asText f = flip rawSQL () . f . unRawSQL     -- See http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS.     -- Remove all unallowed characters and replace them by at most one adjacent dollar sign.     sanitize = T.pack . foldr go [] . T.unpack       where-        go c acc = if isAlphaNum c || c == '_'-          then c : acc-          else case acc of-            ('$':_) ->       acc-            _       -> '$' : acc+        go c acc =+          if isAlphaNum c || c == '_'+            then c : acc+            else case acc of+              ('$' : _) -> acc+              _ -> '$' : acc     -- hash WHERE clause and add it to index name so that indexes     -- with the same columns, but different constraints can coexist-    hashWhere = asText $ T.decodeUtf8 . B16.encode . BS.take 10-      . BA.convert . H.hash @_ @H.RIPEMD160 . T.encodeUtf8+    hashWhere =+      asText $+        T.decodeUtf8+          . B16.encode+          . BS.take 10+          . BA.convert+          . H.hash @_ @H.RIPEMD160+          . T.encodeUtf8  -- | Create an index. Warning: if the affected table is large, this will prevent -- the table from being modified during the creation. If this is not acceptable,@@ -183,27 +213,33 @@ sqlCreateIndexConcurrently = sqlCreateIndex_ True  sqlCreateIndex_ :: Bool -> RawSQL () -> TableIndex -> RawSQL ()-sqlCreateIndex_ concurrently tname idx@TableIndex{..} = mconcat [-    "CREATE"-  , if idxUnique then " UNIQUE" else ""-  , " INDEX "-  , if concurrently then "CONCURRENTLY " else ""-  , indexName tname idx-  , " ON" <+> tname-  , " USING" <+> (rawSQL (T.pack . show $ idxMethod) ()) <+> "("-  , mintercalate ", "-      (map-        (\case-          IndexColumn col Nothing -> col-          IndexColumn col (Just opclass) -> col <+> opclass+sqlCreateIndex_ concurrently tname idx@TableIndex {..} =+  mconcat+    [ "CREATE"+    , if idxUnique then " UNIQUE" else ""+    , " INDEX "+    , if concurrently then "CONCURRENTLY " else ""+    , indexName tname idx+    , " ON" <+> tname+    , " USING" <+> rawSQL (T.pack . show $ idxMethod) () <+> "("+    , mintercalate+        ", "+        ( map+            ( \case+                IndexColumn col Nothing -> col+                IndexColumn col (Just opclass) -> col <+> opclass+            )+            idxColumns         )-        idxColumns)-  , ")"-  , if null idxInclude-    then ""-    else " INCLUDE (" <> mintercalate ", " idxInclude <> ")"-  , maybe "" (" WHERE" <+>) idxWhere-  ]+    , ")"+    , if null idxInclude+        then ""+        else " INCLUDE (" <> mintercalate ", " idxInclude <> ")"+    , if idxUnique && idxNotDistinctNulls+        then " NULLS NOT DISTINCT"+        else ""+    , maybe "" (" WHERE" <+>) idxWhere+    ]  -- | Drop an index. Warning: if you don't want to lock out concurrent operations -- on the index's table, use 'DropIndexConcurrentlyMigration'. See
src/Database/PostgreSQL/PQTypes/Model/Migration.hs view
@@ -1,33 +1,31 @@ {-# LANGUAGE CPP #-}-{- | -Using migrations is fairly easy. After you've defined the lists of-migrations and tables, just run-'Database.PostgreSQL.PQTypes.Checks.migrateDatabase':--@-tables :: [Table]-tables = ...--migrations :: [Migration]-migrations = ...--migrateDatabase options extensions domains tables migrations-@--Migrations are run strictly in the order specified in the migrations-list, starting with the first migration for which the corresponding-table in the DB has the version number equal to the 'mgrFrom' field of-the migration.---}--module Database.PostgreSQL.PQTypes.Model.Migration (-    DropTableMode(..),-    MigrationAction(..),-    Migration(..),-    isStandardMigration, isDropTableMigration-  )  where+-- |+--+-- Using migrations is fairly easy. After you've defined the lists of+-- migrations and tables, just run+-- 'Database.PostgreSQL.PQTypes.Checks.migrateDatabase':+--+-- @+-- definitions = emptyDbDefinitions { ... }+--+-- migrations :: [Migration]+-- migrations = ...+--+-- migrateDatabase options definitions migrations+-- @+--+-- Migrations are run strictly in the order specified in the migrations+-- list, starting with the first migration for which the corresponding+-- table in the DB has the version number equal to the 'mgrFrom' field of+-- the migration.+module Database.PostgreSQL.PQTypes.Model.Migration+  ( DropTableMode (..)+  , MigrationAction (..)+  , Migration (..)+  , isStandardMigration+  , isDropTableMigration+  ) where  import Data.Int @@ -39,74 +37,73 @@  -- | Migration action to run, either an arbitrary 'MonadDB' action, or -- something more fine-grained.-data MigrationAction m =--  -- | Standard migration, i.e. an arbitrary 'MonadDB' action.-  StandardMigration (m ())--  -- | Drop table migration. Parameter is the drop table mode-  -- (@RESTRICT@/@CASCADE@). The 'Migration' record holds the name of-  -- the table to drop.-  | DropTableMigration DropTableMode--  -- | Migration for creating an index concurrently.-  | CreateIndexConcurrentlyMigration-      (RawSQL ()) -- ^ Table name-      TableIndex  -- ^ Index--  -- | Migration for dropping an index concurrently.-  | DropIndexConcurrentlyMigration-      (RawSQL ()) -- ^ Table name-      TableIndex  -- ^ Index--  -- | Migration for modifying columns. Parameters are:-  ---  -- Name of the table that the cursor is associated with. It has to be the same as in the-  -- cursor SQL, see the second parameter.-  ---  -- SQL providing a list of primary keys from the associated table that will be used for the cursor.-  ---  -- Function that takes a batch of primary keys provided by the cursor SQL and runs an arbitrary computation-  -- within MonadDB. The function might be called repeatedly depending on the batch size and total number of-  -- selected primary keys. See the last argument.-  ---  -- Batch size of primary keys to be fetched at once by the cursor SQL and be given to the modification function.-  -- To handle multi-column primary keys, the following needs to be done:-  ---  --   1. Get the list of tuples from PostgreSQL.-  --   2. Unzip them into a tuple of lists in Haskell.-  --   3. Pass the lists to PostgreSQL as separate parameters and zip them back in the SQL,-  --      see https://stackoverflow.com/questions/12414750/is-there-something-like-a-zip-function-in-postgresql-that-combines-two-arrays for more details.-  | forall t . FromRow t => ModifyColumnMigration (RawSQL ()) SQL ([t] -> m ()) Int+data MigrationAction m+  = -- | Standard migration, i.e. an arbitrary 'MonadDB' action.+    StandardMigration (m ())+  | -- | Drop table migration. Parameter is the drop table mode+    -- (@RESTRICT@/@CASCADE@). The 'Migration' record holds the name of+    -- the table to drop.+    DropTableMigration DropTableMode+  | -- | Migration for creating an index concurrently.+    CreateIndexConcurrentlyMigration+      (RawSQL ())+      -- ^ Table name+      TableIndex+      -- ^ Index+  | -- | Migration for dropping an index concurrently.+    DropIndexConcurrentlyMigration+      (RawSQL ())+      -- ^ Table name+      TableIndex+      -- ^ Index+  | -- | Migration for modifying columns. Parameters are:+    --+    -- Name of the table that the cursor is associated with. It has to be the same as in the+    -- cursor SQL, see the second parameter.+    --+    -- SQL providing a list of primary keys from the associated table that will be used for the cursor.+    --+    -- Function that takes a batch of primary keys provided by the cursor SQL and runs an arbitrary computation+    -- within MonadDB. The function might be called repeatedly depending on the batch size and total number of+    -- selected primary keys. See the last argument.+    --+    -- Batch size of primary keys to be fetched at once by the cursor SQL and be given to the modification function.+    -- To handle multi-column primary keys, the following needs to be done:+    --+    --   1. Get the list of tuples from PostgreSQL.+    --   2. Unzip them into a tuple of lists in Haskell.+    --   3. Pass the lists to PostgreSQL as separate parameters and zip them back in the SQL,+    --      see https://stackoverflow.com/questions/12414750/is-there-something-like-a-zip-function-in-postgresql-that-combines-two-arrays for more details.+    forall t. FromRow t => ModifyColumnMigration (RawSQL ()) SQL ([t] -> m ()) Int  -- | Migration object.-data Migration m =-  Migration {-  -- | The name of the table you're migrating.-  mgrTableName :: RawSQL ()-  -- | The version you're migrating *from* (you don't specify what+data Migration m+  = Migration+  { mgrTableName :: RawSQL ()+  -- ^ The name of the table you're migrating.+  , mgrFrom :: Int32+  -- ^ The version you're migrating *from* (you don't specify what   -- version you migrate TO, because version is always increased by 1,   -- so if 'mgrFrom' is 2, that means that after that migration is run,   -- table version will equal 3-, mgrFrom   :: Int32-  -- | Migration action.-, mgrAction :: MigrationAction m+  , mgrAction :: MigrationAction m+  -- ^ Migration action.   }  isStandardMigration :: Migration m -> Bool-isStandardMigration Migration{..} =+isStandardMigration Migration {..} =   case mgrAction of-    StandardMigration{}                -> True-    DropTableMigration{}               -> False-    CreateIndexConcurrentlyMigration{} -> False-    DropIndexConcurrentlyMigration{}   -> False-    ModifyColumnMigration{}            -> False+    StandardMigration {} -> True+    DropTableMigration {} -> False+    CreateIndexConcurrentlyMigration {} -> False+    DropIndexConcurrentlyMigration {} -> False+    ModifyColumnMigration {} -> False  isDropTableMigration :: Migration m -> Bool-isDropTableMigration Migration{..} =+isDropTableMigration Migration {..} =   case mgrAction of-    StandardMigration{}                -> False-    DropTableMigration{}               -> True-    CreateIndexConcurrentlyMigration{} -> False-    DropIndexConcurrentlyMigration{}   -> False-    ModifyColumnMigration{}            -> False+    StandardMigration {} -> False+    DropTableMigration {} -> True+    CreateIndexConcurrentlyMigration {} -> False+    DropIndexConcurrentlyMigration {} -> False+    ModifyColumnMigration {} -> False
src/Database/PostgreSQL/PQTypes/Model/PrimaryKey.hs view
@@ -1,8 +1,9 @@-module Database.PostgreSQL.PQTypes.Model.PrimaryKey (-    PrimaryKey+module Database.PostgreSQL.PQTypes.Model.PrimaryKey+  ( PrimaryKey   , pkOnColumn   , pkOnColumns   , pkName+  , pkColumns   , sqlAddPK   , sqlAddPKUsing   , sqlDropPK@@ -20,32 +21,37 @@ pkOnColumn column = Just . PrimaryKey . toNubList $ [column]  pkOnColumns :: [RawSQL ()] -> Maybe PrimaryKey-pkOnColumns []      = Nothing+pkOnColumns [] = Nothing pkOnColumns columns = Just . PrimaryKey . toNubList $ columns  pkName :: RawSQL () -> RawSQL () pkName tname = mconcat ["pk__", tname] +pkColumns :: PrimaryKey -> [RawSQL ()]+pkColumns (PrimaryKey columns) = fromNubList columns+ sqlAddPK :: RawSQL () -> PrimaryKey -> RawSQL ()-sqlAddPK tname (PrimaryKey columns) = smconcat [-    "ADD CONSTRAINT"-  , pkName tname-  , "PRIMARY KEY ("-  , mintercalate ", " $ fromNubList columns-  , ")"-  ]+sqlAddPK tname (PrimaryKey columns) =+  smconcat+    [ "ADD CONSTRAINT"+    , pkName tname+    , "PRIMARY KEY ("+    , mintercalate ", " $ fromNubList columns+    , ")"+    ]  -- | Convert a unique index into a primary key. Main usage is to build a unique -- index concurrently first (so that its creation doesn't conflict with table -- updates on the modified table) and then convert it into a primary key using -- this function. sqlAddPKUsing :: RawSQL () -> TableIndex -> RawSQL ()-sqlAddPKUsing tname idx = smconcat-  [ "ADD CONSTRAINT"-  , pkName tname-  , "PRIMARY KEY USING INDEX"-  , indexName tname idx-  ]+sqlAddPKUsing tname idx =+  smconcat+    [ "ADD CONSTRAINT"+    , pkName tname+    , "PRIMARY KEY USING INDEX"+    , indexName tname idx+    ]  sqlDropPK :: RawSQL () -> RawSQL () sqlDropPK tname = "DROP CONSTRAINT" <+> pkName tname
src/Database/PostgreSQL/PQTypes/Model/Table.hs view
@@ -1,17 +1,17 @@-module Database.PostgreSQL.PQTypes.Model.Table (-    TableColumn(..)+module Database.PostgreSQL.PQTypes.Model.Table+  ( TableColumn (..)   , tblColumn   , sqlAddColumn   , sqlAlterColumn   , sqlDropColumn-  , Rows(..)-  , Table(..)+  , Rows (..)+  , Table (..)   , tblTable   , sqlCreateTable   , sqlAlterTable-  , DropTableMode(..)+  , DropTableMode (..)   , sqlDropTable-  , TableInitialSetup(..)+  , TableInitialSetup (..)   ) where  import Control.Monad.Catch@@ -27,32 +27,35 @@ import Database.PostgreSQL.PQTypes.Model.PrimaryKey import Database.PostgreSQL.PQTypes.Model.Trigger -data TableColumn = TableColumn {-  colName      :: RawSQL ()-, colType      :: ColumnType-, colCollation :: Maybe (RawSQL ())-, colNullable  :: Bool-, colDefault   :: Maybe (RawSQL ())-} deriving Show+data TableColumn = TableColumn+  { colName :: RawSQL ()+  , colType :: ColumnType+  , colCollation :: Maybe (RawSQL ())+  , colNullable :: Bool+  , colDefault :: Maybe (RawSQL ())+  }+  deriving (Show)  tblColumn :: TableColumn-tblColumn = TableColumn {-  colName = error "tblColumn: column name must be specified"-, colType = error "tblColumn: column type must be specified"-, colCollation = Nothing-, colNullable = True-, colDefault = Nothing-}+tblColumn =+  TableColumn+    { colName = error "tblColumn: column name must be specified"+    , colType = error "tblColumn: column type must be specified"+    , colCollation = Nothing+    , colNullable = True+    , colDefault = Nothing+    }  sqlAddColumn :: TableColumn -> RawSQL ()-sqlAddColumn TableColumn{..} = smconcat [-    "ADD COLUMN"-  , colName-  , columnTypeToSQL colType-  , maybe "" (\c -> "COLLATE \"" <> c <> "\"") colCollation-  , if colNullable then "NULL" else "NOT NULL"-  , maybe "" ("DEFAULT" <+>) colDefault-  ]+sqlAddColumn TableColumn {..} =+  smconcat+    [ "ADD COLUMN"+    , colName+    , columnTypeToSQL colType+    , maybe "" (\c -> "COLLATE \"" <> c <> "\"") colCollation+    , if colNullable then "NULL" else "NOT NULL"+    , maybe "" ("DEFAULT" <+>) colDefault+    ]  sqlAlterColumn :: RawSQL () -> RawSQL () -> RawSQL () sqlAlterColumn cname alter = "ALTER COLUMN" <+> cname <+> alter@@ -64,56 +67,61 @@  data Rows = forall row. (Show row, ToRow row) => Rows [ByteString] [row] -data Table =-  Table {-  tblName               :: RawSQL () -- ^ Must be in lower case.-, tblVersion            :: Int32-, tblColumns            :: [TableColumn]-, tblPrimaryKey         :: Maybe PrimaryKey-, tblChecks             :: [Check]-, tblForeignKeys        :: [ForeignKey]-, tblIndexes            :: [TableIndex]-, tblTriggers           :: [Trigger]-, tblInitialSetup       :: Maybe TableInitialSetup-}+data Table+  = Table+  { tblName :: RawSQL ()+  -- ^ Must be in lower case.+  , tblVersion :: Int32+  , tblColumns :: [TableColumn]+  , tblPrimaryKey :: Maybe PrimaryKey+  , tblChecks :: [Check]+  , tblForeignKeys :: [ForeignKey]+  , tblIndexes :: [TableIndex]+  , tblTriggers :: [Trigger]+  , tblInitialSetup :: Maybe TableInitialSetup+  } -data TableInitialSetup = TableInitialSetup {-  checkInitialSetup :: forall m. (MonadDB m, MonadThrow m) => m Bool-, initialSetup      :: forall m. (MonadDB m, MonadThrow m) => m ()-}+data TableInitialSetup = TableInitialSetup+  { checkInitialSetup :: forall m. (MonadDB m, MonadThrow m) => m Bool+  , initialSetup :: forall m. (MonadDB m, MonadThrow m) => m ()+  }  tblTable :: Table-tblTable = Table {-  tblName = error "tblTable: table name must be specified"-, tblVersion = error "tblTable: table version must be specified"-, tblColumns = error "tblTable: table columns must be specified"-, tblPrimaryKey = Nothing-, tblChecks = []-, tblForeignKeys = []-, tblIndexes = []-, tblTriggers = []-, tblInitialSetup = Nothing-}+tblTable =+  Table+    { tblName = error "tblTable: table name must be specified"+    , tblVersion = error "tblTable: table version must be specified"+    , tblColumns = error "tblTable: table columns must be specified"+    , tblPrimaryKey = Nothing+    , tblChecks = []+    , tblForeignKeys = []+    , tblIndexes = []+    , tblTriggers = []+    , tblInitialSetup = Nothing+    }  sqlCreateTable :: RawSQL () -> RawSQL () sqlCreateTable tname = "CREATE TABLE" <+> tname <+> "()"  -- | Whether to also drop objects that depend on the table.-data DropTableMode =-  -- | Automatically drop objects that depend on the table (such as views).-  DropTableCascade |-  -- | Refuse to drop the table if any objects depend on it. This is the default.-  DropTableRestrict+data DropTableMode+  = -- | Automatically drop objects that depend on the table (such as views).+    DropTableCascade+  | -- | Refuse to drop the table if any objects depend on it. This is the default.+    DropTableRestrict  sqlDropTable :: RawSQL () -> DropTableMode -> RawSQL ()-sqlDropTable tname mode = "DROP TABLE" <+> tname-  <+> case mode of-        DropTableCascade  -> "CASCADE"-        DropTableRestrict -> "RESTRICT"+sqlDropTable tname mode =+  "DROP TABLE"+    <+> tname+    <+> case mode of+      DropTableCascade -> "CASCADE"+      DropTableRestrict -> "RESTRICT"  sqlAlterTable :: RawSQL () -> [RawSQL ()] -> RawSQL ()-sqlAlterTable tname alter_statements = smconcat [-    "ALTER TABLE"-  , tname-  , mintercalate ", " alter_statements-  ]+sqlAlterTable tname alter_statements =+  smconcat+    [ "ALTER TABLE"+    , tname+    , mintercalate ", " alter_statements+    ]
src/Database/PostgreSQL/PQTypes/Model/Trigger.hs view
@@ -1,16 +1,15 @@ -- | -- Module: Database.PostgreSQL.PQTypes.Model.Trigger ----- Trigger name must be unique among triggers of same table. Only @CONSTRAINT@ triggers are--- supported. They can only be run @AFTER@ an event. The associated functions are always--- created with no arguments and always @RETURN TRIGGER@.+-- Trigger name must be unique among triggers of the same table. ----- For details, see <https://www.postgresql.org/docs/11/sql-createtrigger.html>.--module Database.PostgreSQL.PQTypes.Model.Trigger (-  -- * Triggers-    TriggerEvent(..)-  , Trigger(..)+-- For details, see <https://www.postgresql.org/docs/current/sql-createtrigger.html>.+module Database.PostgreSQL.PQTypes.Model.Trigger+  ( -- * Triggers+    TriggerKind (..)+  , TriggerActionTime (..)+  , TriggerEvent (..)+  , Trigger (..)   , triggerMakeName   , triggerBaseName   , sqlCreateTrigger@@ -18,10 +17,14 @@   , createTrigger   , dropTrigger   , getDBTriggers-  -- * Trigger functions++    -- * Trigger functions   , sqlCreateTriggerFunction   , sqlDropTriggerFunction   , triggerFunctionMakeName++    -- * Constraints+  , ConstraintAttributes (..)   ) where  import Data.Bits (testBit)@@ -29,60 +32,95 @@ import Data.Int import Data.Monoid.Utils import Data.Set (Set)+import Data.Set qualified as Set import Data.Text (Text)+import Data.Text qualified as Text import Database.PostgreSQL.PQTypes import Database.PostgreSQL.PQTypes.SQL.Builder-import qualified Data.Set as Set-import qualified Data.Text as Text +-- | Timing for a regular trigger.+--+-- @since 1.17.0.0+data TriggerActionTime+  = -- | An @AFTER@ trigger.+    After+  | -- | A @BEFORE@ trigger.+    Before+  deriving (Eq, Show)++-- | Possible combinations of constraint attributes.+--+-- @since 1.17.0.0+data ConstraintAttributes+  = -- | The @NOT DEFERRABLE [INITIALLY IMMEDIATE]@ constraint.+    -- A @NOT DEFERRABLE@ constraint is @INITIALLY IMMEDIATE@ by default.+    NotDeferrable+  | -- | The @DEFERRABLE [INITIALLY IMMEDIATE]@ constraint.+    -- A @DEFERRABLE@ constraint is @INITIALLY IMMEDIATE@ by default.+    Deferrable+  | -- | The @DEFERRABLE INITIALLY DEFERRED@ constraint.+    DeferrableInitiallyDeferred+  deriving (Eq, Show)++-- | Trigger kind.+--+-- @since 1.17.0.0+data TriggerKind+  = -- | Create a regular trigger: @CREATE TRIGGER@+    TriggerRegular TriggerActionTime+  | -- | Create a constraint trigger: @CREATE CONSTRAINT TRIGGER@+    TriggerConstraint ConstraintAttributes+  deriving (Eq, Show)+ -- | Trigger event name. -- -- @since 1.15.0.0 data TriggerEvent-  = TriggerInsert-  -- ^ The @INSERT@ event.-  | TriggerUpdate-  -- ^ The @UPDATE@ event.-  | TriggerUpdateOf [RawSQL ()]-  -- ^ The @UPDATE OF column1 [, column2 ...]@ event.-  | TriggerDelete-  -- ^ The @DELETE@ event.+  = -- | The @INSERT@ event.+    TriggerInsert+  | -- | The @UPDATE@ event.+    TriggerUpdate+  | -- | The @UPDATE OF column1 [, column2 ...]@ event.+    TriggerUpdateOf [RawSQL ()]+  | -- | The @DELETE@ event.+    TriggerDelete   deriving (Eq, Ord, Show)  -- | Trigger. -- -- @since 1.15.0.0-data Trigger = Trigger {-    triggerTable             :: RawSQL ()-    -- ^ The table that the trigger is associated with.-  , triggerName              :: RawSQL ()-    -- ^ The internal name without any prefixes. Trigger name must be unique among-    -- triggers of same table. See 'triggerMakeName'.-  , triggerEvents            :: Set TriggerEvent-    -- ^ The set of events. Corresponds to the @{ __event__ [ OR ... ] }@ in the trigger-    -- definition. The order in which they are defined doesn't matter and there can-    -- only be one of each.-  , triggerDeferrable        :: Bool-    -- ^ Is the trigger @DEFERRABLE@ or @NOT DEFERRABLE@ ?-  , triggerInitiallyDeferred :: Bool-    -- ^ Is the trigger @INITIALLY DEFERRED@ or @INITIALLY IMMEDIATE@ ?-  , triggerWhen              :: Maybe (RawSQL ())-    -- ^ The condition that specifies whether the trigger should fire. Corresponds to the-    -- @WHEN ( __condition__ )@ in the trigger definition.-  , triggerFunction          :: RawSQL ()-    -- ^ The function to execute when the trigger fires.-} deriving (Show)+data Trigger = Trigger+  { triggerTable :: RawSQL ()+  -- ^ The table that the trigger is associated with.+  , triggerName :: RawSQL ()+  -- ^ The internal name without any prefixes. Trigger name must be unique among+  -- triggers of same table. See 'triggerMakeName'.+  , triggerKind :: TriggerKind+  -- ^ The kind of trigger we want to create.+  -- @since 1.17.0.0+  , triggerEvents :: Set TriggerEvent+  -- ^ The set of events. Corresponds to the @{ __event__ [ OR ... ] }@ in the trigger+  -- definition. The order in which they are defined doesn't matter and there can+  -- only be one of each.+  , triggerWhen :: Maybe (RawSQL ())+  -- ^ The condition that specifies whether the trigger should fire. Corresponds to the+  -- @WHEN ( __condition__ )@ in the trigger definition.+  , triggerFunction :: RawSQL ()+  -- ^ The function to execute when the trigger fires.+  -- The function is declared as taking no arguments and returning type @trigger@.+  }+  deriving (Show)  instance Eq Trigger where   t1 == t2 =     triggerTable t1 == triggerTable t2-    && triggerName t1 == triggerName t2-    && triggerEvents t1 == triggerEvents t2-    && triggerDeferrable t1 == triggerDeferrable t2-    && triggerInitiallyDeferred t1 == triggerInitiallyDeferred t2-    && triggerWhen t1 == triggerWhen t2-    -- Function source code is not guaranteed to be equal, so we ignore it.+      && triggerName t1 == triggerName t2+      && triggerKind t1 == triggerKind t2+      && triggerEvents t1 == triggerEvents t2+      && triggerWhen t1 == triggerWhen t2 +-- Function source code is not guaranteed to be equal, so we ignore it.+ -- | Make a trigger name that can be used in SQL. -- -- Given a base @name@ and @tableName@, return a new name that will be used as the@@ -107,47 +145,57 @@ triggerEventName = \case   TriggerInsert -> "INSERT"   TriggerUpdate -> "UPDATE"-  TriggerUpdateOf columns -> if null columns-                             then error "UPDATE OF must have columns."-                             else "UPDATE OF" <+> mintercalate ", " columns+  TriggerUpdateOf columns ->+    if null columns+      then error "UPDATE OF must have columns."+      else "UPDATE OF" <+> mintercalate ", " columns   TriggerDelete -> "DELETE"  -- | Build an SQL statement that creates a trigger. ----- Only supports @CONSTRAINT@ triggers which can only run @AFTER@.--- -- @since 1.15.0 sqlCreateTrigger :: Trigger -> RawSQL ()-sqlCreateTrigger Trigger{..} =-  "CREATE CONSTRAINT TRIGGER" <+> trgName-    <+> "AFTER" <+> trgEvents-    <+> "ON" <+> triggerTable-    <+> trgTiming+sqlCreateTrigger Trigger {..} =+  "CREATE"+    <+> trgKind+    <+> trgName+    <+> tgrActionTime+    <+> trgEvents+    <+> "ON"+    <+> triggerTable+    <+> trgConstraintAttributes     <+> "FOR EACH ROW"     <+> trgWhen-    <+> "EXECUTE FUNCTION" <+> trgFunction+    <+> "EXECUTE FUNCTION"+    <+> trgFunction     <+> "()"   where+    trgKind = case triggerKind of+      TriggerRegular _ -> "TRIGGER"+      TriggerConstraint _ -> "CONSTRAINT TRIGGER"     trgName       | triggerName == "" = error "Trigger must have a name."       | otherwise = triggerMakeName triggerName triggerTable+    tgrActionTime = case triggerKind of+      TriggerRegular After -> "AFTER"+      TriggerRegular Before -> "BEFORE"+      TriggerConstraint _ -> "AFTER"     trgEvents       | triggerEvents == Set.empty = error "Trigger must have at least one event."       | otherwise = mintercalate " OR " . map triggerEventName $ Set.toList triggerEvents-    trgTiming = let deferrable = (if triggerDeferrable then "" else "NOT") <+> "DEFERRABLE"-                    deferred   = if triggerInitiallyDeferred-                                 then "INITIALLY DEFERRED"-                                 else "INITIALLY IMMEDIATE"-                in deferrable <+> deferred+    trgConstraintAttributes = case triggerKind of+      TriggerRegular _ -> ""+      TriggerConstraint NotDeferrable -> "NOT DEFERRABLE"+      TriggerConstraint Deferrable -> "DEFERRABLE"+      TriggerConstraint DeferrableInitiallyDeferred -> "DEFERRABLE INITIALLY DEFERRED"     trgWhen = maybe "" (\w -> "WHEN (" <+> w <+> ")") triggerWhen     trgFunction = triggerFunctionMakeName triggerName - -- | Build an SQL statement that drops a trigger. -- -- @since 1.15.0 sqlDropTrigger :: Trigger -> RawSQL ()-sqlDropTrigger Trigger{..} =+sqlDropTrigger Trigger {..} =   -- In theory, because the trigger is dependent on its function, it should be enough to   -- 'DROP FUNCTION triggerFunction CASCADE'. However, let's make this safe and go with   -- the default RESTRICT here.@@ -197,8 +245,9 @@   runQuery_ . sqlSelect "pg_trigger t" $ do     sqlResult "t.tgname::text" -- name     sqlResult "t.tgtype" -- smallint == int2 => (2 bytes)+    sqlResult "t.tgconstraint > 0" -- we only check if CONSTRAINT has been specified     sqlResult "t.tgdeferrable" -- boolean-    sqlResult "t.tginitdeferred"-- boolean+    sqlResult "t.tginitdeferred" -- boolean     -- This gets the entire query that created this trigger. Note that it's decompiled and     -- normalized, which means that it's likely not what the user actually typed. For     -- example, if the original query had excessive whitespace in it, it won't be in this@@ -213,16 +262,16 @@     sqlWhereEq "c.relname" $ unRawSQL tableName   fetchMany getTrigger   where-    getTrigger :: (String, Int16, Bool, Bool, String, String, String, String) -> (Trigger, RawSQL ())-    getTrigger (tgname, tgtype, tgdeferrable, tginitdeferrable, triggerdef, proname, prosrc, tblName) =-      ( Trigger { triggerTable = tableName'-                , triggerName = triggerBaseName (unsafeSQL tgname) tableName'-                , triggerEvents = trgEvents-                , triggerDeferrable = tgdeferrable-                , triggerInitiallyDeferred = tginitdeferrable-                , triggerWhen = tgrWhen-                , triggerFunction = unsafeSQL prosrc-                }+    getTrigger :: (String, Int16, Bool, Bool, Bool, String, String, String, String) -> (Trigger, RawSQL ())+    getTrigger (tgname, tgtype, tgconstraint, tgdeferrable, tginitdeferrable, triggerdef, proname, prosrc, tblName) =+      ( Trigger+          { triggerTable = tableName'+          , triggerName = triggerBaseName (unsafeSQL tgname) tableName'+          , triggerKind = tgrKind+          , triggerEvents = trgEvents+          , triggerWhen = tgrWhen+          , triggerFunction = unsafeSQL prosrc+          }       , unsafeSQL proname       )       where@@ -233,8 +282,8 @@         parseBetween left right =           let (prefix, match) = Text.breakOnEnd left $ Text.pack triggerdef           in if Text.null prefix-             then Nothing-             else Just $ (rawSQL . fst $ Text.breakOn right match) ()+              then Nothing+              else Just $ (rawSQL . fst $ Text.breakOn right match) ()          -- Get the WHEN part of the query. Anything between WHEN and EXECUTE is what we         -- want. The Postgres' grammar guarantees that WHEN and EXECUTE are always next to@@ -242,28 +291,53 @@         tgrWhen :: Maybe (RawSQL ())         tgrWhen = parseBetween "WHEN (" ") EXECUTE" +        tgrKind :: TriggerKind+        tgrKind =+          if tgconstraint+            then TriggerConstraint trgConstraintAttrs+            else TriggerRegular tgrActionTime++        trgConstraintAttrs :: ConstraintAttributes+        trgConstraintAttrs = case (tgdeferrable, tginitdeferrable) of+          (False, False) -> NotDeferrable+          (True, False) -> Deferrable+          (True, True) -> DeferrableInitiallyDeferred+          (False, True) -> error "A constraint declared INITIALLY DEFERRED must be DEFERRABLE."++        tgrActionTime :: TriggerActionTime+        tgrActionTime = case (tgtypeInsteadBit, tgtypeBeforeBit) of+          (False, False) -> After+          (False, True) -> Before+          (True, False) -> error "INSTEAD OF triggers are not available on tables."+          (True, True) -> error "The tgtype can't match more than one timing."+          where+            -- Taken from PostgreSQL sources: src/include/catalog/pg_trigger.h:+            tgtypeBeforeBit = testBit tgtype 1 -- #define TRIGGER_TYPE_BEFORE (1 << 1)+            tgtypeInsteadBit = testBit tgtype 6 -- #define TRIGGER_TYPE_INSTEAD (1 << 6)+         -- Similarly, in case of UPDATE OF, the columns can be simply parsed from the         -- original query. Note that UPDATE and UPDATE OF are mutually exclusive and have         -- the same bit set in the underlying tgtype bit field.         trgEvents :: Set TriggerEvent         trgEvents =-          foldl' (\set (mask, event) ->-                    if testBit tgtype mask-                    then-                      Set.insert-                        (if event == TriggerUpdate-                         then maybe event trgUpdateOf $ parseBetween "UPDATE OF " " ON"-                         else event-                        )-                        set-                    else set-                 )-          Set.empty-          -- Taken from PostgreSQL sources: src/include/catalog/pg_trigger.h:-          [ (2, TriggerInsert) -- #define TRIGGER_TYPE_INSERT (1 << 2)-          , (3, TriggerDelete) -- #define TRIGGER_TYPE_DELETE (1 << 3)-          , (4, TriggerUpdate) -- #define TRIGGER_TYPE_UPDATE (1 << 4)-          ]+          foldl'+            ( \set (mask, event) ->+                if testBit tgtype mask+                  then+                    Set.insert+                      ( if event == TriggerUpdate+                          then maybe event trgUpdateOf $ parseBetween "UPDATE OF " " ON"+                          else event+                      )+                      set+                  else set+            )+            Set.empty+            -- Taken from PostgreSQL sources: src/include/catalog/pg_trigger.h:+            [ (2, TriggerInsert) -- #define TRIGGER_TYPE_INSERT (1 << 2)+            , (3, TriggerDelete) -- #define TRIGGER_TYPE_DELETE (1 << 3)+            , (4, TriggerUpdate) -- #define TRIGGER_TYPE_UPDATE (1 << 4)+            ]          trgUpdateOf :: RawSQL () -> TriggerEvent         trgUpdateOf columnsSQL =@@ -277,10 +351,10 @@ -- -- @since 1.15.0.0 sqlCreateTriggerFunction :: Trigger -> RawSQL ()-sqlCreateTriggerFunction Trigger{..} =+sqlCreateTriggerFunction Trigger {..} =   "CREATE FUNCTION"     <+> triggerFunctionMakeName triggerName-    <>  "()"+    <> "()"     <+> "RETURNS TRIGGER"     <+> "AS $$"     <+> triggerFunction@@ -293,7 +367,7 @@ -- -- @since 1.15.0.0 sqlDropTriggerFunction :: Trigger -> RawSQL ()-sqlDropTriggerFunction Trigger{..} =+sqlDropTriggerFunction Trigger {..} =   "DROP FUNCTION" <+> triggerFunctionMakeName triggerName <+> "RESTRICT"  -- | Make a trigger function name that can be used in SQL.@@ -305,4 +379,3 @@ -- @since 1.16.0.0 triggerFunctionMakeName :: RawSQL () -> RawSQL () triggerFunctionMakeName name = "trgfun__" <> name-
src/Database/PostgreSQL/PQTypes/SQL/Builder.hs view
@@ -1,91 +1,88 @@-{- |--Module "Database.PostgreSQL.PQTypes.SQL.Builder" offers a nice-monadic DSL for building SQL statements on the fly. Some examples:-->>> :{-sqlSelect "documents" $ do-  sqlResult "id"-  sqlResult "title"-  sqlResult "mtime"-  sqlOrderBy "documents.mtime DESC"-  sqlWhereILike "documents.title" "%pattern%"-:}-SQL " SELECT  id, title, mtime FROM documents WHERE (documents.title ILIKE <\"%pattern%\">)    ORDER BY documents.mtime DESC  "--@SQL.Builder@ supports SELECT as 'sqlSelect' and data manipulation using-'sqlInsert', 'sqlInsertSelect', 'sqlDelete' and 'sqlUpdate'.-->>> import Data.Time->>> let title = "title" :: String->>> let ctime  = read "2020-01-01 00:00:00 UTC" :: UTCTime->>> :{-sqlInsert "documents" $ do-  sqlSet "title" title-  sqlSet "ctime" ctime-  sqlResult "id"-:}-SQL " INSERT INTO documents (title, ctime) VALUES (<\"title\">, <2020-01-01 00:00:00 UTC>)  RETURNING id"--The 'sqlInsertSelect' is particulary interesting as it supports INSERT-of values taken from a SELECT clause from same or even different-tables.--There is a possibility to do multiple inserts at once. Data given by-'sqlSetList' will be inserted multiple times, data given by 'sqlSet'-will be multiplied as many times as needed to cover all inserted rows-(it is common to all rows). If you use multiple 'sqlSetList' then-lists will be made equal in length by appending @DEFAULT@ as fill-element.-->>> :{-sqlInsert "documents" $ do-  sqlSet "ctime" ctime-  sqlSetList "title" ["title1", "title2", "title3"]-  sqlResult "id"-:}-SQL " INSERT INTO documents (ctime, title) VALUES (<2020-01-01 00:00:00 UTC>, <\"title1\">) , (<2020-01-01 00:00:00 UTC>, <\"title2\">) , (<2020-01-01 00:00:00 UTC>, <\"title3\">)  RETURNING id"--The above will insert 3 new documents.--@SQL.Builder@ provides quite a lot of SQL magic, including @ORDER BY@ as-'sqlOrderBy', @GROUP BY@ as 'sqlGroupBy'.-->>> :{-sqlSelect "documents" $ do-  sqlResult "id"-  sqlResult "title"-  sqlResult "mtime"-  sqlOrderBy "documents.mtime DESC"-  sqlOrderBy "documents.title"-  sqlGroupBy "documents.status"-  sqlJoinOn "users" "documents.user_id = users.id"-  sqlWhere $ mkSQL "documents.title ILIKE" <?> "%pattern%"-:}-SQL " SELECT  id, title, mtime FROM documents  JOIN  users  ON  documents.user_id = users.id WHERE (documents.title ILIKE <\"%pattern%\">)  GROUP BY documents.status  ORDER BY documents.mtime DESC, documents.title  "--Joins are done by 'sqlJoinOn', 'sqlLeftJoinOn', 'sqlRightJoinOn',-'sqlJoinOn', 'sqlFullJoinOn'. If everything fails use 'sqlJoin' and-'sqlFrom' to set join clause as string. Support for a join grammars as-some kind of abstract syntax data type is lacking.-->>> :{-sqlDelete "mails" $ do-  sqlWhere "id > 67"-:}-SQL " DELETE FROM mails  WHERE (id > 67) "-->>> :{-sqlUpdate "document_tags" $ do-  sqlSet "value" (123 :: Int)-  sqlWhere "name = 'abc'"-:}-SQL " UPDATE document_tags SET value=<123>  WHERE (name = 'abc') "---}- -- TODO: clean this up, add more documentation. +-- |+--+-- Module "Database.PostgreSQL.PQTypes.SQL.Builder" offers a nice+-- monadic DSL for building SQL statements on the fly. Some examples:+--+-- >>> :{+-- sqlSelect "documents" $ do+--   sqlResult "id"+--   sqlResult "title"+--   sqlResult "mtime"+--   sqlOrderBy "documents.mtime DESC"+--   sqlWhereILike "documents.title" "%pattern%"+-- :}+-- SQL " SELECT  id, title, mtime FROM documents WHERE (documents.title ILIKE <\"%pattern%\">)    ORDER BY documents.mtime DESC  "+--+-- @SQL.Builder@ supports SELECT as 'sqlSelect' and data manipulation using+-- 'sqlInsert', 'sqlInsertSelect', 'sqlDelete' and 'sqlUpdate'.+--+-- >>> import Data.Time+-- >>> let title = "title" :: String+-- >>> let ctime  = read "2020-01-01 00:00:00 UTC" :: UTCTime+-- >>> :{+-- sqlInsert "documents" $ do+--   sqlSet "title" title+--   sqlSet "ctime" ctime+--   sqlResult "id"+-- :}+-- SQL " INSERT INTO documents (title, ctime) VALUES (<\"title\">, <2020-01-01 00:00:00 UTC>)  RETURNING id"+--+-- The 'sqlInsertSelect' is particulary interesting as it supports INSERT+-- of values taken from a SELECT clause from same or even different+-- tables.+--+-- There is a possibility to do multiple inserts at once. Data given by+-- 'sqlSetList' will be inserted multiple times, data given by 'sqlSet'+-- will be multiplied as many times as needed to cover all inserted rows+-- (it is common to all rows). If you use multiple 'sqlSetList' then+-- lists will be made equal in length by appending @DEFAULT@ as fill+-- element.+--+-- >>> :{+-- sqlInsert "documents" $ do+--   sqlSet "ctime" ctime+--   sqlSetList "title" ["title1", "title2", "title3"]+--   sqlResult "id"+-- :}+-- SQL " INSERT INTO documents (ctime, title) VALUES (<2020-01-01 00:00:00 UTC>, <\"title1\">) , (<2020-01-01 00:00:00 UTC>, <\"title2\">) , (<2020-01-01 00:00:00 UTC>, <\"title3\">)  RETURNING id"+--+-- The above will insert 3 new documents.+--+-- @SQL.Builder@ provides quite a lot of SQL magic, including @ORDER BY@ as+-- 'sqlOrderBy', @GROUP BY@ as 'sqlGroupBy'.+--+-- >>> :{+-- sqlSelect "documents" $ do+--   sqlResult "id"+--   sqlResult "title"+--   sqlResult "mtime"+--   sqlOrderBy "documents.mtime DESC"+--   sqlOrderBy "documents.title"+--   sqlGroupBy "documents.status"+--   sqlJoinOn "users" "documents.user_id = users.id"+--   sqlWhere $ mkSQL "documents.title ILIKE" <?> "%pattern%"+-- :}+-- SQL " SELECT  id, title, mtime FROM documents  JOIN  users  ON  documents.user_id = users.id WHERE (documents.title ILIKE <\"%pattern%\">)  GROUP BY documents.status  ORDER BY documents.mtime DESC, documents.title  "+--+-- Joins are done by 'sqlJoinOn', 'sqlLeftJoinOn', 'sqlRightJoinOn',+-- 'sqlJoinOn', 'sqlFullJoinOn'. If everything fails use 'sqlJoin' and+-- 'sqlFrom' to set join clause as string. Support for a join grammars as+-- some kind of abstract syntax data type is lacking.+--+-- >>> :{+-- sqlDelete "mails" $ do+--   sqlWhere "id > 67"+-- :}+-- SQL " DELETE FROM mails  WHERE (id > 67) "+--+-- >>> :{+-- sqlUpdate "document_tags" $ do+--   sqlSet "value" (123 :: Int)+--   sqlWhere "name = 'abc'"+-- :}+-- SQL " UPDATE document_tags SET value=<123>  WHERE (name = 'abc') " module Database.PostgreSQL.PQTypes.SQL.Builder   ( sqlWhere   , sqlWhereEq@@ -102,7 +99,6 @@   , sqlWhereILike   , sqlWhereIsNULL   , sqlWhereIsNotNULL-   , sqlFrom   , sqlJoin   , sqlJoinOn@@ -127,25 +123,27 @@   , sqlLimit   , sqlDistinct   , sqlWith+  , sqlWithRecursive   , sqlWithMaterialized   , sqlUnion   , sqlUnionAll   , checkAndRememberMaterializationSupport-   , sqlSelect   , sqlSelect2-  , SqlSelect(..)+  , SqlSelect (..)   , sqlInsert-  , SqlInsert(..)+  , SqlInsert (..)   , sqlInsertSelect-  , SqlInsertSelect(..)+  , SqlInsertSelect (..)   , sqlUpdate-  , SqlUpdate(..)+  , SqlUpdate (..)   , sqlDelete-  , SqlDelete(..)-+  , SqlDelete (..)+  , SqlWhereAll (..)+  , sqlAll+  , SqlWhereAny (..)+  , sqlAny   , sqlWhereAny-   , SqlResult   , SqlSet   , SqlFrom@@ -155,25 +153,23 @@   , SqlGroupByHaving   , SqlOffsetLimit   , SqlDistinct--  , SqlCondition(..)+  , SqlCondition (..)   , sqlGetWhereConditions--  , Sqlable(..)+  , Sqlable (..)   , sqlOR   , sqlConcatComma   , sqlConcatAND   , sqlConcatOR   , parenthesize-  , AscDesc(..)+  , AscDesc (..)   )-  where+where -import Control.Monad.State import Control.Monad.Catch-import Data.Int-import Data.IORef+import Control.Monad.State import Data.Either+import Data.IORef+import Data.Int import Data.List import Data.Maybe import Data.Monoid.Utils@@ -221,76 +217,90 @@ -- structure of a condition. For now it seems that the only -- interesting case is EXISTS (SELECT ...), because that internal -- SELECT can have explainable clauses.-data SqlCondition = SqlPlainCondition SQL-                  | SqlExistsCondition SqlSelect-                    deriving (Typeable, Show)+data SqlCondition+  = SqlPlainCondition SQL+  | SqlExistsCondition SqlSelect+  deriving (Typeable, Show)  instance Sqlable SqlCondition where   toSQLCommand (SqlPlainCondition a) = a-  toSQLCommand (SqlExistsCondition a) = "EXISTS (" <> toSQLCommand (a { sqlSelectResult = ["TRUE"] }) <> ")"+  toSQLCommand (SqlExistsCondition a) = "EXISTS (" <> toSQLCommand (a {sqlSelectResult = ["TRUE"]}) <> ")"  data SqlSelect = SqlSelect-  { sqlSelectFrom     :: SQL-  , sqlSelectUnion    :: [SQL]+  { sqlSelectFrom :: SQL+  , sqlSelectUnion :: [SQL]   , sqlSelectUnionAll :: [SQL]   , sqlSelectDistinct :: Bool-  , sqlSelectResult   :: [SQL]-  , sqlSelectWhere    :: [SqlCondition]-  , sqlSelectOrderBy  :: [SQL]-  , sqlSelectGroupBy  :: [SQL]-  , sqlSelectHaving   :: [SQL]-  , sqlSelectOffset   :: Integer-  , sqlSelectLimit    :: Integer-  , sqlSelectWith     :: [(SQL, SQL, Materialized)]+  , sqlSelectResult :: [SQL]+  , sqlSelectWhere :: [SqlCondition]+  , sqlSelectOrderBy :: [SQL]+  , sqlSelectGroupBy :: [SQL]+  , sqlSelectHaving :: [SQL]+  , sqlSelectOffset :: Integer+  , sqlSelectLimit :: Integer+  , sqlSelectWith :: [(SQL, SQL, Materialized)]+  , sqlSelectRecursiveWith :: Recursive   }  data SqlUpdate = SqlUpdate-  { sqlUpdateWhat   :: SQL-  , sqlUpdateFrom   :: SQL-  , sqlUpdateWhere  :: [SqlCondition]-  , sqlUpdateSet    :: [(SQL,SQL)]+  { sqlUpdateWhat :: SQL+  , sqlUpdateFrom :: SQL+  , sqlUpdateWhere :: [SqlCondition]+  , sqlUpdateSet :: [(SQL, SQL)]   , sqlUpdateResult :: [SQL]-  , sqlUpdateWith   :: [(SQL, SQL, Materialized)]+  , sqlUpdateWith :: [(SQL, SQL, Materialized)]+  , sqlUpdateRecursiveWith :: Recursive   }  data SqlInsert = SqlInsert-  { sqlInsertWhat       :: SQL+  { sqlInsertWhat :: SQL   , sqlInsertOnConflict :: Maybe (SQL, Maybe SQL)-  , sqlInsertSet        :: [(SQL, Multiplicity SQL)]-  , sqlInsertResult     :: [SQL]-  , sqlInsertWith       :: [(SQL, SQL, Materialized)]+  , sqlInsertSet :: [(SQL, Multiplicity SQL)]+  , sqlInsertResult :: [SQL]+  , sqlInsertWith :: [(SQL, SQL, Materialized)]+  , sqlInsertRecursiveWith :: Recursive   }  data SqlInsertSelect = SqlInsertSelect-  { sqlInsertSelectWhat       :: SQL+  { sqlInsertSelectWhat :: SQL   , sqlInsertSelectOnConflict :: Maybe (SQL, Maybe SQL)-  , sqlInsertSelectDistinct   :: Bool-  , sqlInsertSelectSet        :: [(SQL, SQL)]-  , sqlInsertSelectResult     :: [SQL]-  , sqlInsertSelectFrom       :: SQL-  , sqlInsertSelectWhere      :: [SqlCondition]-  , sqlInsertSelectOrderBy    :: [SQL]-  , sqlInsertSelectGroupBy    :: [SQL]-  , sqlInsertSelectHaving     :: [SQL]-  , sqlInsertSelectOffset     :: Integer-  , sqlInsertSelectLimit      :: Integer-  , sqlInsertSelectWith       :: [(SQL, SQL, Materialized)]+  , sqlInsertSelectDistinct :: Bool+  , sqlInsertSelectSet :: [(SQL, SQL)]+  , sqlInsertSelectResult :: [SQL]+  , sqlInsertSelectFrom :: SQL+  , sqlInsertSelectWhere :: [SqlCondition]+  , sqlInsertSelectOrderBy :: [SQL]+  , sqlInsertSelectGroupBy :: [SQL]+  , sqlInsertSelectHaving :: [SQL]+  , sqlInsertSelectOffset :: Integer+  , sqlInsertSelectLimit :: Integer+  , sqlInsertSelectWith :: [(SQL, SQL, Materialized)]+  , sqlInsertSelectRecursiveWith :: Recursive   }  data SqlDelete = SqlDelete-  { sqlDeleteFrom   :: SQL-  , sqlDeleteUsing  :: SQL-  , sqlDeleteWhere  :: [SqlCondition]+  { sqlDeleteFrom :: SQL+  , sqlDeleteUsing :: SQL+  , sqlDeleteWhere :: [SqlCondition]   , sqlDeleteResult :: [SQL]-  , sqlDeleteWith   :: [(SQL, SQL, Materialized)]+  , sqlDeleteWith :: [(SQL, SQL, Materialized)]+  , sqlDeleteRecursiveWith :: Recursive   } --- | This is not exported and is used as an implementation detail in--- 'sqlWhereAll'.-data SqlAll = SqlAll-  { sqlAllWhere :: [SqlCondition]+-- | Type representing a set of conditions that are joined by 'AND'.+--+-- When no conditions are given, the result is 'TRUE'.+newtype SqlWhereAll = SqlWhereAll+  { sqlWhereAllWhere :: [SqlCondition]   } +-- | Type representing a set of conditions that are joined by 'OR'.+--+-- When no conditions are given, the result is 'FALSE'.+newtype SqlWhereAny = SqlWhereAny+  { sqlWhereAnyWhere :: [SqlCondition]+  }+ instance Show SqlSelect where   show = show . toSQLCommand @@ -306,14 +316,17 @@ instance Show SqlDelete where   show = show . toSQLCommand -instance Show SqlAll where+instance Show SqlWhereAll where   show = show . toSQLCommand +instance Show SqlWhereAny where+  show = show . toSQLCommand+ emitClause :: Sqlable sql => SQL -> sql -> SQL emitClause name s = case toSQLCommand s of   sql     | isSqlEmpty sql -> ""-    | otherwise   -> name <+> sql+    | otherwise -> name <+> sql  emitClausesSep :: SQL -> SQL -> [SQL] -> SQL emitClausesSep _name _sep [] = mempty@@ -339,99 +352,109 @@   withSQL = withSQL . toSQLCommand  instance Sqlable SqlSelect where-  toSQLCommand cmd = smconcat-    [ emitClausesSepComma "WITH" $-      map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlSelectWith cmd)-    , if hasUnion || hasUnionAll-      then emitClausesSep "" unionKeyword (mainSelectClause : unionCmd)-      else mainSelectClause-    , emitClausesSepComma "GROUP BY" (sqlSelectGroupBy cmd)-    , emitClausesSep "HAVING" "AND" (sqlSelectHaving cmd)-    , orderByClause-    , if sqlSelectOffset cmd > 0-      then unsafeSQL ("OFFSET " ++ show (sqlSelectOffset cmd))-      else ""-    , if sqlSelectLimit cmd >= 0-      then limitClause-      else ""-    ]-    where-      mainSelectClause = smconcat-        [ "SELECT" <+> (if sqlSelectDistinct cmd then "DISTINCT" else mempty)-        , sqlConcatComma (sqlSelectResult cmd)-        , emitClause "FROM" (sqlSelectFrom cmd)-        , emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlSelectWhere cmd)-        -- If there's a union, the result is sorted and has a limit, applying-        -- the order and limit to the main subquery won't reduce the overall-        -- query result, but might reduce its processing time.-        , if hasUnion && not (null $ sqlSelectOrderBy cmd) && sqlSelectLimit cmd >= 0-          then smconcat [orderByClause, limitClause]+  toSQLCommand cmd =+    smconcat+      [ emitClausesSepComma (recursiveClause $ sqlSelectRecursiveWith cmd) $+          map (\(name, command, mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlSelectWith cmd)+      , if hasUnion || hasUnionAll+          then emitClausesSep "" unionKeyword (mainSelectClause : unionCmd)+          else mainSelectClause+      , emitClausesSepComma "GROUP BY" (sqlSelectGroupBy cmd)+      , emitClausesSep "HAVING" "AND" (sqlSelectHaving cmd)+      , orderByClause+      , if sqlSelectOffset cmd > 0+          then unsafeSQL ("OFFSET " ++ show (sqlSelectOffset cmd))           else ""-        ]+      , if sqlSelectLimit cmd >= 0+          then limitClause+          else ""+      ]+    where+      mainSelectClause =+        smconcat+          [ "SELECT" <+> (if sqlSelectDistinct cmd then "DISTINCT" else mempty)+          , sqlConcatComma (sqlSelectResult cmd)+          , emitClause "FROM" (sqlSelectFrom cmd)+          , emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlSelectWhere cmd)+          , -- If there's a union, the result is sorted and has a limit, applying+            -- the order and limit to the main subquery won't reduce the overall+            -- query result, but might reduce its processing time.+            if hasUnion && not (null $ sqlSelectOrderBy cmd) && sqlSelectLimit cmd >= 0+              then smconcat [orderByClause, limitClause]+              else ""+          ] -      hasUnion      = not . null $ sqlSelectUnion cmd-      hasUnionAll   = not . null $ sqlSelectUnionAll cmd+      hasUnion = not . null $ sqlSelectUnion cmd+      hasUnionAll = not . null $ sqlSelectUnionAll cmd       unionKeyword = case (hasUnion, hasUnionAll) of-                        (False, True) -> "UNION ALL"-                        (True, False) -> "UNION"-                        -- False, False is caught by the (hasUnion || hasUnionAll) above.-                        -- Hence, the catch-all is implicitly for (True, True).-                        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."+        (False, True) -> "UNION ALL"+        (True, False) -> "UNION"+        -- False, False is caught by the (hasUnion || hasUnionAll) above.+        -- Hence, the catch-all is implicitly for (True, True).+        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."       unionCmd = case (hasUnion, hasUnionAll) of-                        (False, True) -> sqlSelectUnionAll cmd-                        (True, False) -> sqlSelectUnion cmd-                        -- False, False is caught by the (hasUnion || hasUnionAll) above.-                        -- Hence, the catch-all is implicitly for (True, True).-                        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."+        (False, True) -> sqlSelectUnionAll cmd+        (True, False) -> sqlSelectUnion cmd+        -- False, False is caught by the (hasUnion || hasUnionAll) above.+        -- Hence, the catch-all is implicitly for (True, True).+        _ -> error "Having both `sqlSelectUnion` and `sqlSelectUnionAll` is not supported at the moment."       orderByClause = emitClausesSepComma "ORDER BY" $ sqlSelectOrderBy cmd-      limitClause   = unsafeSQL $ "LIMIT" <+> show (sqlSelectLimit cmd)+      limitClause = unsafeSQL $ "LIMIT" <+> show (sqlSelectLimit cmd)  emitClauseOnConflictForInsert :: Maybe (SQL, Maybe SQL) -> SQL emitClauseOnConflictForInsert = \case-       Nothing                   -> ""-       Just (condition, maction) -> emitClause "ON CONFLICT" $-         condition <+> "DO" <+> fromMaybe "NOTHING" maction+  Nothing -> ""+  Just (condition, maction) ->+    emitClause "ON CONFLICT" $+      condition <+> "DO" <+> fromMaybe "NOTHING" maction  instance Sqlable SqlInsert where   toSQLCommand cmd =-    emitClausesSepComma "WITH" (map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlInsertWith cmd)) <+>-    "INSERT INTO" <+> sqlInsertWhat cmd <+>-    parenthesize (sqlConcatComma (map fst (sqlInsertSet cmd))) <+>-    emitClausesSep "VALUES" "," (map sqlConcatComma (transpose (map (makeLongEnough . snd) (sqlInsertSet cmd)))) <+>-    emitClauseOnConflictForInsert (sqlInsertOnConflict cmd) <+>-    emitClausesSepComma "RETURNING" (sqlInsertResult cmd)-   where-     -- this is the longest list of values-     longest = maximum (1 : (map (lengthOfEither . snd) (sqlInsertSet cmd)))-     lengthOfEither (Single _) = 1-     lengthOfEither (Many x)   = length x-     makeLongEnough (Single x) = take longest (repeat x)-     makeLongEnough (Many x)   = take longest (x ++ repeat "DEFAULT")+    emitClausesSepComma+      (recursiveClause $ sqlInsertRecursiveWith cmd)+      (map (\(name, command, mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlInsertWith cmd))+      <+> "INSERT INTO"+      <+> sqlInsertWhat cmd+      <+> parenthesize (sqlConcatComma (map fst (sqlInsertSet cmd)))+      <+> emitClausesSep "VALUES" "," (map sqlConcatComma (transpose (map (makeLongEnough . snd) (sqlInsertSet cmd))))+      <+> emitClauseOnConflictForInsert (sqlInsertOnConflict cmd)+      <+> emitClausesSepComma "RETURNING" (sqlInsertResult cmd)+    where+      -- this is the longest list of values+      longest = maximum (1 : map (lengthOfEither . snd) (sqlInsertSet cmd))+      lengthOfEither (Single _) = 1+      lengthOfEither (Many x) = length x+      makeLongEnough (Single x) = replicate longest x+      makeLongEnough (Many x) = take longest (x ++ repeat "DEFAULT")  instance Sqlable SqlInsertSelect where-  toSQLCommand cmd = smconcat-    -- WITH clause needs to be at the top level, so we emit it here and not-    -- include it in the SqlSelect below.-    [ emitClausesSepComma "WITH" $-      map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlInsertSelectWith cmd)-    , "INSERT INTO" <+> sqlInsertSelectWhat cmd-    , parenthesize . sqlConcatComma . map fst $ sqlInsertSelectSet cmd-    , parenthesize . toSQLCommand $ SqlSelect { sqlSelectFrom    = sqlInsertSelectFrom cmd-                                              , sqlSelectUnion   = []-                                              , sqlSelectUnionAll = []-                                              , sqlSelectDistinct = sqlInsertSelectDistinct cmd-                                              , sqlSelectResult  = fmap snd $ sqlInsertSelectSet cmd-                                              , sqlSelectWhere   = sqlInsertSelectWhere cmd-                                              , sqlSelectOrderBy = sqlInsertSelectOrderBy cmd-                                              , sqlSelectGroupBy = sqlInsertSelectGroupBy cmd-                                              , sqlSelectHaving  = sqlInsertSelectHaving cmd-                                              , sqlSelectOffset  = sqlInsertSelectOffset cmd-                                              , sqlSelectLimit   = sqlInsertSelectLimit cmd-                                              , sqlSelectWith    = []-                                              }-    , emitClauseOnConflictForInsert (sqlInsertSelectOnConflict cmd)-    , emitClausesSepComma "RETURNING" $ sqlInsertSelectResult cmd-    ]+  toSQLCommand cmd =+    smconcat+      -- WITH clause needs to be at the top level, so we emit it here and not+      -- include it in the SqlSelect below.+      [ emitClausesSepComma (recursiveClause $ sqlInsertSelectRecursiveWith cmd) $+          map (\(name, command, mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlInsertSelectWith cmd)+      , "INSERT INTO" <+> sqlInsertSelectWhat cmd+      , parenthesize . sqlConcatComma . map fst $ sqlInsertSelectSet cmd+      , parenthesize . toSQLCommand $+          SqlSelect+            { sqlSelectFrom = sqlInsertSelectFrom cmd+            , sqlSelectUnion = []+            , sqlSelectUnionAll = []+            , sqlSelectDistinct = sqlInsertSelectDistinct cmd+            , sqlSelectResult = snd <$> sqlInsertSelectSet cmd+            , sqlSelectWhere = sqlInsertSelectWhere cmd+            , sqlSelectOrderBy = sqlInsertSelectOrderBy cmd+            , sqlSelectGroupBy = sqlInsertSelectGroupBy cmd+            , sqlSelectHaving = sqlInsertSelectHaving cmd+            , sqlSelectOffset = sqlInsertSelectOffset cmd+            , sqlSelectLimit = sqlInsertSelectLimit cmd+            , sqlSelectWith = []+            , sqlSelectRecursiveWith = NonRecursive+            }+      , emitClauseOnConflictForInsert (sqlInsertSelectOnConflict cmd)+      , emitClausesSepComma "RETURNING" $ sqlInsertSelectResult cmd+      ]  -- This function has to be called as one of first things in your program -- for the library to make sure that it is aware if the "WITH MATERIALIZED"@@ -439,7 +462,7 @@ checkAndRememberMaterializationSupport :: (MonadDB m, MonadIO m, MonadMask m) => m () checkAndRememberMaterializationSupport = do   res :: Either DBException Int64 <- try . withNewConnection $ do-    runSQL01_ $ "WITH t(n) AS MATERIALIZED (SELECT (1 :: bigint)) SELECT n FROM t LIMIT 1"+    runSQL01_ "WITH t(n) AS MATERIALIZED (SELECT (1 :: bigint)) SELECT n FROM t LIMIT 1"     fetchOne runIdentity   liftIO $ writeIORef withMaterializedSupported (isRight res) @@ -448,138 +471,181 @@ withMaterializedSupported = unsafePerformIO $ newIORef False  isWithMaterializedSupported :: Bool+{-# NOINLINE isWithMaterializedSupported #-} isWithMaterializedSupported = unsafePerformIO $ readIORef withMaterializedSupported  materializedClause :: Materialized -> SQL materializedClause Materialized = if isWithMaterializedSupported then "MATERIALIZED" else "" materializedClause NonMaterialized = if isWithMaterializedSupported then "NOT MATERIALIZED" else "" +recursiveClause :: Recursive -> SQL+recursiveClause Recursive = "WITH RECURSIVE"+recursiveClause NonRecursive = "WITH"+ instance Sqlable SqlUpdate where   toSQLCommand cmd =-    emitClausesSepComma "WITH" (map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlUpdateWith cmd)) <+>-    "UPDATE" <+> sqlUpdateWhat cmd <+> "SET" <+>-    sqlConcatComma (map (\(name, command) -> name <> "=" <> command) (sqlUpdateSet cmd)) <+>-    emitClause "FROM" (sqlUpdateFrom cmd) <+>-    emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlUpdateWhere cmd) <+>-    emitClausesSepComma "RETURNING" (sqlUpdateResult cmd)+    emitClausesSepComma+      (recursiveClause $ sqlUpdateRecursiveWith cmd)+      (map (\(name, command, mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlUpdateWith cmd))+      <+> "UPDATE"+      <+> sqlUpdateWhat cmd+      <+> "SET"+      <+> sqlConcatComma (map (\(name, command) -> name <> "=" <> command) (sqlUpdateSet cmd))+      <+> emitClause "FROM" (sqlUpdateFrom cmd)+      <+> emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlUpdateWhere cmd)+      <+> emitClausesSepComma "RETURNING" (sqlUpdateResult cmd)  instance Sqlable SqlDelete where   toSQLCommand cmd =-    emitClausesSepComma "WITH" (map (\(name,command,mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlDeleteWith cmd)) <+>-    "DELETE FROM" <+> sqlDeleteFrom cmd <+>-    emitClause "USING" (sqlDeleteUsing cmd) <+>-        emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlDeleteWhere cmd) <+>-    emitClausesSepComma "RETURNING" (sqlDeleteResult cmd)+    emitClausesSepComma+      (recursiveClause $ sqlDeleteRecursiveWith cmd)+      (map (\(name, command, mat) -> name <+> "AS" <+> materializedClause mat <+> parenthesize command) (sqlDeleteWith cmd))+      <+> "DELETE FROM"+      <+> sqlDeleteFrom cmd+      <+> emitClause "USING" (sqlDeleteUsing cmd)+      <+> emitClausesSep "WHERE" "AND" (map toSQLCommand $ sqlDeleteWhere cmd)+      <+> emitClausesSepComma "RETURNING" (sqlDeleteResult cmd) -instance Sqlable SqlAll where-  toSQLCommand cmd | null (sqlAllWhere cmd) = "TRUE"-  toSQLCommand cmd =-    "(" <+> smintercalate "AND" (map (parenthesize . toSQLCommand) (sqlAllWhere cmd)) <+> ")"+instance Sqlable SqlWhereAll where+  toSQLCommand cmd = case sqlWhereAllWhere cmd of+    [] -> "TRUE"+    [cond] -> toSQLCommand cond+    conds -> parenthesize $ smintercalate "AND" (map (parenthesize . toSQLCommand) conds) +instance Sqlable SqlWhereAny where+  toSQLCommand cmd = case sqlWhereAnyWhere cmd of+    [] -> "FALSE"+    [cond] -> toSQLCommand cond+    conds -> parenthesize $ smintercalate "OR" (map (parenthesize . toSQLCommand) conds)+ sqlSelect :: SQL -> State SqlSelect () -> SqlSelect sqlSelect table refine =-  execState refine (SqlSelect table [] [] False [] [] [] [] [] 0 (-1) [])+  execState refine (SqlSelect table [] [] False [] [] [] [] [] 0 (-1) [] NonRecursive)  sqlSelect2 :: SQL -> State SqlSelect () -> SqlSelect sqlSelect2 from refine =-  execState refine (SqlSelect from [] [] False [] [] [] [] [] 0 (-1) [])+  execState refine (SqlSelect from [] [] False [] [] [] [] [] 0 (-1) [] NonRecursive)  sqlInsert :: SQL -> State SqlInsert () -> SqlInsert sqlInsert table refine =-  execState refine (SqlInsert table Nothing mempty [] [])+  execState refine (SqlInsert table Nothing mempty [] [] NonRecursive)  sqlInsertSelect :: SQL -> SQL -> State SqlInsertSelect () -> SqlInsertSelect sqlInsertSelect table from refine =-  execState refine (SqlInsertSelect-                    { sqlInsertSelectWhat       = table-                    , sqlInsertSelectOnConflict = Nothing-                    , sqlInsertSelectDistinct   = False-                    , sqlInsertSelectSet        = []-                    , sqlInsertSelectResult     = []-                    , sqlInsertSelectFrom       = from-                    , sqlInsertSelectWhere      = []-                    , sqlInsertSelectOrderBy    = []-                    , sqlInsertSelectGroupBy    = []-                    , sqlInsertSelectHaving     = []-                    , sqlInsertSelectOffset     = 0-                    , sqlInsertSelectLimit      = -1-                    , sqlInsertSelectWith       = []-                    })+  execState+    refine+    ( SqlInsertSelect+        { sqlInsertSelectWhat = table+        , sqlInsertSelectOnConflict = Nothing+        , sqlInsertSelectDistinct = False+        , sqlInsertSelectSet = []+        , sqlInsertSelectResult = []+        , sqlInsertSelectFrom = from+        , sqlInsertSelectWhere = []+        , sqlInsertSelectOrderBy = []+        , sqlInsertSelectGroupBy = []+        , sqlInsertSelectHaving = []+        , sqlInsertSelectOffset = 0+        , sqlInsertSelectLimit = -1+        , sqlInsertSelectWith = []+        , sqlInsertSelectRecursiveWith = NonRecursive+        }+    )  sqlUpdate :: SQL -> State SqlUpdate () -> SqlUpdate sqlUpdate table refine =-  execState refine (SqlUpdate table mempty [] [] [] [])+  execState refine (SqlUpdate table mempty [] [] [] [] NonRecursive)  sqlDelete :: SQL -> State SqlDelete () -> SqlDelete sqlDelete table refine =-  execState refine (SqlDelete  { sqlDeleteFrom   = table-                               , sqlDeleteUsing  = mempty-                               , sqlDeleteWhere  = []-                               , sqlDeleteResult = []-                               , sqlDeleteWith   = []-                               })-+  execState+    refine+    ( SqlDelete+        { sqlDeleteFrom = table+        , sqlDeleteUsing = mempty+        , sqlDeleteWhere = []+        , sqlDeleteResult = []+        , sqlDeleteWith = []+        , sqlDeleteRecursiveWith = NonRecursive+        }+    )  data Materialized = Materialized | NonMaterialized+data Recursive = Recursive | NonRecursive -class SqlWith a where-  sqlWith1 :: a -> SQL -> SQL -> Materialized -> a+-- This instance guarantees that once a single CTE has+-- been marked as recursive, the whole "WITH" block will+-- get the RECURSIVE keyword associated to it.+instance Semigroup Recursive where+  _ <> Recursive = Recursive+  Recursive <> _ = Recursive+  _ <> _ = NonRecursive +class SqlWith a where+  sqlWith1 :: a -> SQL -> SQL -> Materialized -> Recursive -> a  instance SqlWith SqlSelect where-  sqlWith1 cmd name sql mat = cmd { sqlSelectWith = sqlSelectWith cmd ++ [(name,sql, mat)] }+  sqlWith1 cmd name sql mat recurse = cmd {sqlSelectWith = sqlSelectWith cmd ++ [(name, sql, mat)], sqlSelectRecursiveWith = recurse <> sqlSelectRecursiveWith cmd}  instance SqlWith SqlInsertSelect where-  sqlWith1 cmd name sql mat = cmd { sqlInsertSelectWith = sqlInsertSelectWith cmd ++ [(name,sql,mat)] }+  sqlWith1 cmd name sql mat recurse = cmd {sqlInsertSelectWith = sqlInsertSelectWith cmd ++ [(name, sql, mat)], sqlInsertSelectRecursiveWith = recurse <> sqlInsertSelectRecursiveWith cmd}  instance SqlWith SqlUpdate where-  sqlWith1 cmd name sql mat = cmd { sqlUpdateWith = sqlUpdateWith cmd ++ [(name,sql,mat)] }+  sqlWith1 cmd name sql mat recurse = cmd {sqlUpdateWith = sqlUpdateWith cmd ++ [(name, sql, mat)], sqlUpdateRecursiveWith = recurse <> sqlUpdateRecursiveWith cmd}  instance SqlWith SqlDelete where-  sqlWith1 cmd name sql mat = cmd { sqlDeleteWith = sqlDeleteWith cmd ++ [(name,sql,mat)] }+  sqlWith1 cmd name sql mat recurse = cmd {sqlDeleteWith = sqlDeleteWith cmd ++ [(name, sql, mat)], sqlDeleteRecursiveWith = recurse <> sqlDeleteRecursiveWith cmd}  sqlWith :: (MonadState v m, SqlWith v, Sqlable s) => SQL -> s -> m ()-sqlWith name sql = modify (\cmd -> sqlWith1 cmd name (toSQLCommand sql) NonMaterialized)+sqlWith name sql = modify (\cmd -> sqlWith1 cmd name (toSQLCommand sql) NonMaterialized NonRecursive)  sqlWithMaterialized :: (MonadState v m, SqlWith v, Sqlable s) => SQL -> s -> m ()-sqlWithMaterialized name sql = modify (\cmd -> sqlWith1 cmd name (toSQLCommand sql) Materialized)+sqlWithMaterialized name sql = modify (\cmd -> sqlWith1 cmd name (toSQLCommand sql) Materialized NonRecursive) +-- | Note: RECURSIVE only powers SELECTs (but the SELECT can feed an UPDATE outside of the recursive query).+sqlWithRecursive :: (MonadState v m, SqlWith v, Sqlable s) => SQL -> s -> m ()+sqlWithRecursive name sql = modify (\cmd -> sqlWith1 cmd name (toSQLCommand sql) NonMaterialized Recursive)+ -- | Note: WHERE clause of the main SELECT is treated specially, i.e. it only -- applies to the main SELECT, not the whole union. sqlUnion :: (MonadState SqlSelect m, Sqlable sql) => [sql] -> m ()-sqlUnion sqls = modify (\cmd -> cmd { sqlSelectUnion = map toSQLCommand sqls })+sqlUnion sqls = modify (\cmd -> cmd {sqlSelectUnion = map toSQLCommand sqls})  -- | Note: WHERE clause of the main SELECT is treated specially, i.e. it only -- applies to the main SELECT, not the whole union. -- -- @since 1.16.4.0 sqlUnionAll :: (MonadState SqlSelect m, Sqlable sql) => [sql] -> m ()-sqlUnionAll sqls = modify (\cmd -> cmd { sqlSelectUnionAll = map toSQLCommand sqls })+sqlUnionAll sqls = modify (\cmd -> cmd {sqlSelectUnionAll = map toSQLCommand sqls})  class SqlWhere a where   sqlWhere1 :: a -> SqlCondition -> a   sqlGetWhereConditions :: a -> [SqlCondition]  instance SqlWhere SqlSelect where-  sqlWhere1 cmd cond = cmd { sqlSelectWhere = sqlSelectWhere cmd ++ [cond] }+  sqlWhere1 cmd cond = cmd {sqlSelectWhere = sqlSelectWhere cmd ++ [cond]}   sqlGetWhereConditions = sqlSelectWhere  instance SqlWhere SqlInsertSelect where-  sqlWhere1 cmd cond = cmd { sqlInsertSelectWhere = sqlInsertSelectWhere cmd ++ [cond] }+  sqlWhere1 cmd cond = cmd {sqlInsertSelectWhere = sqlInsertSelectWhere cmd ++ [cond]}   sqlGetWhereConditions = sqlInsertSelectWhere  instance SqlWhere SqlUpdate where-  sqlWhere1 cmd cond = cmd { sqlUpdateWhere = sqlUpdateWhere cmd ++ [cond] }+  sqlWhere1 cmd cond = cmd {sqlUpdateWhere = sqlUpdateWhere cmd ++ [cond]}   sqlGetWhereConditions = sqlUpdateWhere  instance SqlWhere SqlDelete where-  sqlWhere1 cmd cond = cmd { sqlDeleteWhere = sqlDeleteWhere cmd ++ [cond] }+  sqlWhere1 cmd cond = cmd {sqlDeleteWhere = sqlDeleteWhere cmd ++ [cond]}   sqlGetWhereConditions = sqlDeleteWhere -instance SqlWhere SqlAll where-  sqlWhere1 cmd cond = cmd { sqlAllWhere = sqlAllWhere cmd ++ [cond] }-  sqlGetWhereConditions = sqlAllWhere+instance SqlWhere SqlWhereAll where+  sqlWhere1 cmd cond = cmd {sqlWhereAllWhere = sqlWhereAllWhere cmd ++ [cond]}+  sqlGetWhereConditions = sqlWhereAllWhere +instance SqlWhere SqlWhereAny where+  sqlWhere1 cmd cond = cmd {sqlWhereAnyWhere = sqlWhereAnyWhere cmd ++ [cond]}+  sqlGetWhereConditions = sqlWhereAnyWhere+ -- | The @WHERE@ part of an SQL query. See above for a usage -- example. See also 'SqlCondition'. sqlWhere :: (MonadState v m, SqlWhere v) => SQL -> m ()@@ -598,12 +664,15 @@ sqlWhereLike name value = sqlWhere $ name <+> "LIKE" <?> value  sqlWhereILike :: (MonadState v m, SqlWhere v, Show a, ToSQL a) => SQL -> a -> m ()-sqlWhereILike name value = sqlWhere  $ name <+> "ILIKE" <?> value+sqlWhereILike name value = sqlWhere $ name <+> "ILIKE" <?> value  -- | Similar to 'sqlWhereIn', but uses @ANY@ instead of @SELECT UNNEST@. sqlWhereEqualsAny :: (MonadState v m, SqlWhere v, Show a, ToSQL a) => SQL -> [a] -> m () sqlWhereEqualsAny name values = sqlWhere $ name <+> "= ANY(" <?> Array1 values <+> ")" +-- | Note: `sqlWhereIn` will unpack the array using `UNNEST`. Using a postgresql function in this way+-- will interfere with the planner. Use `sqlWhereEqualsAny` instead, except if you know that+-- `UNNEST` will optimize better. sqlWhereIn :: (MonadState v m, SqlWhere v, Show a, ToSQL a) => SQL -> [a] -> m () sqlWhereIn name values = do   -- Unpack the array to give query optimizer more options.@@ -624,7 +693,7 @@  sqlWhereNotExists :: (MonadState v m, SqlWhere v) => SqlSelect -> m () sqlWhereNotExists sqlSelectD = do-  sqlWhere ("NOT EXISTS (" <+> toSQLCommand (sqlSelectD { sqlSelectResult = ["TRUE"] }) <+> ")")+  sqlWhere ("NOT EXISTS (" <+> toSQLCommand (sqlSelectD {sqlSelectResult = ["TRUE"]}) <+> ")")  sqlWhereIsNULL :: (MonadState v m, SqlWhere v) => SQL -> m () sqlWhereIsNULL col = sqlWhere $ col <+> "IS NULL"@@ -632,31 +701,44 @@ sqlWhereIsNotNULL :: (MonadState v m, SqlWhere v) => SQL -> m () sqlWhereIsNotNULL col = sqlWhere $ col <+> "IS NOT NULL" +-- | Run monad that joins all conditions using 'AND' operator.+--+-- When no conditions are given, the result is 'TRUE'.+--+-- Note: This is usally not needed as `SqlSelect`, `SqlUpdate` and `SqlDelete`+-- already join conditions using 'AND' by default, but it can be useful when+-- nested in `sqlAny`.+sqlAll :: State SqlWhereAll () -> SQL+sqlAll = toSQLCommand . (`execState` SqlWhereAll [])++-- | Run monad that joins all conditions using 'OR' operator.+--+-- When no conditions are given, the result is 'FALSE'.+sqlAny :: State SqlWhereAny () -> SQL+sqlAny = toSQLCommand . (`execState` SqlWhereAny [])+ -- | Add a condition in the WHERE statement that holds if any of the given -- condition holds.-sqlWhereAny :: (MonadState v m, SqlWhere v) => [State SqlAll ()] -> m ()-sqlWhereAny = sqlWhere . sqlWhereAnyImpl--sqlWhereAnyImpl :: [State SqlAll ()] -> SQL-sqlWhereAnyImpl [] = "FALSE"-sqlWhereAnyImpl l =-  "(" <+> smintercalate "OR" (map (parenthesize . toSQLCommand-                                   . flip execState (SqlAll [])) l) <+> ")"+--+-- These conditions are joined with 'OR' operator.+-- When no conditions are given, the result is 'FALSE'.+sqlWhereAny :: (MonadState v m, SqlWhere v) => [State SqlWhereAll ()] -> m ()+sqlWhereAny = sqlWhere . sqlAny . mapM_ (sqlWhere . sqlAll)  class SqlFrom a where   sqlFrom1 :: a -> SQL -> a  instance SqlFrom SqlSelect where-  sqlFrom1 cmd sql = cmd { sqlSelectFrom = sqlSelectFrom cmd <+> sql }+  sqlFrom1 cmd sql = cmd {sqlSelectFrom = sqlSelectFrom cmd <+> sql}  instance SqlFrom SqlInsertSelect where-  sqlFrom1 cmd sql = cmd { sqlInsertSelectFrom = sqlInsertSelectFrom cmd <+> sql }+  sqlFrom1 cmd sql = cmd {sqlInsertSelectFrom = sqlInsertSelectFrom cmd <+> sql}  instance SqlFrom SqlUpdate where-  sqlFrom1 cmd sql = cmd { sqlUpdateFrom = sqlUpdateFrom cmd <+> sql }+  sqlFrom1 cmd sql = cmd {sqlUpdateFrom = sqlUpdateFrom cmd <+> sql}  instance SqlFrom SqlDelete where-  sqlFrom1 cmd sql = cmd { sqlDeleteUsing = sqlDeleteUsing cmd <+> sql }+  sqlFrom1 cmd sql = cmd {sqlDeleteUsing = sqlDeleteUsing cmd <+> sql}  sqlFrom :: (MonadState v m, SqlFrom v) => SQL -> m () sqlFrom sql = modify (\cmd -> sqlFrom1 cmd sql)@@ -665,46 +747,58 @@ sqlJoin table = sqlFrom (", " <+> table)  sqlJoinOn :: (MonadState v m, SqlFrom v) => SQL -> SQL -> m ()-sqlJoinOn table condition = sqlFrom (" JOIN " <+>-                                     table <+>-                                     " ON " <+>-                                     condition)+sqlJoinOn table condition =+  sqlFrom+    ( " JOIN "+        <+> table+        <+> " ON "+        <+> condition+    )  sqlLeftJoinOn :: (MonadState v m, SqlFrom v) => SQL -> SQL -> m ()-sqlLeftJoinOn table condition = sqlFrom (" LEFT JOIN " <+>-                                         table <+>-                                         " ON " <+>-                                         condition)+sqlLeftJoinOn table condition =+  sqlFrom+    ( " LEFT JOIN "+        <+> table+        <+> " ON "+        <+> condition+    )  sqlRightJoinOn :: (MonadState v m, SqlFrom v) => SQL -> SQL -> m ()-sqlRightJoinOn table condition = sqlFrom (" RIGHT JOIN " <+>-                                          table <+>-                                          " ON " <+>-                                          condition)+sqlRightJoinOn table condition =+  sqlFrom+    ( " RIGHT JOIN "+        <+> table+        <+> " ON "+        <+> condition+    )  sqlFullJoinOn :: (MonadState v m, SqlFrom v) => SQL -> SQL -> m ()-sqlFullJoinOn table condition = sqlFrom (" FULL JOIN " <+>-                                         table <+>-                                         " ON " <+>-                                         condition)+sqlFullJoinOn table condition =+  sqlFrom+    ( " FULL JOIN "+        <+> table+        <+> " ON "+        <+> condition+    )  class SqlSet a where   sqlSet1 :: a -> SQL -> SQL -> a  instance SqlSet SqlUpdate where-  sqlSet1 cmd name v = cmd { sqlUpdateSet = sqlUpdateSet cmd ++ [(name, v)] }+  sqlSet1 cmd name v = cmd {sqlUpdateSet = sqlUpdateSet cmd ++ [(name, v)]}  instance SqlSet SqlInsert where-  sqlSet1 cmd name v = cmd { sqlInsertSet = sqlInsertSet cmd ++ [(name, Single v)] }+  sqlSet1 cmd name v = cmd {sqlInsertSet = sqlInsertSet cmd ++ [(name, Single v)]}  instance SqlSet SqlInsertSelect where-  sqlSet1 cmd name v = cmd { sqlInsertSelectSet = sqlInsertSelectSet cmd ++ [(name, v)] }+  sqlSet1 cmd name v = cmd {sqlInsertSelectSet = sqlInsertSelectSet cmd ++ [(name, v)]}  sqlSetCmd :: (MonadState v m, SqlSet v) => SQL -> SQL -> m () sqlSetCmd name sql = modify (\cmd -> sqlSet1 cmd name sql) -sqlSetCmdList :: (MonadState SqlInsert m) => SQL -> [SQL] -> m ()-sqlSetCmdList name as = modify (\cmd -> cmd { sqlInsertSet = sqlInsertSet cmd ++ [(name, Many as)] })+sqlSetCmdList :: MonadState SqlInsert m => SQL -> [SQL] -> m ()+sqlSetCmdList name as = modify (\cmd -> cmd {sqlInsertSet = sqlInsertSet cmd ++ [(name, Many as)]})  sqlSet :: (MonadState v m, SqlSet v, Show a, ToSQL a) => SQL -> a -> m () sqlSet name a = sqlSetCmd name (sqlParam a)@@ -727,20 +821,20 @@   sqlOnConflictOnColumns1 :: Sqlable sql => a -> [SQL] -> sql -> a  instance SqlOnConflict SqlInsert where-  sqlOnConflictDoNothing1 cmd = -    cmd { sqlInsertOnConflict = Just ("", Nothing) }-  sqlOnConflictOnColumns1 cmd columns sql = -    cmd { sqlInsertOnConflict = Just (parenthesize $ sqlConcatComma columns, Just $ toSQLCommand sql) }-  sqlOnConflictOnColumnsDoNothing1 cmd columns = -    cmd { sqlInsertOnConflict = Just (parenthesize $ sqlConcatComma columns, Nothing) }+  sqlOnConflictDoNothing1 cmd =+    cmd {sqlInsertOnConflict = Just ("", Nothing)}+  sqlOnConflictOnColumns1 cmd columns sql =+    cmd {sqlInsertOnConflict = Just (parenthesize $ sqlConcatComma columns, Just $ toSQLCommand sql)}+  sqlOnConflictOnColumnsDoNothing1 cmd columns =+    cmd {sqlInsertOnConflict = Just (parenthesize $ sqlConcatComma columns, Nothing)}  instance SqlOnConflict SqlInsertSelect where-  sqlOnConflictDoNothing1 cmd = -    cmd { sqlInsertSelectOnConflict = Just ("", Nothing) }-  sqlOnConflictOnColumns1 cmd columns sql = -    cmd { sqlInsertSelectOnConflict = Just (parenthesize $ sqlConcatComma columns, Just $ toSQLCommand sql) }-  sqlOnConflictOnColumnsDoNothing1 cmd columns = -    cmd { sqlInsertSelectOnConflict = Just (parenthesize $ sqlConcatComma columns, Nothing) }+  sqlOnConflictDoNothing1 cmd =+    cmd {sqlInsertSelectOnConflict = Just ("", Nothing)}+  sqlOnConflictOnColumns1 cmd columns sql =+    cmd {sqlInsertSelectOnConflict = Just (parenthesize $ sqlConcatComma columns, Just $ toSQLCommand sql)}+  sqlOnConflictOnColumnsDoNothing1 cmd columns =+    cmd {sqlInsertSelectOnConflict = Just (parenthesize $ sqlConcatComma columns, Nothing)}  sqlOnConflictDoNothing :: (MonadState v m, SqlOnConflict v) => m () sqlOnConflictDoNothing = modify sqlOnConflictDoNothing1@@ -755,19 +849,19 @@   sqlResult1 :: a -> SQL -> a  instance SqlResult SqlSelect where-  sqlResult1 cmd sql = cmd { sqlSelectResult = sqlSelectResult cmd ++ [sql] }+  sqlResult1 cmd sql = cmd {sqlSelectResult = sqlSelectResult cmd ++ [sql]}  instance SqlResult SqlInsert where-  sqlResult1 cmd sql = cmd { sqlInsertResult = sqlInsertResult cmd ++ [sql] }+  sqlResult1 cmd sql = cmd {sqlInsertResult = sqlInsertResult cmd ++ [sql]}  instance SqlResult SqlInsertSelect where-  sqlResult1 cmd sql = cmd { sqlInsertSelectResult = sqlInsertSelectResult cmd ++ [sql] }+  sqlResult1 cmd sql = cmd {sqlInsertSelectResult = sqlInsertSelectResult cmd ++ [sql]}  instance SqlResult SqlUpdate where-  sqlResult1 cmd sql = cmd { sqlUpdateResult = sqlUpdateResult cmd ++ [sql] }+  sqlResult1 cmd sql = cmd {sqlUpdateResult = sqlUpdateResult cmd ++ [sql]}  instance SqlResult SqlDelete where-  sqlResult1 cmd sql = cmd { sqlDeleteResult = sqlDeleteResult cmd ++ [sql] }+  sqlResult1 cmd sql = cmd {sqlDeleteResult = sqlDeleteResult cmd ++ [sql]}  sqlResult :: (MonadState v m, SqlResult v) => SQL -> m () sqlResult sql = modify (\cmd -> sqlResult1 cmd sql)@@ -776,11 +870,10 @@   sqlOrderBy1 :: a -> SQL -> a  instance SqlOrderBy SqlSelect where-  sqlOrderBy1 cmd sql = cmd { sqlSelectOrderBy = sqlSelectOrderBy cmd ++ [sql] }+  sqlOrderBy1 cmd sql = cmd {sqlSelectOrderBy = sqlSelectOrderBy cmd ++ [sql]}  instance SqlOrderBy SqlInsertSelect where-  sqlOrderBy1 cmd sql = cmd { sqlInsertSelectOrderBy = sqlInsertSelectOrderBy cmd ++ [sql] }-+  sqlOrderBy1 cmd sql = cmd {sqlInsertSelectOrderBy = sqlInsertSelectOrderBy cmd ++ [sql]}  sqlOrderBy :: (MonadState v m, SqlOrderBy v) => SQL -> m () sqlOrderBy sql = modify (\cmd -> sqlOrderBy1 cmd sql)@@ -790,12 +883,12 @@   sqlHaving1 :: a -> SQL -> a  instance SqlGroupByHaving SqlSelect where-  sqlGroupBy1 cmd sql = cmd { sqlSelectGroupBy = sqlSelectGroupBy cmd ++ [sql] }-  sqlHaving1 cmd sql = cmd { sqlSelectHaving = sqlSelectHaving cmd ++ [sql] }+  sqlGroupBy1 cmd sql = cmd {sqlSelectGroupBy = sqlSelectGroupBy cmd ++ [sql]}+  sqlHaving1 cmd sql = cmd {sqlSelectHaving = sqlSelectHaving cmd ++ [sql]}  instance SqlGroupByHaving SqlInsertSelect where-  sqlGroupBy1 cmd sql = cmd { sqlInsertSelectGroupBy = sqlInsertSelectGroupBy cmd ++ [sql] }-  sqlHaving1 cmd sql = cmd { sqlInsertSelectHaving = sqlInsertSelectHaving cmd ++ [sql] }+  sqlGroupBy1 cmd sql = cmd {sqlInsertSelectGroupBy = sqlInsertSelectGroupBy cmd ++ [sql]}+  sqlHaving1 cmd sql = cmd {sqlInsertSelectHaving = sqlInsertSelectHaving cmd ++ [sql]}  sqlGroupBy :: (MonadState v m, SqlGroupByHaving v) => SQL -> m () sqlGroupBy sql = modify (\cmd -> sqlGroupBy1 cmd sql)@@ -803,18 +896,17 @@ sqlHaving :: (MonadState v m, SqlGroupByHaving v) => SQL -> m () sqlHaving sql = modify (\cmd -> sqlHaving1 cmd sql) - class SqlOffsetLimit a where   sqlOffset1 :: a -> Integer -> a   sqlLimit1 :: a -> Integer -> a  instance SqlOffsetLimit SqlSelect where-  sqlOffset1 cmd num = cmd { sqlSelectOffset = num }-  sqlLimit1 cmd num = cmd { sqlSelectLimit = num }+  sqlOffset1 cmd num = cmd {sqlSelectOffset = num}+  sqlLimit1 cmd num = cmd {sqlSelectLimit = num}  instance SqlOffsetLimit SqlInsertSelect where-  sqlOffset1 cmd num = cmd { sqlInsertSelectOffset = num }-  sqlLimit1 cmd num = cmd { sqlInsertSelectLimit = num }+  sqlOffset1 cmd num = cmd {sqlInsertSelectOffset = num}+  sqlLimit1 cmd num = cmd {sqlInsertSelectLimit = num}  sqlOffset :: (MonadState v m, SqlOffsetLimit v, Integral int) => int -> m () sqlOffset val = modify (\cmd -> sqlOffset1 cmd $ toInteger val)@@ -826,10 +918,10 @@   sqlDistinct1 :: a -> a  instance SqlDistinct SqlSelect where-  sqlDistinct1 cmd = cmd { sqlSelectDistinct = True }+  sqlDistinct1 cmd = cmd {sqlSelectDistinct = True}  instance SqlDistinct SqlInsertSelect where-  sqlDistinct1 cmd = cmd { sqlInsertSelectDistinct = True }+  sqlDistinct1 cmd = cmd {sqlInsertSelectDistinct = True}  sqlDistinct :: (MonadState v m, SqlDistinct v) => m ()-sqlDistinct = modify (\cmd -> sqlDistinct1 cmd)+sqlDistinct = modify sqlDistinct1
src/Database/PostgreSQL/PQTypes/Utils/NubList.hs view
@@ -1,15 +1,15 @@ module Database.PostgreSQL.PQTypes.Utils.NubList-    ( NubList    -- opaque-    , toNubList  -- smart construtor-    , fromNubList-    , overNubList-    ) where+  ( NubList -- opaque+  , toNubList -- smart construtor+  , fromNubList+  , overNubList+  ) where  import Data.Typeable -import qualified Text.Read as R-import qualified Data.Set as Set-import qualified Data.Semigroup as SG+import Data.Semigroup qualified as SG+import Data.Set qualified as Set+import Text.Read qualified as R  {-   This module is a copy-paste fork of Distribution.Utils.NubList in Cabal@@ -19,9 +19,9 @@ -}  -- | NubList : A de-duplicated list that maintains the original order.-newtype NubList a =-    NubList { fromNubList :: [a] }-    deriving (Eq, Typeable)+newtype NubList a+  = NubList {fromNubList :: [a]}+  deriving (Eq, Typeable)  -- NubList assumes that nub retains the list order while removing duplicate -- elements (keeping the first occurence). Documentation for "Data.List.nub"@@ -30,41 +30,42 @@  -- | Smart constructor for the NubList type. toNubList :: Ord a => [a] -> NubList a-toNubList list = NubList $ (ordNubBy id) list+toNubList = NubList . ordNubBy id  -- | Lift a function over lists to a function over NubLists. overNubList :: Ord a => ([a] -> [a]) -> NubList a -> NubList a overNubList f (NubList list) = toNubList . f $ list  instance Ord a => SG.Semigroup (NubList a) where-    (NubList xs) <> (NubList ys) = NubList $ xs `listUnion` ys-      where-        listUnion :: (Ord a) => [a] -> [a] -> [a]-        listUnion a b = a-          ++ ordNubBy id (filter (`Set.notMember` (Set.fromList a)) b)-+  (NubList xs) <> (NubList ys) = NubList $ xs `listUnion` ys+    where+      listUnion :: Ord a => [a] -> [a] -> [a]+      listUnion a b =+        a+          ++ ordNubBy id (filter (`Set.notMember` Set.fromList a) b)  instance Ord a => Monoid (NubList a) where-    mempty  = NubList []-    mappend = (SG.<>)+  mempty = NubList []+  mappend = (SG.<>)  instance Show a => Show (NubList a) where-    show (NubList list) = show list+  show (NubList list) = show list  instance (Ord a, Read a) => Read (NubList a) where-    readPrec = readNubList toNubList+  readPrec = readNubList toNubList  -- | Helper used by NubList/NubListR's Read instances.-readNubList :: (Read a) => ([a] -> l a) -> R.ReadPrec (l a)+readNubList :: Read a => ([a] -> l a) -> R.ReadPrec (l a) readNubList toList = R.parens . R.prec 10 $ fmap toList R.readPrec  ordNubBy :: Ord b => (a -> b) -> [a] -> [a]-ordNubBy f l = go Set.empty l+ordNubBy f = go Set.empty   where     go !_ [] = []-    go !s (x:xs)+    go !s (x : xs)       | y `Set.member` s = go s xs-      | otherwise        = let !s' = Set.insert y s-                            in x : go s' xs+      | otherwise =+          let !s' = Set.insert y s+          in x : go s' xs       where         y = f x
src/Database/PostgreSQL/PQTypes/Versions.hs view
@@ -3,12 +3,13 @@ import Database.PostgreSQL.PQTypes.Model  tableVersions :: Table-tableVersions = tblTable {-    tblName = "table_versions"-  , tblVersion = 1-  , tblColumns = [-      tblColumn { colName = "name", colType = TextT, colNullable = False }-    , tblColumn { colName = "version", colType = IntegerT, colNullable = False }-    ]-  , tblPrimaryKey = pkOnColumn "name"-  }+tableVersions =+  tblTable+    { tblName = "table_versions"+    , tblVersion = 1+    , tblColumns =+        [ tblColumn {colName = "name", colType = TextT, colNullable = False}+        , tblColumn {colName = "version", colType = IntegerT, colNullable = False}+        ]+    , tblPrimaryKey = pkOnColumn "name"+    }
test/Main.hs view
@@ -1,1641 +1,2441 @@-module Main where--import Control.Monad.Catch-import Control.Monad (forM_)-import Control.Monad.IO.Class-import Data.Either-import Data.List (zip4)-import Data.Monoid-import Prelude-import Data.Typeable-import Data.UUID.Types-import qualified Data.Set as Set-import qualified Data.Text as T--import Data.Monoid.Utils-import Database.PostgreSQL.PQTypes-import Database.PostgreSQL.PQTypes.Checks-import Database.PostgreSQL.PQTypes.Model.ColumnType-import Database.PostgreSQL.PQTypes.Model.CompositeType-import Database.PostgreSQL.PQTypes.Model.ForeignKey-import Database.PostgreSQL.PQTypes.Model.Index-import Database.PostgreSQL.PQTypes.Model.Migration-import Database.PostgreSQL.PQTypes.Model.PrimaryKey-import Database.PostgreSQL.PQTypes.Model.Table-import Database.PostgreSQL.PQTypes.Model.Trigger-import Database.PostgreSQL.PQTypes.SQL.Builder-import Log-import Log.Backend.StandardOutput--import Test.Tasty-import Test.Tasty.HUnit-import Test.Tasty.Options--data ConnectionString = ConnectionString String-  deriving Typeable--instance IsOption ConnectionString where-  defaultValue = ConnectionString-    -- For GitHub Actions CI-    "host=postgres user=postgres password=postgres"-  parseValue   = Just . ConnectionString-  optionName   = return "connection-string"-  optionHelp   = return "Postgres connection string"---- Simple example schemata inspired by the one in--- <  http://www.databaseanswers.org/data_models/bank_robberies/index.htm>------ Schema 1: Bank robberies, tables:---           bank, bad_guy, robbery, participated_in_robbery, witness,---           witnessed_robbery--- Schema 2: Witnesses go into witness protection program,---           (some) bad guys get arrested:---           drop tables witness and witnessed_robbery,---           add table under_arrest--- Schema 3: Bad guys get their prison sentences:---           drop table under_arrest---           add table prison_sentence--- Schema 4: New 'cash' column for the 'bank' table.--- Schema 5: Create a new table 'flash',---           drop the 'cash' column from the 'bank' table,---           drop table 'flash'.--- Cleanup:  drop everything--tableBankSchema1 :: Table-tableBankSchema1 =-  tblTable-  { tblName = "bank"-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "id",       colType = UuidT-                , colNullable = False-                , colDefault = Just "gen_random_uuid()" }-    , tblColumn { colName = "name",     colType = TextT-                , colCollation = Just "en_US"-                , colNullable = False }-    , tblColumn { colName = "location", colType = TextT-                , colCollation = Just "C"-                , colNullable = False }-    ]-  , tblPrimaryKey = pkOnColumn "id"-  , tblTriggers = []-  }--tableBankSchema2 :: Table-tableBankSchema2 = tableBankSchema1--tableBankSchema3 :: Table-tableBankSchema3 = tableBankSchema2--tableBankMigration4 :: (MonadDB m) => Migration m-tableBankMigration4 = Migration-  { mgrTableName = tblName tableBankSchema3-  , mgrFrom      = 1-  , mgrAction    = StandardMigration $ do-      runQuery_ $ sqlAlterTable (tblName tableBankSchema3) [-        sqlAddColumn $ tblColumn-          { colName = "cash"-          , colType = IntegerT-          , colNullable = False-          , colDefault = Just "0"-          }-        ]-  }--tableBankSchema4 :: Table-tableBankSchema4 = tableBankSchema3 {-    tblVersion = (tblVersion tableBankSchema3)  + 1-  , tblColumns = (tblColumns tableBankSchema3) ++ [-      tblColumn-      { colName = "cash", colType = IntegerT-      , colNullable = False-      , colDefault = Just "0"-      }-    ]-  }---tableBankMigration5fst :: (MonadDB m) => Migration m-tableBankMigration5fst = Migration-  { mgrTableName = tblName tableBankSchema3-  , mgrFrom      = 2-  , mgrAction    = StandardMigration $ do-      runQuery_ $ sqlAlterTable (tblName tableBankSchema4) [-        sqlDropColumn $ "cash"-        ]-  }--tableBankMigration5snd :: (MonadDB m) => Migration m-tableBankMigration5snd = Migration-  { mgrTableName = tblName tableBankSchema3-  , mgrFrom      = 3-  , mgrAction    = CreateIndexConcurrentlyMigration-                     (tblName tableBankSchema3)-                     ((indexOnColumn "name") { idxInclude = ["id", "location"] })-  }--tableBankSchema5 :: Table-tableBankSchema5 = tableBankSchema4 {-    tblVersion = (tblVersion tableBankSchema4) + 2-  , tblColumns = filter (\c -> colName c /= "cash")-      (tblColumns tableBankSchema4)-  , tblIndexes = [(indexOnColumn "name") { idxInclude = ["id", "location"] }]-  }--tableBadGuySchema1 :: Table-tableBadGuySchema1 =-  tblTable-  { tblName = "bad_guy"-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "id",        colType = UuidT-                , colNullable = False-                , colDefault = Just "gen_random_uuid()" }-    , tblColumn { colName = "firstname", colType = TextT-                , colNullable = False }-    , tblColumn { colName = "lastname",  colType = TextT-                , colNullable = False }-    ]-  , tblPrimaryKey = pkOnColumn "id" }--tableBadGuySchema2 :: Table-tableBadGuySchema2 = tableBadGuySchema1--tableBadGuySchema3 :: Table-tableBadGuySchema3 = tableBadGuySchema2--tableBadGuySchema4 :: Table-tableBadGuySchema4 = tableBadGuySchema3--tableBadGuySchema5 :: Table-tableBadGuySchema5 = tableBadGuySchema4--tableRobberySchema1 :: Table-tableRobberySchema1 =-  tblTable-  { tblName = "robbery"-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "id",      colType = UuidT-                , colNullable = False-                , colDefault = Just "gen_random_uuid()" }-    , tblColumn { colName = "bank_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "date",    colType = DateT-                , colNullable = False, colDefault = Just "now()" }-    ]-  , tblPrimaryKey  = pkOnColumn "id"-  , tblForeignKeys = [fkOnColumn "bank_id" "bank" "id"] }--tableRobberySchema2 :: Table-tableRobberySchema2 = tableRobberySchema1--tableRobberySchema3 :: Table-tableRobberySchema3 = tableRobberySchema2--tableRobberySchema4 :: Table-tableRobberySchema4 = tableRobberySchema3--tableRobberySchema5 :: Table-tableRobberySchema5 = tableRobberySchema4--tableParticipatedInRobberySchema1 :: Table-tableParticipatedInRobberySchema1 =-  tblTable-  { tblName = "participated_in_robbery"-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "bad_guy_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "robbery_id", colType = UuidT-                , colNullable = False }-    ]-  , tblPrimaryKey  = pkOnColumns ["bad_guy_id", "robbery_id"]-  , tblForeignKeys = [fkOnColumn  "bad_guy_id" "bad_guy" "id"-                     ,fkOnColumn  "robbery_id" "robbery" "id"] }--tableParticipatedInRobberySchema2 :: Table-tableParticipatedInRobberySchema2 = tableParticipatedInRobberySchema1--tableParticipatedInRobberySchema3 :: Table-tableParticipatedInRobberySchema3 = tableParticipatedInRobberySchema2--tableParticipatedInRobberySchema4 :: Table-tableParticipatedInRobberySchema4 = tableParticipatedInRobberySchema3--tableParticipatedInRobberySchema5 :: Table-tableParticipatedInRobberySchema5 = tableParticipatedInRobberySchema4--tableWitnessName :: RawSQL ()-tableWitnessName = "witness"--tableWitnessSchema1 :: Table-tableWitnessSchema1 =-  tblTable-  { tblName = tableWitnessName-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "id",        colType = UuidT-                , colNullable = False-                , colDefault = Just "gen_random_uuid()" }-    , tblColumn { colName = "firstname", colType = TextT-                , colNullable = False }-    , tblColumn { colName = "lastname",  colType = TextT-                , colNullable = False }-    ]-  , tblPrimaryKey = pkOnColumn "id" }--tableWitnessedRobberyName :: RawSQL ()-tableWitnessedRobberyName = "witnessed_robbery"--tableWitnessedRobberySchema1 :: Table-tableWitnessedRobberySchema1 =-  tblTable-  { tblName = tableWitnessedRobberyName-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "witness_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "robbery_id", colType = UuidT-                , colNullable = False }-    ]-  , tblPrimaryKey  = pkOnColumns ["witness_id", "robbery_id"]-  , tblForeignKeys = [fkOnColumn  "witness_id" "witness" "id"-                     ,fkOnColumn  "robbery_id" "robbery" "id"] }--tableUnderArrestName :: RawSQL ()-tableUnderArrestName = "under_arrest"--tableUnderArrestSchema2 :: Table-tableUnderArrestSchema2 =-  tblTable-  { tblName = tableUnderArrestName-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "bad_guy_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "robbery_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "court_date", colType = DateT-                , colNullable = False-                , colDefault  = Just "now()" }-    ]-  , tblPrimaryKey  = pkOnColumns ["bad_guy_id", "robbery_id"]-  , tblForeignKeys = [fkOnColumn  "bad_guy_id" "bad_guy" "id"-                     ,fkOnColumn  "robbery_id" "robbery" "id"] }--tablePrisonSentenceName :: RawSQL ()-tablePrisonSentenceName = "prison_sentence"--tablePrisonSentenceSchema3 :: Table-tablePrisonSentenceSchema3 =-  tblTable-  { tblName = tablePrisonSentenceName-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "bad_guy_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "robbery_id", colType = UuidT-                , colNullable = False }-    , tblColumn { colName = "sentence_start"-                , colType = DateT-                , colNullable = False-                , colDefault  = Just "now()" }-    , tblColumn { colName = "sentence_length"-                , colType = IntegerT-                , colNullable = False-                , colDefault  = Just "6" }-    , tblColumn { colName = "prison_name"-                , colType = TextT-                , colNullable = False }-    ]-  , tblPrimaryKey  = pkOnColumns ["bad_guy_id", "robbery_id"]-  , tblForeignKeys = [fkOnColumn  "bad_guy_id" "bad_guy" "id"-                     ,fkOnColumn  "robbery_id" "robbery" "id"] }--tablePrisonSentenceSchema4 :: Table-tablePrisonSentenceSchema4 = tablePrisonSentenceSchema3--tablePrisonSentenceSchema5 :: Table-tablePrisonSentenceSchema5 = tablePrisonSentenceSchema4--tableFlashName :: RawSQL ()-tableFlashName = "flash"--tableFlash :: Table-tableFlash =-  tblTable-  { tblName = tableFlashName-  , tblVersion = 1-  , tblColumns =-    [ tblColumn { colName = "flash_id", colType = UuidT, colNullable = False }-    ]-  }--createTableMigration :: (MonadDB m) => Table -> Migration m-createTableMigration tbl = Migration-  { mgrTableName = tblName tbl-  , mgrFrom      = 0-  , mgrAction    = StandardMigration $ do-      createTable True tbl-  }--dropTableMigration :: (MonadDB m) => Table -> Migration m-dropTableMigration tbl = Migration-  { mgrTableName = tblName tbl-  , mgrFrom      = tblVersion tbl-  , mgrAction    = DropTableMigration DropTableCascade-  }--schema1Tables :: [Table]-schema1Tables = [ tableBankSchema1-                , tableBadGuySchema1-                , tableRobberySchema1-                , tableParticipatedInRobberySchema1-                , tableWitnessSchema1-                , tableWitnessedRobberySchema1-                ]--schema1Migrations :: (MonadDB m) => [Migration m]-schema1Migrations =-  [ createTableMigration tableBankSchema1-  , createTableMigration tableBadGuySchema1-  , createTableMigration tableRobberySchema1-  , createTableMigration tableParticipatedInRobberySchema1-  , createTableMigration tableWitnessSchema1-  , createTableMigration tableWitnessedRobberySchema1-  ]--schema2Tables :: [Table]-schema2Tables = [ tableBankSchema2-                , tableBadGuySchema2-                , tableRobberySchema2-                , tableParticipatedInRobberySchema2-                , tableUnderArrestSchema2-                ]--schema2Migrations :: (MonadDB m) => [Migration m]-schema2Migrations = schema1Migrations-                 ++ [ dropTableMigration   tableWitnessedRobberySchema1-                    , dropTableMigration   tableWitnessSchema1-                    , createTableMigration tableUnderArrestSchema2-                    ]--schema3Tables :: [Table]-schema3Tables = [ tableBankSchema3-                , tableBadGuySchema3-                , tableRobberySchema3-                , tableParticipatedInRobberySchema3-                , tablePrisonSentenceSchema3-                ]--schema3Migrations :: (MonadDB m) => [Migration m]-schema3Migrations = schema2Migrations-                 ++ [ dropTableMigration   tableUnderArrestSchema2-                    , createTableMigration tablePrisonSentenceSchema3 ]--schema4Tables :: [Table]-schema4Tables = [ tableBankSchema4-                , tableBadGuySchema4-                , tableRobberySchema4-                , tableParticipatedInRobberySchema4-                , tablePrisonSentenceSchema4-                ]--schema4Migrations :: (MonadDB m) => [Migration m]-schema4Migrations = schema3Migrations-                 ++ [ tableBankMigration4 ]--schema5Tables :: [Table]-schema5Tables = [ tableBankSchema5-                , tableBadGuySchema5-                , tableRobberySchema5-                , tableParticipatedInRobberySchema5-                , tablePrisonSentenceSchema5-                ]--schema5Migrations :: (MonadDB m) => [Migration m]-schema5Migrations = schema4Migrations-                 ++ [ createTableMigration tableFlash-                    , tableBankMigration5fst-                    , tableBankMigration5snd-                    , dropTableMigration tableFlash-                    ]--schema6Tables :: [Table]-schema6Tables =-          [ tableBankSchema1-          , tableBadGuySchema1-          , tableRobberySchema1-          , tableParticipatedInRobberySchema1-              { tblVersion = (tblVersion tableParticipatedInRobberySchema1) + 1,-                tblPrimaryKey = Nothing }-          , tableWitnessSchema1-          , tableWitnessedRobberySchema1-          ]--schema6Migrations :: (MonadDB m) => Migration m-schema6Migrations =-    Migration-    { mgrTableName = tblName tableParticipatedInRobberySchema1-    , mgrFrom = tblVersion tableParticipatedInRobberySchema1-    , mgrAction =-      StandardMigration $ do-        runQuery_ $ ("ALTER TABLE participated_in_robbery DROP CONSTRAINT \-                     \pk__participated_in_robbery" :: RawSQL ())-    }---type TestM a = DBT (LogT IO) a--createTablesSchema1 :: (String -> TestM ()) -> TestM ()-createTablesSchema1 step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Creating the database (schema version 1)..."-  migrateDatabase defaultExtrasOptions extensions domains-    composites schema1Tables schema1Migrations--  -- Add a local index that shouldn't trigger validation errors.-  runSQL_ "CREATE INDEX local_idx_bank_name ON bank(name)"--  checkDatabase defaultExtrasOptions composites domains schema1Tables--testDBSchema1 :: (String -> TestM ()) -> TestM ([UUID], [UUID])-testDBSchema1 step = do-  step "Running test queries (schema version 1)..."--  -- Populate the 'bank' table.-  runQuery_ . sqlInsert "bank" $ do-    sqlSetList "name" ["HSBC" :: T.Text, "Swedbank", "Nordea", "Citi"-                      ,"Wells Fargo"]-    sqlSetList "location" ["13 Foo St., Tucson, AZ, USa" :: T.Text-                          , "18 Bargatan, Stockholm, Sweden"-                          , "23 Baz Lane, Liverpool, UK"-                          , "2/3 Quux Ave., Milton Keynes, UK"-                          , "6600 Sunset Blvd., Los Angeles, CA, USA"]-    sqlResult "id"-  (bankIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'bank' table" 5 (length bankIds)--  -- Try to insert with existing ID to check that ON CONFLICT works properly-  let bankId = head bankIds-      name = "Santander" :: T.Text-      location = "Spain" :: T.Text--  runQuery_ . sqlInsert "bank" $ do-    sqlSet "id" bankId-    sqlSet "name" name-    sqlSet "location" location-    sqlOnConflictOnColumns ["id"] . sqlUpdate "" $ do-      sqlSet "name" name-      sqlSet "location" location-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details1 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT updates" (name, location) details1--  runQuery_ . sqlInsert "bank" $ do-    sqlSet "id" bankId-    sqlSet "name" ("" :: T.Text)-    sqlSet "location" ("" :: T.Text)-    sqlOnConflictDoNothing-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details3 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (1)" (name, location) details3--  runQuery_ . sqlInsert "bank" $ do-    sqlSet "id" bankId-    sqlSet "name" ("" :: T.Text)-    sqlSet "location" ("" :: T.Text)-    sqlOnConflictOnColumnsDoNothing ["id"]-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details4 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (2)" (name, location) details4--  -- If NO CONFLICT is not specified, make sure we throw an exception.-  eres :: Either DBException () <- try . withSavepoint "testDBSchema" $ do-    runQuery_ . sqlInsert "bank" $ do-      sqlSet "id" bankId-      sqlSet "name" name-      sqlSet "location" location-  liftIO $ assertBool "If ON CONFLICT is not specified an exception is thrown" (isLeft eres)--  runQuery_ . sqlInsertSelect "bank" "bank" $ do-    sqlSetCmd "id" "id"-    sqlSetCmd "name" "name"-    sqlSetCmd "location" "location"-    sqlWhereEq "id" bankId-    sqlOnConflictOnColumns ["id"] . sqlUpdate "" $ do-      sqlSet "name" name-      sqlSet "location" location-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details5 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT updates" (name, location) details5--  runQuery_ . sqlInsertSelect "bank" "bank" $ do-    sqlSetCmd "id" "id"-    sqlSetCmd "name" "name"-    sqlSetCmd "location" "location"-    sqlWhereEq "id" bankId-    sqlOnConflictDoNothing-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details6 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (1)" (name, location) details6--  runQuery_ . sqlInsertSelect "bank" "bank" $ do-    sqlSetCmd "id" "id"-    sqlSetCmd "name" "name"-    sqlSetCmd "location" "location"-    sqlWhereEq "id" bankId-    sqlOnConflictOnColumnsDoNothing ["id"]-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlResult "location"-    sqlWhereEq "id" bankId-  details7 <- fetchOne id-  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (2)" (name, location) details7--  -- If NO CONFLICT is not specified, make sure we throw an exception.-  eres1 :: Either DBException () <- try . withSavepoint "testDBSchema" $ do-    runQuery_ . sqlInsertSelect "bank" "bank" $ do-      sqlSetCmd "id" "id"-      sqlSetCmd "name" "name"-      sqlSetCmd "location" "location"-      sqlWhereEq "id" bankId-  liftIO $ assertBool "If ON CONFLICT is not specified an exception is thrown" (isLeft eres1)--  -- Populate the 'bad_guy' table.-  runQuery_ . sqlInsert "bad_guy" $ do-    sqlSetList "firstname" ["Neil" :: T.Text, "Lee", "Freddie", "Frankie"-                           ,"James", "Roy"]-    sqlSetList "lastname" ["Hetzel"::T.Text, "Murray", "Foreman", "Fraser"-                          ,"Crosbie", "Shaw"]-    sqlResult "id"-  (badGuyIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'bad_guy' table" 6 (length badGuyIds)--  -- Populate the 'robbery' table.-  runQuery_ . sqlInsert "robbery" $ do-    sqlSetList "bank_id" [bankIds !! idx | idx <- [0,3]]-    sqlResult "id"-  (robberyIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'robbery' table" 2 (length robberyIds)--  -- Populate the 'participated_in_robbery' table.-  runQuery_ . sqlInsert "participated_in_robbery" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [0,2]]-    sqlSet "robbery_id" (robberyIds !! 0)-    sqlResult "bad_guy_id"-  (participatorIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'participated_in_robbery' table" 2-    (length participatorIds)--  runQuery_ . sqlInsert "participated_in_robbery" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [3,4]]-    sqlSet "robbery_id" (robberyIds !! 1)-    sqlResult "bad_guy_id"-  (participatorIds' :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'participated_in_robbery' table" 2-    (length participatorIds')--  -- Populate the 'witness' table.-  runQuery_ . sqlInsert "witness" $ do-    sqlSetList "firstname" ["Meredith" :: T.Text, "Charlie", "Peter", "Emun"-                           ,"Benedict", "Erica"]-    sqlSetList "lastname" ["Vickers"::T.Text, "Holloway", "Weyland", "Eliott"-                          ,"Wong", "Hackett"]-    sqlResult "id"-  (witnessIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'witness' table" 6 (length witnessIds)--  -- Populate the 'witnessed_robbery' table.-  runQuery_ . sqlInsert "witnessed_robbery" $ do-    sqlSetList "witness_id" [witnessIds  !! idx | idx <- [0,1]]-    sqlSet "robbery_id" (robberyIds !! 0)-    sqlResult "witness_id"-  (robberyWitnessIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'witnessed_robbery' table" 2-    (length robberyWitnessIds)--  runQuery_ . sqlInsert "witnessed_robbery" $ do-    sqlSetList "witness_id" [witnessIds  !! idx | idx <- [2,3,4]]-    sqlSet "robbery_id" (robberyIds !! 1)-    sqlResult "witness_id"-  (robberyWitnessIds' :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'witnessed_robbery' table" 3-    (length robberyWitnessIds')--  -- Create a new record to test order-by case sensitivity.-  runQuery_ . sqlInsert "bank" $ do-    sqlSet "name" ("byblos bank" :: T.Text)-    sqlSet "location" ("SYRIA" :: T.Text)--  -- Check that ordering results by the "location" column uses case-sensitive-  -- sorting (since the collation method for that column is "C").-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "location"-    sqlOrderBy "location"--  details8 <- fetchMany runIdentity-  liftIO $ assertEqual "Using collation method \"C\" leads to case-sensitive ordering of results"-    [ "18 Bargatan, Stockholm, Sweden" :: String-    , "2/3 Quux Ave., Milton Keynes, UK"-    , "23 Baz Lane, Liverpool, UK"-    , "6600 Sunset Blvd., Los Angeles, CA, USA"-    , "SYRIA"-    , "Spain"-    ]-    details8--  -- Check that ordering results by the "name" column uses case-insensitive-  -- sorting (since the collation method for that column is "en_US").-  runQuery_ . sqlSelect "bank" $ do-    sqlResult "name"-    sqlOrderBy "name"--  details9 <- fetchMany runIdentity-  liftIO $ assertEqual "Using collation method \"en_US\" leads to case-insensitive ordering of results"-    [ "byblos bank" :: String-    , "Citi"-    , "Nordea"-    , "Santander"-    , "Swedbank"-    , "Wells Fargo"-    ]-    details9--  do-    deletedRows <- runQuery . sqlDelete "witness" $ do-      sqlWhereEq "id" $ witnessIds !! 5-      sqlResult "firstname"-      sqlResult "lastname"-    liftIO $ assertEqual "DELETE FROM 'witness' table" 1 deletedRows--    deletedName <- fetchOne id-    liftIO $ assertEqual "DELETE FROM 'witness' table RETURNING firstname, lastname"-      ("Erica" :: String, "Hackett" :: String)-      deletedName--  return (badGuyIds, robberyIds)--migrateDBToSchema2 :: (String -> TestM ()) -> TestM ()-migrateDBToSchema2 step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Migrating the database (schema version 1 -> schema version 2)..."-  migrateDatabase defaultExtrasOptions { eoLockTimeoutMs = Just 1000 } extensions composites domains-    schema2Tables schema2Migrations-  checkDatabase defaultExtrasOptions composites domains schema2Tables---- | Hacky version of 'migrateDBToSchema2' used by 'migrationTest3'.-migrateDBToSchema2Hacky :: (String -> TestM ()) -> TestM ()-migrateDBToSchema2Hacky step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Hackily migrating the database (schema version 1 \-       \-> schema version 2)..."-  migrateDatabase defaultExtrasOptions extensions composites domains-    schema2Tables schema2Migrations'-  checkDatabase defaultExtrasOptions composites domains schema2Tables-    where-      schema2Migrations' = createTableMigration tableFlash : schema2Migrations--testDBSchema2 :: (String -> TestM ()) -> [UUID] -> [UUID] -> TestM ()-testDBSchema2 step badGuyIds robberyIds = do-  step "Running test queries (schema version 2)..."--  -- Check that table 'witness' doesn't exist.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"-    <> " AND tablename = 'witness')";-  (witnessExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Table 'witness' doesn't exist" False witnessExists--  -- Check that table 'witnessed_robbery' doesn't exist.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"-    <> " AND tablename = 'witnessed_robbery')";-  (witnessedRobberyExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Table 'witnessed_robbery' doesn't exist" False-    witnessedRobberyExists--  -- Populate table 'under_arrest'.-  runQuery_ . sqlInsert "under_arrest" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [0,2]]-    sqlSet "robbery_id" (robberyIds !! 0)-    sqlResult "bad_guy_id"-  (arrestedIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'under_arrest' table" 2-    (length arrestedIds)--  runQuery_ . sqlInsert "under_arrest" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [3,4]]-    sqlSet "robbery_id" (robberyIds !! 1)-    sqlResult "bad_guy_id"-  (arrestedIds' :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'under_arrest' table" 2-    (length arrestedIds')--  return ()--migrateDBToSchema3 :: (String -> TestM ()) -> TestM ()-migrateDBToSchema3 step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Migrating the database (schema version 2 -> schema version 3)..."-  migrateDatabase defaultExtrasOptions extensions composites domains-    schema3Tables schema3Migrations-  checkDatabase defaultExtrasOptions composites domains schema3Tables--testDBSchema3 :: (String -> TestM ()) -> [UUID] -> [UUID] -> TestM ()-testDBSchema3 step badGuyIds robberyIds = do-  step "Running test queries (schema version 3)..."--  -- Check that table 'under_arrest' doesn't exist.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"-    <> " AND tablename = 'under_arrest')";-  (underArrestExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Table 'under_arrest' doesn't exist" False-    underArrestExists--  -- Check that the table 'prison_sentence' exists.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"-    <> " AND tablename = 'prison_sentence')";-  (prisonSentenceExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Table 'prison_sentence' does exist" True-    prisonSentenceExists--  -- Populate table 'prison_sentence'.-  runQuery_ . sqlInsert "prison_sentence" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [0,2]]-    sqlSet "robbery_id" (robberyIds !! 0)-    sqlSet "sentence_length" (12::Int)-    sqlSet "prison_name" ("Long Kesh"::T.Text)-    sqlResult "bad_guy_id"-  (sentencedIds :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'prison_sentence' table" 2-    (length sentencedIds)--  runQuery_ . sqlInsert "prison_sentence" $ do-    sqlSetList "bad_guy_id" [badGuyIds  !! idx | idx <- [3,4]]-    sqlSet "robbery_id" (robberyIds !! 1)-    sqlSet "sentence_length" (9::Int)-    sqlSet "prison_name" ("Wormwood Scrubs"::T.Text)-    sqlResult "bad_guy_id"-  (sentencedIds' :: [UUID]) <- fetchMany runIdentity-  liftIO $ assertEqual "INSERT into 'prison_sentence' table" 2-    (length sentencedIds')--  return ()--migrateDBToSchema4 :: (String -> TestM ()) -> TestM ()-migrateDBToSchema4 step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Migrating the database (schema version 3 -> schema version 4)..."-  migrateDatabase defaultExtrasOptions extensions composites domains-    schema4Tables schema4Migrations-  checkDatabase defaultExtrasOptions composites domains schema4Tables--testDBSchema4 :: (String -> TestM ()) -> TestM ()-testDBSchema4 step = do-  step "Running test queries (schema version 4)..."--  -- Check that the 'bank' table has a 'cash' column.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM information_schema.columns"-    <> " WHERE table_schema = 'public'"-    <> " AND table_name = 'bank'"-    <> " AND column_name = 'cash')";-  (colCashExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Column 'cash' in the table 'bank' does exist" True-    colCashExists--  return ()--migrateDBToSchema5 :: (String -> TestM ()) -> TestM ()-migrateDBToSchema5 step = do-  let extensions    = ["pgcrypto"]-      composites    = []-      domains       = []-  step "Migrating the database (schema version 4 -> schema version 5)..."-  migrateDatabase defaultExtrasOptions extensions composites domains-    schema5Tables schema5Migrations-  checkDatabase defaultExtrasOptions composites domains schema5Tables--testDBSchema5 :: (String -> TestM ()) -> TestM ()-testDBSchema5 step = do-  step "Running test queries (schema version 5)..."--  -- Check that the 'bank' table doesn't have a 'cash' column.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM information_schema.columns"-    <> " WHERE table_schema = 'public'"-    <> " AND table_name = 'bank'"-    <> " AND column_name = 'cash')";-  (colCashExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Column 'cash' in the table 'bank' doesn't exist" False-    colCashExists--  -- Check that the 'flash' table doesn't exist.-  runSQL_ $ "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"-    <> " AND tablename = 'flash')";-  (flashExists :: Bool) <- fetchOne runIdentity-  liftIO $ assertEqual "Table 'flash' doesn't exist" False flashExists--  return ()---- | May require 'ALTER SCHEMA public OWNER TO $user' the first time--- you run this.-freshTestDB ::  (String -> TestM ()) -> TestM ()-freshTestDB step = do-  step "Dropping the test DB schema..."-  runSQL_ "DROP SCHEMA public CASCADE"-  runSQL_ "CREATE SCHEMA public"---- | Re-used by 'migrationTest5'.-migrationTest1Body :: (String -> TestM ()) -> TestM ()-migrationTest1Body step = do-  createTablesSchema1 step-  (badGuyIds, robberyIds) <--    testDBSchema1     step--  migrateDBToSchema2  step-  testDBSchema2       step badGuyIds robberyIds--  migrateDBToSchema3  step-  testDBSchema3       step badGuyIds robberyIds--  migrateDBToSchema4  step-  testDBSchema4       step--  migrateDBToSchema5  step-  testDBSchema5       step--bankTrigger1 :: Trigger-bankTrigger1 =-  Trigger { triggerTable = "bank"-          , triggerName = "trigger_1"-          , triggerEvents = Set.fromList [TriggerInsert]-          , triggerDeferrable = False-          , triggerInitiallyDeferred = False-          , triggerWhen = Nothing-          , triggerFunction =-                "begin"-            <+> "  perform true;"-            <+> "  return null;"-            <+> "end;"-          }--bankTrigger2 :: Trigger-bankTrigger2 =-  bankTrigger1-  { triggerFunction =-          "begin"-      <+> "  return null;"-      <+> "end;"-  }--bankTrigger3 :: Trigger-bankTrigger3 =-  Trigger { triggerTable = "bank"-          , triggerName = "trigger_3"-          , triggerEvents = Set.fromList [TriggerInsert, TriggerUpdateOf [unsafeSQL "location"]]-          , triggerDeferrable = True-          , triggerInitiallyDeferred = True-          , triggerWhen = Nothing-          , triggerFunction =-                "begin"-            <+> "  perform true;"-            <+> "  return null;"-            <+> "end;"-          }--bankTrigger2Proper :: Trigger-bankTrigger2Proper =-  bankTrigger2 { triggerName = "trigger_2" }--testTriggers :: HasCallStack => (String -> TestM ()) -> TestM ()-testTriggers step = do-  step "Running trigger tests..."--  step "create the initial database"-  migrate [tableBankSchema1] [createTableMigration tableBankSchema1]--  do-    let msg = "checkDatabase fails if there are triggers in the database but not in the schema"-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = []-                                }-             ]-        ms = [ createTriggerMigration 1 bankTrigger1 ]-    step msg-    assertException msg $ migrate ts ms--  do-    let msg = "checkDatabase fails if there are triggers in the schema but not in the database"-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [bankTrigger1]-                                }-             ]-        ms = []-    triggerStep msg $ do-      assertException msg $ migrate ts ms--  do-    let msg = "test succeeds when creating a single trigger"-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [bankTrigger1]-                                }-             ]-        ms = [ createTriggerMigration 1 bankTrigger1 ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [bankTrigger1] True--  do-    -- Attempt to create the same triggers twice. Should fail with a DBException saying-    -- that function already exists.-    let msg = "database exception is raised if trigger is created twice"-        ts = [ tableBankSchema1 { tblVersion = 3-                                , tblTriggers = [bankTrigger1]-                                }-             ]-        ms = [ createTriggerMigration 1 bankTrigger1-             , createTriggerMigration 2 bankTrigger1-             ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "database exception is raised if triggers only differ in function name"-        ts = [ tableBankSchema1 { tblVersion = 3-                                , tblTriggers = [bankTrigger1, bankTrigger2]-                                }-             ]-        ms = [ createTriggerMigration 1 bankTrigger1-             , createTriggerMigration 2 bankTrigger2-             ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "successfully migrate two triggers"-        ts = [ tableBankSchema1 { tblVersion = 3-                                , tblTriggers = [bankTrigger1, bankTrigger2Proper]-                                }-             ]-        ms = [ createTriggerMigration 1 bankTrigger1-             , createTriggerMigration 2 bankTrigger2Proper-             ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [bankTrigger1, bankTrigger2Proper] True--  do-    let msg = "database exception is raised if trigger's WHEN is syntactically incorrect"-        trg = bankTrigger1 { triggerWhen = Just "WILL FAIL" }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "database exception is raised if trigger's WHEN uses undefined column"-        trg = bankTrigger1 { triggerWhen = Just "NEW.foobar = 1" }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    -- This trigger is valid. However, the WHEN clause specified in triggerWhen is not-    -- what gets returned from the database. The decompiled and normalized WHEN clause-    -- from the database looks like this:-    --   new.name <> 'foobar'::text-    -- We simply assert an exception, which presumably comes from the migration framework,-    -- while it should actually be a deeper check for just the differing WHEN-    -- clauses. On the other hand, it's probably good enough as it is.-    -- See the comment for 'getDBTriggers' in src/Database/PostgreSQL/PQTypes/Model/Trigger.hs.-    let msg = "checkDatabase fails if WHEN clauses from database and code differ"-        trg = bankTrigger1 { triggerWhen = Just "NEW.name != 'foobar'" }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertException msg $ migrate ts ms--  do-    let msg = "successfully migrate trigger with valid WHEN"-        trg = bankTrigger1 { triggerWhen = Just "new.name <> 'foobar'::text" }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [trg] True--  do-    let msg = "successfully migrate trigger that is deferrable"-        trg = bankTrigger1 { triggerDeferrable = True }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [trg] True--  do-    let msg = "successfully migrate trigger that is deferrable and initially deferred"-        trg = bankTrigger1 { triggerDeferrable = True-                           , triggerInitiallyDeferred = True-                           }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [trg] True--  do-    let msg = "database exception is raised if trigger is initially deferred but not deferrable"-        trg = bankTrigger1 { triggerDeferrable = False-                           , triggerInitiallyDeferred = True-                           }-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "database exception is raised if dropping trigger that does not exist"-        trg = bankTrigger1-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ dropTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "database exception is raised if dropping trigger function of which does not exist"-        trg = bankTrigger2-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ dropTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "successfully drop trigger"-        trg = bankTrigger1-        ts = [ tableBankSchema1 { tblVersion = 3-                                , tblTriggers = []-                                }-             ]-        ms = [ createTriggerMigration 1 trg, dropTriggerMigration 2 trg ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [trg] False--  do-    let msg = "database exception is raised if dropping trigger twice"-        trg = bankTrigger2-        ts = [ tableBankSchema1 { tblVersion = 3-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ dropTriggerMigration 1 trg, dropTriggerMigration 2 trg ]-    triggerStep msg $ do-      assertDBException msg $ migrate ts ms--  do-    let msg = "successfully create trigger with multiple events"-        trg = bankTrigger3-        ts = [ tableBankSchema1 { tblVersion = 2-                                , tblTriggers = [trg]-                                }-             ]-        ms = [ createTriggerMigration 1 trg ]-    triggerStep msg $ do-      assertNoException msg $ migrate ts ms-      verify [trg] True--  where-    triggerStep msg rest = do-      recreateTriggerDB-      step msg-      rest--    migrate tables migrations = do-      migrateDatabase defaultExtrasOptions ["pgcrypto"] [] [] tables migrations-      checkDatabase defaultExtrasOptions [] [] tables--    -- Verify that the given triggers are (not) present in the database.-    verify :: (MonadIO m, MonadDB m, HasCallStack) => [Trigger] -> Bool -> m ()-    verify triggers present = do-      dbTriggers <- getDBTriggers "bank"-      let trgs = map fst dbTriggers-          ok = and $ map (`elem` trgs) triggers-          err = "Triggers " <> (if present then "" else "not ") <> "present in the database."-          trans = if present then id else not-      liftIO . assertBool err $ trans ok--    triggerMigration :: MonadDB m => (Trigger -> m ()) -> Int -> Trigger -> Migration m-    triggerMigration fn from trg = Migration-      { mgrTableName = tblName tableBankSchema1-      , mgrFrom = fromIntegral from-      , mgrAction = StandardMigration $ fn trg-      }--    createTriggerMigration :: MonadDB m => Int -> Trigger -> Migration m-    createTriggerMigration = triggerMigration createTrigger--    dropTriggerMigration :: MonadDB m => Int -> Trigger -> Migration m-    dropTriggerMigration = triggerMigration dropTrigger--    recreateTriggerDB = do-      runSQL_ "DROP TRIGGER IF EXISTS trg__bank__trigger_1 ON bank;"-      runSQL_ "DROP TRIGGER IF EXISTS trg__bank__trigger_2 ON bank;"-      runSQL_ "DROP FUNCTION IF EXISTS trgfun__trigger_1;"-      runSQL_ "DROP FUNCTION IF EXISTS trgfun__trigger_2;"-      runSQL_ "DROP TABLE IF EXISTS bank;"-      runSQL_ "DELETE FROM table_versions WHERE name = 'bank'";-      migrate [tableBankSchema1] [createTableMigration tableBankSchema1]--testSqlWith :: HasCallStack => (String -> TestM ()) -> TestM ()-testSqlWith step = do-  step "Running sql WITH tests"-  testPass-  runSQL_ "DELETE FROM bank"-  step "Checking for WITH MATERIALIZED support"-  checkAndRememberMaterializationSupport-  step "Running sql WITH tests again with WITH MATERIALIZED support flag set"-  testPass-  where-    migrate tables migrations = do-      migrateDatabase defaultExtrasOptions ["pgcrypto"] [] [] tables migrations-      checkDatabase defaultExtrasOptions [] [] tables-    testPass = do-      step "create the initial database"-      migrate [tableBankSchema1] [createTableMigration tableBankSchema1]-      step "inserting initial data"-      runQuery_ . sqlInsert "bank" $ do-        sqlSetList "name" (["HSBC" :: T.Text, "other"])-        sqlSetList "location" (["13 Foo St., Tucson" :: T.Text, "no address"])-        sqlResult "id"-      step "testing WITH .. INSERT SELECT"-      runQuery_ . sqlInsertSelect "bank" "bank_name" $ do-        sqlWith "bank_name" $ do-          sqlSelect "bank" $ do-            sqlResult "'another'"-            sqlLimit (1 :: Int)-        sqlFrom "bank_name"-        sqlSetCmd "name" "bank_name"-        sqlSet "location" ("Other side" :: T.Text)-      step "testing WITH .. UPDATE"-      runQuery_ . sqlUpdate "bank" $ do-        sqlWith "other_bank" $ do-          sqlSelect "bank" $ do-            sqlWhereEq "name" ("other" :: T.Text)-            sqlResult "id"-        sqlFrom "other_bank"-        sqlSet "location" ("abcd" :: T.Text)-        sqlWhereInSql "bank.id" $ mkSQL "other_bank.id"-        sqlResult "bank.id"-      step "testing WITH .. DELETE"-      runQuery_ . sqlDelete "bank" $ do-        sqlWith "other_bank" $ do-          sqlSelect "bank" $ do-            sqlWhereEq "name" ("other" :: T.Text)-            sqlResult "id"-        sqlFrom "other_bank"-        sqlWhereInSql "bank.id" $ mkSQL "other_bank.id"-      step "testing WITH .. SELECT"-      runQuery_ . sqlSelect "bank" $ do-        sqlWith "other_bank" $ do-          sqlSelect "bank" $ do-            sqlResult "name"-        sqlFrom "other_bank"-        sqlResult "other_bank.name"-      (results :: [T.Text]) <- fetchMany runIdentity-      liftIO $ assertEqual "Wrong number of banks left" 2 (length results)--testUnion :: HasCallStack => (String -> TestM ()) -> TestM ()-testUnion step = do-  step "Running SQL UNION tests"-  testPass-  where-    testPass = do-      runQuery_ . sqlSelect "" $ do-        sqlResult "true"-        sqlUnion-          [ sqlSelect "" $ do-              sqlResult "false"-          , sqlSelect "" $ do-              sqlResult "true"-          ]-      result <- fetchMany runIdentity-      liftIO $ assertEqual "UNION of booleans"-        [False, True]-        result---testUnionAll :: HasCallStack => (String -> TestM ()) -> TestM ()-testUnionAll step = do-  step "Running SQL UNION ALL tests"-  testPass-  where-    testPass = do-      runQuery_ . sqlSelect "" $ do-        sqlResult "true"-        sqlUnionAll-          [ sqlSelect "" $ do-              sqlResult "false"-          , sqlSelect "" $ do-              sqlResult "true"-          ]-      result <- fetchMany runIdentity-      liftIO $ assertEqual "UNION ALL of booleans"-        [True, False, True]-        result--migrationTest1 :: ConnectionSourceM (LogT IO) -> TestTree-migrationTest1 connSource =-  testCaseSteps' "Migration test 1" connSource $ \step -> do-  freshTestDB         step--  migrationTest1Body  step---- | Test for behaviour of 'checkDatabase' and 'checkDatabaseAllowUnknownObjects'-migrationTest2 :: ConnectionSourceM (LogT IO) -> TestTree-migrationTest2 connSource =-  testCaseSteps' "Migration test 2" connSource $ \step -> do-  freshTestDB    step--  createTablesSchema1 step--  let composite = CompositeType-        { ctName = "composite"-        , ctColumns =-          [ CompositeColumn { ccName = "cint",  ccType = UuidT }-          , CompositeColumn { ccName = "ctext", ccType = TextT }-          ]-        }-      currentSchema   = schema1Tables-      differentSchema = schema5Tables-      extrasOptions   = defaultExtrasOptions { eoEnforcePKs = True }-      extrasOptionsWithUnknownObjects = extrasOptions { eoObjectsValidationMode = AllowUnknownObjects }--  runQuery_ $ sqlCreateComposite composite--  assertNoException "checkDatabase should run fine for consistent DB" $-    checkDatabase extrasOptions [composite] [] currentSchema-  assertException "checkDatabase fails if composite type definition is not provided" $-    checkDatabase extrasOptions [] [] currentSchema-  assertNoException "checkDatabaseAllowUnknownTables runs fine \-                    \for consistent DB" $-    checkDatabase extrasOptionsWithUnknownObjects [composite] [] currentSchema-  assertNoException "checkDatabaseAllowUnknownTables runs fine \-                    \for consistent DB with unknown composite type in the database" $-    checkDatabase extrasOptionsWithUnknownObjects [] [] currentSchema-  assertException "checkDatabase should throw exception for wrong schema" $-    checkDatabase extrasOptions [] [] differentSchema-  assertException ("checkDatabaseAllowUnknownObjects \-                   \should throw exception for wrong scheme") $-    checkDatabase extrasOptionsWithUnknownObjects [] [] differentSchema--  runSQL_ "INSERT INTO table_versions (name, version) \-          \VALUES ('unknown_table', 0)"-  assertException "checkDatabase throw when extra entry in 'table_versions'" $-    checkDatabase extrasOptions [] [] currentSchema-  assertNoException ("checkDatabaseAllowUnknownObjects \-                     \accepts extra entry in 'table_versions'") $-    checkDatabase extrasOptionsWithUnknownObjects [] [] currentSchema-  runSQL_ "DELETE FROM table_versions where name='unknown_table'"--  runSQL_ "CREATE TABLE unknown_table (title text)"-  assertException "checkDatabase should throw with unknown table" $-    checkDatabase extrasOptions [] [] currentSchema-  assertNoException "checkDatabaseAllowUnknownObjects accepts unknown table" $-    checkDatabase extrasOptionsWithUnknownObjects [] [] currentSchema--  runSQL_ "INSERT INTO table_versions (name, version) \-          \VALUES ('unknown_table', 0)"-  assertException "checkDatabase should throw with unknown table" $-    checkDatabase extrasOptions [] [] currentSchema-  assertNoException ("checkDatabaseAllowUnknownObjects \-                     \accepts unknown tables with version") $-    checkDatabase extrasOptionsWithUnknownObjects [] [] currentSchema--  freshTestDB    step--  let schema1TablesWithMissingPK     = schema6Tables-      schema1MigrationsWithMissingPK = schema6Migrations-      withMissingPKSchema            = schema1TablesWithMissingPK-      optionsNoPKCheck               = defaultExtrasOptions-                                       { eoEnforcePKs = False }-      optionsWithPKCheck             = defaultExtrasOptions-                                       { eoEnforcePKs = True }--  step "Recreating the database (schema version 1, one table is missing PK)..."--  migrateDatabase optionsNoPKCheck ["pgcrypto"] [] []-    schema1TablesWithMissingPK [schema1MigrationsWithMissingPK]-  checkDatabase optionsNoPKCheck [] [] withMissingPKSchema--  assertException-    "checkDatabase should throw when PK missing from table \-    \'participated_in_robbery' and check is enabled" $-    checkDatabase optionsWithPKCheck [] [] withMissingPKSchema-  assertNoException-    "checkDatabase should not throw when PK missing from table \-    \'participated_in_robbery' and check is disabled" $-    checkDatabase optionsNoPKCheck [] [] withMissingPKSchema--  freshTestDB step--migrationTest3 :: ConnectionSourceM (LogT IO) -> TestTree-migrationTest3 connSource =-  testCaseSteps' "Migration test 3" connSource $ \step -> do-  freshTestDB         step--  createTablesSchema1 step-  (badGuyIds, robberyIds) <--    testDBSchema1     step--  migrateDBToSchema2  step-  testDBSchema2       step badGuyIds robberyIds--  assertException ( "Trying to run the same migration twice should fail, \-                     \when starting with a createTable migration" ) $-    migrateDBToSchema2Hacky  step--  freshTestDB         step---- | Test that running the same migrations twice doesn't result in--- unexpected errors.-migrationTest4 :: ConnectionSourceM (LogT IO) -> TestTree-migrationTest4 connSource =-  testCaseSteps' "Migration test 4" connSource $ \step -> do-  freshTestDB         step--  migrationTest1Body  step--  -- Here we run step 5 for the second time. This should be a no-op.-  migrateDBToSchema5  step-  testDBSchema5       step--  freshTestDB         step---- | Test triggers.-triggerTests :: ConnectionSourceM (LogT IO) -> TestTree-triggerTests connSource =-  testCaseSteps' "Trigger tests" connSource $ \step -> do-    freshTestDB  step-    testTriggers step--sqlWithTests :: ConnectionSourceM (LogT IO) -> TestTree-sqlWithTests connSource =-  testCaseSteps' "sql WITH tests" connSource $ \step -> do-    freshTestDB step-    testSqlWith step--unionTests :: ConnectionSourceM (LogT IO) -> TestTree-unionTests connSource =-  testCaseSteps' "SQL UNION Tests" connSource $ \step -> do-    freshTestDB step-    testUnion step--unionAllTests :: ConnectionSourceM (LogT IO) -> TestTree-unionAllTests connSource =-  testCaseSteps' "SQL UNION ALL Tests" connSource $ \step -> do-    freshTestDB step-    testUnionAll step--eitherExc :: MonadCatch m => (SomeException -> m ()) -> (a -> m ()) -> m a -> m ()-eitherExc left right c = try c >>= either left right--migrationTest5 :: ConnectionSourceM (LogT IO) -> TestTree-migrationTest5 connSource =-  testCaseSteps' "Migration test 5" connSource $ \step -> do-    freshTestDB step--    step "Creating the database (schema version 1)..."-    migrateDatabase defaultExtrasOptions ["pgcrypto"] [] [] [table1] [createTableMigration table1]-    checkDatabase defaultExtrasOptions [] [] [table1]--    step "Populating the 'bank' table..."-    runQuery_ . sqlInsert "bank" $ do-      sqlSetList "name" $ (\i -> "bank" <> show i) <$> numbers-      sqlSetList "location" $ (\i -> "location" <> show i) <$> numbers--    -- Explicitly vacuum to update the catalog so that getting the row number estimates-    -- works. The bracket_ trick is here because vacuum can't run inside a transaction-    -- block, which every test runs in.-    bracket_ (runSQL_ "COMMIT")-             (runSQL_ "BEGIN")-             (runSQL_ "VACUUM bank")--    forM_ (zip4 tables migrations steps assertions) $-      \(table, migration, step', assertion) -> do-        step step'-        migrateDatabase defaultExtrasOptions ["pgcrypto"] [] [] [table] [migration]-        checkDatabase defaultExtrasOptions [] [] [table]-        uncurry assertNoException assertion--    freshTestDB step--  where-    -- Chosen by a fair dice roll.-    numbers = [1..101] :: [Int]-    table1 = tableBankSchema1-    tables = [ table1 { tblVersion = 2-                      , tblColumns = tblColumns table1 ++ [stringColumn]-                      }-             , table1 { tblVersion = 3-                      , tblColumns = tblColumns table1 ++ [stringColumn]-                      }-             , table1 { tblVersion = 4-                      , tblColumns = tblColumns table1 ++ [stringColumn, boolColumn]-                      }-             , table1 { tblVersion = 5-                      , tblColumns = tblColumns table1 ++ [stringColumn, boolColumn]-                      }-             ]--    migrations = [ addStringColumnMigration-                 , copyStringColumnMigration-                 , addBoolColumnMigration-                 , modifyBoolColumnMigration-                 ]--    steps = [ "Adding string column (version 1 -> version 2)..."-            , "Copying string column (version 2 -> version 3)..."-            , "Adding bool column (version 3 -> version 4)..."-            , "Modifying bool column (version 4 -> version 5)..."-            ]--    assertions =-      [ ("Check that the string column has been added" :: String, checkAddStringColumn)-      , ("Check that the string data has been copied", checkCopyStringColumn)-      , ("Check that the bool column has been added", checkAddBoolColumn)-      , ("Check that the bool column has been modified", checkModifyBoolColumn)-      ]--    stringColumn = tblColumn { colName = "name_new"-                             , colType = TextT-                             }--    boolColumn = tblColumn { colName = "name_is_true"-                            , colType = BoolT-                            , colNullable = False-                            , colDefault = Just "false"-                            }--    cursorSql = "SELECT id FROM bank" :: SQL--    addStringColumnMigration = Migration-      { mgrTableName = "bank"-      , mgrFrom = 1-      , mgrAction = StandardMigration $-        runQuery_ $ sqlAlterTable "bank" [ sqlAddColumn stringColumn ]-      }--    copyStringColumnMigration = Migration-      { mgrTableName = "bank"-      , mgrFrom = 2-      , mgrAction = ModifyColumnMigration "bank" cursorSql copyColumnSql 1000-      }-    copyColumnSql :: MonadDB m => [Identity UUID] -> m ()-    copyColumnSql primaryKeys =-      runQuery_ . sqlUpdate "bank" $ do-        sqlSetCmd "name_new" "bank.name"-        sqlWhereEqualsAny "bank.id" $ runIdentity <$> primaryKeys--    addBoolColumnMigration = Migration-      { mgrTableName = "bank"-      , mgrFrom = 3-      , mgrAction = StandardMigration $-        runQuery_ $ sqlAlterTable "bank" [ sqlAddColumn boolColumn ]-      }--    modifyBoolColumnMigration = Migration-      { mgrTableName = "bank"-      , mgrFrom = 4-      , mgrAction = ModifyColumnMigration "bank" cursorSql modifyColumnSql 1000-      }-    modifyColumnSql :: MonadDB m => [Identity UUID] -> m ()-    modifyColumnSql primaryKeys =-      runQuery_ . sqlUpdate "bank" $ do-        sqlSet "name_is_true" True-        sqlWhereIn "bank.id" $ runIdentity <$> primaryKeys--    checkAddStringColumn = do-      runQuery_ . sqlSelect "bank" $ sqlResult "name_new"-      rows :: [Maybe T.Text] <- fetchMany runIdentity-      liftIO . assertEqual "No name_new in empty column" True $ all (== Nothing) rows--    checkCopyStringColumn = do-      runQuery_ . sqlSelect "bank" $ sqlResult "name_new"-      rows_new :: [Maybe T.Text] <- fetchMany runIdentity-      runQuery_ . sqlSelect "bank" $ sqlResult "name"-      rows_old :: [Maybe T.Text] <- fetchMany runIdentity-      liftIO . assertEqual "All name_new are equal name" True $-        all (uncurry (==)) $ zip rows_new rows_old--    checkAddBoolColumn = do-      runQuery_ . sqlSelect "bank" $ sqlResult "name_is_true"-      rows :: [Maybe Bool] <- fetchMany runIdentity-      liftIO . assertEqual "All name_is_true default to false" True $ all (== Just False) rows--    checkModifyBoolColumn = do-      runQuery_ . sqlSelect "bank" $ sqlResult "name_is_true"-      rows :: [Maybe Bool] <- fetchMany runIdentity-      liftIO . assertEqual "All name_is_true are true" True $ all (== Just True) rows--assertNoException :: String -> TestM () -> TestM ()-assertNoException t c = eitherExc-  (const $ liftIO $ assertFailure ("Exception thrown for: " ++ t))-  (const $ return ()) c--assertException :: String -> TestM () -> TestM ()-assertException t c = eitherExc-  (const $ return ())-  (const $ liftIO $ assertFailure ("No exception thrown for: " ++ t)) c--assertDBException :: String -> TestM () -> TestM ()-assertDBException t c =-  try c >>= either (\DBException{} -> pure ())-                   (const . liftIO . assertFailure $ "No DBException thrown for: " ++ t)---- | A variant of testCaseSteps that works in TestM monad.-testCaseSteps' :: TestName -> ConnectionSourceM (LogT IO)-               -> ((String -> TestM ()) -> TestM ())-               -> TestTree-testCaseSteps' testName connSource f =-  testCaseSteps testName $ \step' -> do-  let step s = liftIO $ step' s-  withStdOutLogger $ \logger ->-    runLogT "hpqtypes-extras-test" logger defaultLogLevel $-    runDBT connSource defaultTransactionSettings $-    f step--main :: IO ()-main = do-  defaultMainWithIngredients ings $-    askOption $ \(ConnectionString connectionString) ->-    let connSettings = defaultConnectionSettings-                       { csConnInfo = T.pack connectionString }-        ConnectionSource connSource = simpleSource connSettings-    in-    testGroup "DB tests" [ migrationTest1 connSource-                         , migrationTest2 connSource-                         , migrationTest3 connSource-                         , migrationTest4 connSource-                         , migrationTest5 connSource-                         , triggerTests connSource-                         , sqlWithTests connSource-                         , unionTests connSource-                         , unionAllTests connSource-                         ]-  where-    ings =-      includingOptions [Option (Proxy :: Proxy ConnectionString)]-      : defaultIngredients+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}++{-# HLINT ignore "Use head" #-}+module Main where++import Control.Monad (forM_)+import Control.Monad.Catch+import Control.Monad.IO.Class+import Data.Either+import Data.List (zip4)+import Data.Set qualified as Set+import Data.Text qualified as T+import Data.Typeable+import Data.UUID.Types++import Data.Monoid.Utils+import Database.PostgreSQL.PQTypes+import Database.PostgreSQL.PQTypes.Checks+import Database.PostgreSQL.PQTypes.Model.ColumnType+import Database.PostgreSQL.PQTypes.Model.CompositeType+import Database.PostgreSQL.PQTypes.Model.EnumType+import Database.PostgreSQL.PQTypes.Model.ForeignKey+import Database.PostgreSQL.PQTypes.Model.Index+import Database.PostgreSQL.PQTypes.Model.Migration+import Database.PostgreSQL.PQTypes.Model.PrimaryKey+import Database.PostgreSQL.PQTypes.Model.Table+import Database.PostgreSQL.PQTypes.Model.Trigger+import Database.PostgreSQL.PQTypes.SQL.Builder+import Log+import Log.Backend.StandardOutput++import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.Options++newtype ConnectionString = ConnectionString String+  deriving (Typeable)++instance IsOption ConnectionString where+  defaultValue =+    ConnectionString+      -- For GitHub Actions CI+      "host=postgres user=postgres password=postgres"+  parseValue = Just . ConnectionString+  optionName = return "connection-string"+  optionHelp = return "Postgres connection string"++-- Simple example schemata inspired by the one in+-- <  http://www.databaseanswers.org/data_models/bank_robberies/index.htm>+--+-- Schema 1: Bank robberies, tables:+--           bank, bad_guy, robbery, participated_in_robbery, witness,+--           witnessed_robbery+-- Schema 2: Witnesses go into witness protection program,+--           (some) bad guys get arrested:+--           drop tables witness and witnessed_robbery,+--           add table under_arrest+-- Schema 3: Bad guys get their prison sentences:+--           drop table under_arrest+--           add table prison_sentence+-- Schema 4: New 'cash' column for the 'bank' table.+-- Schema 5: Create a new table 'flash',+--           drop the 'cash' column from the 'bank' table,+--           drop table 'flash'.+-- Cleanup:  drop everything++tableBankSchema1 :: Table+tableBankSchema1 =+  tblTable+    { tblName = "bank"+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "id"+            , colType = UuidT+            , colNullable = False+            , colDefault = Just "gen_random_uuid()"+            }+        , tblColumn+            { colName = "name"+            , colType = TextT+            , colCollation = Just "en_US"+            , colNullable = False+            }+        , tblColumn+            { colName = "location"+            , colType = TextT+            , colCollation = Just "C"+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumn "id"+    , tblTriggers = []+    }++tableBankSchema2 :: Table+tableBankSchema2 = tableBankSchema1++tableBankSchema3 :: Table+tableBankSchema3 = tableBankSchema2++tableBankMigration4 :: MonadDB m => Migration m+tableBankMigration4 =+  Migration+    { mgrTableName = tblName tableBankSchema3+    , mgrFrom = 1+    , mgrAction = StandardMigration $ do+        runQuery_ $+          sqlAlterTable+            (tblName tableBankSchema3)+            [ sqlAddColumn $+                tblColumn+                  { colName = "cash"+                  , colType = IntegerT+                  , colNullable = False+                  , colDefault = Just "0"+                  }+            ]+    }++tableBankSchema4 :: Table+tableBankSchema4 =+  tableBankSchema3+    { tblVersion = tblVersion tableBankSchema3 + 1+    , tblColumns =+        tblColumns tableBankSchema3+          ++ [ tblColumn+                { colName = "cash"+                , colType = IntegerT+                , colNullable = False+                , colDefault = Just "0"+                }+             ]+    }++tableBankMigration5fst :: MonadDB m => Migration m+tableBankMigration5fst =+  Migration+    { mgrTableName = tblName tableBankSchema3+    , mgrFrom = 2+    , mgrAction = StandardMigration $ do+        runQuery_ $+          sqlAlterTable+            (tblName tableBankSchema4)+            [ sqlDropColumn "cash"+            ]+    }++tableBankMigration5snd :: MonadDB m => Migration m+tableBankMigration5snd =+  Migration+    { mgrTableName = tblName tableBankSchema3+    , mgrFrom = 3+    , mgrAction =+        CreateIndexConcurrentlyMigration+          (tblName tableBankSchema3)+          ((indexOnColumn "name") {idxInclude = ["id", "location"]})+    }++tableBankSchema5 :: Table+tableBankSchema5 =+  tableBankSchema4+    { tblVersion = tblVersion tableBankSchema4 + 2+    , tblColumns =+        filter+          (\c -> colName c /= "cash")+          (tblColumns tableBankSchema4)+    , tblIndexes = [(indexOnColumn "name") {idxInclude = ["id", "location"]}]+    }++tableBadGuySchema1 :: Table+tableBadGuySchema1 =+  tblTable+    { tblName = "bad_guy"+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "id"+            , colType = UuidT+            , colNullable = False+            , colDefault = Just "gen_random_uuid()"+            }+        , tblColumn+            { colName = "firstname"+            , colType = TextT+            , colNullable = False+            }+        , tblColumn+            { colName = "lastname"+            , colType = TextT+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumn "id"+    }++tableBadGuySchema2 :: Table+tableBadGuySchema2 = tableBadGuySchema1++tableBadGuySchema3 :: Table+tableBadGuySchema3 = tableBadGuySchema2++tableBadGuySchema4 :: Table+tableBadGuySchema4 = tableBadGuySchema3++tableBadGuySchema5 :: Table+tableBadGuySchema5 = tableBadGuySchema4++tableRobberySchema1 :: Table+tableRobberySchema1 =+  tblTable+    { tblName = "robbery"+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "id"+            , colType = UuidT+            , colNullable = False+            , colDefault = Just "gen_random_uuid()"+            }+        , tblColumn+            { colName = "bank_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "date"+            , colType = DateT+            , colNullable = False+            , colDefault = Just "now()"+            }+        ]+    , tblPrimaryKey = pkOnColumn "id"+    , tblForeignKeys = [fkOnColumn "bank_id" "bank" "id"]+    }++tableRobberySchema2 :: Table+tableRobberySchema2 = tableRobberySchema1++tableRobberySchema3 :: Table+tableRobberySchema3 = tableRobberySchema2++tableRobberySchema4 :: Table+tableRobberySchema4 = tableRobberySchema3++tableRobberySchema5 :: Table+tableRobberySchema5 = tableRobberySchema4++tableParticipatedInRobberySchema1 :: Table+tableParticipatedInRobberySchema1 =+  tblTable+    { tblName = "participated_in_robbery"+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "bad_guy_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "robbery_id"+            , colType = UuidT+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumns ["bad_guy_id", "robbery_id"]+    , tblForeignKeys =+        [ fkOnColumn "bad_guy_id" "bad_guy" "id"+        , fkOnColumn "robbery_id" "robbery" "id"+        ]+    }++tableParticipatedInRobberySchema2 :: Table+tableParticipatedInRobberySchema2 = tableParticipatedInRobberySchema1++tableParticipatedInRobberySchema3 :: Table+tableParticipatedInRobberySchema3 = tableParticipatedInRobberySchema2++tableParticipatedInRobberySchema4 :: Table+tableParticipatedInRobberySchema4 = tableParticipatedInRobberySchema3++tableParticipatedInRobberySchema5 :: Table+tableParticipatedInRobberySchema5 = tableParticipatedInRobberySchema4++tableWitnessName :: RawSQL ()+tableWitnessName = "witness"++tableWitnessSchema1 :: Table+tableWitnessSchema1 =+  tblTable+    { tblName = tableWitnessName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "id"+            , colType = UuidT+            , colNullable = False+            , colDefault = Just "gen_random_uuid()"+            }+        , tblColumn+            { colName = "firstname"+            , colType = TextT+            , colNullable = False+            }+        , tblColumn+            { colName = "lastname"+            , colType = TextT+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumn "id"+    }++tableWitnessedRobberyName :: RawSQL ()+tableWitnessedRobberyName = "witnessed_robbery"++tableWitnessedRobberySchema1 :: Table+tableWitnessedRobberySchema1 =+  tblTable+    { tblName = tableWitnessedRobberyName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "witness_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "robbery_id"+            , colType = UuidT+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumns ["witness_id", "robbery_id"]+    , tblForeignKeys =+        [ fkOnColumn "witness_id" "witness" "id"+        , fkOnColumn "robbery_id" "robbery" "id"+        ]+    }++tableUnderArrestName :: RawSQL ()+tableUnderArrestName = "under_arrest"++tableUnderArrestSchema2 :: Table+tableUnderArrestSchema2 =+  tblTable+    { tblName = tableUnderArrestName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "bad_guy_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "robbery_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "court_date"+            , colType = DateT+            , colNullable = False+            , colDefault = Just "now()"+            }+        ]+    , tblPrimaryKey = pkOnColumns ["bad_guy_id", "robbery_id"]+    , tblForeignKeys =+        [ fkOnColumn "bad_guy_id" "bad_guy" "id"+        , fkOnColumn "robbery_id" "robbery" "id"+        ]+    }++tablePrisonSentenceName :: RawSQL ()+tablePrisonSentenceName = "prison_sentence"++tablePrisonSentenceSchema3 :: Table+tablePrisonSentenceSchema3 =+  tblTable+    { tblName = tablePrisonSentenceName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "bad_guy_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "robbery_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "sentence_start"+            , colType = DateT+            , colNullable = False+            , colDefault = Just "now()"+            }+        , tblColumn+            { colName = "sentence_length"+            , colType = IntegerT+            , colNullable = False+            , colDefault = Just "6"+            }+        , tblColumn+            { colName = "prison_name"+            , colType = TextT+            , colNullable = False+            }+        ]+    , tblPrimaryKey = pkOnColumns ["bad_guy_id", "robbery_id"]+    , tblForeignKeys =+        [ fkOnColumn "bad_guy_id" "bad_guy" "id"+        , fkOnColumn "robbery_id" "robbery" "id"+        ]+    }++tablePrisonSentenceSchema4 :: Table+tablePrisonSentenceSchema4 = tablePrisonSentenceSchema3++tablePrisonSentenceSchema5 :: Table+tablePrisonSentenceSchema5 = tablePrisonSentenceSchema4++tableFlashName :: RawSQL ()+tableFlashName = "flash"++tableFlash :: Table+tableFlash =+  tblTable+    { tblName = tableFlashName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn {colName = "flash_id", colType = UuidT, colNullable = False}+        ]+    }++tableCartelName :: RawSQL ()+tableCartelName = "cartel"++tableCartel :: Table+tableCartel =+  tblTable+    { tblName = tableCartelName+    , tblVersion = 1+    , tblColumns =+        [ tblColumn+            { colName = "cartel_member_id"+            , colType = UuidT+            , colNullable = False+            }+        , tblColumn+            { colName = "cartel_boss_id"+            , colType = UuidT+            , colNullable = True+            }+        ]+    , tblPrimaryKey = pkOnColumns ["cartel_member_id"]+    , tblForeignKeys =+        [ fkOnColumn "cartel_member_id" "bad_guy" "id"+        , fkOnColumn "cartel_boss_id" "bad_guy" "id"+        ]+    }++tableCartelSchema1 :: Table+tableCartelSchema1 = tableCartel++createTableMigration :: MonadDB m => Table -> Migration m+createTableMigration tbl =+  Migration+    { mgrTableName = tblName tbl+    , mgrFrom = 0+    , mgrAction = StandardMigration $ do+        createTable True tbl+    }++dropTableMigration :: MonadDB m => Table -> Migration m+dropTableMigration tbl =+  Migration+    { mgrTableName = tblName tbl+    , mgrFrom = tblVersion tbl+    , mgrAction = DropTableMigration DropTableCascade+    }++schema1Tables :: [Table]+schema1Tables =+  [ tableBankSchema1+  , tableBadGuySchema1+  , tableRobberySchema1+  , tableParticipatedInRobberySchema1+  , tableWitnessSchema1+  , tableWitnessedRobberySchema1+  ]++schema1Migrations :: MonadDB m => [Migration m]+schema1Migrations =+  [ createTableMigration tableBankSchema1+  , createTableMigration tableBadGuySchema1+  , createTableMigration tableRobberySchema1+  , createTableMigration tableParticipatedInRobberySchema1+  , createTableMigration tableWitnessSchema1+  , createTableMigration tableWitnessedRobberySchema1+  ]++schema2Tables :: [Table]+schema2Tables =+  [ tableBankSchema2+  , tableBadGuySchema2+  , tableRobberySchema2+  , tableParticipatedInRobberySchema2+  , tableUnderArrestSchema2+  ]++schema2Migrations :: MonadDB m => [Migration m]+schema2Migrations =+  schema1Migrations+    ++ [ dropTableMigration tableWitnessedRobberySchema1+       , dropTableMigration tableWitnessSchema1+       , createTableMigration tableUnderArrestSchema2+       ]++schema3Tables :: [Table]+schema3Tables =+  [ tableBankSchema3+  , tableBadGuySchema3+  , tableRobberySchema3+  , tableParticipatedInRobberySchema3+  , tablePrisonSentenceSchema3+  ]++schema3Migrations :: MonadDB m => [Migration m]+schema3Migrations =+  schema2Migrations+    ++ [ dropTableMigration tableUnderArrestSchema2+       , createTableMigration tablePrisonSentenceSchema3+       ]++schema4Tables :: [Table]+schema4Tables =+  [ tableBankSchema4+  , tableBadGuySchema4+  , tableRobberySchema4+  , tableParticipatedInRobberySchema4+  , tablePrisonSentenceSchema4+  ]++schema4Migrations :: MonadDB m => [Migration m]+schema4Migrations =+  schema3Migrations+    ++ [tableBankMigration4]++schema5Tables :: [Table]+schema5Tables =+  [ tableBankSchema5+  , tableBadGuySchema5+  , tableRobberySchema5+  , tableParticipatedInRobberySchema5+  , tablePrisonSentenceSchema5+  ]++schema5Migrations :: MonadDB m => [Migration m]+schema5Migrations =+  schema4Migrations+    ++ [ createTableMigration tableFlash+       , tableBankMigration5fst+       , tableBankMigration5snd+       , dropTableMigration tableFlash+       ]++schema6Tables :: [Table]+schema6Tables =+  [ tableBankSchema1+  , tableBadGuySchema1+  , tableRobberySchema1+  , tableParticipatedInRobberySchema1+      { tblVersion = tblVersion tableParticipatedInRobberySchema1 + 1+      , tblPrimaryKey = Nothing+      }+  , tableWitnessSchema1+  , tableWitnessedRobberySchema1+  ]++schema6Migrations :: MonadDB m => Migration m+schema6Migrations =+  Migration+    { mgrTableName = tblName tableParticipatedInRobberySchema1+    , mgrFrom = tblVersion tableParticipatedInRobberySchema1+    , mgrAction =+        StandardMigration $ do+          runQuery_+            ( "ALTER TABLE participated_in_robbery DROP CONSTRAINT \+              \pk__participated_in_robbery"+                :: RawSQL ()+            )+    }++type TestM a = DBT (LogT IO) a++createTablesSchema1 :: (String -> TestM ()) -> TestM ()+createTablesSchema1 step = do+  let definitions = tableDefsWithPgCrypto schema1Tables+  step "Creating the database (schema version 1)..."+  migrateDatabase defaultExtrasOptions definitions schema1Migrations++  -- Add a local index that shouldn't trigger validation errors.+  runSQL_ "CREATE INDEX local_idx_bank_name ON bank(name)"++  checkDatabase defaultExtrasOptions definitions++testDBSchema1 :: (String -> TestM ()) -> TestM ([UUID], [UUID])+testDBSchema1 step = do+  step "Running test queries (schema version 1)..."++  -- Populate the 'bank' table.+  runQuery_ . sqlInsert "bank" $ do+    sqlSetList+      "name"+      [ "HSBC" :: T.Text+      , "Swedbank"+      , "Nordea"+      , "Citi"+      , "Wells Fargo"+      ]+    sqlSetList+      "location"+      [ "13 Foo St., Tucson, AZ, USa" :: T.Text+      , "18 Bargatan, Stockholm, Sweden"+      , "23 Baz Lane, Liverpool, UK"+      , "2/3 Quux Ave., Milton Keynes, UK"+      , "6600 Sunset Blvd., Los Angeles, CA, USA"+      ]+    sqlResult "id"+  (bankIds :: [UUID]) <- fetchMany runIdentity+  liftIO $ assertEqual "INSERT into 'bank' table" 5 (length bankIds)++  -- Try to insert with existing ID to check that ON CONFLICT works properly+  let bankId = head bankIds+      name = "Santander" :: T.Text+      location = "Spain" :: T.Text++  runQuery_ . sqlInsert "bank" $ do+    sqlSet "id" bankId+    sqlSet "name" name+    sqlSet "location" location+    sqlOnConflictOnColumns ["id"] . sqlUpdate "" $ do+      sqlSet "name" name+      sqlSet "location" location+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details1 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT updates" (name, location) details1++  runQuery_ . sqlInsert "bank" $ do+    sqlSet "id" bankId+    sqlSet "name" ("" :: T.Text)+    sqlSet "location" ("" :: T.Text)+    sqlOnConflictDoNothing+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details3 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (1)" (name, location) details3++  runQuery_ . sqlInsert "bank" $ do+    sqlSet "id" bankId+    sqlSet "name" ("" :: T.Text)+    sqlSet "location" ("" :: T.Text)+    sqlOnConflictOnColumnsDoNothing ["id"]+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details4 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (2)" (name, location) details4++  -- If NO CONFLICT is not specified, make sure we throw an exception.+  eres :: Either DBException () <- try . withSavepoint "testDBSchema" $ do+    runQuery_ . sqlInsert "bank" $ do+      sqlSet "id" bankId+      sqlSet "name" name+      sqlSet "location" location+  liftIO $ assertBool "If ON CONFLICT is not specified an exception is thrown" (isLeft eres)++  runQuery_ . sqlInsertSelect "bank" "bank" $ do+    sqlSetCmd "id" "id"+    sqlSetCmd "name" "name"+    sqlSetCmd "location" "location"+    sqlWhereEq "id" bankId+    sqlOnConflictOnColumns ["id"] . sqlUpdate "" $ do+      sqlSet "name" name+      sqlSet "location" location+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details5 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT updates" (name, location) details5++  runQuery_ . sqlInsertSelect "bank" "bank" $ do+    sqlSetCmd "id" "id"+    sqlSetCmd "name" "name"+    sqlSetCmd "location" "location"+    sqlWhereEq "id" bankId+    sqlOnConflictDoNothing+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details6 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (1)" (name, location) details6++  runQuery_ . sqlInsertSelect "bank" "bank" $ do+    sqlSetCmd "id" "id"+    sqlSetCmd "name" "name"+    sqlSetCmd "location" "location"+    sqlWhereEq "id" bankId+    sqlOnConflictOnColumnsDoNothing ["id"]+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlResult "location"+    sqlWhereEq "id" bankId+  details7 <- fetchOne id+  liftIO $ assertEqual "INSERT ON CONFLICT does nothing (2)" (name, location) details7++  -- If NO CONFLICT is not specified, make sure we throw an exception.+  eres1 :: Either DBException () <- try . withSavepoint "testDBSchema" $ do+    runQuery_ . sqlInsertSelect "bank" "bank" $ do+      sqlSetCmd "id" "id"+      sqlSetCmd "name" "name"+      sqlSetCmd "location" "location"+      sqlWhereEq "id" bankId+  liftIO $ assertBool "If ON CONFLICT is not specified an exception is thrown" (isLeft eres1)++  -- Populate the 'bad_guy' table.+  runQuery_ . sqlInsert "bad_guy" $ do+    sqlSetList+      "firstname"+      [ "Neil" :: T.Text+      , "Lee"+      , "Freddie"+      , "Frankie"+      , "James"+      , "Roy"+      ]+    sqlSetList+      "lastname"+      [ "Hetzel" :: T.Text+      , "Murray"+      , "Foreman"+      , "Fraser"+      , "Crosbie"+      , "Shaw"+      ]+    sqlResult "id"+  (badGuyIds :: [UUID]) <- fetchMany runIdentity+  liftIO $ assertEqual "INSERT into 'bad_guy' table" 6 (length badGuyIds)++  -- Populate the 'robbery' table.+  runQuery_ . sqlInsert "robbery" $ do+    sqlSetList "bank_id" [bankIds !! idx | idx <- [0, 3]]+    sqlResult "id"+  (robberyIds :: [UUID]) <- fetchMany runIdentity+  liftIO $ assertEqual "INSERT into 'robbery' table" 2 (length robberyIds)++  -- Populate the 'participated_in_robbery' table.+  runQuery_ . sqlInsert "participated_in_robbery" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [0, 2]]+    sqlSet "robbery_id" (robberyIds !! 0)+    sqlResult "bad_guy_id"+  (participatorIds :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'participated_in_robbery' table"+      2+      (length participatorIds)++  runQuery_ . sqlInsert "participated_in_robbery" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [3, 4]]+    sqlSet "robbery_id" (robberyIds !! 1)+    sqlResult "bad_guy_id"+  (participatorIds' :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'participated_in_robbery' table"+      2+      (length participatorIds')++  -- Populate the 'witness' table.+  runQuery_ . sqlInsert "witness" $ do+    sqlSetList+      "firstname"+      [ "Meredith" :: T.Text+      , "Charlie"+      , "Peter"+      , "Emun"+      , "Benedict"+      , "Erica"+      ]+    sqlSetList+      "lastname"+      [ "Vickers" :: T.Text+      , "Holloway"+      , "Weyland"+      , "Eliott"+      , "Wong"+      , "Hackett"+      ]+    sqlResult "id"+  (witnessIds :: [UUID]) <- fetchMany runIdentity+  liftIO $ assertEqual "INSERT into 'witness' table" 6 (length witnessIds)++  -- Populate the 'witnessed_robbery' table.+  runQuery_ . sqlInsert "witnessed_robbery" $ do+    sqlSetList "witness_id" [witnessIds !! idx | idx <- [0, 1]]+    sqlSet "robbery_id" (robberyIds !! 0)+    sqlResult "witness_id"+  (robberyWitnessIds :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'witnessed_robbery' table"+      2+      (length robberyWitnessIds)++  runQuery_ . sqlInsert "witnessed_robbery" $ do+    sqlSetList "witness_id" [witnessIds !! idx | idx <- [2, 3, 4]]+    sqlSet "robbery_id" (robberyIds !! 1)+    sqlResult "witness_id"+  (robberyWitnessIds' :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'witnessed_robbery' table"+      3+      (length robberyWitnessIds')++  -- Create a new record to test order-by case sensitivity.+  runQuery_ . sqlInsert "bank" $ do+    sqlSet "name" ("byblos bank" :: T.Text)+    sqlSet "location" ("SYRIA" :: T.Text)++  -- Check that ordering results by the "location" column uses case-sensitive+  -- sorting (since the collation method for that column is "C").+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "location"+    sqlOrderBy "location"++  details8 <- fetchMany runIdentity+  liftIO $+    assertEqual+      "Using collation method \"C\" leads to case-sensitive ordering of results"+      [ "18 Bargatan, Stockholm, Sweden" :: String+      , "2/3 Quux Ave., Milton Keynes, UK"+      , "23 Baz Lane, Liverpool, UK"+      , "6600 Sunset Blvd., Los Angeles, CA, USA"+      , "SYRIA"+      , "Spain"+      ]+      details8++  -- Check that ordering results by the "name" column uses case-insensitive+  -- sorting (since the collation method for that column is "en_US").+  runQuery_ . sqlSelect "bank" $ do+    sqlResult "name"+    sqlOrderBy "name"++  details9 <- fetchMany runIdentity+  liftIO $+    assertEqual+      "Using collation method \"en_US\" leads to case-insensitive ordering of results"+      [ "byblos bank" :: String+      , "Citi"+      , "Nordea"+      , "Santander"+      , "Swedbank"+      , "Wells Fargo"+      ]+      details9++  do+    deletedRows <- runQuery . sqlDelete "witness" $ do+      sqlWhereEq "id" $ witnessIds !! 5+      sqlResult "firstname"+      sqlResult "lastname"+    liftIO $ assertEqual "DELETE FROM 'witness' table" 1 deletedRows++    deletedName <- fetchOne id+    liftIO $+      assertEqual+        "DELETE FROM 'witness' table RETURNING firstname, lastname"+        ("Erica" :: String, "Hackett" :: String)+        deletedName++  return (badGuyIds, robberyIds)++migrateDBToSchema2 :: (String -> TestM ()) -> TestM ()+migrateDBToSchema2 step = do+  let definitions = tableDefsWithPgCrypto schema2Tables+  step "Migrating the database (schema version 1 -> schema version 2)..."+  migrateDatabase+    defaultExtrasOptions {eoLockTimeoutMs = Just 1000}+    definitions+    schema2Migrations+  checkDatabase defaultExtrasOptions definitions++-- | Hacky version of 'migrateDBToSchema2' used by 'migrationTest3'.+migrateDBToSchema2Hacky :: (String -> TestM ()) -> TestM ()+migrateDBToSchema2Hacky step = do+  let definitions = tableDefsWithPgCrypto schema2Tables+  step+    "Hackily migrating the database (schema version 1 \+    \-> schema version 2)..."+  migrateDatabase+    defaultExtrasOptions+    definitions+    schema2Migrations'+  checkDatabase defaultExtrasOptions definitions+  where+    schema2Migrations' = createTableMigration tableFlash : schema2Migrations++testDBSchema2 :: (String -> TestM ()) -> [UUID] -> [UUID] -> TestM ()+testDBSchema2 step badGuyIds robberyIds = do+  step "Running test queries (schema version 2)..."++  -- Check that table 'witness' doesn't exist.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"+      <> " AND tablename = 'witness')"+  (witnessExists :: Bool) <- fetchOne runIdentity+  liftIO $ assertEqual "Table 'witness' doesn't exist" False witnessExists++  -- Check that table 'witnessed_robbery' doesn't exist.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"+      <> " AND tablename = 'witnessed_robbery')"+  (witnessedRobberyExists :: Bool) <- fetchOne runIdentity+  liftIO $+    assertEqual+      "Table 'witnessed_robbery' doesn't exist"+      False+      witnessedRobberyExists++  -- Populate table 'under_arrest'.+  runQuery_ . sqlInsert "under_arrest" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [0, 2]]+    sqlSet "robbery_id" (robberyIds !! 0)+    sqlResult "bad_guy_id"+  (arrestedIds :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'under_arrest' table"+      2+      (length arrestedIds)++  runQuery_ . sqlInsert "under_arrest" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [3, 4]]+    sqlSet "robbery_id" (robberyIds !! 1)+    sqlResult "bad_guy_id"+  (arrestedIds' :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'under_arrest' table"+      2+      (length arrestedIds')++  return ()++migrateDBToSchema3 :: (String -> TestM ()) -> TestM ()+migrateDBToSchema3 step = do+  let definitions = tableDefsWithPgCrypto schema3Tables+  step "Migrating the database (schema version 2 -> schema version 3)..."+  migrateDatabase+    defaultExtrasOptions+    definitions+    schema3Migrations+  checkDatabase defaultExtrasOptions definitions++testDBSchema3 :: (String -> TestM ()) -> [UUID] -> [UUID] -> TestM ()+testDBSchema3 step badGuyIds robberyIds = do+  step "Running test queries (schema version 3)..."++  -- Check that table 'under_arrest' doesn't exist.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"+      <> " AND tablename = 'under_arrest')"+  (underArrestExists :: Bool) <- fetchOne runIdentity+  liftIO $+    assertEqual+      "Table 'under_arrest' doesn't exist"+      False+      underArrestExists++  -- Check that the table 'prison_sentence' exists.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"+      <> " AND tablename = 'prison_sentence')"+  (prisonSentenceExists :: Bool) <- fetchOne runIdentity+  liftIO $+    assertEqual+      "Table 'prison_sentence' does exist"+      True+      prisonSentenceExists++  -- Populate table 'prison_sentence'.+  runQuery_ . sqlInsert "prison_sentence" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [0, 2]]+    sqlSet "robbery_id" (robberyIds !! 0)+    sqlSet "sentence_length" (12 :: Int)+    sqlSet "prison_name" ("Long Kesh" :: T.Text)+    sqlResult "bad_guy_id"+  (sentencedIds :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'prison_sentence' table"+      2+      (length sentencedIds)++  runQuery_ . sqlInsert "prison_sentence" $ do+    sqlSetList "bad_guy_id" [badGuyIds !! idx | idx <- [3, 4]]+    sqlSet "robbery_id" (robberyIds !! 1)+    sqlSet "sentence_length" (9 :: Int)+    sqlSet "prison_name" ("Wormwood Scrubs" :: T.Text)+    sqlResult "bad_guy_id"+  (sentencedIds' :: [UUID]) <- fetchMany runIdentity+  liftIO $+    assertEqual+      "INSERT into 'prison_sentence' table"+      2+      (length sentencedIds')++  return ()++migrateDBToSchema4 :: (String -> TestM ()) -> TestM ()+migrateDBToSchema4 step = do+  let definitions = tableDefsWithPgCrypto schema4Tables+  step "Migrating the database (schema version 3 -> schema version 4)..."+  migrateDatabase+    defaultExtrasOptions+    definitions+    schema4Migrations+  checkDatabase defaultExtrasOptions definitions++testDBSchema4 :: (String -> TestM ()) -> TestM ()+testDBSchema4 step = do+  step "Running test queries (schema version 4)..."++  -- Check that the 'bank' table has a 'cash' column.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM information_schema.columns"+      <> " WHERE table_schema = 'public'"+      <> " AND table_name = 'bank'"+      <> " AND column_name = 'cash')"+  (colCashExists :: Bool) <- fetchOne runIdentity+  liftIO $+    assertEqual+      "Column 'cash' in the table 'bank' does exist"+      True+      colCashExists++  return ()++migrateDBToSchema5 :: (String -> TestM ()) -> TestM ()+migrateDBToSchema5 step = do+  let definitions = tableDefsWithPgCrypto schema5Tables+  step "Migrating the database (schema version 4 -> schema version 5)..."+  migrateDatabase defaultExtrasOptions definitions schema5Migrations+  checkDatabase defaultExtrasOptions definitions++testDBSchema5 :: (String -> TestM ()) -> TestM ()+testDBSchema5 step = do+  step "Running test queries (schema version 5)..."++  -- Check that the 'bank' table doesn't have a 'cash' column.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM information_schema.columns"+      <> " WHERE table_schema = 'public'"+      <> " AND table_name = 'bank'"+      <> " AND column_name = 'cash')"+  (colCashExists :: Bool) <- fetchOne runIdentity+  liftIO $+    assertEqual+      "Column 'cash' in the table 'bank' doesn't exist"+      False+      colCashExists++  -- Check that the 'flash' table doesn't exist.+  runSQL_ $+    "SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public'"+      <> " AND tablename = 'flash')"+  (flashExists :: Bool) <- fetchOne runIdentity+  liftIO $ assertEqual "Table 'flash' doesn't exist" False flashExists++  return ()++-- | May require 'ALTER SCHEMA public OWNER TO $user' the first time+-- you run this.+freshTestDB :: (String -> TestM ()) -> TestM ()+freshTestDB step = do+  step "Dropping the test DB schema..."+  runSQL_ "DROP SCHEMA public CASCADE"+  runSQL_ "CREATE SCHEMA public"++-- | Re-used by 'migrationTest5'.+migrationTest1Body :: (String -> TestM ()) -> TestM ()+migrationTest1Body step = do+  createTablesSchema1 step+  (badGuyIds, robberyIds) <-+    testDBSchema1 step++  migrateDBToSchema2 step+  testDBSchema2 step badGuyIds robberyIds++  migrateDBToSchema3 step+  testDBSchema3 step badGuyIds robberyIds++  migrateDBToSchema4 step+  testDBSchema4 step++  migrateDBToSchema5 step+  testDBSchema5 step++bankTrigger1 :: Trigger+bankTrigger1 =+  Trigger+    { triggerTable = "bank"+    , triggerName = "trigger_1"+    , triggerKind = TriggerConstraint NotDeferrable+    , triggerEvents = Set.fromList [TriggerInsert]+    , triggerWhen = Nothing+    , triggerFunction =+        "begin"+          <+> "  perform true;"+          <+> "  return null;"+          <+> "end;"+    }++bankTrigger2 :: Trigger+bankTrigger2 =+  bankTrigger1+    { triggerFunction =+        "begin"+          <+> "  return null;"+          <+> "end;"+    }++bankTrigger3 :: Trigger+bankTrigger3 =+  Trigger+    { triggerTable = "bank"+    , triggerName = "trigger_3"+    , triggerKind = TriggerConstraint DeferrableInitiallyDeferred+    , triggerEvents = Set.fromList [TriggerInsert, TriggerUpdateOf [unsafeSQL "location"]]+    , triggerWhen = Nothing+    , triggerFunction =+        "begin"+          <+> "  perform true;"+          <+> "  return null;"+          <+> "end;"+    }++bankTrigger2Proper :: Trigger+bankTrigger2Proper =+  bankTrigger2 {triggerName = "trigger_2"}++testTriggers :: HasCallStack => (String -> TestM ()) -> TestM ()+testTriggers step = do+  step "Running trigger tests..."++  step "create the initial database"+  migrate [tableBankSchema1] [createTableMigration tableBankSchema1]++  do+    let msg = "checkDatabase fails if there are triggers in the database but not in the schema"+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = []+              }+          ]+        ms = [createTriggerMigration 1 bankTrigger1]+    step msg+    assertException msg $ migrate ts ms++  do+    let msg = "checkDatabase fails if there are triggers in the schema but not in the database"+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [bankTrigger1]+              }+          ]+        ms = []+    triggerStep msg $ do+      assertException msg $ migrate ts ms++  do+    let msg = "test succeeds when creating a single trigger"+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [bankTrigger1]+              }+          ]+        ms = [createTriggerMigration 1 bankTrigger1]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [bankTrigger1] True++  do+    -- Attempt to create the same triggers twice. Should fail with a DBException saying+    -- that function already exists.+    let msg = "database exception is raised if trigger is created twice"+        ts =+          [ tableBankSchema1+              { tblVersion = 3+              , tblTriggers = [bankTrigger1]+              }+          ]+        ms =+          [ createTriggerMigration 1 bankTrigger1+          , createTriggerMigration 2 bankTrigger1+          ]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "database exception is raised if triggers only differ in function name"+        ts =+          [ tableBankSchema1+              { tblVersion = 3+              , tblTriggers = [bankTrigger1, bankTrigger2]+              }+          ]+        ms =+          [ createTriggerMigration 1 bankTrigger1+          , createTriggerMigration 2 bankTrigger2+          ]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "successfully migrate two triggers"+        ts =+          [ tableBankSchema1+              { tblVersion = 3+              , tblTriggers = [bankTrigger1, bankTrigger2Proper]+              }+          ]+        ms =+          [ createTriggerMigration 1 bankTrigger1+          , createTriggerMigration 2 bankTrigger2Proper+          ]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [bankTrigger1, bankTrigger2Proper] True++  do+    let msg = "database exception is raised if trigger's WHEN is syntactically incorrect"+        trg = bankTrigger1 {triggerWhen = Just "WILL FAIL"}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "database exception is raised if trigger's WHEN uses undefined column"+        trg = bankTrigger1 {triggerWhen = Just "NEW.foobar = 1"}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    -- This trigger is valid. However, the WHEN clause specified in triggerWhen is not+    -- what gets returned from the database. The decompiled and normalized WHEN clause+    -- from the database looks like this:+    --   new.name <> 'foobar'::text+    -- We simply assert an exception, which presumably comes from the migration framework,+    -- while it should actually be a deeper check for just the differing WHEN+    -- clauses. On the other hand, it's probably good enough as it is.+    -- See the comment for 'getDBTriggers' in src/Database/PostgreSQL/PQTypes/Model/Trigger.hs.+    let msg = "checkDatabase fails if WHEN clauses from database and code differ"+        trg = bankTrigger1 {triggerWhen = Just "NEW.name != 'foobar'"}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertException msg $ migrate ts ms++  do+    let msg = "successfully migrate trigger with valid WHEN"+        trg = bankTrigger1 {triggerWhen = Just "new.name <> 'foobar'::text"}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True++  do+    let msg = "successfully migrate a constraint trigger that is deferrable"+        trg = bankTrigger1 {triggerKind = TriggerConstraint Deferrable}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True++  do+    let msg = "successfully migrate a constraint trigger that is deferrable and initially deferred"+        trg = bankTrigger1 {triggerKind = TriggerConstraint DeferrableInitiallyDeferred}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True++  do+    let msg = "successfully migrate a regular AFTER trigger"+        trg = bankTrigger1 {triggerKind = TriggerRegular After}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True++  do+    let msg = "successfully migrate a regular BEFORE trigger"+        trg = bankTrigger1 {triggerKind = TriggerRegular Before}+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True++  do+    let msg = "database exception is raised if dropping trigger that does not exist"+        trg = bankTrigger1+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [dropTriggerMigration 1 trg]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "database exception is raised if dropping trigger function of which does not exist"+        trg = bankTrigger2+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [dropTriggerMigration 1 trg]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "successfully drop trigger"+        trg = bankTrigger1+        ts =+          [ tableBankSchema1+              { tblVersion = 3+              , tblTriggers = []+              }+          ]+        ms = [createTriggerMigration 1 trg, dropTriggerMigration 2 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] False++  do+    let msg = "database exception is raised if dropping trigger twice"+        trg = bankTrigger2+        ts =+          [ tableBankSchema1+              { tblVersion = 3+              , tblTriggers = [trg]+              }+          ]+        ms = [dropTriggerMigration 1 trg, dropTriggerMigration 2 trg]+    triggerStep msg $ do+      assertDBException msg $ migrate ts ms++  do+    let msg = "successfully create trigger with multiple events"+        trg = bankTrigger3+        ts =+          [ tableBankSchema1+              { tblVersion = 2+              , tblTriggers = [trg]+              }+          ]+        ms = [createTriggerMigration 1 trg]+    triggerStep msg $ do+      assertNoException msg $ migrate ts ms+      verify [trg] True+  where+    triggerStep msg rest = do+      recreateTriggerDB+      step msg+      rest++    migrate tables migrations = do+      let definitions = tableDefsWithPgCrypto tables+      migrateDatabase defaultExtrasOptions definitions migrations+      checkDatabase defaultExtrasOptions definitions++    -- Verify that the given triggers are (not) present in the database.+    verify :: (MonadIO m, MonadDB m, HasCallStack) => [Trigger] -> Bool -> m ()+    verify triggers present = do+      dbTriggers <- getDBTriggers "bank"+      let trgs = map fst dbTriggers+          ok = all (`elem` trgs) triggers+          err = "Triggers " <> (if present then "" else "not ") <> "present in the database."+          trans = if present then id else not+      liftIO . assertBool err $ trans ok++    triggerMigration :: MonadDB m => (Trigger -> m ()) -> Int -> Trigger -> Migration m+    triggerMigration fn from trg =+      Migration+        { mgrTableName = tblName tableBankSchema1+        , mgrFrom = fromIntegral from+        , mgrAction = StandardMigration $ fn trg+        }++    createTriggerMigration :: MonadDB m => Int -> Trigger -> Migration m+    createTriggerMigration = triggerMigration createTrigger++    dropTriggerMigration :: MonadDB m => Int -> Trigger -> Migration m+    dropTriggerMigration = triggerMigration dropTrigger++    recreateTriggerDB = do+      runSQL_ "DROP TRIGGER IF EXISTS trg__bank__trigger_1 ON bank;"+      runSQL_ "DROP TRIGGER IF EXISTS trg__bank__trigger_2 ON bank;"+      runSQL_ "DROP FUNCTION IF EXISTS trgfun__trigger_1;"+      runSQL_ "DROP FUNCTION IF EXISTS trgfun__trigger_2;"+      runSQL_ "DROP TABLE IF EXISTS bank;"+      runSQL_ "DELETE FROM table_versions WHERE name = 'bank'"+      migrate [tableBankSchema1] [createTableMigration tableBankSchema1]++testSqlWith :: HasCallStack => (String -> TestM ()) -> TestM ()+testSqlWith step = do+  step "Running sql WITH tests"+  testPass+  runSQL_ "DELETE FROM bank"+  step "Checking for WITH MATERIALIZED support"+  checkAndRememberMaterializationSupport+  step "Running sql WITH tests again with WITH MATERIALIZED support flag set"+  testPass+  where+    migrate tables migrations = do+      let definitions = tableDefsWithPgCrypto tables+      migrateDatabase defaultExtrasOptions definitions migrations+      checkDatabase defaultExtrasOptions definitions+    testPass = do+      step "create the initial database"+      migrate [tableBankSchema1] [createTableMigration tableBankSchema1]+      step "inserting initial data"+      runQuery_ . sqlInsert "bank" $ do+        sqlSetList "name" ["HSBC" :: T.Text, "other"]+        sqlSetList "location" ["13 Foo St., Tucson" :: T.Text, "no address"]+        sqlResult "id"+      step "testing WITH .. INSERT SELECT"+      runQuery_ . sqlInsertSelect "bank" "bank_name" $ do+        sqlWith "bank_name" $ do+          sqlSelect "bank" $ do+            sqlResult "'another'"+            sqlLimit (1 :: Int)+        sqlFrom "bank_name"+        sqlSetCmd "name" "bank_name"+        sqlSet "location" ("Other side" :: T.Text)+      step "testing WITH .. UPDATE"+      runQuery_ . sqlUpdate "bank" $ do+        sqlWith "other_bank" $ do+          sqlSelect "bank" $ do+            sqlWhereEq "name" ("other" :: T.Text)+            sqlResult "id"+        sqlFrom "other_bank"+        sqlSet "location" ("abcd" :: T.Text)+        sqlWhereInSql "bank.id" $ mkSQL "other_bank.id"+        sqlResult "bank.id"+      step "testing WITH .. DELETE"+      runQuery_ . sqlDelete "bank" $ do+        sqlWith "other_bank" $ do+          sqlSelect "bank" $ do+            sqlWhereEq "name" ("other" :: T.Text)+            sqlResult "id"+        sqlFrom "other_bank"+        sqlWhereInSql "bank.id" $ mkSQL "other_bank.id"+      step "testing WITH .. SELECT"+      runQuery_ . sqlSelect "bank" $ do+        sqlWith "other_bank" $ do+          sqlSelect "bank" $ do+            sqlResult "name"+        sqlFrom "other_bank"+        sqlResult "other_bank.name"+      (results :: [T.Text]) <- fetchMany runIdentity+      liftIO $ assertEqual "Wrong number of banks left" 2 (length results)++testSqlWithRecursive :: HasCallStack => (String -> TestM ()) -> TestM ()+testSqlWithRecursive step = do+  step "Running WITH RECURSIVE tests"+  testPass+  where+    migrate tables migrations = do+      let definitions = tableDefsWithPgCrypto tables+      migrateDatabase defaultExtrasOptions definitions migrations+      checkDatabase defaultExtrasOptions definitions+    testPass = do+      step "create the initial database"+      migrate [tableBadGuySchema1, tableCartelSchema1] [createTableMigration tableBadGuySchema1, createTableMigration tableCartelSchema1]+      step "inserting initial data"+      runQuery_ . sqlInsert "bad_guy" $ do+        sqlSetList "firstname" ["Pablo" :: T.Text, "Gustavo", "Mario"]+        sqlSetList "lastname" ["Escobar" :: T.Text, "Rivero", "Vallejo"]+        sqlResult "id"+      (badGuyIds :: [UUID]) <- fetchMany runIdentity+      -- Populate the 'cartel' table+      -- We will have a simple direct hierarchy just to test the recursion:+      -- Pablo is the boss of Gustavo, who is the boss of Mario+      runQuery_ . sqlInsert "cartel" $ do+        sqlSetList "cartel_member_id" badGuyIds+        sqlSetList "cartel_boss_id" $ Nothing : (Just <$> take 2 badGuyIds)+      step "Checking a recursive query on the cartel table"+      runQuery_ . sqlSelect "rcartel" $ do+        sqlWithRecursive "rcartel" $ do+          sqlSelect "cartel root" $ do+            sqlResult "root.cartel_member_id"+            sqlResult "root.cartel_boss_id"+            sqlWhere "root.cartel_boss_id IS NULL"+            sqlUnionAll+              [ sqlSelect "cartel child" $ do+                  sqlResult "child.cartel_member_id"+                  sqlResult "child.cartel_boss_id"+                  sqlJoinOn "rcartel rcartel1" "child.cartel_boss_id = rcartel1.cartel_member_id"+              ]+        -- This dummy with is not actually used+        -- It's just here to test that further "sqlWith" do not remove the RECURSIVE+        -- keyword when actually producing the SQL+        sqlWith "lcartel" $ do+          sqlSelect "rcartel c" $ do+            sqlResult "c.cartel_member_id"+            sqlResult "c.cartel_boss_id"+        sqlResult "member.firstname"+        sqlResult "member.lastname"+        sqlResult "boss.firstname"+        sqlResult "boss.lastname"+        sqlJoinOn "bad_guy member" "rcartel.cartel_member_id = member.id"+        sqlLeftJoinOn "bad_guy boss" "rcartel.cartel_boss_id = boss.id"+      let toCartel :: (T.Text, T.Text, Maybe T.Text, Maybe T.Text) -> (T.Text, Maybe T.Text)+          toCartel (memberFn, memberLn, bossFn, bossLn) =+            (T.intercalate " " [memberFn, memberLn], T.intercalate " " <$> sequence [bossFn, bossLn])+      results <- fetchMany toCartel+      liftIO $+        assertEqual+          "Wrong cartel hierarchy retrieved"+          results+          [ ("Pablo Escobar", Nothing)+          , ("Gustavo Rivero", Just "Pablo Escobar")+          , ("Mario Vallejo", Just "Gustavo Rivero")+          ]++testUnion :: HasCallStack => (String -> TestM ()) -> TestM ()+testUnion step = do+  step "Running SQL UNION tests"+  testPass+  where+    testPass = do+      runQuery_ . sqlSelect "" $ do+        sqlResult "true"+        sqlUnion+          [ sqlSelect "" $ do+              sqlResult "false"+          , sqlSelect "" $ do+              sqlResult "true"+          ]+      result <- fetchMany runIdentity+      liftIO $+        assertEqual+          "UNION of booleans"+          [False, True]+          result++testUnionAll :: HasCallStack => (String -> TestM ()) -> TestM ()+testUnionAll step = do+  step "Running SQL UNION ALL tests"+  testPass+  where+    testPass = do+      runQuery_ . sqlSelect "" $ do+        sqlResult "true"+        sqlUnionAll+          [ sqlSelect "" $ do+              sqlResult "false"+          , sqlSelect "" $ do+              sqlResult "true"+          ]+      result <- fetchMany runIdentity+      liftIO $+        assertEqual+          "UNION ALL of booleans"+          [True, False, True]+          result++migrationTest1 :: ConnectionSourceM (LogT IO) -> TestTree+migrationTest1 connSource =+  testCaseSteps' "Migration test 1" connSource $ \step -> do+    freshTestDB step++    migrationTest1Body step++-- | Test for behaviour of 'checkDatabase' and 'checkDatabaseAllowUnknownObjects'+migrationTest2 :: ConnectionSourceM (LogT IO) -> TestTree+migrationTest2 connSource =+  testCaseSteps' "Migration test 2" connSource $ \step -> do+    freshTestDB step++    createTablesSchema1 step++    let composite =+          CompositeType+            { ctName = "composite"+            , ctColumns =+                [ CompositeColumn {ccName = "cint", ccType = UuidT}+                , CompositeColumn {ccName = "ctext", ccType = TextT}+                ]+            }+        currentSchema = schema1Tables+        differentSchema = schema5Tables+        extrasOptions = defaultExtrasOptions {eoEnforcePKs = True}+        extrasOptionsWithUnknownObjects = extrasOptions {eoObjectsValidationMode = AllowUnknownObjects}++    runQuery_ $ sqlCreateComposite composite++    assertNoException "checkDatabase should run fine for consistent DB" $+      checkDatabase extrasOptions $+        emptyDbDefinitions {dbComposites = [composite], dbTables = currentSchema}+    assertException "checkDatabase fails if composite type definition is not provided" $+      checkDatabase extrasOptions $+        emptyDbDefinitions {dbTables = currentSchema}+    assertNoException+      "checkDatabaseAllowUnknownTables runs fine \+      \for consistent DB"+      $ checkDatabase extrasOptionsWithUnknownObjects+      $ emptyDbDefinitions {dbComposites = [composite], dbTables = currentSchema}+    assertNoException+      "checkDatabaseAllowUnknownTables runs fine \+      \for consistent DB with unknown composite type in the database"+      $ checkDatabase extrasOptionsWithUnknownObjects+      $ emptyDbDefinitions {dbTables = currentSchema}+    assertException "checkDatabase should throw exception for wrong schema" $+      checkDatabase extrasOptions emptyDbDefinitions {dbTables = differentSchema}+    assertException+      "checkDatabaseAllowUnknownObjects \+      \should throw exception for wrong scheme"+      $ checkDatabase extrasOptionsWithUnknownObjects emptyDbDefinitions {dbTables = differentSchema}++    runSQL_+      "INSERT INTO table_versions (name, version) \+      \VALUES ('unknown_table', 0)"+    assertException "checkDatabase throw when extra entry in 'table_versions'" $+      checkDatabase extrasOptions $+        emptyDbDefinitions {dbTables = currentSchema}+    assertNoException+      "checkDatabaseAllowUnknownObjects \+      \accepts extra entry in 'table_versions'"+      $ checkDatabase extrasOptionsWithUnknownObjects+      $ emptyDbDefinitions {dbTables = currentSchema}+    runSQL_ "DELETE FROM table_versions where name='unknown_table'"++    runSQL_ "CREATE TABLE unknown_table (title text)"+    assertException "checkDatabase should throw with unknown table" $+      checkDatabase extrasOptions $+        emptyDbDefinitions {dbTables = currentSchema}+    assertNoException "checkDatabaseAllowUnknownObjects accepts unknown table" $+      checkDatabase extrasOptionsWithUnknownObjects $+        emptyDbDefinitions {dbTables = currentSchema}++    runSQL_+      "INSERT INTO table_versions (name, version) \+      \VALUES ('unknown_table', 0)"+    assertException "checkDatabase should throw with unknown table" $+      checkDatabase extrasOptions $+        emptyDbDefinitions {dbTables = currentSchema}+    assertNoException+      "checkDatabaseAllowUnknownObjects \+      \accepts unknown tables with version"+      $ checkDatabase extrasOptionsWithUnknownObjects+      $ emptyDbDefinitions {dbTables = currentSchema}++    freshTestDB step++    let withMissingPKSchema = schema6Tables+        schema1MigrationsWithMissingPK = schema6Migrations+        optionsNoPKCheck =+          defaultExtrasOptions+            { eoEnforcePKs = False+            }+        optionsWithPKCheck =+          defaultExtrasOptions+            { eoEnforcePKs = True+            }++    step "Recreating the database (schema version 1, one table is missing PK)..."++    migrateDatabase+      optionsNoPKCheck+      (tableDefsWithPgCrypto withMissingPKSchema)+      [schema1MigrationsWithMissingPK]+    checkDatabase optionsNoPKCheck (tableDefsWithPgCrypto withMissingPKSchema)++    assertException+      "checkDatabase should throw when PK missing from table \+      \'participated_in_robbery' and check is enabled"+      $ checkDatabase optionsWithPKCheck+      $ emptyDbDefinitions {dbTables = withMissingPKSchema}+    assertNoException+      "checkDatabase should not throw when PK missing from table \+      \'participated_in_robbery' and check is disabled"+      $ checkDatabase optionsNoPKCheck+      $ emptyDbDefinitions {dbTables = withMissingPKSchema}++    freshTestDB step++migrationTest3 :: ConnectionSourceM (LogT IO) -> TestTree+migrationTest3 connSource =+  testCaseSteps' "Migration test 3" connSource $ \step -> do+    freshTestDB step++    createTablesSchema1 step+    (badGuyIds, robberyIds) <-+      testDBSchema1 step++    migrateDBToSchema2 step+    testDBSchema2 step badGuyIds robberyIds++    assertException+      "Trying to run the same migration twice should fail, \+      \when starting with a createTable migration"+      $ migrateDBToSchema2Hacky step++    freshTestDB step++-- | Test that running the same migrations twice doesn't result in+-- unexpected errors.+migrationTest4 :: ConnectionSourceM (LogT IO) -> TestTree+migrationTest4 connSource =+  testCaseSteps' "Migration test 4" connSource $ \step -> do+    freshTestDB step++    migrationTest1Body step++    -- Here we run step 5 for the second time. This should be a no-op.+    migrateDBToSchema5 step+    testDBSchema5 step++    freshTestDB step++-- | Test triggers.+triggerTests :: ConnectionSourceM (LogT IO) -> TestTree+triggerTests connSource =+  testCaseSteps' "Trigger tests" connSource $ \step -> do+    freshTestDB step+    testTriggers step++sqlWithTests :: ConnectionSourceM (LogT IO) -> TestTree+sqlWithTests connSource =+  testCaseSteps' "sql WITH tests" connSource $ \step -> do+    freshTestDB step+    testSqlWith step++unionTests :: ConnectionSourceM (LogT IO) -> TestTree+unionTests connSource =+  testCaseSteps' "SQL UNION Tests" connSource $ \step -> do+    freshTestDB step+    testUnion step++sqlWithRecursiveTests :: ConnectionSourceM (LogT IO) -> TestTree+sqlWithRecursiveTests connSource =+  testCaseSteps' "SQL WITH RECURSIVE Tests" connSource $ \step -> do+    freshTestDB step+    testSqlWithRecursive step++unionAllTests :: ConnectionSourceM (LogT IO) -> TestTree+unionAllTests connSource =+  testCaseSteps' "SQL UNION ALL Tests" connSource $ \step -> do+    freshTestDB step+    testUnionAll step++eitherExc :: MonadCatch m => (SomeException -> m ()) -> (a -> m ()) -> m a -> m ()+eitherExc left right c = try c >>= either left right++migrationTest5 :: ConnectionSourceM (LogT IO) -> TestTree+migrationTest5 connSource =+  testCaseSteps' "Migration test 5" connSource $ \step -> do+    freshTestDB step++    step "Creating the database (schema version 1)..."+    migrateDatabase defaultExtrasOptions (tableDefsWithPgCrypto [table1]) [createTableMigration table1]+    checkDatabase defaultExtrasOptions (tableDefsWithPgCrypto [table1])++    step "Populating the 'bank' table..."+    runQuery_ . sqlInsert "bank" $ do+      sqlSetList "name" $ (\i -> "bank" <> show i) <$> numbers+      sqlSetList "location" $ (\i -> "location" <> show i) <$> numbers++    -- Explicitly vacuum to update the catalog so that getting the row number estimates+    -- works. The bracket_ trick is here because vacuum can't run inside a transaction+    -- block, which every test runs in.+    bracket_+      (runSQL_ "COMMIT")+      (runSQL_ "BEGIN")+      (runSQL_ "VACUUM bank")++    forM_ (zip4 tables migrations steps assertions) $+      \(table, migration, step', assertion) -> do+        step step'+        migrateDatabase defaultExtrasOptions (tableDefsWithPgCrypto [table]) [migration]+        checkDatabase defaultExtrasOptions (tableDefsWithPgCrypto [table])+        uncurry assertNoException assertion++    freshTestDB step+  where+    -- Chosen by a fair dice roll.+    numbers = [1 .. 101] :: [Int]+    table1 = tableBankSchema1+    tables =+      [ table1+          { tblVersion = 2+          , tblColumns = tblColumns table1 ++ [stringColumn]+          }+      , table1+          { tblVersion = 3+          , tblColumns = tblColumns table1 ++ [stringColumn]+          }+      , table1+          { tblVersion = 4+          , tblColumns = tblColumns table1 ++ [stringColumn, boolColumn]+          }+      , table1+          { tblVersion = 5+          , tblColumns = tblColumns table1 ++ [stringColumn, boolColumn]+          }+      ]++    migrations =+      [ addStringColumnMigration+      , copyStringColumnMigration+      , addBoolColumnMigration+      , modifyBoolColumnMigration+      ]++    steps =+      [ "Adding string column (version 1 -> version 2)..."+      , "Copying string column (version 2 -> version 3)..."+      , "Adding bool column (version 3 -> version 4)..."+      , "Modifying bool column (version 4 -> version 5)..."+      ]++    assertions =+      [ ("Check that the string column has been added" :: String, checkAddStringColumn)+      , ("Check that the string data has been copied", checkCopyStringColumn)+      , ("Check that the bool column has been added", checkAddBoolColumn)+      , ("Check that the bool column has been modified", checkModifyBoolColumn)+      ]++    stringColumn =+      tblColumn+        { colName = "name_new"+        , colType = TextT+        }++    boolColumn =+      tblColumn+        { colName = "name_is_true"+        , colType = BoolT+        , colNullable = False+        , colDefault = Just "false"+        }++    cursorSql = "SELECT id FROM bank" :: SQL++    addStringColumnMigration =+      Migration+        { mgrTableName = "bank"+        , mgrFrom = 1+        , mgrAction =+            StandardMigration $+              runQuery_ $+                sqlAlterTable "bank" [sqlAddColumn stringColumn]+        }++    copyStringColumnMigration =+      Migration+        { mgrTableName = "bank"+        , mgrFrom = 2+        , mgrAction = ModifyColumnMigration "bank" cursorSql copyColumnSql 1000+        }+    copyColumnSql :: MonadDB m => [Identity UUID] -> m ()+    copyColumnSql primaryKeys =+      runQuery_ . sqlUpdate "bank" $ do+        sqlSetCmd "name_new" "bank.name"+        sqlWhereEqualsAny "bank.id" $ runIdentity <$> primaryKeys++    addBoolColumnMigration =+      Migration+        { mgrTableName = "bank"+        , mgrFrom = 3+        , mgrAction =+            StandardMigration $+              runQuery_ $+                sqlAlterTable "bank" [sqlAddColumn boolColumn]+        }++    modifyBoolColumnMigration =+      Migration+        { mgrTableName = "bank"+        , mgrFrom = 4+        , mgrAction = ModifyColumnMigration "bank" cursorSql modifyColumnSql 1000+        }+    modifyColumnSql :: MonadDB m => [Identity UUID] -> m ()+    modifyColumnSql primaryKeys =+      runQuery_ . sqlUpdate "bank" $ do+        sqlSet "name_is_true" True+        sqlWhereIn "bank.id" $ runIdentity <$> primaryKeys++    checkAddStringColumn = do+      runQuery_ . sqlSelect "bank" $ sqlResult "name_new"+      rows :: [Maybe T.Text] <- fetchMany runIdentity+      liftIO . assertEqual "No name_new in empty column" True $ all (== Nothing) rows++    checkCopyStringColumn = do+      runQuery_ . sqlSelect "bank" $ sqlResult "name_new"+      rows_new :: [Maybe T.Text] <- fetchMany runIdentity+      runQuery_ . sqlSelect "bank" $ sqlResult "name"+      rows_old :: [Maybe T.Text] <- fetchMany runIdentity+      liftIO . assertEqual "All name_new are equal name" True $+        all (uncurry (==)) $+          zip rows_new rows_old++    checkAddBoolColumn = do+      runQuery_ . sqlSelect "bank" $ sqlResult "name_is_true"+      rows :: [Maybe Bool] <- fetchMany runIdentity+      liftIO . assertEqual "All name_is_true default to false" True $ all (== Just False) rows++    checkModifyBoolColumn = do+      runQuery_ . sqlSelect "bank" $ sqlResult "name_is_true"+      rows :: [Maybe Bool] <- fetchMany runIdentity+      liftIO . assertEqual "All name_is_true are true" True $ all (== Just True) rows++foreignKeyIndexesTests :: ConnectionSourceM (LogT IO) -> TestTree+foreignKeyIndexesTests connSource =+  testCaseSteps' "Foreign key indexes tests" connSource $ \step -> do+    freshTestDB step++    step "Create database with two tables, no foreign key checking"+    do+      let options = defaultExtrasOptions+      migrateDatabase+        options+        (tableDefsWithPgCrypto [table1, table2])+        [createTableMigration table1, createTableMigration table2]+      checkDatabase defaultExtrasOptions (tableDefsWithPgCrypto [table1, table2])++    step "Create database with two tables, with foreign key checking"+    do+      let options = defaultExtrasOptions {eoCheckForeignKeysIndexes = True}+      assertException "Foreign keys are missing" $+        migrateDatabase+          options+          (tableDefsWithPgCrypto [table1, table2])+          [createTableMigration table1, createTableMigration table2]++    step "Table is missing several foreign key indexes"+    do+      let options = defaultExtrasOptions {eoCheckForeignKeysIndexes = True}+      assertException "Foreign keys are missing" $+        migrateDatabase+          options+          (tableDefsWithPgCrypto [table1, table2, table3])+          [createTableMigration table1, createTableMigration table2, createTableMigration table3]++    step "Multi column indexes covering a FK pass the checks"+    do+      let options = defaultExtrasOptions {eoCheckForeignKeysIndexes = True}+      migrateDatabase+        options+        (tableDefsWithPgCrypto [table4])+        [ dropTableMigration table1+        , dropTableMigration table2+        , dropTableMigration table3+        , createTableMigration table4+        ]+      checkDatabase options (tableDefsWithPgCrypto [table4])+    step "Multi column indexes not covering a FK fail the checks"+    do+      let options = defaultExtrasOptions {eoCheckForeignKeysIndexes = True}+      assertException "Foreign keys are missing" $+        migrateDatabase+          options+          (tableDefsWithPgCrypto [table5])+          [ dropTableMigration table4+          , createTableMigration table5+          ]+  where+    table1 :: Table+    table1 =+      tblTable+        { tblName = "fktest1"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "name", colType = TextT}+            , tblColumn {colName = "location", colType = TextT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        }+    table2 :: Table+    table2 =+      tblTable+        { tblName = "fktest2"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "fkid", colType = UuidT}+            , tblColumn {colName = "fkname", colType = TextT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblForeignKeys =+            [ fkOnColumn "fkid" "fktest1" "id"+            ]+        }+    table3 :: Table+    table3 =+      tblTable+        { tblName = "fktest3"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "fk1id", colType = UuidT}+            , tblColumn {colName = "fk2id", colType = UuidT}+            , tblColumn {colName = "fkname", colType = TextT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblForeignKeys =+            [ fkOnColumn "fk1id" "fktest1" "id"+            , fkOnColumn "fk2id" "fktest2" "id"+            ]+        }+    table4 :: Table+    table4 =+      tblTable+        { tblName = "fktest4"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "fk4id", colType = UuidT}+            , tblColumn {colName = "fk4name", colType = TextT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblForeignKeys =+            [ fkOnColumn "fk4id" "fktest4" "id"+            ]+        , tblIndexes =+            [ indexOnColumns [indexColumn "fk4id", indexColumn "fk4name"]+            ]+        }+    table5 :: Table+    table5 =+      tblTable+        { tblName = "fktest5"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "fk5id", colType = UuidT}+            , tblColumn {colName = "fk5name", colType = TextT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblForeignKeys =+            [ fkOnColumn "fk5id" "fktest5" "id"+            ]+        , tblIndexes =+            [ indexOnColumns [indexColumn "fk5thing", indexColumn "fk5id"]+            ]+        }++overlapingIndexesTests :: ConnectionSourceM (LogT IO) -> TestTree+overlapingIndexesTests connSource = do+  testCaseSteps' "Overlapping indexes tests" connSource $ \step -> do+    freshTestDB step++    let definitions = tableDefsWithPgCrypto [table1]++    step "Migration is correct if not checking for overlapping indexes"+    do+      let options = defaultExtrasOptions {eoCheckOverlappingIndexes = False}+      migrateDatabase+        options+        definitions+        [createTableMigration table1]++    step "Migration invalid when flagging overlapping indexes"+    do+      let options = defaultExtrasOptions {eoCheckOverlappingIndexes = True}+      assertException "Some indexes are overlapping" $+        migrateDatabase+          options+          definitions+          [createTableMigration table1]+  where+    table1 :: Table+    table1 =+      tblTable+        { tblName = "idx_test"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False}+            , tblColumn {colName = "idx1", colType = UuidT}+            , tblColumn {colName = "idx2", colType = UuidT}+            , tblColumn {colName = "idx3", colType = UuidT}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblIndexes =+            [ indexOnColumns [indexColumn "idx1", indexColumn "idx2"]+            , indexOnColumns [indexColumn "idx1"]+            ]+        }++nullsNotDistinctTests :: ConnectionSourceM (LogT IO) -> TestTree+nullsNotDistinctTests connSource = do+  testCaseSteps' "NULLS NOT DISTINCT tests" connSource $ \step -> do+    freshTestDB step++    step "Create a database with indexes"+    do+      let definitions = tableDefsWithPgCrypto [nullTableTest1, nullTableTest2]+      migrateDatabase+        defaultExtrasOptions+        definitions+        [createTableMigration nullTableTest1, createTableMigration nullTableTest2]+      checkDatabase defaultExtrasOptions definitions++    step "Insert two NULLs on a column with a default UNIQUE index"+    do+      runQuery_ . sqlInsert "nulltests1" $ do+        sqlSet "content" (Nothing @T.Text)+      runQuery_ . sqlInsert "nulltests1" $ do+        sqlSet "content" (Nothing @T.Text)++    step "Insert NULLs on a column with a NULLS NOT DISTINCT index"+    do+      runQuery_ . sqlInsert "nulltests2" $ do+        sqlSet "content" (Nothing @T.Text)+      assertDBException "Cannot insert twice a null value with NULLS NOT DISTINCT" $ runQuery_ . sqlInsert "nulltests2" $ do+        sqlSet "content" (Nothing @T.Text)+  where+    nullTableTest1 =+      tblTable+        { tblName = "nulltests1"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False, colDefault = Just "gen_random_uuid()"}+            , tblColumn {colName = "content", colType = TextT, colNullable = True}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblIndexes =+            [ uniqueIndexOnColumn "content"+            ]+        }++    nullTableTest2 =+      tblTable+        { tblName = "nulltests2"+        , tblVersion = 1+        , tblColumns =+            [ tblColumn {colName = "id", colType = UuidT, colNullable = False, colDefault = Just "gen_random_uuid()"}+            , tblColumn {colName = "content", colType = TextT, colNullable = True}+            ]+        , tblPrimaryKey = pkOnColumn "id"+        , tblIndexes =+            [ (uniqueIndexOnColumn "content") {idxNotDistinctNulls = True}+            ]+        }++sqlAnyAllTests :: TestTree+sqlAnyAllTests =+  testGroup+    "SQL ANY/ALL tests"+    [ testCase "sqlAll produces correct queries" $ do+        assertSqlEqual "empty sqlAll is TRUE" "TRUE" . sqlAll $ pure ()+        assertSqlEqual "sigle condition is emmited as is" "cond" $ sqlAll $ sqlWhere "cond"+        assertSqlEqual+          "each condition as well as entire condition is parenthesized"+          "((cond1) AND (cond2))"+          $ sqlAll+          $ do+            sqlWhere "cond1"+            sqlWhere "cond2"++        assertSqlEqual+          "sqlAll can be nested"+          "((cond1) AND (cond2) AND (((cond3) AND (cond4))))"+          $ sqlAll+          $ do+            sqlWhere "cond1"+            sqlWhere "cond2"+            sqlWhere . sqlAll $ do+              sqlWhere "cond3"+              sqlWhere "cond4"+    , testCase "sqlAny produces correct queries" $ do+        assertSqlEqual "empty sqlAny is FALSE" "FALSE" . sqlAny $ pure ()+        assertSqlEqual "sigle condition is emmited as is" "cond" $ sqlAny $ sqlWhere "cond"+        assertSqlEqual+          "each condition as well as entire condition is parenthesized"+          "((cond1) OR (cond2))"+          $ sqlAny+          $ do+            sqlWhere "cond1"+            sqlWhere "cond2"++        assertSqlEqual+          "sqlAny can be nested"+          "((cond1) OR (cond2) OR (((cond3) OR (cond4))))"+          $ sqlAny+          $ do+            sqlWhere "cond1"+            sqlWhere "cond2"+            sqlWhere . sqlAny $ do+              sqlWhere "cond3"+              sqlWhere "cond4"+    , testCase "mixing sqlAny and all produces correct queries" $ do+        assertSqlEqual+          "sqlAny and sqlAll can be mixed"+          "((((cond1) OR (cond2))) AND (((cond3) OR (cond4))))"+          $ sqlAll+          $ do+            sqlWhere . sqlAny $ do+              sqlWhere "cond1"+              sqlWhere "cond2"+            sqlWhere . sqlAny $ do+              sqlWhere "cond3"+              sqlWhere "cond4"+    , testCase "sqlWhereAny produces correct queries" $ do+        -- `sqlWhereAny` has to be wrapped in `sqlAll` to disambiguate the `SqlWhere` monad.+        assertSqlEqual "empty sqlWhereAny is FALSE" "FALSE" . sqlAll $ sqlWhereAny []+        assertSqlEqual+          "each condition as well as entire condition is parenthesized and joined with OR"+          "((cond1) OR (cond2))"+          . sqlAll+          $ sqlWhereAny [sqlWhere "cond1", sqlWhere "cond2"]+        assertSqlEqual+          "nested multi-conditions are parenthesized and joined with AND"+          "((cond1) OR (((cond2) AND (cond3))) OR (cond4))"+          . sqlAll+          $ sqlWhereAny+            [ sqlWhere "cond1"+            , do+                sqlWhere "cond2"+                sqlWhere "cond3"+            , sqlWhere "cond4"+            ]+    ]+  where+    assertSqlEqual :: Sqlable a => String -> a -> a -> Assertion+    assertSqlEqual msg a b =+      assertEqual+        msg+        (show $ toSQLCommand a)+        (show $ toSQLCommand b)++enumTest :: ConnectionSourceM (LogT IO) -> TestTree+enumTest connSource =+  testCaseSteps' "Enum tests" connSource $ \step -> do+    freshTestDB step++    step "Create a database with an enum"+    migrateDatabase+      defaultExtrasOptions+      (emptyDbDefinitions {dbEnums = [enum1]})+      []++    step "Check the database"+    checkDatabase defaultExtrasOptions (emptyDbDefinitions {dbEnums = [enum1]})++    step "Check the database with missing enum"+    do+      report <- checkDatabaseWithReport defaultExtrasOptions (emptyDbDefinitions {dbEnums = [enum1, enum2]})+      liftIO $+        assertEqual+          "Missing enum2 should be reported"+          (validationError "Enum 'enum2' doesn't exist in the database")+          report++    step "Check the database with reordered enum"+    do+      report <- checkDatabaseWithReport defaultExtrasOptions (emptyDbDefinitions {dbEnums = [enum1misorder]})+      liftIO $+        assertEqual+          "Order mismatch should be reported"+          ( validationInfo+              "Enum 'enum1' has same values, but differs in order \+              \(database: [\"enum-100\",\"enum-101\"], definition: [\"enum-101\",\"enum-100\"]). \+              \This isn't usually a problem, unless the enum is used for ordering."+          )+          report++    step "Check the database with mismatching enum"+    do+      report <- checkDatabaseWithReport defaultExtrasOptions (emptyDbDefinitions {dbEnums = [enum1mismatch]})+      liftIO $+        assertEqual+          "DB mismatch should be reported"+          (validationError "Enum 'enum1' does not match (database: [\"enum-100\",\"enum-101\"], definition: [\"enum-100\",\"enum-102\"])")+          report++    step "Check the database with extra enum values"+    do+      report <- checkDatabaseWithReport defaultExtrasOptions (emptyDbDefinitions {dbEnums = [enum1missing]})+      liftIO $+        assertEqual+          "Extra values in the DB enum should be reported"+          (validationInfo "Enum 'enum1' has all necessary values, but the database has additional ones (database: [\"enum-100\",\"enum-101\"], definition: [\"enum-100\"])")+          report+  where+    enum1 = EnumType {etName = "enum1", etValues = ["enum-100", "enum-101"]}+    enum2 = EnumType {etName = "enum2", etValues = ["enum-200", "enum-201", "enum-202"]}+    enum1misorder = EnumType {etName = "enum1", etValues = ["enum-101", "enum-100"]}+    enum1mismatch = EnumType {etName = "enum1", etValues = ["enum-100", "enum-102"]}+    enum1missing = EnumType {etName = "enum1", etValues = ["enum-100"]}++assertNoException :: String -> TestM () -> TestM ()+assertNoException t =+  eitherExc+    (const $ liftIO $ assertFailure ("Exception thrown for: " ++ t))+    (const $ return ())++assertException :: String -> TestM () -> TestM ()+assertException t =+  eitherExc+    (const $ return ())+    (const $ liftIO $ assertFailure ("No exception thrown for: " ++ t))++assertDBException :: String -> TestM () -> TestM ()+assertDBException t c =+  try c+    >>= either+      (\DBException {} -> pure ())+      (const . liftIO . assertFailure $ "No DBException thrown for: " ++ t)++-- | A variant of testCaseSteps that works in TestM monad.+testCaseSteps'+  :: TestName+  -> ConnectionSourceM (LogT IO)+  -> ((String -> TestM ()) -> TestM ())+  -> TestTree+testCaseSteps' testName connSource f =+  testCaseSteps testName $ \step' -> do+    let step s = liftIO $ step' s+    withStdOutLogger $ \logger ->+      runLogT "hpqtypes-extras-test" logger defaultLogLevel $+        runDBT connSource defaultTransactionSettings $+          f step++tableDefsWithPgCrypto :: [Table] -> DatabaseDefinitions+tableDefsWithPgCrypto tables =+  emptyDbDefinitions {dbTables = tables, dbExtensions = ["pgcrypto"]}++main :: IO ()+main = do+  defaultMainWithIngredients ings $+    askOption $ \(ConnectionString connectionString) ->+      let connSettings =+            defaultConnectionSettings+              { csConnInfo = T.pack connectionString+              }+          ConnectionSource connSource = simpleSource connSettings+      in testGroup+          "DB tests"+          [ migrationTest1 connSource+          , migrationTest2 connSource+          , migrationTest3 connSource+          , migrationTest4 connSource+          , migrationTest5 connSource+          , triggerTests connSource+          , sqlWithTests connSource+          , unionTests connSource+          , unionAllTests connSource+          , sqlWithRecursiveTests connSource+          , foreignKeyIndexesTests connSource+          , overlapingIndexesTests connSource+          , nullsNotDistinctTests connSource+          , sqlAnyAllTests+          , enumTest connSource+          ]+  where+    ings =+      includingOptions [Option (Proxy :: Proxy ConnectionString)]+        : defaultIngredients