Tidy up MSSQL.FromIr

## Description

We go through the module `Hasura.Backends.MSSQL.FromIr` and split it into separate self-contained units, which we document.

Note that this PR has a slightly opinionated follow-up PR #3909 .

### Related Issues

Fix #3666

### Solution and Design

The module `FromIr` has given rise to:

* `FromIr.Expression`
* `FromIr.Query`
* `FromIr.Delete`
* `FromIr.Insert`
* `FromIr.Update`
* `FromIr.SelectIntoTempTable`

And `Execute.MutationResponse` has become `FromIr.MutationResponse` (after some slight adaptation of types).

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3908
GitOrigin-RevId: 364acf1bcdf74f2e19464c31cdded12bd8e9aa59
This commit is contained in:
Philip Lykke Carlsen 2022-03-10 11:33:55 +01:00 committed by hasura-bot
parent 2ff0f25e08
commit 4ccc830bb8
17 changed files with 1757 additions and 1435 deletions

View File

@ -386,11 +386,18 @@ library
, Hasura.Backends.MSSQL.DDL.RunSQL
, Hasura.Backends.MSSQL.DDL.Source
, Hasura.Backends.MSSQL.DDL.Source.Version
, Hasura.Backends.MSSQL.Execute.MutationResponse
, Hasura.Backends.MSSQL.Execute.Delete
, Hasura.Backends.MSSQL.Execute.Insert
, Hasura.Backends.MSSQL.Execute.Update
, Hasura.Backends.MSSQL.FromIr
, Hasura.Backends.MSSQL.FromIr.Constants
, Hasura.Backends.MSSQL.FromIr.Delete
, Hasura.Backends.MSSQL.FromIr.Expression
, Hasura.Backends.MSSQL.FromIr.Insert
, Hasura.Backends.MSSQL.FromIr.MutationResponse
, Hasura.Backends.MSSQL.FromIr.Query
, Hasura.Backends.MSSQL.FromIr.SelectIntoTempTable
, Hasura.Backends.MSSQL.FromIr.Update
, Hasura.Backends.MSSQL.Instances.API
, Hasura.Backends.MSSQL.Instances.Execute
, Hasura.Backends.MSSQL.Instances.Metadata

View File

@ -9,11 +9,13 @@ module Hasura.Backends.MSSQL.Execute.Delete
)
where
import Control.Monad.Validate qualified as V
import Database.MSSQL.Transaction qualified as Tx
import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.MSSQL.Execute.MutationResponse
import Hasura.Backends.MSSQL.FromIr as TSQL
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameDeleted)
import Hasura.Backends.MSSQL.FromIr.Delete qualified as TSQL
import Hasura.Backends.MSSQL.FromIr.MutationResponse
import Hasura.Backends.MSSQL.FromIr.SelectIntoTempTable qualified as TSQL
import Hasura.Backends.MSSQL.Plan
import Hasura.Backends.MSSQL.SQL.Error
import Hasura.Backends.MSSQL.ToQuery as TQ
@ -67,10 +69,11 @@ buildDeleteTx deleteOperation stringifyNum = do
-- Create a temp table
Tx.unitQueryE defaultMSSQLTxErrorHandler createInsertedTempTableQuery
let deleteQuery = TQ.fromDelete <$> TSQL.fromDelete deleteOperation
deleteQueryValidated <- toQueryFlat <$> V.runValidate (runFromIr deleteQuery) `onLeft` (throw500 . tshow)
deleteQueryValidated <- toQueryFlat <$> runFromIr deleteQuery
-- Execute DELETE statement
Tx.unitQueryE mutationMSSQLTxErrorHandler deleteQueryValidated
mutationOutputSelect <- mkMutationOutputSelect stringifyNum withAlias $ _adOutput deleteOperation
mutationOutputSelect <- runFromIr $ mkMutationOutputSelect stringifyNum withAlias $ _adOutput deleteOperation
let withSelect =
emptySelect
{ selectProjections = [StarProjection],

View File

@ -9,11 +9,15 @@ module Hasura.Backends.MSSQL.Execute.Insert
)
where
import Control.Monad.Validate qualified as V
import Database.MSSQL.Transaction qualified as Tx
import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.MSSQL.Execute.MutationResponse
import Hasura.Backends.MSSQL.FromIr as TSQL
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameInserted, tempTableNameValues)
import Hasura.Backends.MSSQL.FromIr.Expression
import Hasura.Backends.MSSQL.FromIr.Insert (toMerge)
import Hasura.Backends.MSSQL.FromIr.Insert qualified as TSQL
import Hasura.Backends.MSSQL.FromIr.MutationResponse
import Hasura.Backends.MSSQL.FromIr.SelectIntoTempTable qualified as TSQL
import Hasura.Backends.MSSQL.Plan
import Hasura.Backends.MSSQL.SQL.Error
import Hasura.Backends.MSSQL.ToQuery as TQ
@ -196,10 +200,7 @@ buildUpsertTx tableName insert ifMatched = do
Tx.unitQueryE mutationMSSQLTxErrorHandler insertValuesIntoTempTableQuery
-- Run the MERGE query and store the mutated rows in #inserted temporary table
merge <-
(V.runValidate . runFromIr)
(toMerge tableName (_aiInsObj $ _aiData insert) allTableColumns ifMatched)
`onLeft` (throw500 . tshow)
merge <- runFromIr (toMerge tableName (_aiInsObj $ _aiData insert) allTableColumns ifMatched)
let mergeQuery = toQueryFlat $ TQ.fromMerge merge
Tx.unitQueryE mutationMSSQLTxErrorHandler mergeQuery
@ -210,13 +211,11 @@ buildUpsertTx tableName insert ifMatched = do
buildInsertResponseTx :: StringifyNumbers -> Text -> AnnInsert 'MSSQL Void Expression -> Tx.TxET QErr IO (Text, Int)
buildInsertResponseTx stringifyNum withAlias insert = do
-- Generate a SQL SELECT statement which outputs the mutation response using the #inserted
mutationOutputSelect <- mkMutationOutputSelect stringifyNum withAlias $ _aiOutput insert
mutationOutputSelect <- runFromIr $ mkMutationOutputSelect stringifyNum withAlias $ _aiOutput insert
-- The check constraint is translated to boolean expression
let checkCondition = fst $ _aiCheckCond $ _aiData insert
checkBoolExp <-
V.runValidate (runFromIr $ runReaderT (fromGBoolExp checkCondition) (EntityAlias withAlias))
`onLeft` (throw500 . tshow)
checkBoolExp <- runFromIr $ runReaderT (fromGBoolExp checkCondition) (EntityAlias withAlias)
let withSelect =
emptySelect

