graphql-engine/server/src-lib/Database/MSSQL/Transaction.hs
Rakesh Emmadi 8e88e73a52 server/mssql: add cascade to mssql_run_sql
<!-- Thank you for ss in the Title above ^ -->

## Description
<!-- Please fill thier. -->
<!-- Describe the changes from a user's perspective -->
We don't have dependency reporting mechanism for `mssql_run_sql` API i.e when a database object (table, column etc.) is dropped through the API we should raise an exception if any dependencies (relationships, permissions etc.) with the database object exists in the metadata.

This PR addresses the above mentioned problem by
-> Integrating transaction to the API to rollback the SQL query execution if dependencies exists and exception is thrown
-> Accepting `cascade` optional field in the API payload to drop the dependencies, if any
-> Accepting `check_metadata_consistency` optional field to bypass (if value set to `false`) the dependency check

### Related Issues
<!-- Please make surt title -->
<!-- Add the issue number below (e.g. #234) -->
Close #1853

### Solution and Design
<!-- How is this iss -->
<!-- It's better if we elaborate -->
The design/solution follows the `run_sql` API implementation for Postgres backend.

### Steps to test and verify
<!-- If this is a fehis is a bug-fix, how do we verify the fix? -->
- Create author - article tables and track them
- Defined object and array relationships
- Try to drop the article table without cascade or cascade set to `false`
- The server should raise the relationship dependency exists exception

## Changelog

-  `CHANGELOG.md` is updated with user-facing content relevant to this PR.
  If no changelog is required, then add the `no-changelog-required` label.

## Affected components
<!-- Remove non-affected components from the list -->

-  Server
-  Console
-  CLI
-  Docs
-  Community Content
-  Build System
-  Tests
-  Other (list it)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2636
GitOrigin-RevId: 0ab152295394056c4ca6f02923142a1658ad25dc
2021-10-22 14:50:13 +00:00

216 lines
6.0 KiB
Haskell

module Database.MSSQL.Transaction
( runTx,
runTxE,
unitQuery,
unitQueryE,
singleRowQuery,
singleRowQueryE,
multiRowQuery,
multiRowQueryE,
rawQuery,
rawQueryE,
buildGenericTxE,
TxT,
TxET (..),
MSSQLTxError (..),
)
where
import Control.Exception (try)
import Control.Monad.Except
( ExceptT (..),
MonadError,
catchError,
throwError,
withExceptT,
)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Morph (MFunctor (hoist), MonadTrans (..))
import Control.Monad.Reader (MonadFix, MonadReader, ReaderT (..))
import Database.ODBC.SQLServer (FromRow)
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Prelude (hoistEither, liftEither, mapLeft)
import Prelude
data MSSQLTxError
= MSSQLTxError !ODBC.Query !ODBC.ODBCException
deriving (Eq, Show)
-- | A successful result from a query is a list of rows where each row contains list of column values
newtype MSSQLResult = MSSQLResult [[ODBC.Value]]
deriving (Eq, Show)
-- | The transaction command to run, parameterised over:
-- e - the exception type
-- m - some Monad
-- a - the successful result type
newtype TxET e m a = TxET {txHandler :: ReaderT ODBC.Connection (ExceptT e m) a}
deriving (Functor, Applicative, Monad, MonadError e, MonadIO, MonadReader ODBC.Connection, MonadFix)
instance MonadTrans (TxET e) where
lift = TxET . lift . lift
-- | The transaction command to run,
-- returning an MSSQLTxError or the result
type TxT m a = TxET MSSQLTxError m a
beginTx :: MonadIO m => TxT m ()
beginTx =
unitQuery "BEGIN TRANSACTION"
commitTx :: MonadIO m => TxT m ()
commitTx =
unitQuery "COMMIT TRANSACTION"
rollbackTx :: MonadIO m => TxT m ()
rollbackTx =
unitQuery "ROLLBACK TRANSACTION"
-- | Useful for building transactions which returns no data
--
-- insertId :: TxT m ()
-- insertId = unitQuery "INSERT INTO some_table VALUES (1, \"hello\")"
unitQuery :: (MonadIO m) => ODBC.Query -> TxT m ()
unitQuery = unitQueryE id
-- | Similar to @'unitQuery' but with an error modifier
unitQueryE ::
(MonadIO m) =>
-- | Error modifier
(MSSQLTxError -> e) ->
-- | Query to run
ODBC.Query ->
TxET e m ()
unitQueryE ef = rawQueryE ef emptyResult
where
emptyResult :: MSSQLResult -> Either String ()
emptyResult (MSSQLResult []) = Right ()
emptyResult (MSSQLResult _) = Left "expecting no data for ()"
-- | Useful for building query transactions which returns only one row.
--
-- returnOne :: TxT m Int
-- returnOne = singleRowQuery "SELECT 1"
singleRowQuery :: (MonadIO m, FromRow a) => ODBC.Query -> TxT m a
singleRowQuery = singleRowQueryE id
-- | Similar to @'multiRowQuery' but with an error modifier
singleRowQueryE ::
forall m a e.
(MonadIO m, FromRow a) =>
-- | Error modifier
(MSSQLTxError -> e) ->
-- | Query to run
ODBC.Query ->
TxET e m a
singleRowQueryE ef = rawQueryE ef singleRowResult
where
singleRowResult :: MSSQLResult -> Either String a
singleRowResult (MSSQLResult [row]) = ODBC.fromRow row
singleRowResult (MSSQLResult _) = Left "expecting single row"
-- | Useful for building query transactions which returns multiple rows.
--
-- selectIds :: TxT m [Int]
-- selectIds = multiRowQuery "SELECT id FROM author"
multiRowQuery :: (MonadIO m, FromRow a) => ODBC.Query -> TxT m [a]
multiRowQuery = multiRowQueryE id
-- | Similar to @'multiRowQuery' but with an error modifier
multiRowQueryE ::
forall m a e.
(MonadIO m, FromRow a) =>
-- | Error modifier
(MSSQLTxError -> e) ->
-- | Query to run
ODBC.Query ->
TxET e m [a]
multiRowQueryE ef = rawQueryE ef multiRowResult
where
multiRowResult :: MSSQLResult -> Either String [a]
multiRowResult (MSSQLResult rows) = mapM ODBC.fromRow rows
-- | Build a raw query transaction which on successful execution returns @'MSSQLResult'
rawQuery :: (MonadIO m) => ODBC.Query -> TxT m MSSQLResult
rawQuery = rawQueryE id pure
-- | Similar to @'rawQuery' but with error modifier and @'MSSQLResult' modifier
rawQueryE ::
(MonadIO m) =>
-- | Error modifier
(MSSQLTxError -> e) ->
-- | Result modifier with a failure
(MSSQLResult -> Either String a) ->
-- | Query to run
ODBC.Query ->
TxET e m a
rawQueryE ef rf q = TxET $
ReaderT $ \conn ->
hoist liftIO $
withExceptT ef $
execQuery conn q
>>= liftEither . mapLeft (MSSQLTxError q . ODBC.DataRetrievalError) . rf
-- | Build a generic transaction out of an IO action
buildGenericTxE ::
(MonadIO m) =>
-- | Exception modifier
(ODBC.ODBCException -> e) ->
-- | IO action
(ODBC.Connection -> IO a) ->
TxET e m a
buildGenericTxE ef f = TxET $
ReaderT $ \conn -> do
result <- liftIO $ try $ f conn
withExceptT ef $ hoistEither result
execQuery ::
(MonadIO m) =>
ODBC.Connection ->
ODBC.Query ->
ExceptT MSSQLTxError m MSSQLResult
execQuery conn query = do
result :: Either ODBC.ODBCException [[ODBC.Value]] <- liftIO $ try $ ODBC.query conn query
withExceptT (MSSQLTxError query) $ hoistEither $ MSSQLResult <$> result
-- | Run a command on the given connection wrapped in a transaction.
runTx ::
MonadIO m =>
TxT m a ->
ODBC.Connection ->
ExceptT MSSQLTxError m a
runTx = runTxE id
-- | Run a command on the given connection wrapped in a transaction.
runTxE ::
MonadIO m =>
(MSSQLTxError -> e) ->
TxET e m a ->
ODBC.Connection ->
ExceptT e m a
runTxE ef tx =
asTransaction ef (`execTx` tx)
{-# INLINE execTx #-}
execTx :: ODBC.Connection -> TxET e m a -> ExceptT e m a
execTx conn tx = runReaderT (txHandler tx) conn
asTransaction ::
MonadIO m =>
(MSSQLTxError -> e) ->
(ODBC.Connection -> ExceptT e m a) ->
ODBC.Connection ->
ExceptT e m a
asTransaction ef f conn = do
-- Begin the transaction. If there is an err, do not rollback
withExceptT ef $ execTx conn beginTx
-- Run the transaction and commit. If there is an err, rollback
flip catchError rollback $ do
result <- f conn
withExceptT ef $ execTx conn commitTx
pure result
where
rollback err = do
withExceptT ef $ execTx conn rollbackTx
throwError err