graphql-engine/server/src-lib/Hasura/Function/Cache.hs

363 lines
12 KiB
Haskell
Raw Normal View History

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE UndecidableInstances #-}
-- | types and helpers for user-defined-functions after they have been resolved
-- in the schema cache
module Hasura.Function.Cache
( DBFunctionsMetadata,
FunctionOverloads (..),
FunctionArgName (..),
FunctionCache,
FunctionConfig (..),
FunctionCustomRootFields (..),
FunctionExposedAs (..),
FunctionInfo (..),
FunctionInputArgument,
FunctionPermissionInfo (..),
FunctionPermissionsMap,
FunctionVolatility (..),
InputArgument (..),
FunctionArgsExpG (..),
FunctionArgsExp,
TrackableFunctionInfo (..),
TrackableTableInfo (..),
TrackableInfo (..),
emptyFunctionConfig,
emptyFunctionCustomRootFields,
funcTypToTxt,
emptyFunctionArgsExp,
)
where
import Autodocodec (HasCodec (codec))
import Autodocodec qualified as AC
import Autodocodec.Extended (graphQLFieldNameCodec)
import Control.Lens
import Data.Aeson
import Data.Aeson.Casing
import Data.Char (toLower)
import Data.HashMap.Strict qualified as HashMap
import Data.List.Extended as LE
import Data.Sequence qualified as Seq
import Data.Text qualified as T
import Data.Text.Extended
import Hasura.Prelude
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.BackendType
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Roles (RoleName)
import Language.GraphQL.Draft.Syntax qualified as G
import Language.Haskell.TH.Syntax
-- | https://www.postgresql.org/docs/current/xfunc-volatility.html
data FunctionVolatility
= FTVOLATILE
| FTIMMUTABLE
| FTSTABLE
deriving (Eq, Generic)
instance NFData FunctionVolatility
instance FromJSON FunctionVolatility where
parseJSON = genericParseJSON defaultOptions {constructorTagModifier = drop 2}
instance ToJSON FunctionVolatility where
toJSON = genericToJSON defaultOptions {constructorTagModifier = drop 2}
toEncoding = genericToEncoding defaultOptions {constructorTagModifier = drop 2}
funcTypToTxt :: FunctionVolatility -> Text
funcTypToTxt FTVOLATILE = "VOLATILE"
funcTypToTxt FTIMMUTABLE = "IMMUTABLE"
funcTypToTxt FTSTABLE = "STABLE"
instance Show FunctionVolatility where
show = T.unpack . funcTypToTxt
newtype FunctionArgName = FunctionArgName {getFuncArgNameTxt :: Text}
server: delete the `Cacheable` type class in favor of `Eq` What is the `Cacheable` type class about? ```haskell class Eq a => Cacheable a where unchanged :: Accesses -> a -> a -> Bool default unchanged :: (Generic a, GCacheable (Rep a)) => Accesses -> a -> a -> Bool unchanged accesses a b = gunchanged (from a) (from b) accesses ``` Its only method is an alternative to `(==)`. The added value of `unchanged` (and the additional `Accesses` argument) arises _only_ for one type, namely `Dependency`. Indeed, the `Cacheable (Dependency a)` instance is non-trivial, whereas every other `Cacheable` instance is completely boilerplate (and indeed either generated from `Generic`, or simply `unchanged _ = (==)`). The `Cacheable (Dependency a)` instance is the only one where the `Accesses` argument is not just passed onwards. The only callsite of the `unchanged` method is in the `ArrowCache (Rule m)` method. That is to say that the `Cacheable` type class is used to decide when we can re-use parts of the schema cache between Metadata operations. So what is the `Cacheable (Dependency a)` instance about? Normally, the output of a `Rule m a b` is re-used when the new input (of type `a`) is equal to the old one. But sometimes, that's too coarse: it might be that a certain `Rule m a b` only depends on a small part of its input of type `a`. A `Dependency` allows us to spell out what parts of `a` are being depended on, and these parts are recorded as values of types `Access a` in the state `Accesses`. If the input `a` changes, but not in a way that touches the recorded `Accesses`, then the output `b` of that rule can be re-used without recomputing. So now you understand _why_ we're passing `Accesses` to the `unchanged` method: `unchanged` is an equality check in disguise that just needs some additional context. But we don't need to pass `Accesses` as a function argument. We can use the `reflection` package to pass it as type-level context. So the core of this PR is that we change the instance declaration from ```haskell instance (Cacheable a) => Cacheable (Dependency a) where ``` to ```haskell instance (Given Accesses, Eq a) => Eq (Dependency a) where ``` and use `(==)` instead of `unchanged`. If you haven't seen `reflection` before: it's like a `MonadReader`, but it doesn't require a `Monad`. In order to pass the current `Accesses` value, instead of simply passing the `Accesses` as a function argument, we need to instantiate the `Given Accesses` context. We use the `give` method from the `reflection` package for that. ```haskell give :: forall r. Accesses -> (Given Accesses => r) -> r unchanged :: (Given Accesses => Eq a) => Accesses -> a -> a -> Bool unchanged accesses a b = give accesses (a == b) ``` With these three components in place, we can delete the `Cacheable` type class entirely. The remainder of this PR is just to remove the `Cacheable` type class and its instances. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6877 GitOrigin-RevId: 7125f5e11d856e7672ab810a23d5bf5ad176e77f
2022-11-21 19:33:56 +03:00
deriving (Show, Eq, Ord, NFData, ToJSON, ToJSONKey, FromJSON, FromJSONKey, ToTxt, IsString, Generic, Hashable, Lift, Data)
instance AC.HasCodec FunctionArgName where
codec = AC.dimapCodec FunctionArgName getFuncArgNameTxt codec
data InputArgument a
= IAUserProvided a
| IASessionVariables FunctionArgName
deriving (Show, Eq, Functor, Generic)
instance (ToJSON a) => ToJSON (InputArgument a) where
toJSON = genericToJSON defaultOptions {constructorTagModifier = snakeCase . drop 2, sumEncoding = TaggedObject "type" "argument"}
toEncoding = genericToEncoding defaultOptions {constructorTagModifier = snakeCase . drop 2, sumEncoding = TaggedObject "type" "argument"}
type FunctionInputArgument b = InputArgument (FunctionArgument b)
-- | Indicates whether the user requested the corresponding function to be
-- tracked as a mutation or a query/subscription, in @track_function@.
data FunctionExposedAs = FEAQuery | FEAMutation
deriving (Show, Eq, Generic)
instance NFData FunctionExposedAs
instance HasCodec FunctionExposedAs where
codec = AC.stringConstCodec [(FEAQuery, "query"), (FEAMutation, "mutation")]
instance FromJSON FunctionExposedAs where
parseJSON = genericParseJSON defaultOptions {sumEncoding = UntaggedValue, constructorTagModifier = map toLower . drop 3}
instance ToJSON FunctionExposedAs where
toJSON = genericToJSON defaultOptions {sumEncoding = UntaggedValue, constructorTagModifier = map toLower . drop 3}
toEncoding = genericToEncoding defaultOptions {sumEncoding = UntaggedValue, constructorTagModifier = map toLower . drop 3}
newtype FunctionPermissionInfo = FunctionPermissionInfo
{ _fpmRole :: RoleName
}
deriving (Show, Eq, Generic)
instance HasCodec FunctionPermissionInfo where
codec =
AC.object "FunctionPermissionInfo"
$ FunctionPermissionInfo
<$> AC.requiredField' "role"
AC..= _fpmRole
instance FromJSON FunctionPermissionInfo where
parseJSON = genericParseJSON hasuraJSON
instance ToJSON FunctionPermissionInfo where
toJSON = genericToJSON hasuraJSON
toEncoding = genericToEncoding hasuraJSON
type FunctionPermissionsMap = HashMap RoleName FunctionPermissionInfo
-- | Custom root fields for functions. When set, will be the names exposed
-- to the user in the schema.
--
-- See rfcs/function-root-field-customisation.md for more information.
data FunctionCustomRootFields = FunctionCustomRootFields
{ _fcrfFunction :: Maybe G.Name,
_fcrfFunctionAggregate :: Maybe G.Name
}
deriving (Show, Eq, Generic)
instance NFData FunctionCustomRootFields
instance HasCodec FunctionCustomRootFields where
codec =
AC.bimapCodec checkForDup id
$ AC.object "FunctionCustomRootFields"
$ FunctionCustomRootFields
<$> AC.optionalFieldWith' "function" graphQLFieldNameCodec
AC..= _fcrfFunction
<*> AC.optionalFieldWith' "function_aggregate" graphQLFieldNameCodec
AC..= _fcrfFunctionAggregate
where
checkForDup (FunctionCustomRootFields (Just f) (Just fa))
| f == fa =
Left
$ T.unpack
$ "the following custom root field names are duplicated: "
<> toTxt f
<<> " and "
<>> toTxt fa
checkForDup fields = Right fields
instance ToJSON FunctionCustomRootFields where
toJSON = genericToJSON hasuraJSON {omitNothingFields = True}
toEncoding = genericToEncoding hasuraJSON {omitNothingFields = True}
instance FromJSON FunctionCustomRootFields where
parseJSON = withObject "Object" $ \obj -> do
function <- obj .:? "function"
functionAggregate <- obj .:? "function_aggregate"
case (function, functionAggregate) of
(Just f, Just fa)
| f == fa ->
fail
$ T.unpack
$ "the following custom root field names are duplicated: "
<> toTxt f
<<> " and "
<>> toTxt fa
_ ->
pure ()
pure $ FunctionCustomRootFields function functionAggregate
-- | A function custom root fields without custom names set. This is the default.
emptyFunctionCustomRootFields :: FunctionCustomRootFields
emptyFunctionCustomRootFields =
FunctionCustomRootFields
{ _fcrfFunction = Nothing,
_fcrfFunctionAggregate = Nothing
}
-- | Tracked SQL function metadata. See 'buildFunctionInfo'.
data FunctionInfo (b :: BackendType) = FunctionInfo
{ _fiSQLName :: FunctionName b,
_fiGQLName :: G.Name,
_fiGQLArgsName :: G.Name,
_fiGQLAggregateName :: G.Name,
_fiSystemDefined :: SystemDefined,
_fiVolatility :: FunctionVolatility,
-- | In which part of the schema should this function be exposed?
--
-- See 'mkFunctionInfo' and '_fcExposedAs'.
_fiExposedAs :: FunctionExposedAs,
_fiInputArgs :: Seq.Seq (FunctionInputArgument b),
-- | NOTE: when a table is created, a new composite type of the same name is
-- automatically created; so strictly speaking this field means "the function
-- returns the composite type corresponding to this table".
_fiReturnType :: TableName b, -- NOTE: We will extend this in future, but for now always resolves to a (TableName b)
-- | this field represents the description of the function as present on the database
_fiDescription :: Maybe Text,
-- | Roles to which the function is accessible
_fiPermissions :: FunctionPermissionsMap,
_fiJsonAggSelect :: JsonAggSelect,
_fiComment :: Maybe Text
}
deriving (Generic)
deriving instance (Backend b) => Show (FunctionInfo b)
deriving instance (Backend b) => Eq (FunctionInfo b)
instance (Backend b) => ToJSON (FunctionInfo b) where
toJSON = genericToJSON hasuraJSON
type FunctionCache b = HashMap (FunctionName b) (FunctionInfo b) -- info of all functions
data TrackableFunctionInfo b = TrackableFunctionInfo
{ tfiFunctionName :: FunctionName b,
tfiFunctionVolitility :: FunctionVolatility
}
deriving (Generic)
deriving instance (Backend b) => Show (TrackableFunctionInfo b)
deriving instance (Backend b) => Eq (TrackableFunctionInfo b)
instance (Backend b) => ToJSON (TrackableFunctionInfo b) where
toJSON (TrackableFunctionInfo name volitility) =
object
[ "name" Data.Aeson..= name,
"volitility" Data.Aeson..= volitility
]
newtype TrackableTableInfo b = TrackableTableInfo
{tfTableiName :: TableName b}
deriving (Generic)
deriving instance (Backend b) => Show (TrackableTableInfo b)
deriving instance (Backend b) => Eq (TrackableTableInfo b)
instance (Backend b) => ToJSON (TrackableTableInfo b) where
toJSON (TrackableTableInfo ti) = object ["name" Data.Aeson..= ti]
data TrackableInfo b = TrackableInfo
{ trackableFunctions :: [TrackableFunctionInfo b],
trackableTables :: [TrackableTableInfo b]
}
deriving (Generic)
deriving instance (Backend b) => Show (TrackableInfo b)
deriving instance (Backend b) => Eq (TrackableInfo b)
instance (Backend b) => ToJSON (TrackableInfo b) where
toJSON (TrackableInfo functions tables) =
object
[ "tables" Data.Aeson..= tables,
"functions" Data.Aeson..= functions
]
-- Metadata requests related types
-- | Tracked function configuration, and payload of the 'pg_track_function' and
-- 'pg_set_function_customization' API calls.
data FunctionConfig b = FunctionConfig
{ _fcSessionArgument :: Maybe FunctionArgName,
-- | In which top-level field should we expose this function?
--
-- The user might omit this, in which case we'll infer the location from the
-- SQL functions volatility. See 'mkFunctionInfo' or the @track_function@ API
-- docs for details of validation, etc.
_fcExposedAs :: Maybe FunctionExposedAs,
_fcCustomRootFields :: FunctionCustomRootFields,
_fcCustomName :: Maybe G.Name,
_fcResponse :: Maybe (FunctionReturnType b)
}
deriving (Generic)
deriving stock instance (Backend b) => Show (FunctionConfig b)
deriving stock instance (Backend b) => Eq (FunctionConfig b)
instance (Backend b) => NFData (FunctionConfig b)
instance (Backend b) => HasCodec (FunctionConfig b) where
codec =
AC.object "FunctionConfig"
$ FunctionConfig
<$> AC.optionalField' "session_argument"
AC..= _fcSessionArgument
<*> AC.optionalField' "exposed_as"
AC..= _fcExposedAs
<*> AC.optionalFieldWithDefault' "custom_root_fields" emptyFunctionCustomRootFields
AC..= _fcCustomRootFields
<*> AC.optionalFieldWith' "custom_name" graphQLFieldNameCodec
AC..= _fcCustomName
<*> AC.optionalFieldWith' "response" codec
AC..= _fcResponse
instance (Backend b) => FromJSON (FunctionConfig b) where
parseJSON = withObject "FunctionConfig" $ \obj ->
FunctionConfig
<$> obj
.:? "session_argument"
<*> obj
.:? "exposed_as"
<*> obj
.:? "custom_root_fields"
.!= emptyFunctionCustomRootFields
<*> obj
.:? "custom_name"
<*> obj
.:? "response"
instance (Backend b) => ToJSON (FunctionConfig b) where
toJSON = genericToJSON hasuraJSON {omitNothingFields = True}
toEncoding = genericToEncoding hasuraJSON {omitNothingFields = True}
-- | The default function config; v1 of the API implies this.
emptyFunctionConfig :: FunctionConfig b
emptyFunctionConfig = FunctionConfig Nothing Nothing emptyFunctionCustomRootFields Nothing Nothing
type DBFunctionsMetadata b = HashMap (FunctionName b) (FunctionOverloads b)
newtype FunctionOverloads b = FunctionOverloads {getFunctionOverloads :: NonEmpty (RawFunctionInfo b)}
deriving newtype instance (Backend b) => Eq (FunctionOverloads b)
deriving newtype instance (Backend b) => Show (FunctionOverloads b)
deriving newtype instance (FromJSON (RawFunctionInfo b)) => FromJSON (FunctionOverloads b)
deriving newtype instance (ToJSON (RawFunctionInfo b)) => ToJSON (FunctionOverloads b)
data FunctionArgsExpG a = FunctionArgsExp
{ _faePositional :: [a],
_faeNamed :: (HashMap.HashMap Text a)
}
deriving stock (Show, Eq, Functor, Foldable, Traversable, Generic)
instance (Hashable a) => Hashable (FunctionArgsExpG a)
instance (NFData a) => NFData (FunctionArgsExpG a)
type FunctionArgsExp b v = FunctionArgsExpG (FunctionArgumentExp b v)
emptyFunctionArgsExp :: FunctionArgsExpG a
emptyFunctionArgsExp = FunctionArgsExp [] HashMap.empty