mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 18:42:30 +03:00
1a052dd44b
This turns on the `partial-fields` warning, which yells at you if you try and create fields on sum types that end up being partial functions. These are dangerous; we had a bug because we introduced a new case to a data type, making the field accessors partial, and leading to a crash in certain cases. This means that we have introduced a few wrappers in various places where the field names are useful, but we want to avoid partial matches. Unfortunately this can be turned off by prefixing the field name with an underscore. Ideally we would try and avoid exporting any field names with underscores, but lenses make this hard. I have removed some underscores for the areas in which we've seen this break in the past. We will have to be vigilant. [NDAT-794]: https://hasurahq.atlassian.net/browse/NDAT-794?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9991 GitOrigin-RevId: fd69b1ef999682969f3507f0e97513f983da4da6
387 lines
13 KiB
Haskell
387 lines
13 KiB
Haskell
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
-- | MSSQL Connection
|
|
--
|
|
-- This module handles the connection against an MS SQL Server.
|
|
-- It defines the connection string, connection pool, default settings,
|
|
-- and conversion functions between MSSQL and graphql-engine.
|
|
module Hasura.Backends.MSSQL.Connection
|
|
( MSSQLConnConfiguration (MSSQLConnConfiguration),
|
|
MSSQLSourceConfig (MSSQLSourceConfig, _mscExecCtx, _mscReadReplicas),
|
|
MSSQLConnectionInfo (..),
|
|
MSSQLPoolSettings (..),
|
|
MSSQLPoolConnectionSettings (..),
|
|
MSSQLExecCtx (..),
|
|
MonadMSSQLTx (..),
|
|
defaultMSSQLMaxConnections,
|
|
createMSSQLPool,
|
|
resizeMSSQLPool,
|
|
getEnv,
|
|
odbcValueToJValue,
|
|
mkMSSQLExecCtx,
|
|
mkMSSQLAnyQueryTx,
|
|
runMSSQLSourceReadTx,
|
|
runMSSQLSourceWriteTx,
|
|
)
|
|
where
|
|
|
|
import Autodocodec (HasCodec (codec), dimapCodec, disjointEitherCodec, optionalFieldOrNull', optionalFieldWithDefault', requiredField')
|
|
import Autodocodec qualified as AC
|
|
import Autodocodec.Extended (fromEnvCodec)
|
|
import Control.Lens (united)
|
|
import Control.Monad.Morph (hoist)
|
|
import Control.Monad.Trans.Control
|
|
import Data.Aeson
|
|
import Data.Aeson qualified as J
|
|
import Data.Environment qualified as Env
|
|
import Data.Has
|
|
import Data.Text (pack, unpack)
|
|
import Data.Text qualified as T
|
|
import Data.Time (localTimeToUTC)
|
|
import Database.MSSQL.Pool qualified as MSPool
|
|
import Database.MSSQL.Transaction qualified as MSTx
|
|
import Database.ODBC.SQLServer qualified as ODBC
|
|
import Hasura.Backends.MSSQL.SQL.Error
|
|
import Hasura.Base.Error
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.Types.ResizePool
|
|
|
|
class (MonadError QErr m) => MonadMSSQLTx m where
|
|
liftMSSQLTx :: MSTx.TxE QErr a -> m a
|
|
|
|
instance (MonadMSSQLTx m) => MonadMSSQLTx (ReaderT s m) where
|
|
liftMSSQLTx = lift . liftMSSQLTx
|
|
|
|
instance (MonadMSSQLTx m) => MonadMSSQLTx (StateT s m) where
|
|
liftMSSQLTx = lift . liftMSSQLTx
|
|
|
|
instance (Monoid w, MonadMSSQLTx m) => MonadMSSQLTx (WriterT w m) where
|
|
liftMSSQLTx = lift . liftMSSQLTx
|
|
|
|
instance (MonadIO m) => MonadMSSQLTx (MSTx.TxET QErr m) where
|
|
liftMSSQLTx = hoist liftIO
|
|
|
|
-- | ODBC connection string for MSSQL server
|
|
newtype MSSQLConnectionString = MSSQLConnectionString {unMSSQLConnectionString :: Text}
|
|
deriving (Show, Eq, ToJSON, FromJSON, Hashable, NFData)
|
|
|
|
-- * Orphan instances
|
|
|
|
instance Hashable MSPool.ConnectionString
|
|
|
|
instance NFData MSPool.ConnectionString
|
|
|
|
data InputConnectionString
|
|
= RawString MSPool.ConnectionString
|
|
| FromEnvironment Text
|
|
deriving stock (Show, Eq, Generic)
|
|
|
|
instance Hashable InputConnectionString
|
|
|
|
instance NFData InputConnectionString
|
|
|
|
instance HasCodec InputConnectionString where
|
|
codec =
|
|
dimapCodec
|
|
(either RawString FromEnvironment)
|
|
(\case RawString m -> Left m; FromEnvironment wEnv -> Right wEnv)
|
|
$ disjointEitherCodec codec fromEnvCodec
|
|
|
|
instance ToJSON InputConnectionString where
|
|
toJSON =
|
|
\case
|
|
(RawString m) -> toJSON m
|
|
(FromEnvironment wEnv) -> object ["from_env" .= wEnv]
|
|
|
|
instance FromJSON InputConnectionString where
|
|
parseJSON =
|
|
\case
|
|
(Object o) -> FromEnvironment <$> o .: "from_env"
|
|
s@(String _) -> RawString <$> parseJSON s
|
|
_ -> fail "one of string or object must be provided"
|
|
|
|
data MSSQLPoolConnectionSettings = MSSQLPoolConnectionSettings
|
|
{ mpsMaxConnections :: Maybe Int,
|
|
mpsTotalMaxConnections :: Maybe Int,
|
|
mpsIdleTimeout :: Int
|
|
}
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable MSSQLPoolConnectionSettings
|
|
|
|
instance NFData MSSQLPoolConnectionSettings
|
|
|
|
data MSSQLPoolSettings
|
|
= MSSQLPoolSettingsPool MSSQLPoolConnectionSettings
|
|
| MSSQLPoolSettingsNoPool
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable MSSQLPoolSettings
|
|
|
|
instance NFData MSSQLPoolSettings
|
|
|
|
deriving via AC.Autodocodec MSSQLPoolSettings instance ToJSON MSSQLPoolSettings
|
|
|
|
deriving via AC.Autodocodec MSSQLPoolSettings instance FromJSON MSSQLPoolSettings
|
|
|
|
instance HasCodec MSSQLPoolSettings where
|
|
codec =
|
|
AC.matchChoiceCodec codecNoPool codecWithPool toInput
|
|
where
|
|
toInput :: MSSQLPoolSettings -> Either MSSQLPoolSettings MSSQLPoolSettings
|
|
toInput = \case
|
|
p@MSSQLPoolSettingsNoPool {} -> Left p
|
|
p@MSSQLPoolSettingsPool {} -> Right p
|
|
|
|
codecNoPool :: AC.JSONCodec MSSQLPoolSettings
|
|
codecNoPool =
|
|
AC.bimapCodec
|
|
( \case
|
|
False -> Right MSSQLPoolSettingsNoPool
|
|
True -> Left "impossible, guarded by 'EqCodec False"
|
|
)
|
|
( \case
|
|
MSSQLPoolSettingsNoPool -> False
|
|
_ -> True
|
|
)
|
|
$ AC.EqCodec False
|
|
$ AC.object "MSSQLPoolSettingsNoPool"
|
|
$ AC.requiredField "enable" "Whether the connection pool is entirely disabled"
|
|
|
|
codecWithPool :: AC.JSONCodec MSSQLPoolSettings
|
|
codecWithPool =
|
|
AC.dimapCodec MSSQLPoolSettingsPool (\case MSSQLPoolSettingsPool p -> p; MSSQLPoolSettingsNoPool -> error "unexpected MSSQLPoolSettingsNoPool")
|
|
$ AC.object
|
|
"MSSQLPoolSettings"
|
|
( MSSQLPoolConnectionSettings
|
|
<$> optionalFieldWithDefault' "max_connections" (Just defaultMSSQLMaxConnections)
|
|
AC..= mpsMaxConnections
|
|
<*> optionalFieldOrNull' "total_max_connections"
|
|
AC..= mpsTotalMaxConnections
|
|
<*> optionalFieldWithDefault' "idle_timeout" defaultMSSQLIdleTimeout
|
|
AC..= mpsIdleTimeout
|
|
)
|
|
|
|
defaultMSSQLMaxConnections :: Int
|
|
defaultMSSQLMaxConnections = 50
|
|
|
|
defaultMSSQLIdleTimeout :: Int
|
|
defaultMSSQLIdleTimeout = 5
|
|
|
|
defaultMSSQLPoolSettings :: MSSQLPoolSettings
|
|
defaultMSSQLPoolSettings =
|
|
MSSQLPoolSettingsPool
|
|
$ MSSQLPoolConnectionSettings
|
|
{ mpsMaxConnections = Nothing,
|
|
mpsTotalMaxConnections = Nothing,
|
|
mpsIdleTimeout = defaultMSSQLIdleTimeout
|
|
}
|
|
|
|
data MSSQLConnectionInfo = MSSQLConnectionInfo
|
|
{ mciConnectionString :: InputConnectionString,
|
|
mciPoolSettings :: MSSQLPoolSettings,
|
|
mciIsolationLevel :: MSTx.TxIsolation
|
|
}
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable MSSQLConnectionInfo
|
|
|
|
instance NFData MSSQLConnectionInfo
|
|
|
|
instance HasCodec MSSQLConnectionInfo where
|
|
codec =
|
|
AC.object "MSSQLConnectionInfo"
|
|
$ MSSQLConnectionInfo
|
|
<$> requiredField' "connection_string"
|
|
AC..= mciConnectionString
|
|
<*> requiredField' "pool_settings"
|
|
AC..= mciPoolSettings
|
|
<*> AC.optionalFieldWithDefault "isolation_level" MSTx.ReadCommitted isolationLevelDoc
|
|
AC..= mciIsolationLevel
|
|
where
|
|
isolationLevelDoc =
|
|
T.unwords
|
|
[ "The transaction isolation level in which the queries made to the",
|
|
"source will be run with (default: read-committed)."
|
|
]
|
|
|
|
instance ToJSON MSSQLConnectionInfo where
|
|
toJSON = genericToJSON hasuraJSON
|
|
toEncoding = genericToEncoding hasuraJSON
|
|
|
|
instance FromJSON MSSQLConnectionInfo where
|
|
parseJSON = withObject "Object" $ \o ->
|
|
MSSQLConnectionInfo
|
|
<$> ((o .: "database_url") <|> (o .: "connection_string"))
|
|
<*> o
|
|
.:? "pool_settings"
|
|
.!= defaultMSSQLPoolSettings
|
|
<*> o
|
|
.:? "isolation_level"
|
|
.!= MSTx.ReadCommitted
|
|
|
|
data MSSQLConnConfiguration = MSSQLConnConfiguration
|
|
{ mccConnectionInfo :: MSSQLConnectionInfo,
|
|
mccReadReplicas :: Maybe (NonEmpty MSSQLConnectionInfo)
|
|
}
|
|
deriving (Show, Eq, Generic)
|
|
|
|
instance Hashable MSSQLConnConfiguration
|
|
|
|
instance NFData MSSQLConnConfiguration
|
|
|
|
instance HasCodec MSSQLConnConfiguration where
|
|
codec =
|
|
AC.object "MSSQLConnConfiguration"
|
|
$ MSSQLConnConfiguration
|
|
<$> requiredField' "connection_info"
|
|
AC..= mccConnectionInfo
|
|
<*> optionalFieldOrNull' "read_replicas"
|
|
AC..= mccReadReplicas
|
|
|
|
instance FromJSON MSSQLConnConfiguration where
|
|
parseJSON = genericParseJSON hasuraJSON {omitNothingFields = True}
|
|
|
|
instance ToJSON MSSQLConnConfiguration where
|
|
toJSON = genericToJSON hasuraJSON {omitNothingFields = True}
|
|
toEncoding = genericToEncoding hasuraJSON {omitNothingFields = True}
|
|
|
|
createMSSQLPool ::
|
|
(MonadIO m) =>
|
|
(QErrM m) =>
|
|
InputConnectionString ->
|
|
MSPool.ConnectionOptions ->
|
|
Env.Environment ->
|
|
m (MSPool.ConnectionString, MSPool.MSSQLPool)
|
|
createMSSQLPool iConnString connOptions env = do
|
|
connString <- resolveInputConnectionString env iConnString
|
|
pool <- liftIO $ MSPool.initMSSQLPool connString connOptions
|
|
pure (connString, pool)
|
|
|
|
resolveInputConnectionString ::
|
|
(QErrM m) =>
|
|
Env.Environment ->
|
|
InputConnectionString ->
|
|
m MSPool.ConnectionString
|
|
resolveInputConnectionString env =
|
|
\case
|
|
(RawString cs) -> pure cs
|
|
(FromEnvironment envVar) -> MSPool.ConnectionString <$> getEnv env envVar
|
|
|
|
getEnv :: (QErrM m) => Env.Environment -> Text -> m Text
|
|
getEnv env k = do
|
|
let mEnv = Env.lookupEnv env (unpack k)
|
|
case mEnv of
|
|
Nothing -> throw400 NotFound $ "environment variable '" <> k <> "' not set"
|
|
Just envVal -> return (pack envVal)
|
|
|
|
type MSSQLRunTx =
|
|
forall m a. (MonadIO m, MonadBaseControl IO m) => MSTx.TxET QErr m a -> ExceptT QErr m a
|
|
|
|
-- | Execution Context required to execute MSSQL transactions
|
|
data MSSQLExecCtx = MSSQLExecCtx
|
|
{ -- | A function that runs read-only queries
|
|
mssqlRunReadOnly :: MSSQLRunTx,
|
|
-- | A function that runs read-write queries; run in a transaction
|
|
mssqlRunReadWrite :: MSSQLRunTx,
|
|
-- | A function that runs a transaction in the SERIALIZABLE transaction isolation
|
|
-- level. This is mainly intended to run source catalog migrations.
|
|
mssqlRunSerializableTx :: MSSQLRunTx,
|
|
-- | Destroys connection pools
|
|
mssqlDestroyConn :: IO (),
|
|
-- | Resize pools based on number of server instances
|
|
mssqlResizePools :: ServerReplicas -> IO SourceResizePoolSummary
|
|
}
|
|
|
|
-- | Creates a MSSQL execution context for a single primary pool
|
|
mkMSSQLExecCtx :: MSTx.TxIsolation -> MSPool.MSSQLPool -> ResizePoolStrategy -> MSSQLExecCtx
|
|
mkMSSQLExecCtx isolationLevel pool resizeStrategy =
|
|
MSSQLExecCtx
|
|
{ mssqlRunReadOnly = \tx -> MSTx.runTxE defaultMSSQLTxErrorHandler isolationLevel tx pool,
|
|
mssqlRunReadWrite = \tx -> MSTx.runTxE defaultMSSQLTxErrorHandler isolationLevel tx pool,
|
|
mssqlRunSerializableTx = \tx -> MSTx.runTxE defaultMSSQLTxErrorHandler MSTx.Serializable tx pool,
|
|
mssqlDestroyConn = MSPool.drainMSSQLPool pool,
|
|
mssqlResizePools =
|
|
case resizeStrategy of
|
|
NeverResizePool -> const $ pure noPoolsResizedSummary
|
|
ResizePool maxConnections -> resizeMSSQLPool' maxConnections
|
|
}
|
|
where
|
|
resizeMSSQLPool' maxConnections serverReplicas = do
|
|
-- Resize the primary pool
|
|
resizeMSSQLPool pool maxConnections serverReplicas
|
|
-- Return the summary. Only the primary pool is resized
|
|
pure
|
|
$ SourceResizePoolSummary
|
|
{ _srpsPrimaryResized = True,
|
|
_srpsReadReplicasResized = False,
|
|
_srpsConnectionSet = []
|
|
}
|
|
|
|
-- | Resize MSSQL pool by setting the number of connections equal to
|
|
-- allowed maximum connections across all server instances divided by
|
|
-- number of instances
|
|
resizeMSSQLPool :: MSPool.MSSQLPool -> Int -> ServerReplicas -> IO ()
|
|
resizeMSSQLPool mssqlPool maxConnections serverReplicas =
|
|
MSPool.resizePool mssqlPool (maxConnections `div` getServerReplicasInt serverReplicas)
|
|
|
|
-- | Run any query discarding its results
|
|
mkMSSQLAnyQueryTx :: ODBC.Query -> MSTx.TxET QErr IO ()
|
|
mkMSSQLAnyQueryTx q = do
|
|
_discard :: [[ODBC.Value]] <- MSTx.multiRowQueryE defaultMSSQLTxErrorHandler q
|
|
pure ()
|
|
|
|
data MSSQLSourceConfig = MSSQLSourceConfig
|
|
{ _mscConnectionString :: MSPool.ConnectionString,
|
|
_mscExecCtx :: MSSQLExecCtx,
|
|
-- | Number of read replicas used by the execution context
|
|
_mscReadReplicas :: Int
|
|
}
|
|
deriving (Generic)
|
|
|
|
instance Show MSSQLSourceConfig where
|
|
show = show . _mscConnectionString
|
|
|
|
instance Eq MSSQLSourceConfig where
|
|
MSSQLSourceConfig connStr1 _ _ == MSSQLSourceConfig connStr2 _ _ =
|
|
connStr1 == connStr2
|
|
|
|
instance ToJSON MSSQLSourceConfig where
|
|
toJSON = toJSON . _mscConnectionString
|
|
|
|
-- Note: () ~ ScalarTypeParsingContext 'MSSQL but we can't use the type family instance in the Has instance.
|
|
instance Has () MSSQLSourceConfig where
|
|
hasLens = united
|
|
|
|
odbcValueToJValue :: ODBC.Value -> J.Value
|
|
odbcValueToJValue = \case
|
|
ODBC.TextValue t -> J.String t
|
|
ODBC.ByteStringValue b -> J.String $ bsToTxt b
|
|
ODBC.BinaryValue b -> J.String $ bsToTxt $ ODBC.unBinary b
|
|
ODBC.BoolValue b -> J.Bool b
|
|
ODBC.DoubleValue d -> J.toJSON d
|
|
ODBC.FloatValue f -> J.toJSON f
|
|
ODBC.IntValue i -> J.toJSON i
|
|
ODBC.ByteValue b -> J.toJSON b
|
|
ODBC.DayValue d -> J.toJSON d
|
|
ODBC.TimeOfDayValue td -> J.toJSON td
|
|
ODBC.LocalTimeValue l -> J.toJSON l
|
|
ODBC.NullValue -> J.Null
|
|
ODBC.ZonedTimeValue lt tz -> J.toJSON (localTimeToUTC tz lt)
|
|
|
|
runMSSQLSourceReadTx ::
|
|
(MonadIO m, MonadBaseControl IO m) =>
|
|
MSSQLSourceConfig ->
|
|
MSTx.TxET QErr m a ->
|
|
m (Either QErr a)
|
|
runMSSQLSourceReadTx msc =
|
|
runExceptT . mssqlRunReadOnly (_mscExecCtx msc)
|
|
|
|
runMSSQLSourceWriteTx ::
|
|
(MonadIO m, MonadBaseControl IO m) =>
|
|
MSSQLSourceConfig ->
|
|
MSTx.TxET QErr m a ->
|
|
m (Either QErr a)
|
|
runMSSQLSourceWriteTx msc =
|
|
runExceptT . mssqlRunReadWrite (_mscExecCtx msc)
|