View File

@ -1,104 +0,0 @@
{-# OPTIONS_HADDOCK ignore-exports #-}
-- | Defines common functionality for building MSSQL execution plans for IR ASTs.
module Hasura.Backends.MSSQL.Execute.MutationResponse
( mkMutationOutputSelect,
selectMutationOutputAndCheckCondition,
)
where
import Control.Monad.Validate qualified as V
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.FromIr as TSQL
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.IR
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types
-- ** Mutation response
-- | Generate a SQL SELECT statement which outputs the mutation response
--
-- For multi row inserts:
-- SELECT (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows], (select_from_returning) AS [returning] FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER
--
-- For single row insert: the selection set is translated to SQL query using @'mkSQLSelect'
mkMutationOutputSelect ::
(MonadError QErr m) =>
StringifyNumbers ->
Text ->
MutationOutputG 'MSSQL Void Expression ->
m Select
mkMutationOutputSelect stringifyNum withAlias = \case
IR.MOutMultirowFields multiRowFields -> do
projections <- forM multiRowFields $ \(fieldName, field') -> do
let mkProjection = ExpressionProjection . flip Aliased (getFieldNameTxt fieldName) . SelectExpression
mkProjection <$> case field' of
IR.MCount -> pure $ countSelect withAlias
IR.MExp t -> pure $ textSelect t
IR.MRet returningFields -> mkSelect stringifyNum withAlias JASMultipleRows returningFields
let forJson = JsonFor $ ForJson JsonSingleton NoRoot
pure emptySelect {selectFor = forJson, selectProjections = projections}
IR.MOutSinglerowObject singleRowField -> mkSelect stringifyNum withAlias JASSingleObject singleRowField
-- | Generate a SQL SELECT statement which outputs the mutation response and check constraint result
--
-- The check constraint boolean expression is evaluated on mutated rows in a CASE expression so that
-- the int value "0" is returned when check constraint is true otherwise the int value "1" is returned.
-- We use "SUM" aggregation on the returned value and if check constraint on any row is not met, the summed
-- value will not equal to "0" (always > 1).
--
-- <check_constraint_select> :=
-- SELECT SUM(CASE WHEN <check_boolean_expression> THEN 0 ELSE 1 END) FROM [with_alias]
--
-- <mutation_output_select> :=
-- SELECT (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows], (select_from_returning) AS [returning] FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER
--
-- SELECT (<mutation_output_select>) AS [mutation_response], (<check_constraint_select>) AS [check_constraint_select]
selectMutationOutputAndCheckCondition :: Text -> Select -> Expression -> Select
selectMutationOutputAndCheckCondition alias mutationOutputSelect checkBoolExp =
let mutationOutputProjection =
ExpressionProjection $ Aliased (SelectExpression mutationOutputSelect) "mutation_response"
checkConstraintProjection =
-- apply ISNULL() to avoid check constraint select statement yielding empty rows
ExpressionProjection $
Aliased (FunctionApplicationExpression $ FunExpISNULL (SelectExpression checkConstraintSelect) (ValueExpression (ODBC.IntValue 0))) "check_constraint_select"
in emptySelect {selectProjections = [mutationOutputProjection, checkConstraintProjection]}
where
checkConstraintSelect =
let zeroValue = ValueExpression $ ODBC.IntValue 0
oneValue = ValueExpression $ ODBC.IntValue 1
caseExpression = ConditionalExpression checkBoolExp zeroValue oneValue
sumAggregate = OpAggregate "SUM" [caseExpression]
in emptySelect
{ selectProjections = [AggregateProjection (Aliased sumAggregate "check")],
selectFrom = Just $ TSQL.FromIdentifier alias
}
mkSelect ::
MonadError QErr m =>
StringifyNumbers ->
Text ->
JsonAggSelect ->
Fields (AnnFieldG 'MSSQL Void Expression) ->
m Select
mkSelect stringifyNum withAlias jsonAggSelect annFields = do
let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier $ FIIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs stringifyNum
V.runValidate (runFromIr $ mkSQLSelect jsonAggSelect annSelect) `onLeft` (throw500 . tshow)
-- SELECT COUNT(*) AS "count" FROM [with_alias]
countSelect :: Text -> Select
countSelect withAlias =
let countProjection = AggregateProjection $ Aliased (CountAggregate StarCountable) "count"
in emptySelect
{ selectProjections = [countProjection],
selectFrom = Just $ TSQL.FromIdentifier withAlias
}
-- SELECT '<text-value>' AS "exp"
textSelect :: Text -> Select
textSelect t =
let textProjection = ExpressionProjection $ Aliased (ValueExpression (ODBC.TextValue t)) "exp"
in emptySelect {selectProjections = [textProjection]}

View File

@ -9,11 +9,14 @@ module Hasura.Backends.MSSQL.Execute.Update
)
where
import Control.Monad.Validate qualified as V
import Database.MSSQL.Transaction qualified as Tx
import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.MSSQL.Execute.MutationResponse
import Hasura.Backends.MSSQL.FromIr as TSQL
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameUpdated)
import Hasura.Backends.MSSQL.FromIr.Expression (fromGBoolExp)
import Hasura.Backends.MSSQL.FromIr.MutationResponse
import Hasura.Backends.MSSQL.FromIr.SelectIntoTempTable qualified as TSQL
import Hasura.Backends.MSSQL.FromIr.Update qualified as TSQL
import Hasura.Backends.MSSQL.Plan
import Hasura.Backends.MSSQL.SQL.Error
import Hasura.Backends.MSSQL.ToQuery as TQ
@ -72,15 +75,13 @@ buildUpdateTx updateOperation stringifyNum = do
-- Create a temp table
Tx.unitQueryE defaultMSSQLTxErrorHandler createInsertedTempTableQuery
let updateQuery = TQ.fromUpdate <$> TSQL.fromUpdate updateOperation
updateQueryValidated <- toQueryFlat <$> V.runValidate (runFromIr updateQuery) `onLeft` (throw500 . tshow)
updateQueryValidated <- toQueryFlat <$> runFromIr updateQuery
-- Execute UPDATE statement
Tx.unitQueryE mutationMSSQLTxErrorHandler updateQueryValidated
mutationOutputSelect <- mkMutationOutputSelect stringifyNum withAlias $ _auOutput updateOperation
mutationOutputSelect <- runFromIr $ mkMutationOutputSelect stringifyNum withAlias $ _auOutput updateOperation
let checkCondition = _auCheck updateOperation
-- The check constraint is translated to boolean expression
checkBoolExp <-
V.runValidate (runFromIr $ runReaderT (fromGBoolExp checkCondition) (EntityAlias withAlias))
`onLeft` (throw500 . tshow)
checkBoolExp <- runFromIr $ runReaderT (fromGBoolExp checkCondition) (EntityAlias withAlias)
let withSelect =
emptySelect

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
-- | This module provides constants that are either:
--
-- * Simply in common user
-- * Define names that that multiple pieces of code reference.
module Hasura.Backends.MSSQL.FromIr.Constants
( trueExpression,
nullExpression,
emptyArrayExpression,
jsonFieldName,
aggSubselectName,
existsFieldName,
aggFieldName,
tempTableNameInserted,
tempTableNameValues,
tempTableNameDeleted,
tempTableNameUpdated,
)
where
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
trueExpression :: Expression
trueExpression = ValueExpression (ODBC.BoolValue True)
nullExpression :: Expression
nullExpression = ValueExpression $ ODBC.TextValue "null"
emptyArrayExpression :: Expression
emptyArrayExpression = ValueExpression $ ODBC.TextValue "[]"
jsonFieldName :: Text
jsonFieldName = "json"
aggSubselectName :: Text
aggSubselectName = "agg_sub"
existsFieldName :: Text
existsFieldName = "exists_placeholder"
aggFieldName :: Text
aggFieldName = "agg"
tempTableNameInserted :: TempTableName
tempTableNameInserted = TempTableName "inserted"
tempTableNameValues :: TempTableName
tempTableNameValues = TempTableName "values"
tempTableNameDeleted :: TempTableName
tempTableNameDeleted = TempTableName "deleted"
tempTableNameUpdated :: TempTableName
tempTableNameUpdated = TempTableName "updated"

View File

@ -0,0 +1,35 @@
-- | This module defines the translation function for delete mutations.
module Hasura.Backends.MSSQL.FromIr.Delete (fromDelete) where
import Hasura.Backends.MSSQL.FromIr (FromIr, NameTemplate (..), generateAlias)
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameDeleted)
import Hasura.Backends.MSSQL.FromIr.Expression (fromGBoolExp)
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Column qualified as IR
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (unName)
fromDelete :: IR.AnnDel 'MSSQL -> FromIr Delete
fromDelete (IR.AnnDel table (permFilter, whereClause) _ allColumns) = do
tableAlias <- generateAlias (TableTemplate (tableName table))
runReaderT
( do
permissionsFilter <- fromGBoolExp permFilter
whereExpression <- fromGBoolExp whereClause
let columnNames = map (ColumnName . unName . IR.ciName) allColumns
pure
Delete
{ deleteTable =
Aliased
{ aliasedAlias = tableAlias,
aliasedThing = table
},
deleteOutput = Output Deleted (map OutputColumn columnNames),
deleteTempTable = TempTable tempTableNameDeleted columnNames,
deleteWhere = Where [permissionsFilter, whereExpression]
}
)
(EntityAlias tableAlias)

