mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 09:51:59 +03:00
cdac24c79f
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
138 lines
5.3 KiB
Haskell
138 lines
5.3 KiB
Haskell
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
module Hasura.SQL.BackendMap
|
|
( BackendMap,
|
|
singleton,
|
|
lookup,
|
|
lookupD,
|
|
elems,
|
|
alter,
|
|
modify,
|
|
overridesDeeply,
|
|
)
|
|
where
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
import Data.Aeson (FromJSON, Key, ToJSON)
|
|
import Data.Aeson qualified as Aeson
|
|
import Data.Aeson.Key qualified as Key
|
|
import Data.Aeson.KeyMap qualified as KeyMap
|
|
import Data.Data
|
|
import Data.Kind (Type)
|
|
import Data.Map.Strict (Map)
|
|
import Data.Map.Strict qualified as Map
|
|
import Data.Text.Extended (toTxt)
|
|
import Hasura.Incremental.Internal.Dependency (Dependency (..), selectD)
|
|
import Hasura.Incremental.Select
|
|
import Hasura.Prelude hiding (empty, lookup, modify)
|
|
import Hasura.SQL.AnyBackend (AnyBackend, SatisfiesForAllBackends, dispatchAnyBackend'', mergeAnyBackend, mkAnyBackend, parseAnyBackendFromJSON, unpackAnyBackend)
|
|
import Hasura.SQL.Backend (BackendType, parseBackendTypeFromText)
|
|
import Hasura.SQL.Tag (BackendTag, HasTag, backendTag, reify)
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
-- | A BackendMap is a data structure that can contain at most one value of an 'i' per 'BackendType'
|
|
-- The 'i' type must be one that is parameterized by a BackendType-kinded type parameter
|
|
newtype BackendMap (i :: BackendType -> Type) = BackendMap (Map BackendType (AnyBackend i))
|
|
deriving stock (Generic)
|
|
deriving newtype (Semigroup, Monoid)
|
|
|
|
deriving newtype instance i `SatisfiesForAllBackends` Show => Show (BackendMap i)
|
|
|
|
deriving newtype instance i `SatisfiesForAllBackends` Eq => Eq (BackendMap i)
|
|
|
|
instance i `SatisfiesForAllBackends` FromJSON => FromJSON (BackendMap i) where
|
|
parseJSON =
|
|
Aeson.withObject "BackendMap" $ \obj -> do
|
|
BackendMap . Map.fromList
|
|
<$> traverse
|
|
( \(backendTypeStr, val) -> do
|
|
backendType <- parseBackendTypeFromText $ Key.toText backendTypeStr
|
|
(backendType,) <$> parseAnyBackendFromJSON backendType val
|
|
)
|
|
(KeyMap.toList obj)
|
|
|
|
instance i `SatisfiesForAllBackends` ToJSON => ToJSON (BackendMap i) where
|
|
toJSON (BackendMap backendMap) =
|
|
Aeson.object $ valueToPair <$> Map.elems backendMap
|
|
where
|
|
valueToPair :: AnyBackend i -> (Key, Aeson.Value)
|
|
valueToPair value = dispatchAnyBackend'' @ToJSON @HasTag value $ \(v :: i b) ->
|
|
let backendTypeText = Key.fromText . toTxt . reify $ backendTag @b
|
|
in (backendTypeText, Aeson.toJSON v)
|
|
|
|
instance Select (BackendMap i) where
|
|
type Selector (BackendMap i) = BackendMapS i
|
|
select (BackendMapS (_ :: BackendTag b)) = lookup @b
|
|
|
|
data BackendMapS i a where
|
|
BackendMapS :: forall (b :: BackendType) (i :: BackendType -> Type). HasTag b => BackendTag b -> BackendMapS i (Maybe (i b))
|
|
|
|
instance GEq (BackendMapS i) where
|
|
BackendMapS a `geq` BackendMapS b = case a `geq` b of
|
|
Just Refl -> Just Refl
|
|
Nothing -> Nothing
|
|
|
|
instance GCompare (BackendMapS i) where
|
|
BackendMapS a `gcompare` BackendMapS b = case a `gcompare` b of
|
|
GLT -> GLT
|
|
GEQ -> GEQ
|
|
GGT -> GGT
|
|
|
|
lookupD ::
|
|
forall (b :: BackendType) (i :: BackendType -> Type).
|
|
HasTag b =>
|
|
Dependency (BackendMap i) ->
|
|
Dependency (Maybe (i b))
|
|
lookupD = selectD (BackendMapS (backendTag @b))
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
singleton :: forall b i. HasTag b => i b -> BackendMap i
|
|
singleton value = BackendMap $ Map.singleton (reify $ backendTag @b) (mkAnyBackend value)
|
|
|
|
-- | Get a value from the map for the particular 'BackendType' 'b'. This function
|
|
-- is usually used with a type application.
|
|
-- @
|
|
-- lookup @('Postgres 'Vanilla) backendMap
|
|
-- @
|
|
lookup :: forall (b :: BackendType) i. HasTag b => BackendMap i -> Maybe (i b)
|
|
lookup (BackendMap backendMap) =
|
|
Map.lookup (reify $ backendTag @b) backendMap >>= unpackAnyBackend @b
|
|
|
|
-- | Get all values in the map
|
|
elems :: forall i. BackendMap i -> [AnyBackend i]
|
|
elems (BackendMap backendMap) = Map.elems backendMap
|
|
|
|
-- | The expression @modify f bmap@ alters the value @x@ at
|
|
-- @b@. @modify@ is a restricted version of 'alter' which cannot
|
|
-- delete entries and if there is no @b@ key present in the map, it
|
|
-- will apply the modification function to the @i b@ unit value and
|
|
-- insert the result at @b@.
|
|
modify :: forall b i. (HasTag b, Monoid (i b)) => (i b -> i b) -> BackendMap i -> BackendMap i
|
|
modify f = alter \case
|
|
Nothing -> Just $ f mempty
|
|
Just ab -> Just $ f ab
|
|
|
|
-- | The expression @alter f bmap@ alters the value @x@ at @b@, or
|
|
-- absence thereof. alter can be used to insert, delete, or update a
|
|
-- value in a Map.
|
|
--
|
|
-- In short : @lookup k (alter f k m) = f (lookup k m)@.
|
|
alter :: forall b i. HasTag b => (Maybe (i b) -> Maybe (i b)) -> BackendMap i -> BackendMap i
|
|
alter f (BackendMap bmap) = BackendMap $ Map.alter (wrap . f . unwrap) (reify @b backendTag) bmap
|
|
where
|
|
wrap :: Maybe (i b) -> Maybe (AnyBackend i)
|
|
wrap = fmap mkAnyBackend
|
|
|
|
unwrap :: Maybe (AnyBackend i) -> Maybe (i b)
|
|
unwrap x = x >>= unpackAnyBackend @b
|
|
|
|
-- | The expression @a `overridesDeeply b@ applies the values from @a@ on top of the defaults @b@.
|
|
-- In practice this should union the maps for each backend type.
|
|
overridesDeeply :: i `SatisfiesForAllBackends` Semigroup => BackendMap i -> BackendMap i -> BackendMap i
|
|
overridesDeeply (BackendMap a) (BackendMap b) = BackendMap (Map.unionWith override a b)
|
|
where
|
|
override a' b' = mergeAnyBackend @Semigroup (<>) a' b' a'
|