graphql-engine/server/src-lib/Hasura/Backends/MSSQL/DDL/EventTrigger.hs
Karthikeyan Chinnakonda a3e6b1c741 server/mssql: add support for creating event triggers (incremental PR #2)
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2634
Co-authored-by: Naveen Naidu <30195193+Naveenaidu@users.noreply.github.com>
GitOrigin-RevId: 29567fa336c22d1812dfcfa6294f54e74a741f0c
2022-03-03 09:53:49 +00:00

219 lines
9.7 KiB
Haskell

module Hasura.Backends.MSSQL.DDL.EventTrigger (createTableEventTrigger) where
import Data.FileEmbed (makeRelativeToProject)
import Data.Text qualified as T
import Data.Text.Extended (commaSeparated)
import Data.Text.Lazy qualified as LT
import Database.MSSQL.Transaction (TxE, unitQueryE)
import Database.ODBC.SQLServer (rawUnescapedText)
import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.MSSQL.SQL.Error qualified as HGE
import Hasura.Backends.MSSQL.Types (SchemaName (..), TableName (..))
import Hasura.Backends.MSSQL.Types.Internal (columnNameText, geoTypes)
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.EventTrigger
import Hasura.RQL.Types.Eventing (OpVar (..))
import Hasura.RQL.Types.Table (PrimaryKey (..))
import Hasura.SQL.Backend
import Hasura.Server.Types
import Text.Shakespeare.Text qualified as ST
createTableEventTrigger ::
(MonadIO m) =>
ServerConfigCtx ->
MSSQLSourceConfig ->
TableName ->
[ColumnInfo 'MSSQL] ->
TriggerName ->
TriggerOpsDef 'MSSQL ->
Maybe (PrimaryKey 'MSSQL (ColumnInfo 'MSSQL)) ->
m (Either QErr ())
createTableEventTrigger _serverConfigCtx (MSSQLSourceConfig _ mssqlExecCtx) table columns triggerName opsDefinition primaryKeyMaybe = do
liftIO $
runExceptT $ do
mssqlRunReadWrite mssqlExecCtx $ do
dropTriggerQ triggerName (tableSchema table)
mkAllTriggersQ triggerName table columns opsDefinition primaryKeyMaybe
newtype QualifiedTriggerName = QualifiedTriggerName {unQualifiedTriggerName :: Text}
msssqlIdenTrigger :: Ops -> SchemaName -> TriggerName -> QualifiedTriggerName
msssqlIdenTrigger op (SchemaName schemaName) triggerName =
QualifiedTriggerName $ qualifyHasuraTriggerName op $ triggerNameToTxt triggerName
where
qualifyHasuraTriggerName op' triggerName' = schemaName <> "." <> "notify_hasura_" <> triggerName' <> "_" <> tshow op'
dropTriggerQ :: TriggerName -> SchemaName -> TxE QErr ()
dropTriggerQ triggerName schemaName =
mapM_
( \op ->
unitQueryE
HGE.defaultMSSQLTxErrorHandler
(rawUnescapedText $ getDropTriggerSQL op)
)
[INSERT, UPDATE, DELETE]
where
getDropTriggerSQL :: Ops -> Text
getDropTriggerSQL op =
"DROP TRIGGER IF EXISTS " <> unQualifiedTriggerName (msssqlIdenTrigger op schemaName triggerName)
mkAllTriggersQ ::
MonadMSSQLTx m =>
TriggerName ->
TableName ->
[ColumnInfo 'MSSQL] ->
TriggerOpsDef 'MSSQL ->
Maybe (PrimaryKey 'MSSQL (ColumnInfo 'MSSQL)) ->
m ()
mkAllTriggersQ triggerName tableName allCols fullSpec primaryKey = do
onJust (tdInsert fullSpec) (mkInsertTriggerQ triggerName tableName allCols)
onJust (tdDelete fullSpec) (mkDeleteTriggerQ triggerName tableName allCols)
onJust (tdUpdate fullSpec) (mkUpdateTriggerQ triggerName tableName allCols primaryKey)
getApplicableColumns :: [ColumnInfo 'MSSQL] -> SubscribeColumns 'MSSQL -> [ColumnInfo 'MSSQL]
getApplicableColumns allColumnInfos = \case
SubCStar -> allColumnInfos
SubCArray cols -> getColInfos cols allColumnInfos
-- | Currently we do not support Event Triggers on columns of Spatial data types.
-- We do this because, currently the graphQL API for these types is broken
-- for MSSQL sources. Ref: https://github.com/hasura/graphql-engine-mono/issues/787
checkSpatialDataTypeColumns ::
MonadMSSQLTx m =>
[ColumnInfo 'MSSQL] ->
SubscribeOpSpec 'MSSQL ->
m ()
checkSpatialDataTypeColumns allCols (SubscribeOpSpec listenCols deliveryCols) = do
let listenColumns = getApplicableColumns allCols listenCols
deliveryColumns = getApplicableColumns allCols $ fromMaybe SubCStar deliveryCols
isGeoTypesInListenCols = any (\c -> isScalarColumnWhere isGeoType (ciType c)) listenColumns
isGeoTypesInDeliversCols = any (\c -> isScalarColumnWhere isGeoType (ciType c)) deliveryColumns
when (isGeoTypesInListenCols || isGeoTypesInDeliversCols) $
throw400 NotSupported "Event triggers for MS-SQL sources are not supported on tables having Geometry or Geography column types"
where
isGeoType = (`elem` geoTypes)
mkInsertTriggerQ ::
MonadMSSQLTx m =>
TriggerName ->
TableName ->
[ColumnInfo 'MSSQL] ->
SubscribeOpSpec 'MSSQL ->
m ()
mkInsertTriggerQ triggerName table allCols subOpSpec@(SubscribeOpSpec _listenCols deliveryCols) = do
checkSpatialDataTypeColumns allCols subOpSpec
liftMSSQLTx $ do
unitQueryE HGE.defaultMSSQLTxErrorHandler $
rawUnescapedText . LT.toStrict $ do
let deliveryColumns = getApplicableColumns allCols $ fromMaybe SubCStar deliveryCols
mkInsertTriggerQuery table triggerName deliveryColumns
mkDeleteTriggerQ ::
MonadMSSQLTx m =>
TriggerName ->
TableName ->
[ColumnInfo 'MSSQL] ->
SubscribeOpSpec 'MSSQL ->
m ()
mkDeleteTriggerQ triggerName table allCols subOpSpec@(SubscribeOpSpec _listenCols deliveryCols) = do
checkSpatialDataTypeColumns allCols subOpSpec
liftMSSQLTx $ do
unitQueryE HGE.defaultMSSQLTxErrorHandler $
rawUnescapedText . LT.toStrict $ do
let deliveryColumns = getApplicableColumns allCols $ fromMaybe SubCStar deliveryCols
mkDeleteTriggerQuery table triggerName deliveryColumns
mkUpdateTriggerQ ::
MonadMSSQLTx m =>
TriggerName ->
TableName ->
[ColumnInfo 'MSSQL] ->
Maybe (PrimaryKey 'MSSQL (ColumnInfo 'MSSQL)) ->
SubscribeOpSpec 'MSSQL ->
m ()
mkUpdateTriggerQ triggerName table allCols primaryKeyMaybe subOpSpec@(SubscribeOpSpec listenCols deliveryCols) = do
checkSpatialDataTypeColumns allCols subOpSpec
liftMSSQLTx $ do
primaryKey <- onNothing primaryKeyMaybe (throw400 NotSupported "Update event triggers for MS-SQL sources are only supported on tables with primary keys")
let deliveryColumns = getApplicableColumns allCols $ fromMaybe SubCStar deliveryCols
listenColumns = getApplicableColumns allCols listenCols
unitQueryE HGE.defaultMSSQLTxErrorHandler $
rawUnescapedText . LT.toStrict $
mkUpdateTriggerQuery table triggerName listenColumns deliveryColumns primaryKey
generateColumnTriggerAlias :: OpVar -> Maybe Text -> ColumnInfo 'MSSQL -> Text
generateColumnTriggerAlias op colPrefixMaybe colInfo =
let opText =
case op of
OLD -> "old"
NEW -> "new"
dbColNameText = columnNameText $ ciColumn colInfo
joinPrefixedDbColNameText =
-- prefix with the joining table's name
-- `id` -> `inserted.id` (prefix = 'inserted')
case colPrefixMaybe of
Just colPrefix -> colPrefix <> "." <> dbColNameText
Nothing -> dbColNameText
dbColAlias = "data" <> "." <> opText <> "." <> dbColNameText
in LT.toStrict $ [ST.stext| #{joinPrefixedDbColNameText} as [#{dbColAlias}]|]
qualifyTableName :: TableName -> Text
qualifyTableName (TableName tableName (SchemaName schemaName)) =
if schemaName == "dbo"
then tableName
else schemaName <> "." <> tableName
mkInsertTriggerQuery :: TableName -> TriggerName -> [ColumnInfo 'MSSQL] -> LT.Text
mkInsertTriggerQuery table@(TableName tableName schema@(SchemaName schemaName)) triggerName columns =
let QualifiedTriggerName qualifiedTriggerName = msssqlIdenTrigger INSERT schema triggerName
triggerNameText = triggerNameToTxt triggerName
qualifiedTableName = qualifyTableName table
deliveryColsSQLExpression :: Text =
commaSeparated $ map (generateColumnTriggerAlias NEW Nothing) columns
in $(makeRelativeToProject "src-rsr/mssql_insert_trigger.sql.shakespeare" >>= ST.stextFile)
mkDeleteTriggerQuery :: TableName -> TriggerName -> [ColumnInfo 'MSSQL] -> LT.Text
mkDeleteTriggerQuery table@(TableName tableName schema@(SchemaName schemaName)) triggerName columns =
let QualifiedTriggerName qualifiedTriggerName = msssqlIdenTrigger DELETE schema triggerName
triggerNameText = triggerNameToTxt triggerName
qualifiedTableName = qualifyTableName table
deliveryColsSQLExpression :: Text = commaSeparated $ map (generateColumnTriggerAlias OLD Nothing) columns
in $(makeRelativeToProject "src-rsr/mssql_delete_trigger.sql.shakespeare" >>= ST.stextFile)
mkPrimaryKeyJoinExp :: Text -> Text -> [ColumnInfo 'MSSQL] -> Text
mkPrimaryKeyJoinExp lhsPrefix rhsPrefix columns =
T.intercalate " AND " $ singleColExp <$> columns
where
singleColExp colInfo =
let dbColNameText = columnNameText $ ciColumn colInfo
in LT.toStrict $ [ST.stext| #{lhsPrefix}.#{dbColNameText} = #{rhsPrefix}.#{dbColNameText} |]
mkListenColumnsExp :: Text -> Text -> [ColumnInfo 'MSSQL] -> Text
mkListenColumnsExp _ _ [] = ""
mkListenColumnsExp lhsPrefix rhsPrefix columns =
"where " <> (T.intercalate " OR " $ singleColExp <$> columns)
where
singleColExp colInfo =
let dbColNameText = columnNameText $ ciColumn colInfo
in LT.toStrict $ [ST.stext| #{lhsPrefix}.#{dbColNameText} != #{rhsPrefix}.#{dbColNameText} |]
--
mkUpdateTriggerQuery :: TableName -> TriggerName -> [ColumnInfo 'MSSQL] -> [ColumnInfo 'MSSQL] -> PrimaryKey 'MSSQL (ColumnInfo 'MSSQL) -> LT.Text
mkUpdateTriggerQuery
table@(TableName tableName schema@(SchemaName schemaName))
triggerName
listenColumns
deliveryColumns
primaryKey =
let QualifiedTriggerName qualifiedTriggerName = msssqlIdenTrigger UPDATE schema triggerName
triggerNameText = triggerNameToTxt triggerName
qualifiedTableName = qualifyTableName table
oldDeliveryColsSQLExp :: Text = commaSeparated $ map (generateColumnTriggerAlias NEW (Just "INSERTED")) deliveryColumns
newDeliveryColsSQLExp :: Text = commaSeparated $ map (generateColumnTriggerAlias OLD (Just "DELETED")) deliveryColumns
primaryKeyJoinExp = mkPrimaryKeyJoinExp "INSERTED" "DELETED" (toList (_pkColumns primaryKey))
listenColumnExp = mkListenColumnsExp "INSERTED" "DELETED" listenColumns
in $(makeRelativeToProject "src-rsr/mssql_update_trigger.sql.shakespeare" >>= ST.stextFile)