graphql-engine/server/src-lib/Hasura/Backends/MySQL/Connection.hs
Auke Booij 83ea4a254d server: plumb StoredIntrospection while building the Schema Cache
We'd like to be able to build a Schema Cache from only serializable data. We already have Metadata. The data that's missing to build a Schema Cache is referred to as "stored introspection", and this includes:
- DB introspection
- User-defined enum values (i.e. contents of specific DB tables)
- Remote schema introspection

This PR introduces a new `StoredIntrospection` container that holds that data, and plumbs it through to the right parts of the schema cache building process, so that stored introspection can be used as a substitute for fresh introspection requests against live data sources.

The serialization of `StoredIntrospection` is intended to be straightforward: just take the serialized source introspection results, and put them in an appropriate JSON object. Though I don't think that this PR achieves that entirely.

In order for `StoredIntrospection` to be deserializable (through `aeson` instances), while keeping the required code changes low, this piggy-backs off of the `ResolvedSource` data type. `ResolvedSource` is _almost_ exactly what we want, and _almost_ deserializable, so this PR brings it across the finish line by moving a few things out of that type, and adding a `FromJSON (RawFunctionInfo b)` context to the `Backend` type class.

[PLAT-270]: https://hasurahq.atlassian.net/browse/PLAT-270?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[PLAT-270]: https://hasurahq.atlassian.net/browse/PLAT-270?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[PLAT-276]: https://hasurahq.atlassian.net/browse/PLAT-276?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[PLAT-276]: https://hasurahq.atlassian.net/browse/PLAT-276?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7053
GitOrigin-RevId: 5001b4ea086195cb5e65886747eac2a0a657b64c
2023-01-20 14:52:36 +00:00

169 lines
5.7 KiB
Haskell