View File

@ -0,0 +1,217 @@
-- | This module translates the IR of boolean expressions into TSQL boolean
-- expressions.
--
-- Boolean expressions typically arise from permissions and where-clause
-- filters.
module Hasura.Backends.MSSQL.FromIr.Expression
( fromGBoolExp,
)
where
import Control.Monad.Validate
import Data.HashMap.Strict qualified as HM
import Hasura.Backends.MSSQL.FromIr
( Error (UnsupportedOpExpG),
FromIr,
NameTemplate (TableTemplate),
generateAlias,
)
import Hasura.Backends.MSSQL.FromIr.Constants (existsFieldName, trueExpression)
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Column qualified as IR
import Hasura.RQL.Types.Relationships.Local qualified as IR
import Hasura.SQL.Backend
-- | Translate boolean expressions into TSQL 'Expression's.
--
-- The `IR.AnnBoolExpFld` references fields and columns. The entity (e.g. table)
-- that binds these columns is supplied in the `ReaderT EntityAlias`
-- environment, such that the columns can be referred to unambiguously.
fromGBoolExp ::
IR.GBoolExp 'MSSQL (IR.AnnBoolExpFld 'MSSQL Expression) ->
ReaderT EntityAlias FromIr Expression
fromGBoolExp =
\case
IR.BoolAnd expressions ->
fmap AndExpression (traverse fromGBoolExp expressions)
IR.BoolOr expressions ->
fmap OrExpression (traverse fromGBoolExp expressions)
IR.BoolNot expression ->
fmap NotExpression (fromGBoolExp expression)
IR.BoolExists gExists ->
fromGExists gExists
IR.BoolFld expression ->
fromAnnBoolExpFld expression
where
fromGExists :: IR.GExists 'MSSQL (IR.AnnBoolExpFld 'MSSQL Expression) -> ReaderT EntityAlias FromIr Expression
fromGExists IR.GExists {_geTable, _geWhere} = do
selectFrom <- lift (aliasQualifiedTable _geTable)
scopedTo selectFrom $ do
whereExpression <- fromGBoolExp _geWhere
pure $
ExistsExpression $
emptySelect
{ selectOrderBy = Nothing,
selectProjections =
[ ExpressionProjection
( Aliased
{ aliasedThing = trueExpression,
aliasedAlias = existsFieldName
}
)
],
selectFrom = Just selectFrom,
selectJoins = mempty,
selectWhere = Where [whereExpression],
selectTop = NoTop,
selectFor = NoFor,
selectOffset = Nothing
}
-- | Translate boolean expressions into TSQL 'Expression's.
--
-- The `IR.AnnBoolExpFld` references fields and columns. The entity (e.g. table)
-- that binds these columns is supplied in the `ReaderT EntityAlias`
-- environment, such that the columns can be referred to unambiguously.
fromAnnBoolExpFld ::
IR.AnnBoolExpFld 'MSSQL Expression ->
ReaderT EntityAlias FromIr Expression
fromAnnBoolExpFld =
\case
IR.AVColumn columnInfo opExpGs -> do
expressions <- traverse (fromOpExpG columnInfo) opExpGs
pure (AndExpression expressions)
IR.AVRelationship IR.RelInfo {riMapping = mapping, riRTable = table} annBoolExp -> do
selectFrom <- lift (aliasQualifiedTable table)
mappingExpression <- translateMapping selectFrom mapping
whereExpression <- scopedTo selectFrom (fromGBoolExp annBoolExp)
pure
( ExistsExpression
emptySelect
{ selectOrderBy = Nothing,
selectProjections =
[ ExpressionProjection
( Aliased
{ aliasedThing = trueExpression,
aliasedAlias = existsFieldName
}
)
],
selectFrom = Just selectFrom,
selectJoins = mempty,
selectWhere = Where (mappingExpression <> [whereExpression]),
selectTop = NoTop,
selectFor = NoFor,
selectOffset = Nothing
}
)
where
-- Translate a relationship field mapping into column equality comparisons.
translateMapping ::
From ->
HashMap ColumnName ColumnName ->
ReaderT EntityAlias FromIr [Expression]
translateMapping localFrom =
traverse
( \(remoteColumn, localColumn) -> do
localFieldName <- scopedTo localFrom (fromColumn localColumn)
remoteFieldName <- fromColumn remoteColumn
pure
( OpExpression
TSQL.EQ'
(ColumnExpression localFieldName)
(ColumnExpression remoteFieldName)
)
)
. HM.toList
-- | Scope a translation action to the table bound in a FROM clause.
scopedTo :: From -> ReaderT EntityAlias FromIr a -> ReaderT EntityAlias FromIr a
scopedTo from = local (const (fromAlias from))
-- | Translate a column reference occurring in a boolean expression into an
-- equivalent 'Expression'.
--
-- Different text types support different operators. Therefore we cast some text
-- types to "varchar(max)", which supports the most operators.
fromColumnInfo :: IR.ColumnInfo 'MSSQL -> ReaderT EntityAlias FromIr Expression
fromColumnInfo IR.ColumnInfo {ciColumn = column, ciType} = do
fieldName <- TSQL.columnNameToFieldName column <$> ask
if shouldCastToVarcharMax ciType
then pure (CastExpression (ColumnExpression fieldName) WvarcharType DataLengthMax)
else pure (ColumnExpression fieldName)
where
shouldCastToVarcharMax :: IR.ColumnType 'MSSQL -> Bool
shouldCastToVarcharMax typ =
typ == IR.ColumnScalar TextType || typ == IR.ColumnScalar WtextType
-- | Get FieldSource from a TAFExp type table aggregate field
fromColumn :: ColumnName -> ReaderT EntityAlias FromIr FieldName
fromColumn column = columnNameToFieldName column <$> ask
-- | Translate a single `IR.OpExpG` operation on a column into an expression.
fromOpExpG :: IR.ColumnInfo 'MSSQL -> IR.OpExpG 'MSSQL Expression -> ReaderT EntityAlias FromIr Expression
fromOpExpG columnInfo op = do
column <- fromColumnInfo columnInfo
case op of
IR.ANISNULL -> pure $ TSQL.IsNullExpression column
IR.ANISNOTNULL -> pure $ TSQL.IsNotNullExpression column
IR.AEQ False val -> pure $ nullableBoolEquality column val
IR.AEQ True val -> pure $ OpExpression TSQL.EQ' column val
IR.ANE False val -> pure $ nullableBoolInequality column val
IR.ANE True val -> pure $ OpExpression TSQL.NEQ' column val
IR.AGT val -> pure $ OpExpression TSQL.GT column val
IR.ALT val -> pure $ OpExpression TSQL.LT column val
IR.AGTE val -> pure $ OpExpression TSQL.GTE column val
IR.ALTE val -> pure $ OpExpression TSQL.LTE column val
IR.AIN val -> pure $ OpExpression TSQL.IN column val
IR.ANIN val -> pure $ OpExpression TSQL.NIN column val
IR.ALIKE val -> pure $ OpExpression TSQL.LIKE column val
IR.ANLIKE val -> pure $ OpExpression TSQL.NLIKE column val
IR.ABackendSpecific o -> case o of
ASTContains val -> pure $ TSQL.STOpExpression TSQL.STContains column val
ASTCrosses val -> pure $ TSQL.STOpExpression TSQL.STCrosses column val
ASTEquals val -> pure $ TSQL.STOpExpression TSQL.STEquals column val
ASTIntersects val -> pure $ TSQL.STOpExpression TSQL.STIntersects column val
ASTOverlaps val -> pure $ TSQL.STOpExpression TSQL.STOverlaps column val
ASTTouches val -> pure $ TSQL.STOpExpression TSQL.STTouches column val
ASTWithin val -> pure $ TSQL.STOpExpression TSQL.STWithin column val
-- As of March 2021, only geometry/geography casts are supported
IR.ACast _casts -> refute (pure (UnsupportedOpExpG op)) -- mkCastsExp casts
-- We do not yet support column names in permissions
IR.CEQ _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SEQ lhs $ mkQCol rhsCol
IR.CNE _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SNE lhs $ mkQCol rhsCol
IR.CGT _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SGT lhs $ mkQCol rhsCol
IR.CLT _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SLT lhs $ mkQCol rhsCol
IR.CGTE _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SGTE lhs $ mkQCol rhsCol
IR.CLTE _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SLTE lhs $ mkQCol rhsCol
nullableBoolEquality :: Expression -> Expression -> Expression
nullableBoolEquality x y =
OrExpression
[ OpExpression TSQL.EQ' x y,
AndExpression [IsNullExpression x, IsNullExpression y]
]
nullableBoolInequality :: Expression -> Expression -> Expression
nullableBoolInequality x y =
OrExpression
[ OpExpression TSQL.NEQ' x y,
AndExpression [IsNotNullExpression x, IsNullExpression y]
]
aliasQualifiedTable :: TableName -> FromIr From
aliasQualifiedTable schemadTableName@(TableName {tableName}) = do
alias <- generateAlias (TableTemplate tableName)
pure
( FromQualifiedTable
( Aliased
{ aliasedThing = schemadTableName,
aliasedAlias = alias
}
)
)

