2021-02-14 09:07:52 +03:00
|
|
|
|
module Hasura.Backends.Postgres.DDL.RunSQL
|
2021-05-27 18:06:13 +03:00
|
|
|
|
( runRunSQL
|
|
|
|
|
, RunSQL(..)
|
|
|
|
|
, isSchemaCacheBuildRequiredRunSQL
|
|
|
|
|
) where
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
import Hasura.Prelude
|
|
|
|
|
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import qualified Data.HashMap.Strict as M
|
|
|
|
|
import qualified Data.HashSet as HS
|
|
|
|
|
import qualified Database.PG.Query as Q
|
|
|
|
|
import qualified Text.Regex.TDFA as TDFA
|
2021-05-21 05:46:58 +03:00
|
|
|
|
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import Control.Monad.Trans.Control (MonadBaseControl)
|
2021-05-27 18:06:13 +03:00
|
|
|
|
import Data.Aeson
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Data.Text.Extended
|
2021-03-15 16:02:58 +03:00
|
|
|
|
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import qualified Hasura.SQL.AnyBackend as AB
|
|
|
|
|
import qualified Hasura.Tracing as Tracing
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import Hasura.Backends.Postgres.DDL.EventTrigger
|
|
|
|
|
import Hasura.Backends.Postgres.DDL.Source (ToMetadataFetchQuery,
|
|
|
|
|
fetchFunctionMetadata,
|
|
|
|
|
fetchTableMetadata)
|
2021-07-19 18:35:16 +03:00
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
2021-05-11 18:18:31 +03:00
|
|
|
|
import Hasura.Base.Error
|
2021-05-27 18:06:13 +03:00
|
|
|
|
import Hasura.EncJSON
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import Hasura.RQL.DDL.Deps (reportDepsExt)
|
2021-05-27 18:06:13 +03:00
|
|
|
|
import Hasura.RQL.DDL.Schema
|
2021-02-14 09:07:52 +03:00
|
|
|
|
import Hasura.RQL.DDL.Schema.Common
|
2021-05-27 18:06:13 +03:00
|
|
|
|
import Hasura.RQL.DDL.Schema.Diff
|
2021-09-09 14:54:19 +03:00
|
|
|
|
import Hasura.RQL.Types hiding (ConstraintName, fmFunction,
|
|
|
|
|
tmComputedFields, tmTable)
|
|
|
|
|
import Hasura.Server.Utils (quoteRegex)
|
2021-09-01 20:56:46 +03:00
|
|
|
|
import Hasura.Session
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-05-27 18:06:13 +03:00
|
|
|
|
data RunSQL
|
|
|
|
|
= RunSQL
|
|
|
|
|
{ rSql :: Text
|
|
|
|
|
, rSource :: !SourceName
|
|
|
|
|
, rCascade :: !Bool
|
|
|
|
|
, rCheckMetadataConsistency :: !(Maybe Bool)
|
|
|
|
|
, rTxAccessMode :: !Q.TxAccess
|
2021-02-14 09:07:52 +03:00
|
|
|
|
} deriving (Show, Eq)
|
|
|
|
|
|
2021-05-27 18:06:13 +03:00
|
|
|
|
instance FromJSON RunSQL where
|
|
|
|
|
parseJSON = withObject "RunSQL" $ \o -> do
|
|
|
|
|
rSql <- o .: "sql"
|
|
|
|
|
rSource <- o .:? "source" .!= defaultSource
|
|
|
|
|
rCascade <- o .:? "cascade" .!= False
|
|
|
|
|
rCheckMetadataConsistency <- o .:? "check_metadata_consistency"
|
|
|
|
|
isReadOnly <- o .:? "read_only" .!= False
|
|
|
|
|
let rTxAccessMode = if isReadOnly then Q.ReadOnly else Q.ReadWrite
|
|
|
|
|
pure RunSQL{..}
|
|
|
|
|
|
|
|
|
|
instance ToJSON RunSQL where
|
|
|
|
|
toJSON RunSQL {..} =
|
|
|
|
|
object
|
|
|
|
|
[ "sql" .= rSql
|
|
|
|
|
, "source" .= rSource
|
|
|
|
|
, "cascade" .= rCascade
|
|
|
|
|
, "check_metadata_consistency" .= rCheckMetadataConsistency
|
|
|
|
|
, "read_only" .=
|
|
|
|
|
case rTxAccessMode of
|
|
|
|
|
Q.ReadOnly -> True
|
|
|
|
|
Q.ReadWrite -> False
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
-- | see Note [Checking metadata consistency in run_sql]
|
|
|
|
|
isSchemaCacheBuildRequiredRunSQL :: RunSQL -> Bool
|
|
|
|
|
isSchemaCacheBuildRequiredRunSQL RunSQL {..} =
|
|
|
|
|
case rTxAccessMode of
|
|
|
|
|
Q.ReadOnly -> False
|
|
|
|
|
Q.ReadWrite -> fromMaybe (containsDDLKeyword rSql) rCheckMetadataConsistency
|
|
|
|
|
where
|
|
|
|
|
containsDDLKeyword = TDFA.match $$(quoteRegex
|
|
|
|
|
TDFA.defaultCompOpt
|
|
|
|
|
{ TDFA.caseSensitive = False
|
|
|
|
|
, TDFA.multiline = True
|
|
|
|
|
, TDFA.lastStarGreedy = True }
|
|
|
|
|
TDFA.defaultExecOpt
|
|
|
|
|
{ TDFA.captureGroups = False }
|
|
|
|
|
"\\balter\\b|\\bdrop\\b|\\breplace\\b|\\bcreate function\\b|\\bcomment on\\b")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{- Note [Checking metadata consistency in run_sql]
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
SQL queries executed by run_sql may change the Postgres schema in arbitrary
|
|
|
|
|
ways. We attempt to automatically update the metadata to reflect those changes
|
|
|
|
|
as much as possible---for example, if a table is renamed, we want to update the
|
|
|
|
|
metadata to track the table under its new name instead of its old one. This
|
|
|
|
|
schema diffing (plus some integrity checking) is handled by withMetadataCheck.
|
|
|
|
|
|
|
|
|
|
But this process has overhead---it involves reloading the metadata, diffing it,
|
|
|
|
|
and rebuilding the schema cache---so we don’t want to do it if it isn’t
|
|
|
|
|
necessary. The user can explicitly disable the check via the
|
|
|
|
|
check_metadata_consistency option, and we also skip it if the current
|
|
|
|
|
transaction is in READ ONLY mode, since the schema can’t be modified in that
|
|
|
|
|
case, anyway.
|
|
|
|
|
|
|
|
|
|
However, even if neither read_only or check_metadata_consistency is passed, lots
|
|
|
|
|
of queries may not modify the schema at all. As a (fairly stupid) heuristic, we
|
|
|
|
|
check if the query contains any keywords for DDL operations, and if not, we skip
|
|
|
|
|
the metadata check as well. -}
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
fetchMeta
|
2021-05-21 05:46:58 +03:00
|
|
|
|
:: (ToMetadataFetchQuery pgKind, BackendMetadata ('Postgres pgKind), MonadTx m)
|
|
|
|
|
=> TableCache ('Postgres pgKind)
|
|
|
|
|
-> FunctionCache ('Postgres pgKind)
|
2021-05-27 18:06:13 +03:00
|
|
|
|
-> m ([TableMeta ('Postgres pgKind)], [FunctionMeta ('Postgres pgKind)])
|
2021-02-14 09:07:52 +03:00
|
|
|
|
fetchMeta tables functions = do
|
|
|
|
|
tableMetaInfos <- fetchTableMetadata
|
|
|
|
|
functionMetaInfos <- fetchFunctionMetadata
|
|
|
|
|
|
|
|
|
|
let getFunctionMetas function =
|
|
|
|
|
let mkFunctionMeta rawInfo =
|
|
|
|
|
FunctionMeta (rfiOid rawInfo) function (rfiFunctionType rawInfo)
|
|
|
|
|
in maybe [] (map mkFunctionMeta) $ M.lookup function functionMetaInfos
|
|
|
|
|
|
|
|
|
|
mkComputedFieldMeta computedField =
|
|
|
|
|
let function = _cffName $ _cfiFunction computedField
|
|
|
|
|
in map (ComputedFieldMeta (_cfiName computedField)) $ getFunctionMetas function
|
|
|
|
|
|
|
|
|
|
tableMetas = flip map (M.toList tableMetaInfos) $ \(table, tableMetaInfo) ->
|
|
|
|
|
TableMeta table tableMetaInfo $ fromMaybe [] $
|
|
|
|
|
M.lookup table tables <&> \tableInfo ->
|
|
|
|
|
let tableCoreInfo = _tiCoreInfo tableInfo
|
|
|
|
|
computedFields = getComputedFieldInfos $ _tciFieldInfoMap tableCoreInfo
|
|
|
|
|
in concatMap mkComputedFieldMeta computedFields
|
|
|
|
|
|
|
|
|
|
functionMetas = concatMap getFunctionMetas $ M.keys functions
|
|
|
|
|
|
|
|
|
|
pure (tableMetas, functionMetas)
|
|
|
|
|
|
2021-05-27 18:06:13 +03:00
|
|
|
|
runRunSQL
|
|
|
|
|
:: forall (pgKind :: PostgresKind) m
|
|
|
|
|
. ( BackendMetadata ('Postgres pgKind)
|
|
|
|
|
, ToMetadataFetchQuery pgKind
|
|
|
|
|
, CacheRWM m
|
|
|
|
|
, HasServerConfigCtx m
|
|
|
|
|
, MetadataM m
|
|
|
|
|
, MonadBaseControl IO m
|
|
|
|
|
, MonadError QErr m
|
|
|
|
|
, MonadIO m
|
2021-09-01 20:56:46 +03:00
|
|
|
|
, Tracing.MonadTrace m
|
|
|
|
|
, UserInfoM m
|
2021-05-27 18:06:13 +03:00
|
|
|
|
)
|
|
|
|
|
=> RunSQL
|
|
|
|
|
-> m EncJSON
|
2021-09-01 20:56:46 +03:00
|
|
|
|
runRunSQL q@RunSQL{..} = do
|
|
|
|
|
sourceConfig <- askSourceConfig @('Postgres pgKind) rSource
|
|
|
|
|
traceCtx <- Tracing.currentContext
|
|
|
|
|
userInfo <- askUserInfo
|
|
|
|
|
let pgExecCtx = _pscExecCtx sourceConfig
|
|
|
|
|
if (isSchemaCacheBuildRequiredRunSQL q)
|
|
|
|
|
then do
|
|
|
|
|
-- see Note [Checking metadata consistency in run_sql]
|
|
|
|
|
withMetadataCheck @pgKind rSource rCascade rTxAccessMode
|
|
|
|
|
$ withTraceContext traceCtx
|
|
|
|
|
$ withUserInfo userInfo
|
|
|
|
|
$ execRawSQL rSql
|
|
|
|
|
else do
|
2021-09-15 23:45:49 +03:00
|
|
|
|
runTxWithCtx pgExecCtx rTxAccessMode $ execRawSQL rSql
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
2021-05-27 18:06:13 +03:00
|
|
|
|
execRawSQL :: (MonadTx n) => Text -> n EncJSON
|
|
|
|
|
execRawSQL =
|
|
|
|
|
fmap (encJFromJValue @RunSQLRes) . liftTx . Q.multiQE rawSqlErrHandler . Q.fromText
|
|
|
|
|
where
|
|
|
|
|
rawSqlErrHandler txe =
|
2021-09-17 10:43:43 +03:00
|
|
|
|
(err400 PostgresError "query execution failed") { qeInternal = Just $ ExtraInternal $ toJSON txe }
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
-- | @'withMetadataCheck' cascade action@ runs @action@ and checks if the schema changed as a
|
|
|
|
|
-- result. If it did, it checks to ensure the changes do not violate any integrity constraints, and
|
|
|
|
|
-- if not, incorporates them into the schema cache.
|
2021-07-23 02:06:10 +03:00
|
|
|
|
-- TODO(antoine): shouldn't this be generalized?
|
2021-02-14 09:07:52 +03:00
|
|
|
|
withMetadataCheck
|
2021-05-21 05:46:58 +03:00
|
|
|
|
:: forall (pgKind :: PostgresKind) a m
|
2021-05-27 18:06:13 +03:00
|
|
|
|
. ( BackendMetadata ('Postgres pgKind)
|
2021-05-21 05:46:58 +03:00
|
|
|
|
, ToMetadataFetchQuery pgKind
|
|
|
|
|
, CacheRWM m
|
|
|
|
|
, HasServerConfigCtx m
|
|
|
|
|
, MetadataM m
|
|
|
|
|
, MonadBaseControl IO m
|
|
|
|
|
, MonadError QErr m
|
|
|
|
|
, MonadIO m
|
|
|
|
|
)
|
2021-09-15 23:45:49 +03:00
|
|
|
|
=> SourceName -> Bool -> Q.TxAccess -> Q.TxET QErr m a -> m a
|
2021-02-14 09:07:52 +03:00
|
|
|
|
withMetadataCheck source cascade txAccess action = do
|
2021-05-21 05:46:58 +03:00
|
|
|
|
SourceInfo _ preActionTables preActionFunctions sourceConfig <- askSourceInfo @('Postgres pgKind) source
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
(actionResult, metadataUpdater) <-
|
2021-09-15 23:45:49 +03:00
|
|
|
|
liftEitherM $ runExceptT $ runTx (_pscExecCtx sourceConfig) txAccess $ do
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-- Drop event triggers so no interference is caused to the sql query
|
|
|
|
|
forM_ (M.elems preActionTables) $ \tableInfo -> do
|
|
|
|
|
let eventTriggers = _tiEventTriggerInfoMap tableInfo
|
2021-09-09 14:54:19 +03:00
|
|
|
|
forM_ (M.keys eventTriggers) (liftTx . dropTriggerQ)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
-- Get the metadata before the sql query, everything, need to filter this
|
|
|
|
|
(preActionTableMeta, preActionFunctionMeta) <- fetchMeta preActionTables preActionFunctions
|
|
|
|
|
|
|
|
|
|
-- Run the action
|
|
|
|
|
actionResult <- action
|
2021-07-23 02:06:10 +03:00
|
|
|
|
|
2021-02-14 09:07:52 +03:00
|
|
|
|
-- Get the metadata after the sql query
|
|
|
|
|
(postActionTableMeta, postActionFunctionMeta) <- fetchMeta preActionTables preActionFunctions
|
|
|
|
|
|
|
|
|
|
let preActionTableMeta' = filter (flip M.member preActionTables . tmTable) preActionTableMeta
|
|
|
|
|
schemaDiff = getSchemaDiff preActionTableMeta' postActionTableMeta
|
|
|
|
|
FunctionDiff droppedFuncs alteredFuncs = getFuncDiff preActionFunctionMeta postActionFunctionMeta
|
|
|
|
|
overloadedFuncs = getOverloadedFuncs (M.keys preActionFunctions) postActionFunctionMeta
|
|
|
|
|
|
|
|
|
|
-- Do not allow overloading functions
|
|
|
|
|
unless (null overloadedFuncs) $
|
|
|
|
|
throw400 NotSupported $ "the following tracked function(s) cannot be overloaded: "
|
|
|
|
|
<> commaSeparated overloadedFuncs
|
|
|
|
|
|
|
|
|
|
-- Report back with an error if cascade is not set
|
2021-07-23 02:06:10 +03:00
|
|
|
|
indirectDeps <- getSchemaChangeDeps source schemaDiff
|
2021-02-14 09:07:52 +03:00
|
|
|
|
when (indirectDeps /= [] && not cascade) $ reportDepsExt indirectDeps []
|
|
|
|
|
|
|
|
|
|
metadataUpdater <- execWriterT $ do
|
|
|
|
|
-- Purge all the indirect dependents from state
|
2021-07-23 02:06:10 +03:00
|
|
|
|
for_ indirectDeps \case
|
|
|
|
|
SOSourceObj sourceName objectID -> do
|
|
|
|
|
AB.dispatchAnyBackend @BackendMetadata objectID $ purgeDependentObject sourceName >=> tell
|
|
|
|
|
_ ->
|
|
|
|
|
pure ()
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
-- Purge all dropped functions
|
2021-07-23 02:06:10 +03:00
|
|
|
|
let purgedFuncs = flip mapMaybe indirectDeps \case
|
|
|
|
|
SOSourceObj _ objectID
|
|
|
|
|
| Just (SOIFunction qf) <- AB.unpackAnyBackend @('Postgres pgKind) objectID
|
|
|
|
|
-> Just qf
|
|
|
|
|
_ -> Nothing
|
|
|
|
|
for_ (droppedFuncs \\ purgedFuncs) $
|
|
|
|
|
tell . dropFunctionInMetadata @('Postgres pgKind) source
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
-- Process altered functions
|
|
|
|
|
forM_ alteredFuncs $ \(qf, newTy) -> do
|
|
|
|
|
when (newTy == FTVOLATILE) $
|
|
|
|
|
throw400 NotSupported $
|
|
|
|
|
"type of function " <> qf <<> " is altered to \"VOLATILE\" which is not supported now"
|
|
|
|
|
|
|
|
|
|
-- update the metadata with the changes
|
2021-05-27 18:06:13 +03:00
|
|
|
|
processSchemaDiff source preActionTables schemaDiff
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
pure (actionResult, metadataUpdater)
|
|
|
|
|
|
|
|
|
|
-- Build schema cache with updated metadata
|
|
|
|
|
withNewInconsistentObjsCheck $
|
|
|
|
|
buildSchemaCacheWithInvalidations mempty{ciSources = HS.singleton source} metadataUpdater
|
|
|
|
|
|
|
|
|
|
postActionSchemaCache <- askSchemaCache
|
|
|
|
|
|
|
|
|
|
-- Recreate event triggers in hdb_catalog
|
2021-05-21 05:46:58 +03:00
|
|
|
|
let postActionTables = fromMaybe mempty $ unsafeTableCache @('Postgres pgKind) source $ scSources postActionSchemaCache
|
2021-02-14 09:07:52 +03:00
|
|
|
|
serverConfigCtx <- askServerConfigCtx
|
|
|
|
|
liftEitherM $ runPgSourceWriteTx sourceConfig $
|
|
|
|
|
forM_ (M.elems postActionTables) $ \(TableInfo coreInfo _ eventTriggers) -> do
|
|
|
|
|
let table = _tciName coreInfo
|
|
|
|
|
columns = getCols $ _tciFieldInfoMap coreInfo
|
|
|
|
|
forM_ (M.toList eventTriggers) $ \(triggerName, eti) -> do
|
|
|
|
|
let opsDefinition = etiOpsDef eti
|
|
|
|
|
flip runReaderT serverConfigCtx $ mkAllTriggersQ triggerName table columns opsDefinition
|
|
|
|
|
|
|
|
|
|
pure actionResult
|