module Hasura.Backends.Postgres.DDL.Source ( resolveSourceConfig , postDropSourceHook , resolveDatabaseMetadata ) where import Hasura.Prelude import qualified Data.HashMap.Strict as Map import qualified Database.PG.Query as Q import qualified Language.Haskell.TH.Lib as TH import qualified Language.Haskell.TH.Syntax as TH import Control.Monad.Trans.Control (MonadBaseControl) import Data.FileEmbed (makeRelativeToProject) import Hasura.Backends.Postgres.Connection import Hasura.Backends.Postgres.SQL.Types import Hasura.RQL.Types.Backend import Hasura.RQL.Types.Common import Hasura.RQL.Types.Error import Hasura.RQL.Types.Function import Hasura.RQL.Types.Source import Hasura.RQL.Types.Table import Hasura.SQL.Backend import Hasura.Server.Migrate.Internal import Hasura.Server.Types (MaintenanceMode (..)) resolveSourceConfig :: (MonadIO m, MonadResolveSource m) => SourceName -> PostgresConnConfiguration -> m (Either QErr (SourceConfig ('Postgres pgKind))) resolveSourceConfig name config = runExceptT do sourceResolver <- getSourceResolver liftEitherM $ liftIO $ sourceResolver name config resolveDatabaseMetadata :: forall pgKind m . (Backend ('Postgres pgKind), MonadIO m, MonadBaseControl IO m) => SourceConfig ('Postgres pgKind) -> MaintenanceMode -> m (Either QErr (ResolvedSource ('Postgres pgKind))) resolveDatabaseMetadata sourceConfig maintenanceMode = runExceptT do (tablesMeta, functionsMeta, pgScalars) <- runLazyTx (_pscExecCtx sourceConfig) Q.ReadWrite $ do initSource maintenanceMode tablesMeta <- fetchTableMetadata functionsMeta <- fetchFunctionMetadata pgScalars <- fetchPgScalars pure (tablesMeta, functionsMeta, pgScalars) pure $ ResolvedSource sourceConfig tablesMeta functionsMeta pgScalars initSource :: MonadTx m => MaintenanceMode -> m () initSource maintenanceMode = do hdbCatalogExist <- doesSchemaExist "hdb_catalog" eventLogTableExist <- doesTableExist "hdb_catalog" "event_log" sourceVersionTableExist <- doesTableExist "hdb_catalog" "hdb_source_catalog_version" -- when maintenance mode is enabled, don't perform any migrations if | maintenanceMode == MaintenanceModeEnabled -> pure () -- Fresh database | not hdbCatalogExist -> liftTx do Q.unitQE defaultTxErrorHandler "CREATE SCHEMA hdb_catalog" () False enablePgcryptoExtension initPgSourceCatalog -- Only 'hdb_catalog' schema defined | not sourceVersionTableExist && not eventLogTableExist -> liftTx initPgSourceCatalog -- Source is initialised by pre multisource support servers | not sourceVersionTableExist && eventLogTableExist -> do -- Update the Source Catalog to v43 to include the new migration -- changes. Skipping this step will result in errors. currCatalogVersion <- liftTx getCatalogVersion migrateTo43 currCatalogVersion liftTx createVersionTable | otherwise -> migrateSourceCatalog where initPgSourceCatalog = do () <- Q.multiQE defaultTxErrorHandler $(makeRelativeToProject "src-rsr/init_pg_source.sql" >>= Q.sqlFromFile) setSourceCatalogVersion createVersionTable = do () <- Q.multiQE defaultTxErrorHandler [Q.sql| CREATE TABLE hdb_catalog.hdb_source_catalog_version( version TEXT NOT NULL, upgraded_on TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX hdb_source_catalog_version_one_row ON hdb_catalog.hdb_source_catalog_version((version IS NOT NULL)); |] setSourceCatalogVersion migrateSourceCatalog = do version <- getSourceCatalogVersion case version of "1" -> pure () _ -> throw500 $ "unexpected source catalog version: " <> version migrateTo43 prevVersion = do let neededMigrations = dropWhile ((/= prevVersion) . fst) upMigrationsUntil43 traverse_ snd neededMigrations -- Upgrade the hdb_catalog schema to v43 upMigrationsUntil43 :: MonadTx m => [(Text, m ())] upMigrationsUntil43 = $(let migrationFromFile from to = let path = "src-rsr/migrations/" <> from <> "_to_" <> to <> ".sql" in [| runTx $(makeRelativeToProject path >>= Q.sqlFromFile) |] migrationsFromFile = map $ \(to :: Integer) -> let from = to - 1 in [| ( $(TH.lift $ tshow from) , $(migrationFromFile (show from) (show to)) ) |] in TH.listE -- version 0.8 is the only non-integral catalog version $ [| ("0.8", $(migrationFromFile "08" "1")) |] : migrationsFromFile [2..3] ++ [| ("3", from3To4) |] : migrationsFromFile [5..43] ) currentSourceCatalogVersion :: Text currentSourceCatalogVersion = "1" setSourceCatalogVersion :: MonadTx m => m () setSourceCatalogVersion = liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql| INSERT INTO hdb_catalog.hdb_source_catalog_version(version, upgraded_on) VALUES ($1, NOW()) ON CONFLICT ((version IS NOT NULL)) DO UPDATE SET version = $1, upgraded_on = NOW() |] (Identity currentSourceCatalogVersion) False getSourceCatalogVersion :: MonadTx m => m Text getSourceCatalogVersion = liftTx $ runIdentity . Q.getRow <$> Q.withQE defaultTxErrorHandler [Q.sql| SELECT version FROM hdb_catalog.hdb_source_catalog_version |] () False -- | Fetch Postgres metadata of all user tables fetchTableMetadata :: forall pgKind m . (Backend ('Postgres pgKind), MonadTx m) => m (DBTablesMetadata ('Postgres pgKind)) fetchTableMetadata = do results <- liftTx $ Q.withQE defaultTxErrorHandler $(makeRelativeToProject "src-rsr/pg_table_metadata.sql" >>= Q.sqlFromFile) () True pure $ Map.fromList $ flip map results $ \(schema, table, Q.AltJ info) -> (QualifiedObject schema table, info) -- | Fetch Postgres metadata for all user functions fetchFunctionMetadata :: (MonadTx m) => m (DBFunctionsMetadata ('Postgres pgKind)) fetchFunctionMetadata = do results <- liftTx $ Q.withQE defaultTxErrorHandler $(makeRelativeToProject "src-rsr/pg_function_metadata.sql" >>= Q.sqlFromFile) () True pure $ Map.fromList $ flip map results $ \(schema, table, Q.AltJ infos) -> (QualifiedObject schema table, infos) -- | Fetch all scalar types from Postgres fetchPgScalars :: MonadTx m => m (HashSet PGScalarType) fetchPgScalars = liftTx $ Q.getAltJ . runIdentity . Q.getRow <$> Q.withQE defaultTxErrorHandler [Q.sql| SELECT coalesce(json_agg(typname), '[]') FROM pg_catalog.pg_type where typtype = 'b' |] () True -- | Clean source database after dropping in metadata postDropSourceHook :: (MonadIO m, MonadError QErr m, MonadBaseControl IO m) => PGSourceConfig -> m () postDropSourceHook sourceConfig = do -- Clean traces of Hasura in source database liftEitherM $ runPgSourceWriteTx sourceConfig $ do hdbMetadataTableExist <- doesTableExist "hdb_catalog" "hdb_metadata" eventLogTableExist <- doesTableExist "hdb_catalog" "event_log" -- If "hdb_metadata" and "event_log" tables found in the "hdb_catalog" schema -- then this infers the source is being used as default potgres source (--database-url option). -- In this case don't drop any thing in the catalog schema. if | hdbMetadataTableExist && eventLogTableExist -> pure () -- Otherwise, if only "hdb_metadata" table exist, then this infers the source is -- being used as metadata storage (--metadata-database-url option). In this case -- drop only source related tables and not "hdb_catalog" schema | hdbMetadataTableExist -> Q.multiQE defaultTxErrorHandler $(makeRelativeToProject "src-rsr/drop_pg_source.sql" >>= Q.sqlFromFile) -- Otherwise, drop "hdb_catalog" schema. | otherwise -> dropHdbCatalogSchema -- Destory postgres source connection liftIO $ _pecDestroyConn $ _pscExecCtx sourceConfig