graphql-engine/server/src-lib/Hasura/Backends/MSSQL/FromIr/Insert.hs
Philip Lykke Carlsen 4ccc830bb8 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
2022-03-10 10:34:52 +00:00

118 lines
4.8 KiB
Haskell

-- | 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
}