From 27dbcc44cc71c3c62f38cdfc326e2787bc634bbf Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Wed, 6 Mar 2019 13:59:15 +0530 Subject: [PATCH 1/3] fix mutation returning relationship data, fix #1576 TODO:- Add a test case --- server/graphql-engine.cabal | 1 + server/src-lib/Hasura/GraphQL/Context.hs | 8 +- server/src-lib/Hasura/GraphQL/Explain.hs | 2 - .../src-lib/Hasura/GraphQL/Resolve/Context.hs | 4 + .../Hasura/GraphQL/Resolve/ContextTypes.hs | 1 + .../src-lib/Hasura/GraphQL/Resolve/Insert.hs | 183 ++++++++++-------- .../Hasura/GraphQL/Resolve/Mutation.hs | 10 +- server/src-lib/Hasura/GraphQL/Schema.hs | 120 +++++++++--- server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs | 37 +++- server/src-lib/Hasura/RQL/DDL/Schema/Table.hs | 4 +- server/src-lib/Hasura/RQL/DML/Delete.hs | 32 +-- server/src-lib/Hasura/RQL/DML/Insert.hs | 24 ++- server/src-lib/Hasura/RQL/DML/Mutation.hs | 105 ++++++++++ .../src-lib/Hasura/RQL/DML/QueryTemplate.hs | 3 +- server/src-lib/Hasura/RQL/DML/Returning.hs | 18 +- server/src-lib/Hasura/RQL/DML/Update.hs | 36 ++-- server/src-lib/Hasura/RQL/Types/Common.hs | 8 + .../src-lib/Hasura/RQL/Types/SchemaCache.hs | 37 +++- server/src-lib/Hasura/SQL/DML.hs | 15 ++ server/src-lib/Hasura/SQL/Rewrite.hs | 2 + server/src-lib/Hasura/SQL/Value.hs | 2 +- server/src-rsr/table_info.sql | 38 ++-- 22 files changed, 495 insertions(+), 195 deletions(-) create mode 100644 server/src-lib/Hasura/RQL/DML/Mutation.hs diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 9cf8c4f08a5f6..3ebbad6e484df 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -178,6 +178,7 @@ library , Hasura.RQL.DML.Delete , Hasura.RQL.DML.Internal , Hasura.RQL.DML.Insert + , Hasura.RQL.DML.Mutation , Hasura.RQL.DML.Returning , Hasura.RQL.DML.Select.Internal , Hasura.RQL.DML.Select.Types diff --git a/server/src-lib/Hasura/GraphQL/Context.hs b/server/src-lib/Hasura/GraphQL/Context.hs index 763d37a8ab319..273692dc04e20 100644 --- a/server/src-lib/Hasura/GraphQL/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Context.hs @@ -57,13 +57,15 @@ data UpdOpCtx , _uocHeaders :: ![T.Text] , _uocFilter :: !AnnBoolExpSQL , _uocPresetCols :: !PreSetCols + , _uocUniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) data DelOpCtx = DelOpCtx - { _docTable :: !QualifiedTable - , _docHeaders :: ![T.Text] - , _docFilter :: !AnnBoolExpSQL + { _docTable :: !QualifiedTable + , _docHeaders :: ![T.Text] + , _docFilter :: !AnnBoolExpSQL + , _docUniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) data OpCtx diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index 239fd9d4721fc..f39df6812d92c 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -20,7 +20,6 @@ import Hasura.Prelude import Hasura.RQL.DML.Internal import Hasura.RQL.Types import Hasura.SQL.Types -import Hasura.SQL.Value import qualified Hasura.GraphQL.Resolve.Select as RS import qualified Hasura.GraphQL.Transport.HTTP as TH @@ -94,7 +93,6 @@ explainField userInfo gCtx fld = return $ FieldPlan fName (Just txtSQL) $ Just planLines where fName = _fName fld - txtConverter = return . uncurry toTxtValue opCtxMap = _gOpCtxMap gCtx fldMap = _gFields gCtx diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index ad4ed3f0d3c19..12b229f5c83cc 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -24,6 +24,7 @@ module Hasura.GraphQL.Resolve.Context , Convert , runConvert , prepare + , txtConverter , module Hasura.GraphQL.Utils ) where @@ -134,6 +135,9 @@ prepare (colTy, colVal) = do put (preparedArgs Seq.|> binEncoder colVal) return $ toPrepParam (Seq.length preparedArgs + 1) colTy +txtConverter :: Monad m => PrepFn m +txtConverter = return . uncurry toTxtValue + runConvert :: (MonadError QErr m) => (FieldMap, OrdByCtx, InsCtxMap) -> Convert a -> m (a, PrepArgs) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs b/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs index 09577dba8c4a8..9d90dbda37f46 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs @@ -49,6 +49,7 @@ data InsCtx , icSet :: !PreSetCols , icRelations :: !RelationInfoMap , icUpdPerm :: !(Maybe UpdPermForIns) + , icUniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) type InsCtxMap = Map.HashMap QualifiedTable InsCtx diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs index 36096b47d2f54..223cbd9628a09 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs @@ -18,7 +18,6 @@ import qualified Language.GraphQL.Draft.Syntax as G import qualified Database.PG.Query as Q import qualified Hasura.RQL.DML.Insert as RI import qualified Hasura.RQL.DML.Returning as RR -import qualified Hasura.RQL.DML.Select as RS import qualified Hasura.RQL.GBoolExp as RB import qualified Hasura.SQL.DML as S @@ -30,6 +29,7 @@ import Hasura.GraphQL.Resolve.Select import Hasura.GraphQL.Validate.Field import Hasura.GraphQL.Validate.Types import Hasura.RQL.DML.Internal (dmlTxErrorHandler) +import Hasura.RQL.DML.Mutation import Hasura.RQL.GBoolExp (toSQLBoolExp) import Hasura.RQL.Types import Hasura.SQL.Types @@ -48,11 +48,18 @@ data AnnIns a , _aiView :: !QualifiedTable , _aiTableCols :: ![PGColInfo] , _aiDefVals :: !(Map.HashMap PGCol S.SQLExp) - } deriving (Show, Eq) + , _aiUniqCols :: !(Maybe [PGColInfo]) + } deriving (Show, Eq, Functor, Foldable, Traversable) type SingleObjIns = AnnIns AnnInsObj type MultiObjIns = AnnIns [AnnInsObj] +singleToMulti :: SingleObjIns -> MultiObjIns +singleToMulti = fmap pure + +multiToSingles :: MultiObjIns -> [SingleObjIns] +multiToSingles = sequenceA + data RelIns a = RelIns { _riAnnIns :: !a @@ -64,11 +71,10 @@ type ArrRelIns = RelIns MultiObjIns type PGColWithValue = (PGCol, PGColValue) -data InsWithExp - = InsWithExp - { _iweExp :: !S.CTE - , _iweConflictCtx :: !(Maybe RI.ConflictCtx) - , _iwePrepArgs :: !(Seq.Seq Q.PrepArg) +data CTEExp + = CTEExp + { _iweExp :: !S.CTE + , _iwePrepArgs :: !(Seq.Seq Q.PrepArg) } deriving (Show, Eq) data AnnInsObj @@ -114,14 +120,14 @@ traverseInsObj rim (gName, annVal) defVal@(AnnInsObj cols objRels arrRels) = throw500 $ "relation " <> relName <<> " not found" let rTable = riRTable relInfo - InsCtx rtView rtCols rtDefVals rtRelInfoMap rtUpdPerm <- getInsCtx rTable + InsCtx rtView rtCols rtDefVals rtRelInfoMap rtUpdPerm rtUniqCols <- getInsCtx rTable withPathK (G.unName gName) $ case riType relInfo of ObjRel -> do dataObj <- asObject dataVal annDataObj <- mkAnnInsObj rtRelInfoMap dataObj ccM <- forM onConflictM $ parseOnConflict rTable rtUpdPerm - let singleObjIns = AnnIns annDataObj ccM rtView rtCols rtDefVals + let singleObjIns = AnnIns annDataObj ccM rtView rtCols rtDefVals rtUniqCols objRelIns = RelIns singleObjIns relInfo return (AnnInsObj cols (objRelIns:objRels) arrRels) @@ -132,7 +138,7 @@ traverseInsObj rim (gName, annVal) defVal@(AnnInsObj cols objRels arrRels) = dataObj <- asObject arrDataVal mkAnnInsObj rtRelInfoMap dataObj ccM <- forM onConflictM $ parseOnConflict rTable rtUpdPerm - let multiObjIns = AnnIns annDataObjs ccM rtView rtCols rtDefVals + let multiObjIns = AnnIns annDataObjs ccM rtView rtCols rtDefVals rtUniqCols arrRelIns = RelIns multiObjIns relInfo return (AnnInsObj cols objRels (arrRelIns:arrRels)) -- if array relation insert input data has empty objects @@ -182,17 +188,17 @@ mkSQLRow defVals withPGCol = mkInsertQ :: MonadError QErr m => QualifiedTable -> Maybe RI.ConflictClauseP1 -> [(PGCol, AnnGValue)] -> [PGCol] -> Map.HashMap PGCol S.SQLExp -> RoleName - -> m InsWithExp + -> m (CTEExp, Maybe RI.ConflictCtx) mkInsertQ vn onConflictM insCols tableCols defVals role = do (givenCols, args) <- flip runStateT Seq.Empty $ toSQLExps insCols let sqlConflict = RI.toSQLConflict <$> onConflictM sqlExps = mkSQLRow defVals givenCols sqlInsert = S.SQLInsert vn tableCols [sqlExps] sqlConflict $ Just S.returningStar - adminIns = return $ InsWithExp (S.CTEInsert sqlInsert) Nothing args + adminIns = return (CTEExp (S.CTEInsert sqlInsert) args, Nothing) nonAdminInsert = do ccM <- mapM RI.extractConflictCtx onConflictM let cteIns = S.CTEInsert sqlInsert{S.siConflict=Nothing} - return $ InsWithExp cteIns ccM args + return (CTEExp cteIns args, ccM) bool nonAdminInsert adminIns $ isAdmin role @@ -200,6 +206,7 @@ mkBoolExp :: (MonadError QErr m, MonadState PrepArgs m) => QualifiedTable -> [(PGColInfo, PGColValue)] -> m S.BoolExp +mkBoolExp _ [] = return $ S.BELit False mkBoolExp tn colInfoVals = RB.toSQLBoolExp (S.mkQual tn) . BoolAnd <$> mapM (fmap BoolFld . uncurry f) colInfoVals @@ -207,52 +214,57 @@ mkBoolExp tn colInfoVals = f ci@(PGColInfo _ colTy _) colVal = AVCol ci . pure . AEQ True <$> prepare (colTy, colVal) -mkSelQ :: MonadError QErr m => QualifiedTable - -> [PGColInfo] -> [PGColWithValue] -> m InsWithExp -mkSelQ tn allColInfos pgColsWithVal = do - (whereExp, args) <- flip runStateT Seq.Empty $ mkBoolExp tn colWithInfos +asSingleObject + :: MonadError QErr m + => [ColVals] -> m (Maybe ColVals) +asSingleObject = \case + [] -> return Nothing + [a] -> return $ Just a + _ -> throw500 "more than one row returned" + +fetchFromColVals + :: MonadError QErr m + => ColVals + -> [PGColInfo] + -> (PGColInfo -> a) + -> m [(a, PGColValue)] +fetchFromColVals colVal reqCols f = + forM reqCols $ \ci -> do + let valM = Map.lookup (pgiName ci) colVal + val <- onNothing valM $ throw500 $ "column " + <> pgiName ci <<> " not found in given colVal" + pgColVal <- RB.pgValParser (pgiType ci) val + return (f ci, pgColVal) + +mkSelCTE + :: MonadError QErr m + => QualifiedTable + -> [PGColInfo] + -> Maybe ColVals + -> m CTEExp +mkSelCTE tn uniqCols colValM = do + (whereExp, args) <- case colValM of + Nothing -> return (S.BELit False, Seq.empty) + Just colVal -> do + colInfoWithVals <- fetchFromColVals colVal uniqCols id + flip runStateT Seq.Empty $ mkBoolExp tn colInfoWithVals let sqlSel = S.mkSelect { S.selExtr = [S.selectStar] , S.selFrom = Just $ S.mkSimpleFromExp tn , S.selWhere = Just $ S.WhereFrag whereExp } - return $ InsWithExp (S.CTESelect sqlSel) Nothing args - where - colWithInfos = mergeListsWith pgColsWithVal allColInfos - (\(c, _) ci -> c == pgiName ci) - (\(_, v) ci -> (ci, v)) + return $ CTEExp (S.CTESelect sqlSel) args -execWithExp +execCTEExp :: QualifiedTable - -> InsWithExp + -> CTEExp -> RR.MutFlds -> Q.TxE QErr RespBody -execWithExp tn (InsWithExp withExp ccM args) flds = do - RI.setConflictCtx ccM +execCTEExp tn (CTEExp cteExp args) flds = runIdentity . Q.getRow <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sqlBuilder) (toList args) True where - sqlBuilder = toSQL $ RR.mkSelWith tn withExp flds True - - -insertAndRetCols - :: QualifiedTable - -> InsWithExp - -> T.Text - -> [PGColInfo] - -> Q.TxE QErr [PGColWithValue] -insertAndRetCols tn withExp errMsg retCols = do - resBS <- execWithExp tn withExp [("response", RR.MRet annSelFlds)] - insResp <- decodeFromBS resBS - resObj <- onNothing (_irResponse insResp) $ throwVE errMsg - forM retCols $ \(PGColInfo col colty _) -> do - val <- onNothing (Map.lookup (getPGColTxt col) resObj) $ - throw500 $ "column " <> col <<> "not returned by postgres" - pgColVal <- RB.pgValParser colty val - return (col, pgColVal) - where - annSelFlds = flip map retCols $ \pgci -> - (fromPGCol $ pgiName pgci, RS.FCol pgci) + sqlBuilder = toSQL $ RR.mkSelWith tn cteExp flds True -- | validate an insert object based on insert columns, -- | insert object relations and additional columns from parent @@ -288,23 +300,33 @@ insertObjRel -> Q.TxE QErr (Int, [PGColWithValue]) insertObjRel role objRelIns = withPathK relNameTxt $ do - (aRows, withExp) <- insertObj role tn singleObjIns [] - let errMsg = "cannot proceed to insert object relation " - <> relName <<> " since insert to table " <> tn <<> " affects zero rows" - retColsWithVals <- insertAndRetCols tn withExp errMsg $ - getColInfos rCols allCols + resp <- insertMultipleObjects role tn multiObjIns [] mutFlds "data" + MutateResp aRows colVals <- decodeFromBS resp + colValM <- asSingleObject colVals + colVal <- onNothing colValM $ throw400 NotSupported errMsg + retColsWithVals <- fetchFromColVals colVal rColInfos pgiName let c = mergeListsWith mapCols retColsWithVals (\(_, rCol) (col, _) -> rCol == col) - (\(lCol, _) (_, colVal) -> (lCol, colVal)) + (\(lCol, _) (_, cVal) -> (lCol, cVal)) return (aRows, c) where RelIns singleObjIns relInfo = objRelIns + multiObjIns = singleToMulti singleObjIns relName = riName relInfo relNameTxt = getRelTxt relName mapCols = riMapping relInfo tn = riRTable relInfo - rCols = map snd mapCols allCols = _aiTableCols singleObjIns + rCols = map snd mapCols + rColInfos = getColInfos rCols allCols + errMsg = "cannot proceed to insert object relation " + <> relName <<> " since insert to table " + <> tn <<> " affects zero rows" + mutFlds = [ ("affected_rows", RR.MCount) + , ( "returning_columns" + , RR.MRet $ RR.pgColsToSelFlds rColInfos + ) + ] -- | insert an array relationship and return affected rows insertArrRel @@ -335,7 +357,7 @@ insertObj -> QualifiedTable -> SingleObjIns -> [PGColWithValue] -- ^ additional fields - -> Q.TxE QErr (Int, InsWithExp) + -> Q.TxE QErr (Int, CTEExp) insertObj role tn singleObjIns addCols = do -- validate insert validateInsert (map _1 cols) (map _riRelInfo objRels) $ map fst addCols @@ -344,7 +366,7 @@ insertObj role tn singleObjIns addCols = do objInsRes <- forM objRels $ insertObjRel role -- prepare final insert columns - let objInsAffRows = sum $ map fst objInsRes + let objRelAffRows = sum $ map fst objInsRes objRelDeterminedCols = concatMap snd objInsRes objRelInsCols = mkPGColWithTypeAndVal allCols objRelDeterminedCols addInsCols = mkPGColWithTypeAndVal allCols addCols @@ -355,29 +377,31 @@ insertObj role tn singleObjIns addCols = do arrDepColsWithInfo = getColInfos arrDepCols allCols -- prepare insert query as with expression - insQ <- mkInsertQ vn onConflictM finalInsCols (map pgiName allCols) defVals role + (CTEExp cte insPArgs, ccM) <- + mkInsertQ vn onConflictM finalInsCols (map pgiName allCols) defVals role + uniqCols <- onNothing uniqColsM $ throw500 "unique columns not found in relational insert" + + RI.setConflictCtx ccM + MutateResp affRows colVals <- + mutateAndFetchCols tn (uniqCols `union` arrDepColsWithInfo) (cte, insPArgs) + colValM <- asSingleObject colVals + cteExp <- mkSelCTE tn uniqCols colValM - let preArrRelInsAffRows = objInsAffRows + 1 - insertWithArrRels = withArrRels preArrRelInsAffRows insQ - arrDepColsWithInfo - insertWithoutArrRels = return (preArrRelInsAffRows, insQ) + arrRelAffRows <- bool (withArrRels arrDepColsWithInfo colValM) (return 0) $ null arrRels + let totAffRows = objRelAffRows + affRows + arrRelAffRows - -- insert object - bool insertWithArrRels insertWithoutArrRels $ null arrDepColsWithInfo + return (totAffRows, cteExp) where - AnnIns annObj onConflictM vn allCols defVals = singleObjIns + AnnIns annObj onConflictM vn allCols defVals uniqColsM = singleObjIns AnnInsObj cols objRels arrRels = annObj - withArrRels preAffRows insQ arrDepColsWithType = do - arrDepColsWithVal <- - insertAndRetCols tn insQ cannotInsArrRelErr arrDepColsWithType + withArrRels arrDepCols colValM = do + colVal <- onNothing colValM $ throw400 NotSupported cannotInsArrRelErr + arrDepColsWithVal <- fetchFromColVals colVal arrDepCols pgiName arrInsARows <- forM arrRels $ insertArrRel role arrDepColsWithVal - let totalAffRows = preAffRows + sum arrInsARows - - selQ <- mkSelQ tn allCols arrDepColsWithVal - return (totalAffRows, selQ) + return $ sum arrInsARows cannotInsArrRelErr = "cannot proceed to insert array relations since insert to table " @@ -396,9 +420,8 @@ insertMultipleObjects insertMultipleObjects role tn multiObjIns addCols mutFlds errP = bool withoutRelsInsert withRelsInsert anyRelsToInsert where - AnnIns insObjs onConflictM vn tableColInfos defVals = multiObjIns - singleObjInserts = flip map insObjs $ \o -> - AnnIns o onConflictM vn tableColInfos defVals + AnnIns insObjs onConflictM vn tableColInfos defVals uniqCols = multiObjIns + singleObjInserts = multiToSingles multiObjIns insCols = map _aioColumns insObjs allInsObjRels = concatMap _aioObjRels insObjs allInsArrRels = concatMap _aioArrRels insObjs @@ -419,7 +442,7 @@ insertMultipleObjects role tn multiObjIns addCols mutFlds errP = rowsWithCol <- mapM (toSQLExps . map pgColToAnnGVal) withAddCols return $ map (mkSQLRow defVals) rowsWithCol - let insQP1 = RI.InsertQueryP1 tn vn tableCols sqlRows onConflictM mutFlds + let insQP1 = RI.InsertQueryP1 tn vn tableCols sqlRows onConflictM mutFlds uniqCols p1 = (insQP1, prepArgs) bool (RI.nonAdminInsert p1) (RI.insertP2 p1) $ isAdmin role @@ -429,10 +452,10 @@ insertMultipleObjects role tn multiObjIns addCols mutFlds errP = insertObj role tn objIns addCols let affRows = sum $ map fst insResps - withExps = map snd insResps + cteExps = map snd insResps retFlds = mapMaybe getRet mutFlds - rawResps <- forM withExps - $ \withExp -> execWithExp tn withExp retFlds + rawResps <- forM cteExps + $ \cteExp -> execCTEExp tn cteExp retFlds respVals :: [J.Object] <- mapM decodeFromBS rawResps respTups <- forM mutFlds $ \(t, mutFld) -> do jsonVal <- case mutFld of @@ -462,11 +485,11 @@ convertInsert role tn fld = prefixErrPath fld $ do bool (withNonEmptyObjs annVals mutFlds) (buildEmptyMutResp mutFlds) $ null annVals where withNonEmptyObjs annVals mutFlds = do - InsCtx vn tableCols defValMap relInfoMap updPerm <- getInsCtx tn + InsCtx vn tableCols defValMap relInfoMap updPerm uniqCols <- getInsCtx tn annObjs <- mapM asObject annVals annInsObjs <- forM annObjs $ mkAnnInsObj relInfoMap conflictClauseM <- forM onConflictM $ parseOnConflict tn updPerm - let multiObjIns = AnnIns annInsObjs conflictClauseM vn tableCols defValMap + let multiObjIns = AnnIns annInsObjs conflictClauseM vn tableCols defValMap uniqCols return $ prefixErrPath fld $ insertMultipleObjects role tn multiObjIns [] mutFlds "objects" diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index d64f7e9b37e0b..9c56cffc0b6e5 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -37,7 +37,7 @@ convertMutResp ty selSet = "__typename" -> return $ RR.MExp $ G.unName $ G.unNamedType ty "affected_rows" -> return RR.MCount "returning" -> fmap RR.MRet $ - fromSelSet prepare (_fType fld) $ _fSelSet fld + fromSelSet txtConverter (_fType fld) $ _fSelSet fld G.Name t -> throw500 $ "unexpected field in mutation resp : " <> t convertRowObj @@ -127,14 +127,14 @@ convertUpdate opCtx fld = do unless (any isJust updExpsM || not (null preSetItems)) $ throwVE $ "atleast any one of _set, _inc, _append, _prepend, _delete_key, _delete_elem and " <> " _delete_at_path operator is expected" - let p1 = RU.UpdateQueryP1 tn setItems (filterExp, whereExp) mutFlds + let p1 = RU.UpdateQueryP1 tn setItems (filterExp, whereExp) mutFlds uniqCols whenNonEmptyItems = return $ RU.updateQueryToTx (p1, prepArgs) whenEmptyItems = buildEmptyMutResp mutFlds -- if there are not set items then do not perform -- update and return empty mutation response bool whenNonEmptyItems whenEmptyItems $ null setItems where - UpdOpCtx tn _ filterExp preSetCols = opCtx + UpdOpCtx tn _ filterExp preSetCols uniqCols = opCtx args = _fArguments fld preSetItems = Map.toList preSetCols @@ -146,10 +146,10 @@ convertDelete opCtx fld = do whereExp <- withArg (_fArguments fld) "where" (parseBoolExp prepare) mutFlds <- convertMutResp (_fType fld) $ _fSelSet fld args <- get - let p1 = RD.DeleteQueryP1 tn (filterExp, whereExp) mutFlds + let p1 = RD.DeleteQueryP1 tn (filterExp, whereExp) mutFlds uniqCols return $ RD.deleteQueryToTx (p1, args) where - DelOpCtx tn _ filterExp = opCtx + DelOpCtx tn _ filterExp uniqCols = opCtx -- | build mutation response for empty objects buildEmptyMutResp :: Monad m => RR.MutFlds -> m RespTx diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index ecb3b56a0b289..1b1fab90ddb16 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -97,9 +97,9 @@ getValidCols = fst . validPartitionFieldInfoMap getValidRels :: FieldInfoMap -> [RelInfo] getValidRels = snd . validPartitionFieldInfoMap -mkValidConstraints :: [ConstraintName] -> [ConstraintName] +mkValidConstraints :: [TableConstraint] -> [TableConstraint] mkValidConstraints = - filter (isValidName . G.Name . getConstraintTxt) + filter (isValidName . G.Name . getConstraintTxt . tcName) isRelNullable :: FieldInfoMap -> RelInfo -> Bool isRelNullable fim ri = isNullable @@ -149,6 +149,10 @@ mkTableTy :: QualifiedTable -> G.NamedType mkTableTy = G.NamedType . qualObjectToName +mkTableColTy :: QualifiedTable -> G.NamedType +mkTableColTy tn = + G.NamedType $ qualObjectToName tn <> "_columns" + mkTableAggTy :: QualifiedTable -> G.NamedType mkTableAggTy tn = G.NamedType $ qualObjectToName tn <> "_aggregate" @@ -256,6 +260,26 @@ mkTableObj tn allowedFlds = mkRelFld allowAgg relInfo isNullable desc = G.Description $ "columns and relationships of " <>> tn +{- +type table_columns { + col1: colty1 + . + . + coln: coltyn +} +-} + +mkTableColObj + :: QualifiedTable + -> [PGColInfo] + -> ObjTyInfo +mkTableColObj tn allowedCols = + mkHsraObjTyInfo (Just desc) (mkTableColTy tn) Set.empty $ + mapFromL _fiName flds + where + flds = map mkPGColFld allowedCols + desc = G.Description $ "columns of " <>> tn + {- type table_aggregate { agg: table_aggregate_fields @@ -479,8 +503,9 @@ type table_mutation_response { mkMutRespObj :: QualifiedTable -> Bool -- is sel perm defined + -> Bool -- is nested allowed -> ObjTyInfo -mkMutRespObj tn sel = +mkMutRespObj tn sel nestAlwd = mkHsraObjTyInfo (Just objDesc) (mkMutRespTy tn) Set.empty $ mapFromL _fiName $ affectedRowsFld : bool [] [returningFld] sel where @@ -493,9 +518,10 @@ mkMutRespObj tn sel = desc = "number of affected rows by the mutation" returningFld = mkHsraObjFldInfo (Just desc) "returning" Map.empty $ - G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkTableTy tn + G.toGT $ G.toNT $ G.toLT $ G.toNT retTy where desc = "data of the affected rows by the mutation" + retTy = bool (mkTableColTy tn) (mkTableTy tn) nestAlwd -- table_bool_exp mkBoolExpInp @@ -928,10 +954,11 @@ mkInsInp :: QualifiedTable -> InsCtx -> InpObjTyInfo mkInsInp tn insCtx = mkHsraInpTyInfo (Just desc) (mkInsInpTy tn) $ fromInpValL $ - map mkPGColInp insCols <> relInps + map mkPGColInp insCols <> bool [] relInps alwNestedIns where desc = G.Description $ "input type for inserting data into table " <>> tn + alwNestedIns = isJust $ icUniqCols insCtx cols = icColumns insCtx setCols = Map.keys $ icSet insCtx insCols = flip filter cols $ \ci -> pgiName ci `notElem` setCols @@ -1216,6 +1243,8 @@ mkOnConflictTypes tn uniqueOrPrimaryCons cols = mkGCtxRole' :: QualifiedTable + -- all columns + -> [PGColInfo] -- insert perm -> Maybe (InsCtx, Bool) -- select permission @@ -1227,21 +1256,24 @@ mkGCtxRole' -- primary key columns -> [PGColInfo] -- constraints - -> [ConstraintName] + -> [TableConstraint] -> Maybe ViewInfo -- all functions -> [FunctionInfo] -> TyAgg -mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM funcs = - TyAgg (mkTyInfoMap allTypes) fieldMap scalars ordByCtx +mkGCtxRole' tn allCols insPermM selPermM updColsM + delPermM pkeyCols constraints viM funcs = + TyAgg (mkTyInfoMap allTypes) fieldMap scalars ordByCtx where ordByCtx = fromMaybe Map.empty ordByCtxM upsertPerm = or $ fmap snd insPermM - isUpsertable = upsertable constraints upsertPerm $ isJust viM + alwNestMutFld = isJust $ getUniqCols allCols constraints + constNames = map tcName constraints + isUpsertable = upsertable constNames upsertPerm $ isJust viM updatableCols = maybe [] (map pgiName) updColsM - onConflictTypes = mkOnConflictTypes tn constraints updatableCols isUpsertable + onConflictTypes = mkOnConflictTypes tn constNames updatableCols isUpsertable jsonOpTys = fromMaybe [] updJSONOpInpObjTysM relInsInpObjTys = maybe [] (map TIInpObj) $ mutHelper viIsInsertable relInsInpObjsM @@ -1256,6 +1288,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func [ TIInpObj <$> boolExpInpObjM , TIInpObj <$> ordByInpObjM , TIObj <$> selObjM + , TIObj <$> selColObjM ] aggQueryTypes = map TIObj aggObjs <> map TIInpObj aggOrdByInps @@ -1273,6 +1306,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func fieldMap = Map.unions $ catMaybes [ insInpObjFldsM, updSetInpObjFldsM , boolExpInpObjFldsM , selObjFldsM + , selColObjFldsM ] scalars = Set.unions [selByPkScalarSet, funcArgScalarSet] @@ -1338,7 +1372,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func -- mut resp obj mutRespObjM = if isMut - then Just $ mkMutRespObj tn $ isJust selFldsM + then Just $ mkMutRespObj tn (isJust selFldsM) alwNestMutFld else Nothing isMut = (isJust insColsM || isJust updColsM || isJust delPermM) @@ -1346,6 +1380,11 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func -- table obj selObjM = mkTableObj tn <$> selFldsM + -- table columns obj + selColObjM = if not alwNestMutFld then + (mkTableColObj tn . lefts) <$> selFldsM + else Nothing + -- aggregate objs and order by inputs (aggObjs, aggOrdByInps) = case selPermM of Just (True, selFlds) -> @@ -1376,6 +1415,10 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func in numFldsObjs <> compFldsObjs -- the fields used in table object selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM + -- the fields used in table_columns object + selColObjFldsM = if not alwNestMutFld then + (mkColFldMap (mkTableColTy tn) . lefts) <$> selFldsM + else Nothing -- the scalar set for table_by_pk arguments selByPkScalarSet = Set.fromList $ map pgiType pkeyCols @@ -1387,7 +1430,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM func getRootFldsRole' :: QualifiedTable -> [PGCol] - -> [ConstraintName] + -> [TableConstraint] -> FieldInfoMap -> [FunctionInfo] -> Maybe ([T.Text], Bool) -- insert perm @@ -1418,19 +1461,21 @@ getRootFldsRole' tn primCols constraints fields funcs insM selM updM delM viM = bool Nothing (getDet <$> mutM) $ isMutable f viM colInfos = fst $ validPartitionFieldInfoMap fields + constNames = map tcName constraints + uniqCols = getUniqCols colInfos constraints getInsDet (hdrs, upsertPerm) = - let isUpsertable = upsertable constraints upsertPerm $ isJust viM + let isUpsertable = upsertable constNames upsertPerm $ isJust viM in ( OCInsert $ InsOpCtx tn $ hdrs `union` maybe [] (\(_, _, _, x) -> x) updM , Right $ mkInsMutFld tn isUpsertable ) getUpdDet (updCols, preSetCols, updFltr, hdrs) = - ( OCUpdate $ UpdOpCtx tn hdrs updFltr preSetCols + ( OCUpdate $ UpdOpCtx tn hdrs updFltr preSetCols uniqCols , Right $ mkUpdMutFld tn $ getColInfos updCols colInfos ) getDelDet (delFltr, hdrs) = - ( OCDelete $ DelOpCtx tn hdrs delFltr + ( OCDelete $ DelOpCtx tn hdrs delFltr uniqCols , Right $ mkDelMutFld tn ) getSelDet (selFltr, pLimit, hdrs, _) = @@ -1504,8 +1549,13 @@ getSelPerm tableCache fields role selPermInfo = do mkInsCtx :: MonadError QErr m => RoleName - -> TableCache -> FieldInfoMap -> InsPermInfo -> Maybe UpdPermInfo -> m InsCtx -mkInsCtx role tableCache fields insPermInfo updPermM = do + -> TableCache + -> FieldInfoMap + -> InsPermInfo + -> Maybe [PGColInfo] + -> Maybe UpdPermInfo + -> m InsCtx +mkInsCtx role tableCache fields insPermInfo uniqCols updPermM = do relTupsM <- forM rels $ \relInfo -> do let remoteTable = riRTable relInfo relName = riName relInfo @@ -1516,7 +1566,7 @@ mkInsCtx role tableCache fields insPermInfo updPermM = do isInsertable insPermM viewInfoM let relInfoMap = Map.fromList $ catMaybes relTupsM - return $ InsCtx iView cols setCols relInfoMap updPermForIns + return $ InsCtx iView cols setCols relInfoMap updPermForIns uniqCols where cols = getValidCols fields rels = getValidRels fields @@ -1531,8 +1581,12 @@ mkInsCtx role tableCache fields insPermInfo updPermM = do mkAdminInsCtx :: MonadError QErr m - => QualifiedTable -> TableCache -> FieldInfoMap -> m InsCtx -mkAdminInsCtx tn tc fields = do + => QualifiedTable + -> TableCache + -> FieldInfoMap + -> Maybe [PGColInfo] + -> m InsCtx +mkAdminInsCtx tn tc fields uniqCols = do relTupsM <- forM rels $ \relInfo -> do let remoteTable = riRTable relInfo relName = riName relInfo @@ -1544,7 +1598,7 @@ mkAdminInsCtx tn tc fields = do let relInfoMap = Map.fromList $ catMaybes relTupsM updPerm = UpdPermForIns (map pgiName cols) noFilter Map.empty - return $ InsCtx tn cols Map.empty relInfoMap $ Just updPerm + return $ InsCtx tn cols Map.empty relInfoMap (Just updPerm) uniqCols where cols = getValidCols fields rels = getValidRels fields @@ -1555,7 +1609,7 @@ mkGCtxRole -> QualifiedTable -> FieldInfoMap -> [PGCol] - -> [ConstraintName] + -> [TableConstraint] -> [FunctionInfo] -> Maybe ViewInfo -> RoleName @@ -1564,24 +1618,26 @@ mkGCtxRole mkGCtxRole tableCache tn fields pCols constraints funcs viM role permInfo = do selPermM <- mapM (getSelPerm tableCache fields role) $ _permSel permInfo tabInsCtxM <- forM (_permIns permInfo) $ \ipi -> do - tic <- mkInsCtx role tableCache fields ipi $ _permUpd permInfo + tic <- mkInsCtx role tableCache fields ipi uniqCols $ _permUpd permInfo return (tic, isJust $ _permUpd permInfo) let updColsM = filterColInfos . upiCols <$> _permUpd permInfo - tyAgg = mkGCtxRole' tn tabInsCtxM selPermM updColsM + tyAgg = mkGCtxRole' tn allCols tabInsCtxM selPermM updColsM (void $ _permDel permInfo) pColInfos constraints viM funcs rootFlds = getRootFldsRole tn pCols constraints fields funcs viM permInfo insCtxMap = maybe Map.empty (Map.singleton tn) $ fmap fst tabInsCtxM return (tyAgg, rootFlds, insCtxMap) where + allCols = getCols fields + uniqCols = getUniqCols allCols constraints colInfos = getValidCols fields - pColInfos = getColInfos pCols colInfos + pColInfos = getColInfos pCols allCols filterColInfos allowedSet = filter ((`Set.member` allowedSet) . pgiName) colInfos getRootFldsRole :: QualifiedTable -> [PGCol] - -> [ConstraintName] + -> [TableConstraint] -> FieldInfoMap -> [FunctionInfo] -> Maybe ViewInfo @@ -1610,19 +1666,21 @@ mkGCtxMapTable -> FunctionCache -> TableInfo -> m (Map.HashMap RoleName (TyAgg, RootFlds, InsCtxMap)) -mkGCtxMapTable tableCache funcCache (TableInfo tn _ fields rolePerms constraints pkeyCols viewInfo _) = do +mkGCtxMapTable tableCache funcCache tabInfo = do m <- Map.traverseWithKey (mkGCtxRole tableCache tn fields pkeyCols validConstraints tabFuncs viewInfo) rolePerms - adminInsCtx <- mkAdminInsCtx tn tableCache fields - let adminCtx = mkGCtxRole' tn (Just (adminInsCtx, True)) + adminInsCtx <- mkAdminInsCtx tn tableCache fields $ getUniqCols allCols constraints + let adminCtx = mkGCtxRole' tn allCols (Just (adminInsCtx, True)) (Just (True, selFlds)) (Just colInfos) (Just ()) pkeyColInfos validConstraints viewInfo tabFuncs adminInsCtxMap = Map.singleton tn adminInsCtx return $ Map.insert adminRole (adminCtx, adminRootFlds, adminInsCtxMap) m where + TableInfo tn _ fields rolePerms constraints pkeyCols viewInfo _ = tabInfo validConstraints = mkValidConstraints constraints + allCols = getCols fields colInfos = getValidCols fields - allCols = map pgiName colInfos + validColNames = map pgiName colInfos pkeyColInfos = getColInfos pkeyCols colInfos tabFuncs = filter (isValidObjectName . fiName) $ getFuncsOfTable tn funcCache @@ -1632,7 +1690,7 @@ mkGCtxMapTable tableCache funcCache (TableInfo tn _ fields rolePerms constraints adminRootFlds = getRootFldsRole' tn pkeyCols validConstraints fields tabFuncs (Just ([], True)) (Just (noFilter, Nothing, [], True)) - (Just (allCols, mempty, noFilter, [])) (Just (noFilter, [])) + (Just (validColNames, mempty, noFilter, [])) (Just (noFilter, [])) viewInfo noFilter :: AnnBoolExpSQL diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs index 560db3558bcfb..6467da7c2d95a 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Diff.hs @@ -48,6 +48,7 @@ data ConstraintMeta { cmName :: !ConstraintName , cmOid :: !Int , cmType :: !ConstraintType + , cmCols :: ![PGCol] } deriving (Show, Eq) $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''ConstraintMeta) @@ -99,11 +100,27 @@ fetchTableMeta = do json_build_object( 'name', tc.constraint_name, 'oid', r.oid::integer, - 'type', tc.constraint_type + 'type', tc.constraint_type, + 'cols', tc.columns ) ) as constraints FROM - information_schema.table_constraints tc + ( + SELECT table_name, table_schema, + constraint_name, columns, + 'PRIMARY KEY' as constraint_type + FROM hdb_catalog.hdb_primary_key + UNION ALL + SELECT table_name, table_schema, + constraint_name, columns, + 'UNIQUE' as constraint_type + FROM hdb_catalog.hdb_unique_constraint + UNION ALL + SELECT table_name, table_schema, + constraint_name, '[]'::json as columns, + 'FOREIGN KEY' as constraint_type + FROM hdb_catalog.hdb_foreign_key_constraint + ) tc JOIN pg_catalog.pg_constraint r ON tc.constraint_name = r.conname GROUP BY @@ -114,8 +131,8 @@ fetchTableMeta = do AND t.table_schema <> 'information_schema' AND t.table_schema <> 'hdb_catalog' |] () False - forM res $ \(ts, tn, toid, cols, constrnts) -> - return $ TableMeta toid (QualifiedObject ts tn) (Q.getAltJ cols) (Q.getAltJ constrnts) + forM res $ \(ts, tn, toid, cols, constrnts) + -> return $ TableMeta toid (QualifiedObject ts tn) (Q.getAltJ cols) (Q.getAltJ constrnts) getOverlap :: (Eq k, Hashable k) => (v -> k) -> [v] -> [v] -> [(v, v)] getOverlap getKey left right = @@ -139,7 +156,7 @@ data TableDiff -- The final list of uniq/primary constraint names -- used for generating types on_conflict clauses -- TODO: this ideally should't be part of TableDiff - , _tdUniqOrPriCons :: ![ConstraintName] + , _tdUniqOrPriCons :: ![TableConstraint] } deriving (Show, Eq) getTableDiff :: TableMeta -> TableMeta -> TableDiff @@ -152,7 +169,9 @@ getTableDiff oldtm newtm = newCols = tmColumns newtm uniqueOrPrimaryCons = - [cmName cm | cm <- tmConstraints newtm, isUniqueOrPrimary $ cmType cm] + [ TableConstraint (cmType cm) (cmName cm) (cmCols cm) + | cm <- tmConstraints newtm, isUniqueOrPrimary (cmType cm) + ] droppedCols = map pcmColumnName $ getDifference pcmOrdinalPosition oldCols newCols @@ -167,7 +186,7 @@ getTableDiff oldtm newtm = alteredCols = flip map (filter (uncurry (/=)) existingCols) $ \(pcmo, pcmn) -> - (pcmToPci pcmo, pcmToPci pcmn) + (pcmToPci pcmo, pcmToPci pcmn) droppedFKeyConstraints = map cmName $ filter (isForeignKey . cmType) $ getDifference cmOid @@ -225,7 +244,7 @@ getSchemaChangeDeps schemaDiff = do where SchemaDiff droppedTables alteredTables = schemaDiff - isDirectDep (SOTableObj tn _) = tn `HS.member` (HS.fromList droppedTables) + isDirectDep (SOTableObj tn _) = tn `HS.member` HS.fromList droppedTables isDirectDep _ = False data FunctionMeta @@ -241,7 +260,7 @@ funcFromMeta :: FunctionMeta -> QualifiedFunction funcFromMeta fm = QualifiedObject (fmSchema fm) (fmName fm) fetchFunctionMeta :: Q.Tx [FunctionMeta] -fetchFunctionMeta = do +fetchFunctionMeta = map (Q.getAltJ . runIdentity) <$> Q.listQ [Q.sql| SELECT json_build_object( diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs index 8535d0188a47a..a15238a116525 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Table.hs @@ -52,8 +52,8 @@ getTableInfo qt@(QualifiedObject sn tn) isSystemDefined = do Q.listQ $(Q.sqlFromFile "src-rsr/table_info.sql")(sn, tn) True case tableData of [] -> throw400 NotExists $ "no such table/view exists in postgres : " <>> qt - [(Q.AltJ cols, Q.AltJ pkeyCols, Q.AltJ cons, Q.AltJ viewInfoM)] -> - return $ mkTableInfo qt isSystemDefined cons cols pkeyCols viewInfoM + [(Q.AltJ cols, Q.AltJ cons, Q.AltJ viewInfoM)] -> + return $ mkTableInfo qt isSystemDefined cons cols viewInfoM _ -> throw500 $ "more than one row found for: " <>> qt newtype TrackTable diff --git a/server/src-lib/Hasura/RQL/DML/Delete.hs b/server/src-lib/Hasura/RQL/DML/Delete.hs index 382919b806877..c2f88ce851604 100644 --- a/server/src-lib/Hasura/RQL/DML/Delete.hs +++ b/server/src-lib/Hasura/RQL/DML/Delete.hs @@ -14,6 +14,7 @@ import qualified Data.Sequence as DS import Hasura.Prelude import Hasura.RQL.DML.Internal +import Hasura.RQL.DML.Mutation import Hasura.RQL.DML.Returning import Hasura.RQL.GBoolExp import Hasura.RQL.Types @@ -24,15 +25,16 @@ import qualified Hasura.SQL.DML as S data DeleteQueryP1 = DeleteQueryP1 - { dqp1Table :: !QualifiedTable - , dqp1Where :: !(AnnBoolExpSQL, AnnBoolExpSQL) - , dqp1MutFlds :: !MutFlds + { dqp1Table :: !QualifiedTable + , dqp1Where :: !(AnnBoolExpSQL, AnnBoolExpSQL) + , dqp1MutFlds :: !MutFlds + , dqp1UniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) -mkSQLDelete - :: DeleteQueryP1 -> S.SelectWith -mkSQLDelete (DeleteQueryP1 tn (fltr, wc) mutFlds) = - mkSelWith tn (S.CTEDelete delete) mutFlds False +mkDeleteCTE + :: DeleteQueryP1 -> S.CTE +mkDeleteCTE (DeleteQueryP1 tn (fltr, wc) _ _) = + S.CTEDelete delete where delete = S.SQLDelete tn Nothing tableFltr $ Just S.returningStar tableFltr = Just $ S.WhereFrag $ @@ -40,10 +42,12 @@ mkSQLDelete (DeleteQueryP1 tn (fltr, wc) mutFlds) = getDeleteDeps :: DeleteQueryP1 -> [SchemaDependency] -getDeleteDeps (DeleteQueryP1 tn (_, wc) mutFlds) = - mkParentDep tn : whereDeps <> retDeps +getDeleteDeps (DeleteQueryP1 tn (_, wc) mutFlds uniqCols) = + mkParentDep tn : uniqColDeps <> whereDeps <> retDeps where whereDeps = getBoolExpDeps tn wc + uniqColDeps = map (mkColDep "on_type" tn) $ + maybe [] (map pgiName) uniqCols retDeps = map (mkColDep "untyped" tn . fst) $ pgColsFromMutFlds mutFlds @@ -70,6 +74,8 @@ validateDeleteQWith prepValBuilder (DeleteQuery tableName rqlBE mRetCols) = do askSelPermInfo tableInfo let fieldInfoMap = tiFieldInfoMap tableInfo + uniqCols = getUniqCols (getCols fieldInfoMap) $ + tiUniqOrPrimConstraints tableInfo -- convert the returning cols into sql returing exp mAnnRetCols <- forM mRetCols $ \retCols -> @@ -81,7 +87,7 @@ validateDeleteQWith prepValBuilder (DeleteQuery tableName rqlBE mRetCols) = do return $ DeleteQueryP1 tableName (dpiFilter delPerm, annSQLBoolExp) - (mkDefaultMutFlds mAnnRetCols) + (mkDefaultMutFlds mAnnRetCols) uniqCols where selNecessaryMsg = @@ -97,10 +103,10 @@ validateDeleteQ = deleteQueryToTx :: (DeleteQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody deleteQueryToTx (u, p) = - runIdentity . Q.getRow - <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder deleteSQL) (toList p) True + runMutation $ Mutation (dqp1Table u) (deleteCTE, p) + (dqp1MutFlds u) (dqp1UniqCols u) where - deleteSQL = toSQL $ mkSQLDelete u + deleteCTE = mkDeleteCTE u runDelete :: (QErrM m, UserInfoM m, CacheRM m, MonadTx m) diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index a0ddf09154465..0adc3c6afd34d 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -11,6 +11,7 @@ import qualified Data.Text.Lazy as LT import Hasura.Prelude import Hasura.RQL.DML.Internal +import Hasura.RQL.DML.Mutation import Hasura.RQL.DML.Returning import Hasura.RQL.GBoolExp import Hasura.RQL.Instances () @@ -38,11 +39,12 @@ data InsertQueryP1 , iqp1Tuples :: ![[S.SQLExp]] , iqp1Conflict :: !(Maybe ConflictClauseP1) , iqp1MutFlds :: !MutFlds + , iqp1UniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) -mkSQLInsert :: InsertQueryP1 -> S.SelectWith -mkSQLInsert (InsertQueryP1 tn vn cols vals c mutFlds) = - mkSelWith tn (S.CTEInsert insert) mutFlds False +mkInsertCTE :: InsertQueryP1 -> S.CTE +mkInsertCTE (InsertQueryP1 _ vn cols vals c _ _) = + S.CTEInsert insert where insert = S.SQLInsert vn cols vals (toSQLConflict <$> c) $ Just S.returningStar @@ -66,7 +68,7 @@ mkDefValMap cim = getInsertDeps :: InsertQueryP1 -> [SchemaDependency] -getInsertDeps (InsertQueryP1 tn _ _ _ _ mutFlds) = +getInsertDeps (InsertQueryP1 tn _ _ _ _ mutFlds _) = mkParentDep tn : retDeps where retDeps = map (mkColDep "untyped" tn . fst) $ @@ -145,7 +147,8 @@ buildConflictClause tableInfo inpCols (OnConflict mTCol mTCons act) = \pgCol -> askPGType fieldInfoMap pgCol "" validateConstraint c = do - let tableConsNames = tiUniqOrPrimConstraints tableInfo + let tableConsNames = map tcName $ + tiUniqOrPrimConstraints tableInfo withPathK "constraint" $ unless (c `elem` tableConsNames) $ throw400 Unexpected $ "constraint " <> getConstraintTxt c @@ -186,6 +189,8 @@ convInsertQuery objsParser prepFn (InsertQuery tableName val oC mRetCols) = do let fieldInfoMap = tiFieldInfoMap tableInfo setInsVals = ipiSet insPerm + uniqCols = getUniqCols (getCols fieldInfoMap) $ + tiUniqOrPrimConstraints tableInfo -- convert the returning cols into sql returing exp mAnnRetCols <- forM mRetCols $ \retCols -> do @@ -214,7 +219,7 @@ convInsertQuery objsParser prepFn (InsertQuery tableName val oC mRetCols) = do buildConflictClause tableInfo inpCols c return $ InsertQueryP1 tableName insView insCols sqlExps - conflictClause mutFlds + conflictClause mutFlds uniqCols where selNecessaryMsg = @@ -237,10 +242,10 @@ convInsQ = insertP2 :: (InsertQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody insertP2 (u, p) = - runIdentity . Q.getRow - <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder insertSQL) (toList p) True + runMutation $ Mutation (iqp1Table u) (insertCTE, p) + (iqp1MutFlds u) (iqp1UniqCols u) where - insertSQL = toSQL $ mkSQLInsert u + insertCTE = mkInsertCTE u data ConflictCtx = CCUpdate !ConstraintName ![PGCol] !PreSetCols !S.BoolExp @@ -295,3 +300,4 @@ runInsert q = do res <- convInsQ q role <- userRole <$> askUserInfo liftTx $ bool (nonAdminInsert res) (insertP2 res) $ isAdmin role + diff --git a/server/src-lib/Hasura/RQL/DML/Mutation.hs b/server/src-lib/Hasura/RQL/DML/Mutation.hs new file mode 100644 index 0000000000000..2b4331f823b83 --- /dev/null +++ b/server/src-lib/Hasura/RQL/DML/Mutation.hs @@ -0,0 +1,105 @@ +module Hasura.RQL.DML.Mutation + ( Mutation(..) + , runMutation + , mutateAndFetchCols + ) +where + +import qualified Data.Sequence as DS + +import Hasura.Prelude +import Hasura.RQL.DML.Internal +import Hasura.RQL.DML.Returning +import Hasura.RQL.DML.Select +import Hasura.RQL.Instances () +import Hasura.RQL.Types +import Hasura.SQL.Types +import Hasura.SQL.Value + +import qualified Data.HashMap.Strict as Map +import qualified Database.PG.Query as Q +import qualified Hasura.SQL.DML as S + +data Mutation + = Mutation + { _mTable :: !QualifiedTable + , _mQuery :: !(S.CTE, DS.Seq Q.PrepArg) + , _mFields :: !MutFlds + , _mUniqCols :: !(Maybe [PGColInfo]) + } deriving (Show, Eq) + +runMutation :: Mutation -> Q.TxE QErr RespBody +runMutation mut = + bool (mutateAndReturn mut) (mutateAndSel mut) $ + hasNestedFld $ _mFields mut + +mutateAndReturn :: Mutation -> Q.TxE QErr RespBody +mutateAndReturn (Mutation qt (cte, p) mutFlds _) = + runIdentity . Q.getRow + <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) + (toList p) True + where + selWith = mkSelWith qt cte mutFlds False + +mutateAndSel :: Mutation -> Q.TxE QErr RespBody +mutateAndSel (Mutation qt q mutFlds mUniqCols) = do + uniqCols <- onNothing mUniqCols $ + throw500 "uniqCols not found in mutateAndSel" + let colMap = Map.fromList $ flip map uniqCols $ + \ci -> (pgiName ci, ci) + -- Perform mutation and fetch unique columns + MutateResp _ colVals <- mutateAndFetchCols qt uniqCols q + colExps <- mapM (colValToColExp colMap) colVals + let selWhere = S.beFromColVal mkQIdenExp colExps + selCTE = S.CTESelect $ + S.mkSelect + { S.selExtr = [S.selectStar] + , S.selFrom = Just $ S.FromExp [S.FISimple qt Nothing] + , S.selWhere = Just $ S.WhereFrag selWhere + } + selWith = mkSelWith qt selCTE mutFlds False + -- Perform select query and fetch returning fields + runIdentity . Q.getRow + <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder $ toSQL selWith) [] True + where + colValToColExp colMap colVal = + fmap Map.fromList $ forM (Map.toList colVal) $ + \(pgCol, val) -> do + colInfo <- onNothing (Map.lookup pgCol colMap) $ + throw500 "colInfo not found; colValToColExp" + sqlExp <- runAesonParser (convToTxt (pgiType colInfo)) val + return (pgCol, sqlExp) + + mkQIdenExp col = + S.SEQIden $ S.QIden (S.mkQual qt) $ Iden $ getPGColTxt col + + +mutateAndFetchCols + :: QualifiedTable + -> [PGColInfo] + -> (S.CTE, DS.Seq Q.PrepArg) -> Q.TxE QErr MutateResp +mutateAndFetchCols qt cols (cte, p) = + Q.getAltJ . runIdentity . Q.getRow + <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder sql) (toList p) True + where + aliasIden = Iden $ qualObjectToText qt <> "__mutation_result" + tabFrom = TableFrom qt $ Just aliasIden + tabPerm = TablePerm annBoolExpTrue Nothing + selFlds = flip map cols $ + \ci -> (fromPGCol $ pgiName ci, FCol ci) + + sql = toSQL selectWith + selectWith = S.SelectWith [(S.Alias aliasIden, cte)] select + select = S.mkSelect {S.selExtr = [S.Extractor extrExp Nothing]} + extrExp = S.applyJsonBuildObj + [ S.SELit "affected_rows", affRowsSel + , S.SELit "returning_columns", colSel + ] + + affRowsSel = S.SESelect $ + S.mkSelect + { S.selExtr = [S.Extractor S.countStar Nothing] + , S.selFrom = Just $ S.FromExp [S.FIIden aliasIden] + } + colSel = S.SESelect $ mkSQLSelect False $ + AnnSelG selFlds tabFrom tabPerm noTableArgs diff --git a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs index 9411e22375de7..f79304eb7e9d7 100644 --- a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs +++ b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs @@ -120,7 +120,8 @@ execQueryTemplateP1 (ExecQueryTemplate qtn args) = do (QueryTemplateInfo _ qt) <- askQTemplateInfo qtn convQT args qt -execQueryTP2 :: (QErrM m, CacheRM m, MonadTx m) => QueryTProc -> m RespBody +execQueryTP2 + :: (QErrM m, CacheRM m, MonadTx m) => QueryTProc -> m RespBody execQueryTP2 qtProc = case qtProc of QTPInsert qp -> liftTx $ R.insertP2 qp QTPSelect qp -> liftTx $ R.selectP2 False qp diff --git a/server/src-lib/Hasura/RQL/DML/Returning.hs b/server/src-lib/Hasura/RQL/DML/Returning.hs index f5658c83d5ff2..c11cc604dbba2 100644 --- a/server/src-lib/Hasura/RQL/DML/Returning.hs +++ b/server/src-lib/Hasura/RQL/DML/Returning.hs @@ -19,6 +19,17 @@ data MutFld type MutFlds = [(T.Text, MutFld)] +hasNestedFld :: MutFlds -> Bool +hasNestedFld = any isNestedMutFld + where + isNestedMutFld (_, mutFld) = case mutFld of + MRet annFlds -> any isNestedAnnFld annFlds + _ -> False + isNestedAnnFld (_, annFld) = case annFld of + FObj _ -> True + FArr _ -> True + _ -> False + pgColsFromMutFld :: MutFld -> [(PGCol, PGColType)] pgColsFromMutFld = \case MCount -> [] @@ -31,14 +42,17 @@ pgColsFromMutFld = \case pgColsFromMutFlds :: MutFlds -> [(PGCol, PGColType)] pgColsFromMutFlds = concatMap (pgColsFromMutFld . snd) +pgColsToSelFlds :: [PGColInfo] -> [(FieldName, AnnFld)] +pgColsToSelFlds cols = + flip map cols $ + \pgColInfo -> (fromPGCol $ pgiName pgColInfo, FCol pgColInfo) + mkDefaultMutFlds :: Maybe [PGColInfo] -> MutFlds mkDefaultMutFlds = \case Nothing -> mutFlds Just cols -> ("returning", MRet $ pgColsToSelFlds cols):mutFlds where mutFlds = [("affected_rows", MCount)] - pgColsToSelFlds cols = flip map cols $ \pgColInfo -> - (fromPGCol $ pgiName pgColInfo, FCol pgColInfo) qualTableToAliasIden :: QualifiedTable -> Iden qualTableToAliasIden qt = diff --git a/server/src-lib/Hasura/RQL/DML/Update.hs b/server/src-lib/Hasura/RQL/DML/Update.hs index 4df5c211ddba2..0629998381575 100644 --- a/server/src-lib/Hasura/RQL/DML/Update.hs +++ b/server/src-lib/Hasura/RQL/DML/Update.hs @@ -15,6 +15,7 @@ import qualified Data.Sequence as DS import Hasura.Prelude import Hasura.RQL.DML.Internal +import Hasura.RQL.DML.Mutation import Hasura.RQL.DML.Returning import Hasura.RQL.GBoolExp import Hasura.RQL.Instances () @@ -26,16 +27,17 @@ import qualified Hasura.SQL.DML as S data UpdateQueryP1 = UpdateQueryP1 - { uqp1Table :: !QualifiedTable - , uqp1SetExps :: ![(PGCol, S.SQLExp)] - , uqp1Where :: !(AnnBoolExpSQL, AnnBoolExpSQL) - , pqp1MutFlds :: !MutFlds + { uqp1Table :: !QualifiedTable + , uqp1SetExps :: ![(PGCol, S.SQLExp)] + , uqp1Where :: !(AnnBoolExpSQL, AnnBoolExpSQL) + , uqp1MutFlds :: !MutFlds + , uqp1UniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) -mkSQLUpdate - :: UpdateQueryP1 -> S.SelectWith -mkSQLUpdate (UpdateQueryP1 tn setExps (permFltr, wc) mutFlds) = - mkSelWith tn (S.CTEUpdate update) mutFlds False +mkUpdateCTE + :: UpdateQueryP1 -> S.CTE +mkUpdateCTE (UpdateQueryP1 tn setExps (permFltr, wc) _ _) = + S.CTEUpdate update where update = S.SQLUpdate tn setExp Nothing tableFltr $ Just S.returningStar setExp = S.SetExp $ map S.SetExpItem setExps @@ -45,10 +47,12 @@ mkSQLUpdate (UpdateQueryP1 tn setExps (permFltr, wc) mutFlds) = getUpdateDeps :: UpdateQueryP1 -> [SchemaDependency] -getUpdateDeps (UpdateQueryP1 tn setExps (_, wc) mutFlds) = - mkParentDep tn : colDeps <> whereDeps <> retDeps +getUpdateDeps (UpdateQueryP1 tn setExps (_, wc) mutFlds uniqCols) = + mkParentDep tn : colDeps <> uniqColDeps <> whereDeps <> retDeps where colDeps = map (mkColDep "on_type" tn . fst) setExps + uniqColDeps = map (mkColDep "on_type" tn) $ + maybe [] (map pgiName) uniqCols whereDeps = getBoolExpDeps tn wc retDeps = map (mkColDep "untyped" tn . fst) $ pgColsFromMutFlds mutFlds @@ -140,6 +144,8 @@ validateUpdateQueryWith f uq = do let fieldInfoMap = tiFieldInfoMap tableInfo preSetObj = upiSet updPerm preSetCols = M.keys preSetObj + uniqCols = getUniqCols (getCols fieldInfoMap) $ + tiUniqOrPrimConstraints tableInfo -- convert the object to SQL set expression setItems <- withPathK "$set" $ @@ -173,6 +179,7 @@ validateUpdateQueryWith f uq = do setExpItems (upiFilter updPerm, annSQLBoolExp) (mkDefaultMutFlds mAnnRetCols) + uniqCols where mRetCols = uqReturning uq selNecessaryMsg = @@ -186,12 +193,13 @@ validateUpdateQuery validateUpdateQuery = liftDMLP1 . validateUpdateQueryWith binRHSBuilder -updateQueryToTx :: (UpdateQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody +updateQueryToTx + :: (UpdateQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody updateQueryToTx (u, p) = - runIdentity . Q.getRow - <$> Q.rawQE dmlTxErrorHandler (Q.fromBuilder updateSQL) (toList p) True + runMutation $ Mutation (uqp1Table u) (updateCTE, p) + (uqp1MutFlds u) (uqp1UniqCols u) where - updateSQL = toSQL $ mkSQLUpdate u + updateCTE = mkUpdateCTE u runUpdate :: (QErrM m, UserInfoM m, CacheRWM m, MonadTx m) diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index 69fccf9d2bed8..4c0616eca47cb 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -16,6 +16,7 @@ module Hasura.RQL.Types.Common , WithTable(..) , ColVals , PreSetCols + , MutateResp(..) ) where import Hasura.Prelude @@ -140,3 +141,10 @@ instance (ToAesonPairs a) => ToJSON (WithTable a) where type ColVals = HM.HashMap PGCol Value type PreSetCols = HM.HashMap PGCol S.SQLExp + +data MutateResp + = MutateResp + { _mrAffectedRows :: !Int + , _mrReturningColumns :: ![ColVals] + } deriving (Show, Eq) +$(deriveJSON (aesonDrop 3 snakeCase) ''MutateResp) diff --git a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs index dbc712cc541b6..c22b58a33d249 100644 --- a/server/src-lib/Hasura/RQL/Types/SchemaCache.hs +++ b/server/src-lib/Hasura/RQL/Types/SchemaCache.hs @@ -7,6 +7,7 @@ module Hasura.RQL.Types.SchemaCache , emptySchemaCache , TableInfo(..) , TableConstraint(..) + , getUniqCols , ConstraintType(..) , ViewInfo(..) , isMutable @@ -152,8 +153,8 @@ onlyComparableCols :: [PGColInfo] -> [PGColInfo] onlyComparableCols = filter (isComparableType . pgiType) getColInfos :: [PGCol] -> [PGColInfo] -> [PGColInfo] -getColInfos cols allColInfos = flip filter allColInfos $ \ci -> - pgiName ci `elem` cols +getColInfos cols allColInfos = + flip filter allColInfos $ \ci -> pgiName ci `elem` cols type WithDeps a = (a, [SchemaDependency]) @@ -309,10 +310,32 @@ data TableConstraint = TableConstraint { tcType :: !ConstraintType , tcName :: !ConstraintName + , tcCols :: ![PGCol] } deriving (Show, Eq) $(deriveJSON (aesonDrop 2 snakeCase) ''TableConstraint) +getUniqCols :: [PGColInfo] -> [TableConstraint] -> Maybe [PGColInfo] +getUniqCols allCols = travConstraints + where + colsNotNull = all (not . pgiIsNullable) + + travConstraints [] = Nothing + travConstraints (h:t) = + let cols = getColInfos (tcCols h) allCols + in case tcType h of + CTPRIMARYKEY -> Just cols + CTUNIQUE -> if colsNotNull cols then Just cols + else travConstraints t + _ -> travConstraints t + +getAllPkeyCols :: [TableConstraint] -> [PGCol] +getAllPkeyCols constraints = + flip concatMap constraints $ + \c -> case tcType c of + CTPRIMARYKEY -> tcCols c + _ -> [] + data ViewInfo = ViewInfo { viIsUpdatable :: !Bool @@ -339,7 +362,7 @@ data TableInfo , tiSystemDefined :: !Bool , tiFieldInfoMap :: !FieldInfoMap , tiRolePermInfoMap :: !RolePermInfoMap - , tiUniqOrPrimConstraints :: ![ConstraintName] + , tiUniqOrPrimConstraints :: ![TableConstraint] , tiPrimaryKeyCols :: ![PGCol] , tiViewInfo :: !(Maybe ViewInfo) , tiEventTriggerInfoMap :: !EventTriggerInfoMap @@ -350,14 +373,14 @@ $(deriveToJSON (aesonDrop 2 snakeCase) ''TableInfo) mkTableInfo :: QualifiedTable -> Bool - -> [ConstraintName] + -> [TableConstraint] -> [PGColInfo] - -> [PGCol] -> Maybe ViewInfo -> TableInfo -mkTableInfo tn isSystemDefined uniqCons cols pcols mVI = +mkTableInfo tn isSystemDefined uniqCons cols mVI = TableInfo tn isSystemDefined colMap (M.fromList []) - uniqCons pcols mVI (M.fromList []) + uniqCons pCols mVI (M.fromList []) where + pCols = getAllPkeyCols uniqCons colMap = M.fromList $ map f cols f colInfo = (fromPGCol $ pgiName colInfo, FIColumn colInfo) diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 4751a35bf46a4..15f45241ac656 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -267,6 +267,7 @@ data SQLExp | SEBool !BoolExp | SEExcluded !T.Text | SEArray ![SQLExp] + | SETuples ![SQLExp] | SECount !CountType deriving (Show, Eq) @@ -323,6 +324,7 @@ instance ToSQL SQLExp where <> toSQL (PGCol t) toSQL (SEArray exps) = "ARRAY" <> TB.char '[' <> (", " <+> exps) <> TB.char ']' + toSQL (SETuples exps) = paren $ ", " <+> exps toSQL (SECount ty) = "COUNT" <> paren (toSQL ty) intToSQLExp :: Int -> SQLExp @@ -497,6 +499,19 @@ mkExists fromItem whereFrag = , selWhere = Just $ WhereFrag whereFrag } +beFromColVal + :: (PGCol -> SQLExp) + -> [HM.HashMap PGCol SQLExp] + -> BoolExp +beFromColVal f colValMaps = + case colValMaps of + [] -> BELit False + l@(h:_) -> + let cols = map f $ HM.keys h + colTup = SETuples cols + valTups = map (SETuples . HM.elems) l + in BEIN colTup valTups + instance ToSQL BoolExp where toSQL (BELit True) = TB.text $ T.squote "true" toSQL (BELit False) = TB.text $ T.squote "false" diff --git a/server/src-lib/Hasura/SQL/Rewrite.hs b/server/src-lib/Hasura/SQL/Rewrite.hs index 5aca1654e112e..ef4195167da75 100644 --- a/server/src-lib/Hasura/SQL/Rewrite.hs +++ b/server/src-lib/Hasura/SQL/Rewrite.hs @@ -171,6 +171,8 @@ uSqlExp = restoringIdens . \case S.SEExcluded <$> return t S.SEArray l -> S.SEArray <$> mapM uSqlExp l + S.SETuples l -> + S.SEArray <$> mapM uSqlExp l S.SECount cty -> return $ S.SECount cty where uQual = \case diff --git a/server/src-lib/Hasura/SQL/Value.hs b/server/src-lib/Hasura/SQL/Value.hs index d5de51ea2358d..fc60f85120552 100644 --- a/server/src-lib/Hasura/SQL/Value.hs +++ b/server/src-lib/Hasura/SQL/Value.hs @@ -176,7 +176,7 @@ convToTxt :: PGColType -> Value -> AT.Parser S.SQLExp convToTxt ty val = - txtEncoder <$> parsePGValue ty val + toTxtValue ty <$> parsePGValue ty val readEitherTxt :: (Read a) => T.Text -> Either String a readEitherTxt = readEither . T.unpack diff --git a/server/src-rsr/table_info.sql b/server/src-rsr/table_info.sql index cf53f85c57cdd..5a83147c3801d 100644 --- a/server/src-rsr/table_info.sql +++ b/server/src-rsr/table_info.sql @@ -1,6 +1,5 @@ select coalesce(columns.columns, '[]') as columns, - coalesce(pk.columns, '[]') as primary_key_columns, coalesce(constraints.constraints, '[]') as constraints, coalesce(views.view_info, 'null') as view_info from @@ -28,25 +27,32 @@ from tables.table_schema = columns.table_schema AND tables.table_name = columns.table_name ) - left outer join ( - select * from hdb_catalog.hdb_primary_key - ) pk on ( - tables.table_schema = pk.table_schema - AND tables.table_name = pk.table_name - ) left outer join ( select - c.table_schema, - c.table_name, - json_agg(constraint_name) as constraints + cm.table_schema, + cm.table_name, + json_agg( + json_build_object( + 'type', cm.constraint_type, + 'name', cm.constraint_name, + 'cols', cm.columns + ) + ) as constraints from - information_schema.table_constraints c - where - c.constraint_type = 'UNIQUE' - or c.constraint_type = 'PRIMARY KEY' + ( + select table_name, table_schema, + constraint_name, columns, + 'PRIMARY KEY' as constraint_type + from hdb_catalog.hdb_primary_key + union all + select table_name, table_schema, + constraint_name, columns, + 'UNIQUE' as constraint_type + from hdb_catalog.hdb_unique_constraint + ) cm group by - c.table_schema, - c.table_name + cm.table_schema, + cm.table_name ) constraints on ( tables.table_schema = constraints.table_schema AND tables.table_name = constraints.table_name From 6c31e3deec040a3a41a1065ed3e95307ce7ed1b4 Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Wed, 6 Mar 2019 14:39:34 +0530 Subject: [PATCH 2/3] add test case for insert mutation --- .../articles_with_author_returning.yaml | 57 +++++++++++++++++++ server/tests-py/test_graphql_mutations.py | 3 + 2 files changed, 60 insertions(+) create mode 100644 server/tests-py/queries/graphql_mutation/insert/nested/articles_with_author_returning.yaml diff --git a/server/tests-py/queries/graphql_mutation/insert/nested/articles_with_author_returning.yaml b/server/tests-py/queries/graphql_mutation/insert/nested/articles_with_author_returning.yaml new file mode 100644 index 0000000000000..0efbd7bc49a67 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/nested/articles_with_author_returning.yaml @@ -0,0 +1,57 @@ +description: Insert article along with author and returning author with articles as relationship +url: /v1alpha1/graphql +status: 200 +response: + data: + insert_article: + affected_rows: 2 + returning: + - content: Content for Article 5 + author: + name: Author 5 + articles: + - content: Content for Article 5 + id: 5 + title: Article by Author 5 + id: 5 + articles_aggregate: + aggregate: + count: 1 + id: 5 + title: Article by Author 5 +query: + query: | + mutation { + insert_article( + objects: [{ + id: 5 + title: "Article by Author 5" + content: "Content for Article 5" + author: { + data: { + id: 5 + name: "Author 5" + } + } + }] + ){ + affected_rows + returning{ + id + title + content + author{ + id + name + articles_aggregate{ + aggregate{count} + } + articles{ + id + title + content + } + } + } + } + } diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index 1423e5507d76a..9fd55f7a547d2 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -229,6 +229,9 @@ def test_author_upsert_articles_fail(self, hge_ctx): def test_articles_author_upsert_fail(self, hge_ctx): check_query_f(hge_ctx, self.dir() + "/articles_author_upsert_fail.yaml") + def test_articles_with_author_returning(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/articles_with_author_returning.yaml") + @classmethod def dir(cls): return "queries/graphql_mutation/insert/nested" From 7fa726096d6cb6a9eda530f2753053a7f8ecf790 Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Wed, 6 Mar 2019 15:17:31 +0530 Subject: [PATCH 3/3] remove unwanted comments --- server/src-lib/Hasura/RQL/DML/Insert.hs | 6 ------ server/src-lib/Hasura/RQL/DML/Mutation.hs | 2 +- server/src-lib/Hasura/SQL/DML.hs | 4 ++-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index 8b0a2ccb40a0e..58610536bd9ef 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -42,15 +42,9 @@ data InsertQueryP1 , iqp1UniqCols :: !(Maybe [PGColInfo]) } deriving (Show, Eq) --- <<<<<<< HEAD mkInsertCTE :: InsertQueryP1 -> S.CTE mkInsertCTE (InsertQueryP1 _ vn cols vals c _ _) = S.CTEInsert insert --- ======= --- mkSQLInsert :: Bool -> InsertQueryP1 -> S.SelectWith --- mkSQLInsert strfyNum (InsertQueryP1 tn vn cols vals c mutFlds) = --- mkSelWith tn (S.CTEInsert insert) mutFlds False strfyNum --- >>>>>>> master where insert = S.SQLInsert vn cols vals (toSQLConflict <$> c) $ Just S.returningStar diff --git a/server/src-lib/Hasura/RQL/DML/Mutation.hs b/server/src-lib/Hasura/RQL/DML/Mutation.hs index 434459bc2c97c..d5af695623a1b 100644 --- a/server/src-lib/Hasura/RQL/DML/Mutation.hs +++ b/server/src-lib/Hasura/RQL/DML/Mutation.hs @@ -51,7 +51,7 @@ mutateAndSel (Mutation qt q mutFlds mUniqCols strfyNum) = do -- Perform mutation and fetch unique columns MutateResp _ colVals <- mutateAndFetchCols qt uniqCols q strfyNum colExps <- mapM (colValToColExp colMap) colVals - let selWhere = S.beFromColVal mkQIdenExp colExps + let selWhere = S.mkBoolExpWithColVal mkQIdenExp colExps selCTE = S.CTESelect $ S.mkSelect { S.selExtr = [S.selectStar] diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 15f45241ac656..6354398e03943 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -499,11 +499,11 @@ mkExists fromItem whereFrag = , selWhere = Just $ WhereFrag whereFrag } -beFromColVal +mkBoolExpWithColVal :: (PGCol -> SQLExp) -> [HM.HashMap PGCol SQLExp] -> BoolExp -beFromColVal f colValMaps = +mkBoolExpWithColVal f colValMaps = case colValMaps of [] -> BELit False l@(h:_) ->