mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 13:02:11 +03:00
e8e4f30dd6
Remote relationships are now supported on SQL Server and BigQuery. The major change though is the re-architecture of remote join execution logic. Prior to this PR, each backend is responsible for processing the remote relationships that are part of their AST. This is not ideal as there is nothing specific about a remote join's execution that ties it to a backend. The only backend specific part is whether or not the specification of the remote relationship is valid (i.e, we'll need to validate whether the scalars are compatible). The approach now changes to this: 1. Before delegating the AST to the backend, we traverse the AST, collect all the remote joins while modifying the AST to add necessary join fields where needed. 1. Once the remote joins are collected from the AST, the database call is made to fetch the response. The necessary data for the remote join(s) is collected from the database's response and one or more remote schema calls are constructed as necessary. 1. The remote schema calls are then executed and the data from the database and from the remote schemas is joined to produce the final response. ### Known issues 1. Ideally the traversal of the IR to collect remote joins should return an AST which does not include remote join fields. This operation can be type safe but isn't taken up as part of the PR. 1. There is a lot of code duplication between `Transport/HTTP.hs` and `Transport/Websocket.hs` which needs to be fixed ASAP. This too hasn't been taken up by this PR. 1. The type which represents the execution plan is only modified to handle our current remote joins and as such it will have to be changed to accommodate general remote joins. 1. Use of lenses would have reduced the boilerplate code to collect remote joins from the base AST. 1. The current remote join logic assumes that the join columns of a remote relationship appear with their names in the database response. This however is incorrect as they could be aliased. This can be taken up by anyone, I've left a comment in the code. ### Notes to the reviewers I think it is best reviewed commit by commit. 1. The first one is very straight forward. 1. The second one refactors the remote join execution logic but other than moving things around, it doesn't change the user facing functionality. This moves Postgres specific parts to `Backends/Postgres` module from `Execute`. Some IR related code to `Hasura.RQL.IR` module. Simplifies various type class function signatures as a backend doesn't have to handle remote joins anymore 1. The third one fixes partial case matches that for some weird reason weren't shown as warnings before this refactor 1. The fourth one generalizes the validation logic of remote relationships and implements `scalarTypeGraphQLName` function on SQL Server and BigQuery which is used by the validation logic. This enables remote relationships on BigQuery and SQL Server. https://github.com/hasura/graphql-engine-mono/pull/1497 GitOrigin-RevId: 77dd8eed326602b16e9a8496f52f46d22b795598
375 lines
14 KiB
Haskell
375 lines
14 KiB
Haskell
module Hasura.Backends.Postgres.Execute.Insert
|
|
( traverseAnnInsert
|
|
, convertToSQLTransaction
|
|
) where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Data.List as L
|
|
import qualified Data.Sequence as Seq
|
|
import qualified Data.Text as T
|
|
import qualified Database.PG.Query as Q
|
|
|
|
import Data.Text.Extended
|
|
|
|
import qualified Hasura.Backends.Postgres.Execute.Mutation as PGE
|
|
import qualified Hasura.Backends.Postgres.SQL.DML as PG
|
|
import qualified Hasura.Backends.Postgres.Translate.BoolExp as PGT
|
|
import qualified Hasura.Backends.Postgres.Translate.Insert as PGT
|
|
import qualified Hasura.Backends.Postgres.Translate.Mutation as PGT
|
|
import qualified Hasura.Backends.Postgres.Translate.Returning as PGT
|
|
import qualified Hasura.RQL.IR.Insert as IR
|
|
import qualified Hasura.RQL.IR.Returning as IR
|
|
import qualified Hasura.Tracing as Tracing
|
|
|
|
import Hasura.Backends.Postgres.Connection
|
|
import Hasura.Backends.Postgres.SQL.Types
|
|
import Hasura.Backends.Postgres.SQL.Value
|
|
import Hasura.Backends.Postgres.Translate.Select (PostgresAnnotatedFieldJSON)
|
|
import Hasura.Base.Error
|
|
import Hasura.EncJSON
|
|
import Hasura.RQL.Types
|
|
import Hasura.Server.Version (HasVersion)
|
|
import Hasura.Session
|
|
|
|
|
|
traverseAnnInsert
|
|
:: (Applicative f, Backend backend)
|
|
=> (a -> f b)
|
|
-> IR.AnnInsert backend a
|
|
-> f (IR.AnnInsert backend b)
|
|
traverseAnnInsert f (IR.AnnInsert fieldName isSingle annIns mutationOutput) =
|
|
IR.AnnInsert fieldName isSingle
|
|
<$> traverseMulti annIns
|
|
<*> IR.traverseMutationOutput f mutationOutput
|
|
where
|
|
traverseMulti (IR.AnnIns objs tableName conflictClause checkCond columns defaultValues) = IR.AnnIns
|
|
<$> traverse traverseObject objs
|
|
<*> pure tableName
|
|
<*> traverse (traverse f) conflictClause
|
|
<*> ( (,)
|
|
<$> traverseAnnBoolExp f (fst checkCond)
|
|
<*> traverse (traverseAnnBoolExp f) (snd checkCond)
|
|
)
|
|
<*> pure columns
|
|
<*> traverse f defaultValues
|
|
traverseSingle (IR.AnnIns obj tableName conflictClause checkCond columns defaultValues) = IR.AnnIns
|
|
<$> traverseObject obj
|
|
<*> pure tableName
|
|
<*> traverse (traverse f) conflictClause
|
|
<*> ( (,)
|
|
<$> traverseAnnBoolExp f (fst checkCond)
|
|
<*> traverse (traverseAnnBoolExp f) (snd checkCond)
|
|
)
|
|
<*> pure columns
|
|
<*> traverse f defaultValues
|
|
traverseObject (IR.AnnInsObj columns objRels arrRels) = IR.AnnInsObj
|
|
<$> traverse (traverse f) columns
|
|
<*> traverse (traverseRel traverseSingle) objRels
|
|
<*> traverse (traverseRel traverseMulti) arrRels
|
|
traverseRel z (IR.RelIns object relInfo) = IR.RelIns <$> z object <*> pure relInfo
|
|
|
|
|
|
convertToSQLTransaction
|
|
:: ( HasVersion
|
|
, MonadTx m
|
|
, MonadIO m
|
|
, Tracing.MonadTrace m
|
|
, Backend ('Postgres pgKind)
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
)
|
|
=> IR.AnnInsert ('Postgres pgKind) PG.SQLExp
|
|
-> UserInfo
|
|
-> Seq.Seq Q.PrepArg
|
|
-> Bool
|
|
-> m EncJSON
|
|
convertToSQLTransaction (IR.AnnInsert fieldName isSingle annIns mutationOutput) userInfo planVars stringifyNum =
|
|
if null $ IR._aiInsObj annIns
|
|
then pure $ IR.buildEmptyMutResp mutationOutput
|
|
else withPaths ["selectionSet", fieldName, "args", suffix] $
|
|
insertMultipleObjects annIns [] userInfo mutationOutput planVars stringifyNum
|
|
where
|
|
withPaths p x = foldr ($) x $ withPathK <$> p
|
|
suffix = bool "objects" "object" isSingle
|
|
|
|
insertMultipleObjects
|
|
:: ( HasVersion
|
|
, MonadTx m
|
|
, MonadIO m
|
|
, Tracing.MonadTrace m
|
|
, Backend ('Postgres pgKind)
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
)
|
|
=> IR.MultiObjIns ('Postgres pgKind) PG.SQLExp
|
|
-> [(PGCol, PG.SQLExp)]
|
|
-> UserInfo
|
|
-> IR.MutationOutput ('Postgres pgKind)
|
|
-> Seq.Seq Q.PrepArg
|
|
-> Bool
|
|
-> m EncJSON
|
|
insertMultipleObjects multiObjIns additionalColumns userInfo mutationOutput planVars stringifyNum =
|
|
bool withoutRelsInsert withRelsInsert anyRelsToInsert
|
|
where
|
|
IR.AnnIns insObjs table conflictClause checkCondition columnInfos defVals = multiObjIns
|
|
allInsObjRels = concatMap IR._aioObjRels insObjs
|
|
allInsArrRels = concatMap IR._aioArrRels insObjs
|
|
anyRelsToInsert = not $ null allInsArrRels && null allInsObjRels
|
|
|
|
withoutRelsInsert = do
|
|
indexedForM_ (IR._aioColumns <$> insObjs) \column ->
|
|
validateInsert (map fst column) [] (map fst additionalColumns)
|
|
let columnValues = map (mkSQLRow defVals) $ union additionalColumns . IR._aioColumns <$> insObjs
|
|
columnNames = Map.keys defVals
|
|
insertQuery = IR.InsertQueryP1
|
|
table
|
|
columnNames
|
|
columnValues
|
|
conflictClause
|
|
checkCondition
|
|
mutationOutput
|
|
columnInfos
|
|
rowCount = tshow . length $ IR._aiInsObj multiObjIns
|
|
Tracing.trace ("Insert (" <> rowCount <> ") " <> qualifiedObjectToText table) do
|
|
Tracing.attachMetadata [("count", rowCount)]
|
|
PGE.execInsertQuery stringifyNum userInfo (insertQuery, planVars)
|
|
|
|
withRelsInsert = do
|
|
insertRequests <- indexedForM insObjs \obj -> do
|
|
let singleObj = IR.AnnIns obj table conflictClause checkCondition columnInfos defVals
|
|
insertObject singleObj additionalColumns userInfo planVars stringifyNum
|
|
let affectedRows = sum $ map fst insertRequests
|
|
columnValues = mapMaybe snd insertRequests
|
|
selectExpr <- PGT.mkSelectExpFromColumnValues table columnInfos columnValues
|
|
PGE.executeMutationOutputQuery table columnInfos (Just affectedRows) (PGT.MCSelectValues selectExpr)
|
|
mutationOutput stringifyNum []
|
|
|
|
insertObject
|
|
:: forall pgKind m
|
|
. ( HasVersion
|
|
, MonadTx m
|
|
, MonadIO m
|
|
, Tracing.MonadTrace m
|
|
, Backend ('Postgres pgKind)
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
)
|
|
=> IR.SingleObjIns ('Postgres pgKind) PG.SQLExp
|
|
-> [(PGCol, PG.SQLExp)]
|
|
-> UserInfo
|
|
-> Seq.Seq Q.PrepArg
|
|
-> Bool
|
|
-> m (Int, Maybe (ColumnValues ('Postgres pgKind) TxtEncodedVal))
|
|
insertObject singleObjIns additionalColumns userInfo planVars stringifyNum = Tracing.trace ("Insert " <> qualifiedObjectToText table) do
|
|
validateInsert (map fst columns) (map IR._riRelInfo objectRels) (map fst additionalColumns)
|
|
|
|
-- insert all object relations and fetch this insert dependent column values
|
|
objInsRes <- forM beforeInsert $ insertObjRel planVars userInfo stringifyNum
|
|
|
|
-- prepare final insert columns
|
|
let objRelAffRows = sum $ map fst objInsRes
|
|
objRelDeterminedCols = concatMap snd objInsRes
|
|
finalInsCols = columns <> objRelDeterminedCols <> additionalColumns
|
|
|
|
cte <- mkInsertQ table onConflict finalInsCols defaultValues checkCond
|
|
|
|
PGE.MutateResp affRows colVals <- liftTx $
|
|
PGE.mutateAndFetchCols @pgKind table allColumns (PGT.MCCheckConstraint cte, planVars) stringifyNum
|
|
colValM <- asSingleObject colVals
|
|
|
|
arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null allAfterInsertRels
|
|
let totAffRows = objRelAffRows + affRows + arrRelAffRows
|
|
|
|
return (totAffRows, colValM)
|
|
where
|
|
IR.AnnIns annObj table onConflict checkCond allColumns defaultValues = singleObjIns
|
|
IR.AnnInsObj columns objectRels arrayRels = annObj
|
|
|
|
afterInsert, beforeInsert :: [IR.ObjRelIns ('Postgres pgKind) PG.SQLExp]
|
|
(afterInsert, beforeInsert) =
|
|
L.partition ((== AfterParent) . riInsertOrder . IR._riRelInfo) objectRels
|
|
|
|
allAfterInsertRels :: [IR.ArrRelIns ('Postgres pgKind) PG.SQLExp]
|
|
allAfterInsertRels = arrayRels <> map objToArr afterInsert
|
|
|
|
afterInsertDepCols :: [ColumnInfo ('Postgres pgKind)]
|
|
afterInsertDepCols = flip (getColInfos @('Postgres pgKind)) allColumns $
|
|
concatMap (Map.keys . riMapping . IR._riRelInfo) allAfterInsertRels
|
|
|
|
objToArr :: forall a b. IR.ObjRelIns b a -> IR.ArrRelIns b a
|
|
objToArr IR.RelIns {..} = IR.RelIns (singleToMulti _riAnnIns) _riRelInfo
|
|
|
|
singleToMulti :: forall a b. IR.SingleObjIns b a -> IR.MultiObjIns b a
|
|
singleToMulti IR.AnnIns {..} =
|
|
IR.AnnIns
|
|
[_aiInsObj]
|
|
_aiTableName
|
|
_aiConflictClause
|
|
_aiCheckCond
|
|
_aiTableCols
|
|
_aiDefVals
|
|
|
|
withArrRels
|
|
:: Maybe (ColumnValues ('Postgres pgKind) TxtEncodedVal)
|
|
-> m Int
|
|
withArrRels colValM = do
|
|
colVal <- onNothing colValM $ throw400 NotSupported cannotInsArrRelErr
|
|
afterInsertDepColsWithVal <- fetchFromColVals colVal afterInsertDepCols
|
|
arrInsARows <- forM allAfterInsertRels
|
|
$ insertArrRel afterInsertDepColsWithVal userInfo planVars stringifyNum
|
|
return $ sum arrInsARows
|
|
|
|
asSingleObject
|
|
:: [ColumnValues ('Postgres pgKind) TxtEncodedVal]
|
|
-> m (Maybe (ColumnValues ('Postgres pgKind) TxtEncodedVal))
|
|
asSingleObject = \case
|
|
[] -> pure Nothing
|
|
[r] -> pure $ Just r
|
|
_ -> throw500 "more than one row returned"
|
|
|
|
cannotInsArrRelErr :: Text
|
|
cannotInsArrRelErr =
|
|
"cannot proceed to insert array relations since insert to table "
|
|
<> table <<> " affects zero rows"
|
|
|
|
insertObjRel
|
|
:: ( HasVersion
|
|
, MonadTx m
|
|
, MonadIO m
|
|
, Tracing.MonadTrace m
|
|
, Backend ('Postgres pgKind)
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
)
|
|
=> Seq.Seq Q.PrepArg
|
|
-> UserInfo
|
|
-> Bool
|
|
-> IR.ObjRelIns ('Postgres pgKind) PG.SQLExp
|
|
-> m (Int, [(PGCol, PG.SQLExp)])
|
|
insertObjRel planVars userInfo stringifyNum objRelIns =
|
|
withPathK (relNameToTxt relName) $ do
|
|
(affRows, colValM) <- withPathK "data" $ insertObject singleObjIns [] userInfo planVars stringifyNum
|
|
colVal <- onNothing colValM $ throw400 NotSupported errMsg
|
|
retColsWithVals <- fetchFromColVals colVal rColInfos
|
|
let columns = flip mapMaybe (Map.toList mapCols) \(column, target) -> do
|
|
value <- lookup target retColsWithVals
|
|
Just (column, value)
|
|
pure (affRows, columns)
|
|
where
|
|
IR.RelIns singleObjIns relInfo = objRelIns
|
|
relName = riName relInfo
|
|
table = riRTable relInfo
|
|
mapCols = riMapping relInfo
|
|
allCols = IR._aiTableCols singleObjIns
|
|
rCols = Map.elems mapCols
|
|
rColInfos = getColInfos rCols allCols
|
|
errMsg = "cannot proceed to insert object relation "
|
|
<> relName <<> " since insert to table "
|
|
<> table <<> " affects zero rows"
|
|
|
|
insertArrRel
|
|
:: ( HasVersion
|
|
, MonadTx m
|
|
, MonadIO m
|
|
, Tracing.MonadTrace m
|
|
, Backend ('Postgres pgKind)
|
|
, PostgresAnnotatedFieldJSON pgKind
|
|
)
|
|
=> [(PGCol, PG.SQLExp)]
|
|
-> UserInfo
|
|
-> Seq.Seq Q.PrepArg
|
|
-> Bool
|
|
-> IR.ArrRelIns ('Postgres pgKind) PG.SQLExp
|
|
-> m Int
|
|
insertArrRel resCols userInfo planVars stringifyNum arrRelIns =
|
|
withPathK (relNameToTxt $ riName relInfo) $ do
|
|
let additionalColumns = flip mapMaybe resCols \(column, value) -> do
|
|
target <- Map.lookup column mapping
|
|
Just (target, value)
|
|
resBS <- withPathK "data" $
|
|
insertMultipleObjects multiObjIns additionalColumns userInfo mutOutput planVars stringifyNum
|
|
resObj <- decodeEncJSON resBS
|
|
onNothing (Map.lookup ("affected_rows" :: Text) resObj) $
|
|
throw500 "affected_rows not returned in array rel insert"
|
|
where
|
|
IR.RelIns multiObjIns relInfo = arrRelIns
|
|
mapping = riMapping relInfo
|
|
mutOutput = IR.MOutMultirowFields [("affected_rows", IR.MCount)]
|
|
|
|
-- | validate an insert object based on insert columns,
|
|
-- | insert object relations and additional columns from parent
|
|
validateInsert
|
|
:: (MonadError QErr m)
|
|
=> [PGCol] -- ^ inserting columns
|
|
-> [RelInfo ('Postgres pgKind)] -- ^ object relation inserts
|
|
-> [PGCol] -- ^ additional fields from parent
|
|
-> m ()
|
|
validateInsert insCols objRels addCols = do
|
|
-- validate insertCols
|
|
unless (null insConflictCols) $ throw400 ValidationFailed $
|
|
"cannot insert " <> showPGCols insConflictCols
|
|
<> " columns as their values are already being determined by parent insert"
|
|
|
|
forM_ objRels $ \relInfo -> do
|
|
let lCols = Map.keys $ riMapping relInfo
|
|
relName = riName relInfo
|
|
relNameTxt = relNameToTxt relName
|
|
lColConflicts = lCols `intersect` (addCols <> insCols)
|
|
withPathK relNameTxt $ unless (null lColConflicts) $ throw400 ValidationFailed $
|
|
"cannot insert object relationship " <> relName
|
|
<<> " as " <> showPGCols lColConflicts
|
|
<> " column values are already determined"
|
|
where
|
|
insConflictCols = insCols `intersect` addCols
|
|
|
|
|
|
mkInsertQ
|
|
:: (MonadError QErr m, Backend ('Postgres pgKind))
|
|
=> QualifiedTable
|
|
-> Maybe (IR.ConflictClauseP1 ('Postgres pgKind) PG.SQLExp)
|
|
-> [(PGCol, PG.SQLExp)]
|
|
-> Map.HashMap PGCol PG.SQLExp
|
|
-> (AnnBoolExpSQL ('Postgres pgKind), Maybe (AnnBoolExpSQL ('Postgres pgKind)))
|
|
-> m PG.CTE
|
|
mkInsertQ table onConflictM insCols defVals (insCheck, updCheck) = do
|
|
let sqlConflict = PGT.toSQLConflict table <$> onConflictM
|
|
sqlExps = mkSQLRow defVals insCols
|
|
valueExp = PG.ValuesExp [PG.TupleExp sqlExps]
|
|
tableCols = Map.keys defVals
|
|
sqlInsert =
|
|
PG.SQLInsert table tableCols valueExp sqlConflict
|
|
. Just
|
|
$ PG.RetExp
|
|
[ PG.selectStar
|
|
, PGT.insertOrUpdateCheckExpr table onConflictM
|
|
(PGT.toSQLBoolExp (PG.QualTable table) insCheck)
|
|
(fmap (PGT.toSQLBoolExp (PG.QualTable table)) updCheck)
|
|
]
|
|
pure $ PG.CTEInsert sqlInsert
|
|
|
|
fetchFromColVals
|
|
:: MonadError QErr m
|
|
=> ColumnValues ('Postgres pgKind) TxtEncodedVal
|
|
-> [ColumnInfo ('Postgres pgKind)]
|
|
-> m [(PGCol, PG.SQLExp)]
|
|
fetchFromColVals colVal reqCols =
|
|
forM reqCols $ \ci -> do
|
|
let valM = Map.lookup (pgiColumn ci) colVal
|
|
val <- onNothing valM $ throw500 $ "column "
|
|
<> pgiColumn ci <<> " not found in given colVal"
|
|
let pgColVal = case val of
|
|
TENull -> PG.SENull
|
|
TELit t -> PG.SELit t
|
|
return (pgiColumn ci, pgColVal)
|
|
|
|
mkSQLRow :: Map.HashMap PGCol PG.SQLExp -> [(PGCol, PG.SQLExp)] -> [PG.SQLExp]
|
|
mkSQLRow defVals withPGCol = map snd $
|
|
flip map (Map.toList defVals) $
|
|
\(col, defVal) -> (col,) $ fromMaybe defVal $ Map.lookup col withPGColMap
|
|
where
|
|
withPGColMap = Map.fromList withPGCol
|
|
|
|
decodeEncJSON :: (J.FromJSON a, QErrM m) => EncJSON -> m a
|
|
decodeEncJSON =
|
|
either (throw500 . T.pack) decodeValue .
|
|
J.eitherDecode . encJToLBS
|