module Database.PostgreSQL.PQTypes.Checks (
migrateDatabase
, checkDatabase
, createTable
, createDomain
) where
import Control.Applicative ((<$>))
import Control.Monad.Catch
import Control.Monad.Reader
import Data.Int
import Data.Maybe
import Data.Monoid
import Data.Monoid.Utils
import Database.PostgreSQL.PQTypes hiding (def)
import Log
import Prelude
import TextShow
import qualified Data.Foldable as F
import qualified Data.List as L
import qualified Data.Text as T
import Database.PostgreSQL.PQTypes.Model
import Database.PostgreSQL.PQTypes.SQL.Builder
import Database.PostgreSQL.PQTypes.Versions
newtype ValidationResult = ValidationResult [T.Text]
instance Monoid ValidationResult where
mempty = ValidationResult []
mappend (ValidationResult a) (ValidationResult b) = ValidationResult (a ++ b)
topMessage :: T.Text -> T.Text -> ValidationResult -> ValidationResult
topMessage objtype objname = \case
ValidationResult [] -> ValidationResult []
ValidationResult es -> ValidationResult ("There are problems with the" <+> objtype <+> "'" <> objname <> "'" : es)
resultCheck
:: (MonadLog m, MonadThrow m)
=> ValidationResult
-> m ()
resultCheck = \case
ValidationResult [] -> return ()
ValidationResult msgs -> do
mapM_ logAttention_ msgs
error "resultCheck: validation failed"
migrateDatabase
:: (MonadDB m, MonadLog m, MonadThrow m)
=> [Extension] -> [Domain] -> [Table] -> [Migration m]
-> m ()
migrateDatabase extensions domains tables migrations = do
void checkDBTimeZone
mapM_ checkExtension extensions
checkDBConsistency domains (tableVersions : tables) migrations
resultCheck =<< checkDomainsStructure domains
resultCheck =<< checkDBStructure (tableVersions : tables)
checkUnknownTables tables
commit
checkDatabase
:: (MonadDB m, MonadLog m, MonadThrow m)
=> [Domain] -> [Table]
-> m ()
checkDatabase domains tables = do
versions <- mapM checkTableVersion tables
let tablesWithVersions = zip tables (map (fromMaybe 0) versions)
resultCheck . mconcat $ checkVersions tablesWithVersions
resultCheck =<< checkDomainsStructure domains
resultCheck =<< checkDBStructure (tableVersions : tables)
resultCheck . mconcat =<< checkInitialSetups tables
where
checkVersions = map $ \(t@Table{..}, v) -> ValidationResult $ if
| tblVersion == v -> []
| v == 0 -> ["Table '" <> tblNameText t <> "' must be created"]
| otherwise -> ["Table '" <> tblNameText t <> "' must be migrated" <+> showt v <+> "->" <+> showt tblVersion]
checkInitialSetups = mapM $ \t -> case tblInitialSetup t of
Nothing -> return $ ValidationResult []
Just is -> checkInitialSetup is >>= \case
True -> return $ ValidationResult []
False -> return $ ValidationResult ["Initial setup for table '" <> tblNameText t <> "' is not valid"]
currentCatalog :: (MonadDB m, MonadThrow m) => m (RawSQL ())
currentCatalog = do
runSQL_ "SELECT current_catalog::text"
dbname <- fetchOne runIdentity
return $ unsafeSQL $ "\"" ++ dbname ++ "\""
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
checkDBTimeZone :: (MonadDB m, MonadLog m, MonadThrow m) => m Bool
checkDBTimeZone = do
runSQL_ "SHOW timezone"
timezone :: String <- fetchOne runIdentity
if timezone /= "UTC"
then do
dbname <- currentCatalog
logInfo_ $ "Setting '" <> unRawSQL dbname <> "' database to return timestamps in UTC"
runQuery_ $ "ALTER DATABASE" <+> dbname <+> "SET TIMEZONE = 'UTC'"
return True
else return False
checkUnknownTables :: (MonadDB m, MonadLog m) => [Table] -> m ()
checkUnknownTables tables = do
runQuery_ $ sqlSelect "information_schema.tables" $ do
sqlResult "table_name::text"
sqlWhere "table_name <> 'table_versions'"
sqlWhere "table_type = 'BASE TABLE'"
sqlWhere "table_schema NOT IN ('information_schema','pg_catalog')"
desc <- fetchMany runIdentity
let absent = desc L.\\ map (unRawSQL . tblName) tables
when (not (null absent)) $
mapM_ (\t -> logInfo_ $ "Unknown table:" <+> t) absent
createTable :: MonadDB m => Bool -> Table -> m ()
createTable withConstraints table@Table{..} = do
runQuery_ $ sqlCreateTable tblName
runQuery_ $ sqlAlterTable tblName $ map sqlAddColumn tblColumns
forM_ tblIndexes $ runQuery_ . sqlCreateIndex tblName
when withConstraints $ createTableConstraints table
runQuery_ . sqlInsert "table_versions" $ do
sqlSet "name" (tblNameText table)
sqlSet "version" tblVersion
createTableConstraints :: MonadDB m => Table -> m ()
createTableConstraints Table{..} = when (not $ null addConstraints) $ do
runQuery_ $ sqlAlterTable tblName addConstraints
where
addConstraints = concat [
[sqlAddPK tblName pk | Just pk <- return tblPrimaryKey]
, map sqlAddCheck tblChecks
, map (sqlAddFK tblName) tblForeignKeys
]
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"
sqlResult "(SELECT pg_catalog.format_type(t2.oid, t2.typtypmod) FROM pg_catalog.pg_type t2 WHERE t2.oid = t1.typbasetype)"
sqlResult "NOT t1.typnotnull"
sqlResult "t1.typdefault"
sqlResult "ARRAY(SELECT c.conname::text FROM pg_catalog.pg_constraint c WHERE c.contypid = t1.oid ORDER by c.oid)"
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)"
sqlWhereEq "t1.typname" $ unRawSQL $ domName def
mdom <- fetchMaybe $ \(dname, dtype, nullable, defval, cnames, conds) -> Domain {
domName = unsafeSQL dname
, domType = dtype
, domNullable = nullable
, domDefault = unsafeSQL <$> defval
, domChecks = mkChecks $ zipWith (\cname cond -> Check {
chkName = unsafeSQL cname
, chkCondition = unsafeSQL cond
}) (unArray1 cnames) (unArray1 conds)
}
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 -> ValidationResult ["Domain '" <> unRawSQL (domName def) <> "' doesn't exist in the database"]
where
compareAttr :: (Eq a, Show a) => Domain -> Domain -> T.Text -> (Domain -> a) -> ValidationResult
compareAttr dom def attrname attr
| attr dom == attr def = ValidationResult []
| otherwise = ValidationResult ["Attribute '" <> attrname <> "' does not match (database:" <+> T.pack (show $ attr dom) <> ", definition:" <+> T.pack (show $ attr def) <> ")"]
createDomain :: MonadDB m => Domain -> m ()
createDomain dom@Domain{..} = do
runQuery_ $ sqlCreateDomain dom
F.forM_ domChecks $ runQuery_ . sqlAlterDomain domName . sqlAddCheck
checkDBStructure :: forall m. (MonadDB m, MonadThrow m)
=> [Table] -> m ValidationResult
checkDBStructure tables = fmap mconcat . forM tables $ \table ->
topMessage "table" (tblNameText table) <$> checkTableStructure table
where
checkTableStructure :: Table -> m ValidationResult
checkTableStructure table@Table{..} = do
runQuery_ $ sqlSelect "pg_catalog.pg_attribute a" $ do
sqlResult "a.attname::text"
sqlResult "pg_catalog.format_type(a.atttypid, a.atttypmod)"
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
runQuery_ $ sqlGetPrimaryKey table
pk <- join <$> fetchMaybe fetchPrimaryKey
runQuery_ $ sqlGetChecks table
checks <- fetchMany fetchTableCheck
runQuery_ $ sqlGetIndexes table
indexes <- fetchMany fetchTableIndex
runQuery_ $ sqlGetForeignKeys table
fkeys <- fetchMany fetchForeignKey
return $ mconcat [
checkColumns 1 tblColumns desc
, checkPrimaryKey tblPrimaryKey pk
, checkChecks tblChecks checks
, checkIndexes tblIndexes indexes
, checkForeignKeys tblForeignKeys fkeys
]
where
fetchTableColumn :: (String, ColumnType, Bool, Maybe String) -> TableColumn
fetchTableColumn (name, ctype, nullable, mdefault) = TableColumn {
colName = unsafeSQL name
, colType = ctype
, colNullable = nullable
, colDefault = unsafeSQL `liftM` mdefault
}
checkColumns :: Int -> [TableColumn] -> [TableColumn] -> ValidationResult
checkColumns _ [] [] = mempty
checkColumns _ rest [] = ValidationResult [tableHasLess "columns" rest]
checkColumns _ [] rest = ValidationResult [tableHasMore "columns" rest]
checkColumns !n (d:defs) (c:cols) = mconcat [
validateNames $ colName d == colName c
, validateTypes $ colType d == colType c || (colType d == BigSerialT && colType c == BigIntT)
, 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 = ValidationResult [errorMsg ("no. " <> showt n) "names" (unRawSQL . colName)]
validateTypes True = mempty
validateTypes False = ValidationResult [errorMsg cname "types" (T.pack . show . colType) <+> sqlHint ("TYPE" <+> columnTypeToSQL (colType d))]
validateNullables True = mempty
validateNullables False = ValidationResult [errorMsg cname "nullables" (showt . colNullable) <+> sqlHint ((if colNullable d then "DROP" else "SET") <+> "NOT NULL")]
validateDefaults True = mempty
validateDefaults False = ValidationResult [(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
]
where
def = maybeToList mdef
pk = maybeToList mpk
checkChecks :: [Check] -> [Check] -> ValidationResult
checkChecks defs checks = case checkEquality "CHECKs" defs checks of
ValidationResult [] -> ValidationResult []
ValidationResult errmsgs -> ValidationResult $ 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 indexes = mconcat [
checkEquality "INDEXes" defs (map fst indexes)
, checkNames (indexName tblName) indexes
]
checkForeignKeys :: [ForeignKey] -> [(ForeignKey, RawSQL ())] -> ValidationResult
checkForeignKeys defs fkeys = mconcat [
checkEquality "FOREIGN KEYs" defs (map fst fkeys)
, checkNames (fkName tblName) fkeys
]
checkDBConsistency
:: forall m. (MonadDB m, MonadLog m, MonadThrow m)
=> [Domain] -> [Table] -> [Migration m]
-> m ()
checkDBConsistency domains tables migrations = do
forM_ tables $ \table -> do
let presentMigrationVersions = map mgrFrom $ filter (\m -> tblName (mgrTable m) == tblName table) migrations
expectedMigrationVersions = reverse $ take (length presentMigrationVersions) $ reverse [0 .. tblVersion table 1]
when (presentMigrationVersions /= expectedMigrationVersions) $ do
logAttention "Migrations are invalid" $ object [
"table" .= tblNameText table
, "migration_versions" .= presentMigrationVersions
, "expected_migration_versions" .= expectedMigrationVersions
]
error $ "checkDBConsistency: invalid migrations for table" <+> tblNameString table
versions <- mapM checkTableVersion tables
let tablesWithVersions = zip tables (map (fromMaybe 0) versions)
if all ((==) 0 . snd) tablesWithVersions
then do
logInfo_ "Creating domains..."
mapM_ createDomain domains
logInfo_ "Creating tables..."
mapM_ (createTable False) tables
logInfo_ "Creating table constraints..."
mapM_ createTableConstraints tables
logInfo_ "Done."
logInfo_ "Running initial setup for tables..."
forM_ tables $ \t -> case tblInitialSetup t of
Nothing -> return ()
Just tis -> do
logInfo_ $ "Intializing" <+> tblNameText t <> "..."
initialSetup tis
logInfo_ "Done."
else do
forM_ tablesWithVersions $ \(table, ver) -> when (tblVersion table /= ver) $ do
case L.find (\m -> tblNameString (mgrTable m) == tblNameString table) migrations of
Nothing -> do
error $ "checkDBConsistency: no migrations found for table '" ++ tblNameString table ++ "', cannot migrate " ++ show ver ++ " -> " ++ show (tblVersion table)
Just m | mgrFrom m > ver -> do
error $ "checkDBConsistency: earliest migration for table '" ++ tblNameString table ++ "' is from version " ++ show (mgrFrom m) ++ ", cannot migrate " ++ show ver ++ " -> " ++ show (tblVersion table)
Just _ -> return ()
let migrationsToRun = filter (\m -> any (\(t, from) -> tblName (mgrTable m) == tblName t && mgrFrom m >= from) tablesWithVersions) migrations
when (not . null $ migrationsToRun) $ do
logInfo_ "Running migrations..."
forM_ migrationsToRun $ \migration -> do
logInfo_ $ arrListTable (mgrTable migration) <> showt (mgrFrom migration) <+> "->" <+> showt (succ $ mgrFrom migration)
mgrDo migration
runQuery_ $ sqlUpdate "table_versions" $ do
sqlSet "version" $ succ (mgrFrom migration)
sqlWhereEq "name" $ tblNameString (mgrTable migration)
logInfo_ "Running migrations... done."
checkTableVersion :: (MonadDB m, MonadThrow m) => Table -> m (Maybe Int32)
checkTableVersion table = 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" $ tblNameString table
sqlWhere "pg_catalog.pg_table_is_visible(c.oid)"
if doesExist
then do
runQuery_ $ "SELECT version FROM table_versions WHERE name =" <?> tblNameString table
mver <- fetchMaybe runIdentity
case mver of
Just ver -> return $ Just ver
Nothing -> error $ "checkTableVersion: table '" ++ tblNameString table ++ "' is present in the database, but there is no corresponding version info in 'table_versions'."
else do
return Nothing
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)"
sqlGetPrimaryKey :: Table -> SQL
sqlGetPrimaryKey table = toSQLCommand . sqlSelect "pg_catalog.pg_constraint c" $ do
sqlResult "c.conname::text"
sqlResult "array(SELECT a.attname::text FROM pg_catalog.pg_attribute a WHERE a.attrelid = c.conrelid AND a.attnum = ANY (c.conkey)) as columns"
sqlWhereEq "c.contype" 'p'
sqlWhereEqSql "c.conrelid" $ sqlGetTableID table
fetchPrimaryKey :: (String, Array1 String) -> Maybe (PrimaryKey, RawSQL ())
fetchPrimaryKey (name, Array1 columns) = (, unsafeSQL name)
<$> (pkOnColumns $ map unsafeSQL columns)
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"
sqlWhereEq "c.contype" 'c'
sqlWhereEqSql "c.conrelid" $ sqlGetTableID table
fetchTableCheck :: (String, String) -> Check
fetchTableCheck (name, condition) = Check {
chkName = unsafeSQL name
, chkCondition = unsafeSQL condition
}
sqlGetIndexes :: Table -> SQL
sqlGetIndexes table = toSQLCommand . sqlSelect "pg_catalog.pg_class c" $ do
sqlResult "c.relname::text"
sqlResult $ "ARRAY(" <> selectCoordinates <> ")"
sqlResult "i.indisunique"
sqlResult "pg_catalog.pg_get_expr(i.indpred, i.indrelid, true)"
sqlJoinOn "pg_catalog.pg_index i" "c.oid = i.indexrelid"
sqlLeftJoinOn "pg_catalog.pg_constraint r" "r.conrelid = i.indrelid AND r.conindid = i.indexrelid"
sqlWhereEqSql "i.indrelid" $ sqlGetTableID table
sqlWhereIsNULL "r.contype"
where
selectCoordinates = smconcat [
"WITH RECURSIVE coordinates(k, name) AS ("
, " VALUES (0, NULL)"
, " UNION ALL"
, " SELECT k+1, pg_catalog.pg_get_indexdef(i.indexrelid, k+1, true)"
, " FROM coordinates"
, " WHERE pg_catalog.pg_get_indexdef(i.indexrelid, k+1, true) != ''"
, ")"
, "SELECT name FROM coordinates WHERE k > 0"
]
fetchTableIndex :: (String, Array1 String, Bool, Maybe String) -> (TableIndex, RawSQL ())
fetchTableIndex (name, Array1 columns, unique, mconstraint) = (TableIndex {
idxColumns = map unsafeSQL columns
, idxUnique = unique
, idxWhere = unsafeSQL `liftM` mconstraint
}, unsafeSQL name)
sqlGetForeignKeys :: Table -> SQL
sqlGetForeignKeys table = toSQLCommand . sqlSelect "pg_catalog.pg_constraint r" $ do
sqlResult "r.conname::text"
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)"
sqlResult "c.relname::text"
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)"
sqlResult "r.confupdtype"
sqlResult "r.confdeltype"
sqlResult "r.condeferrable"
sqlResult "r.condeferred"
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) -> (ForeignKey, RawSQL ())
fetchForeignKey (name, Array1 columns, reftable, Array1 refcolumns, on_update, on_delete, deferrable, deferred) = (ForeignKey {
fkColumns = map unsafeSQL columns
, fkRefTable = unsafeSQL reftable
, fkRefColumns = map unsafeSQL refcolumns
, fkOnUpdate = charToForeignKeyAction on_update
, fkOnDelete = charToForeignKeyAction on_delete
, fkDeferrable = deferrable
, fkDeferred = deferred
}, 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
tblNameText :: Table -> T.Text
tblNameText = unRawSQL . tblName
tblNameString :: Table -> String
tblNameString = T.unpack . tblNameText
checkEquality :: (Eq t, Show t) => T.Text -> [t] -> [t] -> ValidationResult
checkEquality pname defs props = case (defs L.\\ props, props L.\\ defs) of
([], []) -> mempty
(def_diff, db_diff) -> ValidationResult [mconcat [
"Table and its definition have diverged and have "
, showt $ length db_diff
, " and "
, showt $ length def_diff
, " different "
, pname
, " each, respectively (table: "
, T.pack $ show db_diff
, ", definition: "
, T.pack $ show def_diff
, ")."
]]
checkNames :: Show t => (t -> RawSQL ()) -> [(t, RawSQL ())] -> ValidationResult
checkNames prop_name = mconcat . map check
where
check (prop, name) = case prop_name prop of
pname
| pname == name -> mempty
| otherwise -> ValidationResult [mconcat [
"Property "
, T.pack $ show prop
, " has invalid name (expected: "
, unRawSQL pname
, ", given: "
, unRawSQL name
, ")."
]]
tableHasLess :: Show t => T.Text -> t -> T.Text
tableHasLess ptype missing = "Table in the database has *less*" <+> ptype <+> "than its definition (missing:" <+> T.pack (show missing) <> ")"
tableHasMore :: Show t => T.Text -> t -> T.Text
tableHasMore ptype extra = "Table in the database has *more*" <+> ptype <+> "than its definition (extra:" <+> T.pack (show extra) <> ")"
arrListTable :: Table -> T.Text
arrListTable table = " ->" <+> tblNameText table <> ": "