View File

@ -0,0 +1,117 @@
-- | This module defines the translation functions for insert and upsert
-- mutations.
module Hasura.Backends.MSSQL.FromIr.Insert
( fromInsert,
toMerge,
toInsertValuesIntoTempTable,
)
where
import Data.Containers.ListUtils (nubOrd)
import Data.HashMap.Strict qualified as HM
import Hasura.Backends.MSSQL.FromIr (FromIr)
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameInserted, tempTableNameValues)
import Hasura.Backends.MSSQL.FromIr.Expression (fromGBoolExp)
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Insert (IfMatched (..))
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Column qualified as IR
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (unName)
fromInsert :: IR.AnnInsert 'MSSQL Void Expression -> Insert
fromInsert IR.AnnInsert {..} =
let IR.AnnIns {..} = _aiData
insertRows = normalizeInsertRows $ map (IR.getInsertColumns) _aiInsObj
insertColumnNames = maybe [] (map fst) $ listToMaybe insertRows
insertValues = map (Values . map snd) insertRows
allColumnNames = map (ColumnName . unName . IR.ciName) _aiTableCols
insertOutput = Output Inserted $ map OutputColumn allColumnNames
tempTable = TempTable tempTableNameInserted allColumnNames
in Insert _aiTableName insertColumnNames insertOutput tempTable insertValues
-- | Normalize a row by adding missing columns with @DEFAULT@ value and sort by
-- column name to make sure all rows are consistent in column values and order.
--
-- Example: A table "author" is defined as:
--
-- > CREATE TABLE author ([id] INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, age INTEGER)
--
-- Consider the following mutation:
--
-- > mutation {
-- > insert_author(
-- > objects: [{id: 1, name: "Foo", age: 21}, {id: 2, name: "Bar"}]
-- > ){
-- > affected_rows
-- > }
-- > }
--
-- We consider @DEFAULT@ value for @age@ column which is missing in second
-- insert row.
--
-- The corresponding @INSERT@ statement looks like:
--
-- > INSERT INTO author (id, name, age)
-- > OUTPUT INSERTED.id
-- > VALUES (1, 'Foo', 21), (2, 'Bar', DEFAULT)
normalizeInsertRows ::
[[(Column 'MSSQL, Expression)]] ->
[[(Column 'MSSQL, Expression)]]
normalizeInsertRows insertRows =
let insertColumns = nubOrd (concatMap (map fst) insertRows)
allColumnsWithDefaultValue = map (,DefaultExpression) $ insertColumns
addMissingColumns insertRow =
HM.toList $ HM.fromList insertRow `HM.union` HM.fromList allColumnsWithDefaultValue
sortByColumn = sortBy (\l r -> compare (fst l) (fst r))
in map (sortByColumn . addMissingColumns) insertRows
-- | Construct a MERGE statement from AnnInsert information.
-- A MERGE statement is responsible for actually inserting and/or updating
-- the data in the table.
toMerge ::
TableName ->
[IR.AnnotatedInsertRow 'MSSQL Expression] ->
[IR.ColumnInfo 'MSSQL] ->
IfMatched Expression ->
FromIr Merge
toMerge tableName insertRows allColumns IfMatched {..} = do
let normalizedInsertRows = normalizeInsertRows $ map (IR.getInsertColumns) insertRows
insertColumnNames = maybe [] (map fst) $ listToMaybe normalizedInsertRows
allColumnNames = map (ColumnName . unName . IR.ciName) allColumns
matchConditions <-
flip runReaderT (EntityAlias "target") $ -- the table is aliased as "target" in MERGE sql
fromGBoolExp _imConditions
pure $
Merge
{ mergeTargetTable = tableName,
mergeUsing = MergeUsing tempTableNameValues insertColumnNames,
mergeOn = MergeOn _imMatchColumns,
mergeWhenMatched = MergeWhenMatched _imUpdateColumns matchConditions _imColumnPresets,
mergeWhenNotMatched = MergeWhenNotMatched insertColumnNames,
mergeInsertOutput = Output Inserted $ map OutputColumn allColumnNames,
mergeOutputTempTable = TempTable tempTableNameInserted allColumnNames
}
-- | As part of an INSERT/UPSERT process, insert VALUES into a temporary table.
-- The content of the temporary table will later be inserted into the original table
-- using a MERGE statement.
--
-- We insert the values into a temporary table first in order to replace the missing
-- fields with @DEFAULT@ in @normalizeInsertRows@, and we can't do that in a
-- MERGE statement directly.
toInsertValuesIntoTempTable :: TempTableName -> IR.AnnInsert 'MSSQL Void Expression -> InsertValuesIntoTempTable
toInsertValuesIntoTempTable tempTable IR.AnnInsert {..} =
let IR.AnnIns {..} = _aiData
insertRows = normalizeInsertRows $ map IR.getInsertColumns _aiInsObj
insertColumnNames = maybe [] (map fst) $ listToMaybe insertRows
insertValues = map (Values . map snd) insertRows
in InsertValuesIntoTempTable
{ ivittTempTableName = tempTable,
ivittColumns = insertColumnNames,
ivittValues = insertValues
}

View File

@ -0,0 +1,119 @@
-- | This module defines translation functions that yield the results of
-- mutation requests that return the data of rows that were affected.
module Hasura.Backends.MSSQL.FromIr.MutationResponse
( mkMutationOutputSelect,
selectMutationOutputAndCheckCondition,
)
where
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.FromIr (FromIr)
import Hasura.Backends.MSSQL.FromIr.Query (fromSelect)
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.IR.Returning (MutationOutputG)
import Hasura.RQL.Types.Common qualified as IR
import Hasura.SQL.Backend
-- | Generate a SQL SELECT statement which outputs the mutation response
--
-- For multi row inserts:
--
-- SELECT
-- (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows],
-- (select_from_returning) AS [returning]
-- FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER
--
-- For single row insert: the selection set is translated to SQL query using @'fromSelect'
mkMutationOutputSelect ::
IR.StringifyNumbers ->
Text ->
MutationOutputG 'MSSQL Void Expression ->
FromIr Select
mkMutationOutputSelect stringifyNum withAlias = \case
IR.MOutMultirowFields multiRowFields -> do
projections <- forM multiRowFields $ \(fieldName, field') -> do
let mkProjection = ExpressionProjection . flip Aliased (IR.getFieldNameTxt fieldName) . SelectExpression
mkProjection <$> case field' of
IR.MCount -> pure $ countSelect
IR.MExp t -> pure $ textSelect t
IR.MRet returningFields -> mkSelect IR.JASMultipleRows returningFields
let forJson = JsonFor $ ForJson JsonSingleton NoRoot
pure emptySelect {selectFor = forJson, selectProjections = projections}
IR.MOutSinglerowObject singleRowField -> mkSelect IR.JASSingleObject singleRowField
where
mkSelect ::
IR.JsonAggSelect ->
IR.Fields (IR.AnnFieldG 'MSSQL Void Expression) ->
FromIr Select
mkSelect jsonAggSelect annFields = do
let annSelect = IR.AnnSelectG annFields (IR.FromIdentifier $ IR.FIIdentifier withAlias) IR.noTablePermissions IR.noSelectArgs stringifyNum
fromSelect jsonAggSelect annSelect
-- SELECT COUNT(*) AS "count" FROM [with_alias]
countSelect :: Select
countSelect =
let countProjection = AggregateProjection $ Aliased (CountAggregate StarCountable) "count"
in emptySelect
{ selectProjections = [countProjection],
selectFrom = Just $ TSQL.FromIdentifier withAlias
}
-- SELECT '<text-value>' AS "exp"
textSelect :: Text -> Select
textSelect t =
let textProjection = ExpressionProjection $ Aliased (ValueExpression (ODBC.TextValue t)) "exp"
in emptySelect {selectProjections = [textProjection]}
-- | Generate a SQL SELECT statement which outputs both mutation response and
-- check constraint result.
--
-- A @check constraint@ applies to the data that has been changed, while
-- @permissions@ filter the data that is made available.
--
-- This function applies to @insert@ and @update@ mutations.
--
-- The check constraint boolean expression is evaluated on mutated rows in a
-- CASE expression so that the int value "0" is returned when check constraint
-- is true otherwise the int value "1" is returned. We use "SUM" aggregation on
-- the returned value and if check constraint on any row is not met, the summed
-- value will not equal to "0" (always > 1).
--
-- <check_constraint_select> :=
-- SELECT
-- SUM(CASE WHEN <check_boolean_expression>
-- THEN 0
-- ELSE 1
-- END)
-- FROM [with_alias]
--
-- <mutation_output_select> :=
-- SELECT
-- (SELECT COUNT(*) FROM [with_alias]) AS [affected_rows],
-- (select_from_returning) AS [returning]
-- FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER
--
-- SELECT
-- (<mutation_output_select>) AS [mutation_response],
-- (<check_constraint_select>) AS [check_constraint_select]
selectMutationOutputAndCheckCondition :: Text -> Select -> Expression -> Select
selectMutationOutputAndCheckCondition alias mutationOutputSelect checkBoolExp =
let mutationOutputProjection =
ExpressionProjection $ Aliased (SelectExpression mutationOutputSelect) "mutation_response"
checkConstraintProjection =
-- apply ISNULL() to avoid check constraint select statement yielding empty rows
ExpressionProjection $
Aliased (FunctionApplicationExpression $ FunExpISNULL (SelectExpression checkConstraintSelect) (ValueExpression (ODBC.IntValue 0))) "check_constraint_select"
in emptySelect {selectProjections = [mutationOutputProjection, checkConstraintProjection]}
where
checkConstraintSelect =
let zeroValue = ValueExpression $ ODBC.IntValue 0
oneValue = ValueExpression $ ODBC.IntValue 1
caseExpression = ConditionalExpression checkBoolExp zeroValue oneValue
sumAggregate = OpAggregate "SUM" [caseExpression]
in emptySelect
{ selectProjections = [AggregateProjection (Aliased sumAggregate "check")],
selectFrom = Just $ TSQL.FromIdentifier alias
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
-- | This module contains supporting definitions for building temporary tables
-- based off of the schema of other tables. This is used in mutations to capture
-- the data of rows that are affected.
module Hasura.Backends.MSSQL.FromIr.SelectIntoTempTable
( toSelectIntoTempTable,
)
where
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Prelude
import Hasura.RQL.Types.Column qualified as IR
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (unName)
-- | Create a temporary table with the same schema as the given table.
toSelectIntoTempTable :: TempTableName -> TableName -> [IR.ColumnInfo 'MSSQL] -> SITTConstraints -> SelectIntoTempTable
toSelectIntoTempTable tempTableName fromTable allColumns withConstraints = do
SelectIntoTempTable
{ sittTempTableName = tempTableName,
sittColumns = map columnInfoToUnifiedColumn allColumns,
sittFromTableName = fromTable,
sittConstraints = withConstraints
}
-- | Extracts the type and column name of a ColumnInfo
columnInfoToUnifiedColumn :: IR.ColumnInfo 'MSSQL -> UnifiedColumn
columnInfoToUnifiedColumn colInfo =
case IR.ciType colInfo of
IR.ColumnScalar t ->
UnifiedColumn
{ name = unName $ IR.ciName colInfo,
type' = t
}
-- Enum values are represented as text value so they will always be of type text
IR.ColumnEnumReference {} ->
UnifiedColumn
{ name = unName $ IR.ciName colInfo,
type' = TextType
}

View File

@ -0,0 +1,44 @@
-- | This module defines the translation functions for update mutations.
module Hasura.Backends.MSSQL.FromIr.Update
( fromUpdate,
)
where
import Hasura.Backends.MSSQL.FromIr
( FromIr,
NameTemplate (TableTemplate),
generateAlias,
)
import Hasura.Backends.MSSQL.FromIr.Constants (tempTableNameUpdated)
import Hasura.Backends.MSSQL.FromIr.Expression (fromGBoolExp)
import Hasura.Backends.MSSQL.Instances.Types ()
import Hasura.Backends.MSSQL.Types.Internal as TSQL
import Hasura.Backends.MSSQL.Types.Update as TSQL (BackendUpdate (..), Update (..))
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.Types.Column qualified as IR
import Hasura.SQL.Backend
import Language.GraphQL.Draft.Syntax (unName)
fromUpdate :: IR.AnnotatedUpdate 'MSSQL -> FromIr Update
fromUpdate (IR.AnnotatedUpdateG table (permFilter, whereClause) _ backendUpdate _ allColumns) = do
tableAlias <- generateAlias (TableTemplate (tableName table))
runReaderT
( do
permissionsFilter <- fromGBoolExp permFilter
whereExpression <- fromGBoolExp whereClause
let columnNames = map (ColumnName . unName . IR.ciName) allColumns
pure
Update
{ updateTable =
Aliased
{ aliasedAlias = tableAlias,
aliasedThing = table
},
updateSet = updateOperations backendUpdate,
updateOutput = Output Inserted (map OutputColumn columnNames),
updateTempTable = TempTable tempTableNameUpdated columnNames,
updateWhere = Where [permissionsFilter, whereExpression]
}
)
(EntityAlias tableAlias)

View File

@ -28,7 +28,7 @@ import Hasura.Backends.MSSQL.Connection
import Hasura.Backends.MSSQL.Execute.Delete
import Hasura.Backends.MSSQL.Execute.Insert
import Hasura.Backends.MSSQL.Execute.Update
import Hasura.Backends.MSSQL.FromIr as TSQL
import Hasura.Backends.MSSQL.FromIr.Constants (jsonFieldName)
import Hasura.Backends.MSSQL.Plan
import Hasura.Backends.MSSQL.SQL.Error
import Hasura.Backends.MSSQL.SQL.Value (txtEncodedColVal)
@ -184,7 +184,7 @@ multiplexRootReselect variables rootReselect =
ColumnExpression
( TSQL.FieldName
{ fieldNameEntity = resultAlias,
fieldName = TSQL.jsonFieldName
fieldName = jsonFieldName
}
),
aliasedAlias = resultAlias
@ -213,7 +213,7 @@ multiplexRootReselect variables rootReselect =
joinJoinAlias =
JoinAlias
{ joinAliasEntity = resultAlias,
joinAliasField = Just TSQL.jsonFieldName
joinAliasField = Just jsonFieldName
}
}
],

View File

@ -19,7 +19,6 @@ where
-- , planSubscription
-- ) where
import Control.Monad.Validate
import Data.Aeson qualified as J
import Data.ByteString.Lazy (toStrict)
import Data.HashMap.Strict qualified as HM
@ -29,6 +28,7 @@ import Data.Text qualified as T
import Data.Text.Extended
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.FromIr
import Hasura.Backends.MSSQL.FromIr.Query (fromQueryRootField)
import Hasura.Backends.MSSQL.Types.Internal
import Hasura.Base.Error
import Hasura.GraphQL.Parser qualified as GraphQL
@ -50,8 +50,7 @@ planQuery ::
m Select
planQuery sessionVariables queryDB = do
rootField <- traverse (prepareValueQuery sessionVariables) queryDB
runValidate (runFromIr (fromRootField rootField))
`onLeft` (throw400 NotSupported . tshow)
runFromIr (fromQueryRootField rootField)
-- | Prepare a value without any query planning; we just execute the
-- query with the values embedded.
@ -97,9 +96,7 @@ planSubscription unpreparedMap sessionVariables = do
unpreparedMap
)
emptyPrepareState
selectMap <-
runValidate (runFromIr (traverse fromRootField rootFieldMap))
`onLeft` (throw400 NotSupported . tshow)
selectMap <- runFromIr (traverse fromQueryRootField rootFieldMap)
pure (collapseMap selectMap, prepareState)
-- Plan a query without prepare/exec.

View File

@ -20,6 +20,7 @@ module Hasura.Backends.MSSQL.Types.Internal
BooleanOperators (..),
Column,
ColumnName (..),
columnNameToFieldName,
ColumnType,
Comment (..),
Countable (..),
@ -27,6 +28,7 @@ module Hasura.Backends.MSSQL.Types.Internal
Delete (..),
DeleteOutput,
EntityAlias (..),
fromAlias,
Expression (..),
FieldName (..),
For (..),
@ -93,10 +95,6 @@ module Hasura.Backends.MSSQL.Types.Internal
scalarTypeDBName,
snakeCaseTableName,
stringTypes,
tempTableNameInserted,
tempTableNameValues,
tempTableNameDeleted,
tempTableNameUpdated,
)
where
@ -297,18 +295,6 @@ data InsertValuesIntoTempTable = InsertValuesIntoTempTable
-- | A temporary table name is prepended by a hash-sign
newtype TempTableName = TempTableName Text
tempTableNameInserted :: TempTableName
tempTableNameInserted = TempTableName "inserted"
tempTableNameValues :: TempTableName
tempTableNameValues = TempTableName "values"
tempTableNameDeleted :: TempTableName
tempTableNameDeleted = TempTableName "deleted"
tempTableNameUpdated :: TempTableName
tempTableNameUpdated = TempTableName "updated"
-- | A name of a regular table or temporary table
data SomeTableName
= RegularTableName TableName
@ -453,6 +439,14 @@ data From
| FromIdentifier Text
| FromTempTable (Aliased TempTableName)
-- | Extract the name bound in a 'From' clause as an 'EntityAlias'.
fromAlias :: From -> EntityAlias
fromAlias (FromQualifiedTable Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromOpenJson Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromSelect Aliased {aliasedAlias}) = EntityAlias aliasedAlias
fromAlias (FromIdentifier identifier) = EntityAlias identifier
fromAlias (FromTempTable Aliased {aliasedAlias}) = EntityAlias aliasedAlias
data OpenJson = OpenJson
{ openJsonExpression :: Expression,
openJsonWith :: Maybe (NonEmpty JsonFieldSpec)
@ -488,6 +482,10 @@ newtype EntityAlias = EntityAlias
{ entityAliasText :: Text
}
columnNameToFieldName :: ColumnName -> EntityAlias -> FieldName
columnNameToFieldName (ColumnName fieldName) EntityAlias {entityAliasText = fieldNameEntity} =
FieldName {fieldName, fieldNameEntity}
data Op
= LT
| LTE