module Hasura.Backends.MySQL.Connection
( runJSONPathQuery,
resolveSourceConfig,
resolveDatabaseMetadata,
postDropSourceHook,
fetchAllRows,
runQueryYieldingRows,
withMySQLPool,
parseTextRows,
)
where
import Data.Aeson hiding (Result)
import Data.Aeson qualified as J
import Data.Aeson.Key qualified as K
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Text (encodeToTextBuilder)
import Data.ByteString (ByteString)
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Pool
import Data.Scientific (fromFloatDigits)
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (toLazyText)
import Data.Vector (Vector)
import Data.Vector qualified as V
import Database.MySQL.Base
import Database.MySQL.Base.Types (Field (..))
import Database.MySQL.Simple.Result qualified as MySQL
import Hasura.Backends.MySQL.DataLoader.Plan qualified as DataLoaderPlan
import Hasura.Backends.MySQL.Meta (getMetadata)
import Hasura.Backends.MySQL.ToQuery (Query (..))
import Hasura.Backends.MySQL.Types
import Hasura.Base.Error
import Hasura.Logging (Hasura, Logger)
import Hasura.Prelude
import Hasura.RQL.Types.Backend (BackendConfig)
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Source
import Hasura.RQL.Types.Table (TableEventTriggers)
import Hasura.SQL.Backend
resolveSourceConfig :: (MonadIO m) => Logger Hasura -> SourceName -> ConnSourceConfig -> BackendSourceKind 'MySQL -> BackendConfig 'MySQL -> environment -> manager -> m (Either QErr SourceConfig)
resolveSourceConfig _logger _name csc@ConnSourceConfig {_cscPoolSettings = ConnPoolSettings {..}, ..} _backendKind _backendConfig _env _manager = do
let connectInfo =
defaultConnectInfo
{ connectHost = T.unpack _cscHost,
connectPort = _cscPort,
connectUser = T.unpack _cscUser,
connectPassword = T.unpack _cscPassword,
connectDatabase = T.unpack _cscDatabase
}
runExceptT $
SourceConfig csc
<$> liftIO
( createPool
(connect connectInfo)
close
1
(fromIntegral _cscIdleTimeout)
(fromIntegral _cscMaxConnections)
)
resolveDatabaseMetadata :: (MonadIO m) => SourceConfig -> m (Either QErr (DBObjectsIntrospection 'MySQL))
resolveDatabaseMetadata SourceConfig {..} = runExceptT do
metadata <- liftIO $ withResource scConnectionPool (getMetadata scConfig)
pure $ DBObjectsIntrospection metadata mempty mempty
postDropSourceHook ::
(MonadIO m) =>
SourceConfig ->
TableEventTriggers 'MySQL ->
m ()
postDropSourceHook _ _ =
-- As of now, we do not add any Hasura related stuff to source DB hence
-- no need to clean things up.
pure ()
parseFieldResult :: Field -> Maybe ByteString -> Value
parseFieldResult f@Field {..} mBs =
case fieldType of
Long ->
let fvalue :: Double = MySQL.convert f mBs
in Number $ fromFloatDigits fvalue
VarString ->
let fvalue :: Text = MySQL.convert f mBs
in J.String fvalue
Blob ->
let fvalue :: Text = MySQL.convert f mBs
in J.String fvalue
DateTime -> maybe J.Null (J.String . decodeUtf8) mBs
_ -> error $ "parseResult: not implemented yet " <> show f <> " " <> show mBs
-- TODO: handle remaining cases
fieldsToAeson :: [Field] -> [[Maybe ByteString]] -> [Value]
fieldsToAeson column rows =
[ Object $
KM.fromList $
[ (K.fromText (decodeUtf8 (fieldName c))) .= (parseFieldResult c r)
| (c, r) <- (zip column row :: [(Field, Maybe ByteString)])
]
| row <- (rows :: [[Maybe ByteString]])
]
runJSONPathQuery ::
(MonadError QErr m, MonadIO m) =>
(Pool Connection) ->
Query ->
m Text
runJSONPathQuery pool (Query querySql) = do
result <- liftIO $
withResource pool $ \conn -> do
query conn querySql
result <- storeResult conn
fields <- fetchFields result
rows <- fetchAllRows result
pure $ fieldsToAeson fields rows
pure $ toStrict $ toLazyText $ encodeToTextBuilder $ toJSON result
-- | Used by the dataloader to produce rows of records. Those rows of
-- records are then manipulated by the dataloader to do Haskell-side
-- joins. Is a Vector of HashMaps the most efficient choice? A
-- pandas-style data frame could also be more efficient,
-- dependingly. However, this is a legible approach; efficiency
-- improvements can be added later.
parseAndCollectRows ::
[Field] ->
[[Maybe ByteString]] ->
Vector (InsOrdHashMap DataLoaderPlan.FieldName J.Value)
parseAndCollectRows columns rows =
V.fromList
[ OMap.fromList
[ (DataLoaderPlan.FieldName . decodeUtf8 . fieldName $ column, parseFieldResult column value)
| (column, value) <- zip columns row :: [(Field, Maybe ByteString)]
]
| row <- rows :: [[Maybe ByteString]]
]
-- | Run a query immediately and parse up the results into a vector.
runQueryYieldingRows ::
(MonadIO m) =>
Pool Connection ->
Query ->
m (Vector (InsOrdHashMap DataLoaderPlan.FieldName J.Value))
runQueryYieldingRows pool (Query querySql) = do
liftIO $
withResource pool $ \conn -> do
query conn querySql
result <- storeResult conn
fields <- fetchFields result
rows <- fetchAllRows result
pure (parseAndCollectRows fields rows)
fetchAllRows :: Result -> IO [[Maybe ByteString]]
fetchAllRows r = reverse <$> go [] r
where
go acc res =
fetchRow res >>= \case
[] -> pure acc
r' -> go (r' : acc) res
parseTextRows :: [Field] -> [[Maybe ByteString]] -> [[Text]]
parseTextRows columns rows = zipWith (\column row -> map (MySQL.convert column) row) columns rows
withMySQLPool :: (MonadIO m) => Pool Connection -> (Connection -> IO a) -> m a
withMySQLPool pool = liftIO . withResource pool