mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
793aede022
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3487 GitOrigin-RevId: d3f696072e8290b45c2f81509ce31cb5c13a4aef
227 lines
9.3 KiB
Haskell
227 lines
9.3 KiB
Haskell
-- | Functions and datatypes for interpreting MSSQL database errors.
|
|
module Hasura.Backends.MSSQL.SQL.Error
|
|
( ErrorClass (..),
|
|
defaultMSSQLTxErrorHandler,
|
|
mutationMSSQLTxErrorHandler,
|
|
mkMSSQLTxErrorHandler,
|
|
|
|
-- * Exposed for testing
|
|
ErrorSubclass (..),
|
|
DataExceptionSubclass (..),
|
|
SyntaxErrorOrAccessViolationSubclass (..),
|
|
parseErrorClass,
|
|
)
|
|
where
|
|
|
|
import Data.Aeson
|
|
import Data.Text qualified as T
|
|
import Database.MSSQL.Transaction
|
|
import Database.ODBC.SQLServer qualified as ODBC
|
|
import Hasura.Base.Error qualified as Error
|
|
import Hasura.Prelude
|
|
|
|
-- | The top-level error class. Errors in MSSQL are divided into different /classes/, which
|
|
-- are further subdivided into individual error subclasses. It is useful to determine the class of
|
|
-- database exception and handle it appropriately.
|
|
data ErrorClass
|
|
= DataException (ErrorSubclass DataExceptionSubclass)
|
|
| IntegrityConstraintViolation
|
|
| SyntaxErrorOrAccessViolation (ErrorSubclass SyntaxErrorOrAccessViolationSubclass)
|
|
deriving (Eq)
|
|
|
|
instance Show ErrorClass where
|
|
show = \case
|
|
DataException subclass -> withSubclass "Data exception." subclass
|
|
IntegrityConstraintViolation -> "Integrity constraint violation."
|
|
SyntaxErrorOrAccessViolation subclass -> withSubclass "Syntax error or access violation." subclass
|
|
where
|
|
withSubclass :: (Show a) => String -> ErrorSubclass a -> String
|
|
withSubclass classError = \case
|
|
NoSubclass -> classError
|
|
Subclass subclass -> classError <> " " <> show subclass
|
|
|
|
data ErrorSubclass a
|
|
= -- | represents non-specific @000@ subclass code
|
|
NoSubclass
|
|
| -- | represents known, more specific sub class
|
|
Subclass a
|
|
deriving (Eq)
|
|
|
|
data DataExceptionSubclass
|
|
= StringDataRightTruncated
|
|
| NumericValueOutOfRange
|
|
| InvalidDatetimeFormat
|
|
| DatetimeFieldOverflow
|
|
| IntervalFieldOverflow
|
|
| InvalidEscapeCharacter
|
|
| InvalidEscapeSequence
|
|
deriving (Eq)
|
|
|
|
instance Show DataExceptionSubclass where
|
|
show = \case
|
|
StringDataRightTruncated -> "String data, right-truncated."
|
|
NumericValueOutOfRange -> "Numeric value out of range."
|
|
InvalidDatetimeFormat -> "Invalid datetime format."
|
|
DatetimeFieldOverflow -> "Datetime field overflow."
|
|
IntervalFieldOverflow -> "Interval field overflow."
|
|
InvalidEscapeCharacter -> "Invalid escape character."
|
|
InvalidEscapeSequence -> "Invalid escape sequence."
|
|
|
|
data SyntaxErrorOrAccessViolationSubclass
|
|
= TableOrViewAlreadyExists
|
|
| TableOrViewNotFound
|
|
| IndexAlreadyExists
|
|
| IndexNotFound
|
|
| ColumnAlreadyExists
|
|
| ColumnNotFound
|
|
deriving (Eq)
|
|
|
|
instance Show SyntaxErrorOrAccessViolationSubclass where
|
|
show = \case
|
|
TableOrViewAlreadyExists -> "Table or view already exists."
|
|
TableOrViewNotFound -> "Table or view not found."
|
|
IndexAlreadyExists -> "Index already exists."
|
|
IndexNotFound -> "Index not found."
|
|
ColumnAlreadyExists -> "Column already exists."
|
|
ColumnNotFound -> "Column not found."
|
|
|
|
-- | Assign each error class' subclasses an appropriate API error code
|
|
errorClassCode :: ErrorClass -> Error.Code
|
|
errorClassCode = \case
|
|
DataException _ -> Error.DataException
|
|
IntegrityConstraintViolation -> Error.ConstraintViolation
|
|
SyntaxErrorOrAccessViolation NoSubclass -> Error.BadRequest
|
|
SyntaxErrorOrAccessViolation (Subclass subclass) -> case subclass of
|
|
TableOrViewAlreadyExists -> Error.AlreadyExists
|
|
TableOrViewNotFound -> Error.NotFound
|
|
IndexAlreadyExists -> Error.AlreadyExists
|
|
IndexNotFound -> Error.NotFound
|
|
ColumnAlreadyExists -> Error.AlreadyExists
|
|
ColumnNotFound -> Error.NotFound
|
|
|
|
-- | Parsing error class and subclass information from a SQLSTATE code.
|
|
-- SQLSTATE provides detailed information about the cause of a warning or error.
|
|
-- A SQLSTATE consists of 5 chars. They are divided into two parts: the first and
|
|
-- second chars contain a class and the following three a subclass.
|
|
parseErrorClass :: String -> Maybe ErrorClass
|
|
parseErrorClass sqlStateCode =
|
|
choice
|
|
[ withClass
|
|
"22"
|
|
DataException
|
|
[ withSubclass "001" StringDataRightTruncated,
|
|
withSubclass "003" NumericValueOutOfRange,
|
|
withSubclass "007" InvalidDatetimeFormat,
|
|
withSubclass "008" DatetimeFieldOverflow,
|
|
withSubclass "015" IntervalFieldOverflow,
|
|
withSubclass "019" InvalidEscapeCharacter,
|
|
withSubclass "025" InvalidEscapeSequence
|
|
],
|
|
withClass "23" (const IntegrityConstraintViolation) [],
|
|
withClass
|
|
"42"
|
|
SyntaxErrorOrAccessViolation
|
|
[ withSubclass "S01" TableOrViewAlreadyExists,
|
|
withSubclass "S02" TableOrViewNotFound,
|
|
withSubclass "S11" IndexAlreadyExists,
|
|
withSubclass "S12" IndexNotFound,
|
|
withSubclass "S21" ColumnAlreadyExists,
|
|
withSubclass "S22" ColumnNotFound
|
|
]
|
|
]
|
|
where
|
|
(classText, subclassText) = T.splitAt 2 $ T.pack sqlStateCode
|
|
|
|
withClass ::
|
|
Text ->
|
|
(ErrorSubclass a -> ErrorClass) ->
|
|
[Maybe (ErrorSubclass a)] ->
|
|
Maybe ErrorClass
|
|
withClass expectedClassText mkClass subclasses = do
|
|
guard (classText == expectedClassText)
|
|
mkClass
|
|
<$> if subclassText == "000"
|
|
then Just NoSubclass
|
|
else choice subclasses
|
|
|
|
withSubclass :: Text -> a -> Maybe (ErrorSubclass a)
|
|
withSubclass expectedSubclassText subclassValue =
|
|
guard (subclassText == expectedSubclassText) $> Subclass subclassValue
|
|
|
|
-- | A default transaction error handler where all errors are unexpected.
|
|
defaultMSSQLTxErrorHandler :: MSSQLTxError -> Error.QErr
|
|
defaultMSSQLTxErrorHandler = mkMSSQLTxErrorHandler (const False)
|
|
|
|
-- | A transaction error handler to be used in constructing mutation transactions,
|
|
-- i.e INSERT, UPDATE and DELETE. We expect data exception and integrity constraint violation.
|
|
mutationMSSQLTxErrorHandler :: MSSQLTxError -> Error.QErr
|
|
mutationMSSQLTxErrorHandler = mkMSSQLTxErrorHandler $ \case
|
|
DataException _ -> True
|
|
IntegrityConstraintViolation -> True
|
|
SyntaxErrorOrAccessViolation _ -> False
|
|
|
|
-- | Constructs a transaction error handler given a predicate that determines which error
|
|
-- classes (and subclasses) are expected and should be reported to the user. All other errors
|
|
-- are considered internal errors.
|
|
-- Example:-
|
|
-- Consider a insert mutation where we insert some data into columns of a table.
|
|
-- Except for the basic data type, such as Boolean, String, Float, Int etc.
|
|
-- we cannot invalidate data any further, such as validating timestamp string format.
|
|
-- In this case, a @'DataException' is expected from the database and it is handled and
|
|
-- thrown with proper error message.
|
|
mkMSSQLTxErrorHandler :: (ErrorClass -> Bool) -> MSSQLTxError -> Error.QErr
|
|
mkMSSQLTxErrorHandler isExpectedError = \case
|
|
MSSQLQueryError query exception ->
|
|
let unexpectedQueryError =
|
|
(Error.internalError "database query error")
|
|
{ Error.qeInternal =
|
|
Just $
|
|
Error.ExtraInternal $
|
|
object
|
|
[ "query" .= ODBC.renderQuery query,
|
|
"exception" .= odbcExceptionToJSONValue exception
|
|
]
|
|
}
|
|
in fromMaybe unexpectedQueryError $
|
|
asExpectedError exception
|
|
<&> \err -> err {Error.qeInternal = Just $ Error.ExtraInternal $ object ["query" .= ODBC.renderQuery query]}
|
|
MSSQLConnError exception ->
|
|
let unexpectedConnError =
|
|
(Error.internalError "mssql connection error")
|
|
{ Error.qeInternal =
|
|
Just $
|
|
Error.ExtraInternal $
|
|
object ["exception" .= odbcExceptionToJSONValue exception]
|
|
}
|
|
in fromMaybe unexpectedConnError $ asExpectedError exception
|
|
MSSQLInternal err ->
|
|
(Error.internalError "mssql internal error")
|
|
{ Error.qeInternal = Just $ Error.ExtraInternal $ object ["error" .= err]
|
|
}
|
|
where
|
|
asExpectedError :: ODBC.ODBCException -> Maybe Error.QErr
|
|
asExpectedError = \case
|
|
ODBC.UnsuccessfulReturnCode _ _ databaseMessage sqlState -> do
|
|
errorClass <- parseErrorClass =<< sqlState
|
|
guard $ isExpectedError errorClass
|
|
let errorMessage = show errorClass <> " " <> databaseMessage
|
|
pure $ Error.err400 (errorClassCode errorClass) $ T.pack errorMessage
|
|
_ -> Nothing
|
|
|
|
-- | The @'ODBC.ODBCException' type has no @'ToJSON' instance.
|
|
-- This is an attempt to convert the odbc exception to a JSON @'Value'
|
|
odbcExceptionToJSONValue :: ODBC.ODBCException -> Value
|
|
odbcExceptionToJSONValue exception =
|
|
let (ty, message) = decodeODBCException exception
|
|
in object ["type" .= ty, "message" .= message]
|
|
where
|
|
decodeODBCException :: ODBC.ODBCException -> (String, String)
|
|
decodeODBCException = \case
|
|
ODBC.UnsuccessfulReturnCode _ _ errMessage _ -> ("unsuccessful_return_code", errMessage)
|
|
ODBC.AllocationReturnedNull _ -> ("allocation_returned_null", "allocating an ODBC resource failed")
|
|
ODBC.UnknownDataType _ _ -> ("unknown_data_type", "An unsupported/unknown data type was returned from the ODBC driver")
|
|
ODBC.DatabaseIsClosed _ -> ("database_is_closed", "Using database connection that is no longer available")
|
|
ODBC.DatabaseAlreadyClosed -> ("database_already_closed", "The database is already closed")
|
|
ODBC.NoTotalInformation _ -> ("no_total_information", "No total length information for column")
|
|
ODBC.DataRetrievalError errMessage -> ("data_retrieval_error", errMessage)
|