graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs

282 lines
10 KiB
Haskell

{- |
Description: Create/delete SQL functions to/from Hasura metadata.
-}
module Hasura.RQL.DDL.Schema.Function where
import Hasura.Prelude
import qualified Control.Monad.Validate as MV
import qualified Data.HashMap.Strict as M
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import Control.Lens
import Data.Aeson
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Text.Extended
import Language.Haskell.TH.Syntax (Lift)
import qualified Language.GraphQL.Draft.Syntax as G
import Hasura.Backends.Postgres.SQL.Types
import Hasura.EncJSON
import Hasura.Incremental (Cacheable)
import Hasura.RQL.Types
import Hasura.Server.Utils (englishList, makeReasonMessage)
data RawFunctionInfo
= RawFunctionInfo
{ rfiHasVariadic :: !Bool
, rfiFunctionType :: !FunctionType
, rfiReturnTypeSchema :: !SchemaName
, rfiReturnTypeName :: !PGScalarType
, rfiReturnTypeType :: !PGTypeKind
, rfiReturnsSet :: !Bool
, rfiInputArgTypes :: ![QualifiedPGType]
, rfiInputArgNames :: ![FunctionArgName]
, rfiDefaultArgs :: !Int
, rfiReturnsTable :: !Bool
, rfiDescription :: !(Maybe PGDescription)
} deriving (Show, Eq, Generic)
instance NFData RawFunctionInfo
instance Cacheable RawFunctionInfo
$(deriveJSON (aesonDrop 3 snakeCase) ''RawFunctionInfo)
mkFunctionArgs :: Int -> [QualifiedPGType] -> [FunctionArgName] -> [FunctionArg]
mkFunctionArgs defArgsNo tys argNames =
bool withNames withNoNames $ null argNames
where
hasDefaultBoolSeq = replicate (length tys - defArgsNo) (HasDefault False)
-- only last arguments can have default expression
<> replicate defArgsNo (HasDefault True)
tysWithHasDefault = zip tys hasDefaultBoolSeq
withNoNames = flip map tysWithHasDefault $ uncurry $ FunctionArg Nothing
withNames = zipWith mkArg argNames tysWithHasDefault
mkArg "" (ty, hasDef) = FunctionArg Nothing ty hasDef
mkArg n (ty, hasDef) = FunctionArg (Just n) ty hasDef
validateFuncArgs :: MonadError QErr m => [FunctionArg] -> m ()
validateFuncArgs args =
for_ (nonEmpty invalidArgs) \someInvalidArgs ->
throw400 NotSupported $
"arguments: " <> englishList "and" someInvalidArgs
<> " are not in compliance with GraphQL spec"
where
funcArgsText = mapMaybe (fmap getFuncArgNameTxt . faName) args
invalidArgs = filter (isNothing . G.mkName) funcArgsText
data FunctionIntegrityError
= FunctionNameNotGQLCompliant
| FunctionVariadic
| FunctionReturnNotCompositeType
| FunctionReturnNotSetof
| FunctionReturnNotSetofTable
| FunctionVolatile
| FunctionSessionArgumentNotJSON !FunctionArgName
| FunctionInvalidSessionArgument !FunctionArgName
| FunctionInvalidArgumentNames [FunctionArgName]
deriving (Show, Eq)
mkFunctionInfo
:: (QErrM m)
=> QualifiedFunction
-> SystemDefined
-> FunctionConfig
-> RawFunctionInfo
-> m (FunctionInfo, SchemaDependency)
mkFunctionInfo qf systemDefined config rawFuncInfo =
either (throw400 NotSupported . showErrors) pure
=<< MV.runValidateT validateFunction
where
functionArgs = mkFunctionArgs defArgsNo inpArgTyps inpArgNames
RawFunctionInfo hasVariadic funTy retSn retN retTyTyp retSet
inpArgTyps inpArgNames defArgsNo returnsTab descM
= rawFuncInfo
returnType = QualifiedPGType retSn retN retTyTyp
throwValidateError = MV.dispute . pure
validateFunction = do
unless (has _Right $ qualifiedObjectToName qf) $
throwValidateError FunctionNameNotGQLCompliant
when hasVariadic $ throwValidateError FunctionVariadic
when (retTyTyp /= PGKindComposite) $ throwValidateError FunctionReturnNotCompositeType
unless retSet $ throwValidateError FunctionReturnNotSetof
unless returnsTab $ throwValidateError FunctionReturnNotSetofTable
when (funTy == FTVOLATILE) $ throwValidateError FunctionVolatile
-- validate function argument names
validateFunctionArgNames
inputArguments <- makeInputArguments
let retTable = typeToTable returnType
pure ( FunctionInfo qf systemDefined funTy inputArguments retTable descM
, SchemaDependency (SOTable retTable) DRTable
)
validateFunctionArgNames = do
let argNames = mapMaybe faName functionArgs
invalidArgs = filter (isNothing . G.mkName . getFuncArgNameTxt) argNames
unless (null invalidArgs) $
throwValidateError $ FunctionInvalidArgumentNames invalidArgs
makeInputArguments =
case _fcSessionArgument config of
Nothing -> pure $ Seq.fromList $ map IAUserProvided functionArgs
Just sessionArgName -> do
unless (any (\arg -> Just sessionArgName == faName arg) functionArgs) $
throwValidateError $ FunctionInvalidSessionArgument sessionArgName
fmap Seq.fromList $ forM functionArgs $ \arg ->
if Just sessionArgName == faName arg then do
let argTy = _qptName $ faType arg
if argTy == PGJSON then pure $ IASessionVariables sessionArgName
else MV.refute $ pure $ FunctionSessionArgumentNotJSON sessionArgName
else pure $ IAUserProvided arg
showErrors allErrors =
"the function " <> qf <<> " cannot be tracked "
<> makeReasonMessage allErrors showOneError
showOneError = \case
FunctionNameNotGQLCompliant -> "function name is not a legal GraphQL identifier"
FunctionVariadic -> "function with \"VARIADIC\" parameters are not supported"
FunctionReturnNotCompositeType -> "the function does not return a \"COMPOSITE\" type"
FunctionReturnNotSetof -> "the function does not return a SETOF"
FunctionReturnNotSetofTable -> "the function does not return a SETOF table"
FunctionVolatile -> "function of type \"VOLATILE\" is not supported now"
FunctionSessionArgumentNotJSON argName ->
"given session argument " <> argName <<> " is not of type json"
FunctionInvalidSessionArgument argName ->
"given session argument " <> argName <<> " not the input argument of the function"
FunctionInvalidArgumentNames args ->
let argsText = T.intercalate "," $ map getFuncArgNameTxt args
in "the function arguments " <> argsText <> " are not in compliance with GraphQL spec"
saveFunctionToCatalog
:: (MonadTx m, HasSystemDefined m)
=> QualifiedFunction -> FunctionConfig -> m ()
saveFunctionToCatalog (QualifiedObject sn fn) config = do
systemDefined <- askSystemDefined
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO "hdb_catalog"."hdb_function"
(function_schema, function_name, configuration, is_system_defined)
VALUES ($1, $2, $3, $4)
|] (sn, fn, Q.AltJ config, systemDefined) False
delFunctionFromCatalog :: QualifiedFunction -> Q.TxE QErr ()
delFunctionFromCatalog (QualifiedObject sn fn) =
Q.unitQE defaultTxErrorHandler [Q.sql|
DELETE FROM hdb_catalog.hdb_function
WHERE function_schema = $1
AND function_name = $2
|] (sn, fn) False
newtype TrackFunction
= TrackFunction
{ tfName :: QualifiedFunction}
deriving (Show, Eq, FromJSON, ToJSON, Lift)
data FunctionConfig
= FunctionConfig
{ _fcSessionArgument :: !(Maybe FunctionArgName)
} deriving (Show, Eq, Generic, Lift)
instance NFData FunctionConfig
instance Cacheable FunctionConfig
$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields = True} ''FunctionConfig)
emptyFunctionConfig :: FunctionConfig
emptyFunctionConfig = FunctionConfig Nothing
-- | Track function, Phase 1:
-- Validate function tracking operation. Fails if function is already being
-- tracked, or if a table with the same name is being tracked.
trackFunctionP1
:: (CacheRM m, QErrM m) => QualifiedFunction -> m ()
trackFunctionP1 qf = do
rawSchemaCache <- askSchemaCache
when (M.member qf $ scFunctions rawSchemaCache) $
throw400 AlreadyTracked $ "function already tracked : " <>> qf
let qt = fmap (TableName . getFunctionTxt) qf
when (M.member qt $ scTables rawSchemaCache) $
throw400 NotSupported $ "table with name " <> qf <<> " already exists"
trackFunctionP2 :: (MonadTx m, CacheRWM m, HasSystemDefined m)
=> QualifiedFunction -> FunctionConfig -> m EncJSON
trackFunctionP2 qf config = do
saveFunctionToCatalog qf config
buildSchemaCacheFor $ MOFunction qf
return successMsg
handleMultipleFunctions :: (QErrM m) => QualifiedFunction -> [a] -> m a
handleMultipleFunctions qf = \case
[] ->
throw400 NotExists $ "no such function exists in postgres : " <>> qf
[fi] -> return fi
_ ->
throw400 NotSupported $
"function " <> qf <<> " is overloaded. Overloaded functions are not supported"
fetchRawFunctionInfo :: MonadTx m => QualifiedFunction -> m RawFunctionInfo
fetchRawFunctionInfo qf@(QualifiedObject sn fn) =
handleMultipleFunctions qf =<< map (Q.getAltJ . runIdentity) <$> fetchFromDatabase
where
fetchFromDatabase = liftTx $
Q.listQE defaultTxErrorHandler [Q.sql|
SELECT function_info
FROM hdb_catalog.hdb_function_info_agg
WHERE function_schema = $1
AND function_name = $2
|] (sn, fn) True
runTrackFunc
:: (MonadTx m, CacheRWM m, HasSystemDefined m)
=> TrackFunction -> m EncJSON
runTrackFunc (TrackFunction qf)= do
trackFunctionP1 qf
trackFunctionP2 qf emptyFunctionConfig
data TrackFunctionV2
= TrackFunctionV2
{ _tfv2Function :: !QualifiedFunction
, _tfv2Configuration :: !FunctionConfig
} deriving (Show, Eq, Lift, Generic)
$(deriveToJSON (aesonDrop 5 snakeCase) ''TrackFunctionV2)
instance FromJSON TrackFunctionV2 where
parseJSON = withObject "Object" $ \o ->
TrackFunctionV2
<$> o .: "function"
<*> o .:? "configuration" .!= emptyFunctionConfig
runTrackFunctionV2
:: ( QErrM m, CacheRWM m, HasSystemDefined m
, MonadTx m
)
=> TrackFunctionV2 -> m EncJSON
runTrackFunctionV2 (TrackFunctionV2 qf config) = do
trackFunctionP1 qf
trackFunctionP2 qf config
newtype UnTrackFunction
= UnTrackFunction
{ utfName :: QualifiedFunction }
deriving (Show, Eq, FromJSON, ToJSON, Lift)
runUntrackFunc
:: (QErrM m, CacheRWM m, MonadTx m)
=> UnTrackFunction -> m EncJSON
runUntrackFunc (UnTrackFunction qf) = do
void $ askFunctionInfo qf
liftTx $ delFunctionFromCatalog qf
withNewInconsistentObjsCheck buildSchemaCache
return successMsg