From d5427b693116946e354b07c731a320ec188cf303 Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Fri, 16 Nov 2018 00:17:43 +0530 Subject: [PATCH 1/5] allow ordering with aggregated fields, close #1039 TODO:- Add relevant tests --- .../src-lib/Hasura/GraphQL/Resolve/Context.hs | 1 + .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 28 +++++ server/src-lib/Hasura/GraphQL/Schema.hs | 88 ++++++++++++-- server/src-lib/Hasura/Prelude.hs | 3 +- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 114 ++++++++++++++---- 5 files changed, 198 insertions(+), 36 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index 24415f4d6038e..018368967c2c6 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -65,6 +65,7 @@ type RespTx = Q.TxE QErr BL.ByteString data OrdByItem = OBIPGCol !PGColInfo | OBIRel !RelInfo !S.BoolExp + | OBIAgg !RelInfo !S.BoolExp deriving (Show, Eq) type OrdByItemMap = Map.HashMap G.Name OrdByItem diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 38949d1cbe31c..19be26a989e77 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -146,10 +146,38 @@ getAnnObItems f nt obj = do (_, enumVal) <- asEnumVal v (ordTy, nullsOrd) <- parseOrderByEnum enumVal return [OrderByItemG (Just ordTy) aobCol (Just nullsOrd)] + OBIRel ri fltr -> do let annObColFn = f . RS.AOCRel ri fltr withObject (getAnnObItems annObColFn) v + OBIAgg ri fltr -> do + let aobColFn = f . RS.AOCAgg ri fltr + flip withObject v $ \_ o -> parseAggOrdBy aobColFn o + +parseAggOrdBy + :: (MonadError QErr m) + => (RS.AnnAggOrdBy -> RS.AnnObCol) + -> AnnGObject + -> m [RS.AnnOrderByItem] +parseAggOrdBy f annObj = + fmap concat <$> forM (OMap.toList annObj) $ \(op, obVal) -> + case op of + "count" -> do + (ordTy, nullsOrd) <- parseAsEnum obVal + return [OrderByItemG (Just ordTy) (f RS.AAOCount) $ Just nullsOrd] + + G.Name opT -> + flip withObject obVal $ \_ opObObj -> + forM (OMap.toList opObObj) $ \(col, eVal) -> do + (ordTy, nullsOrd) <- parseAsEnum eVal + let aobCol = f $ RS.AAOOp opT $ PGCol $ G.unName col + return $ OrderByItemG (Just ordTy) aobCol $ Just nullsOrd + where + parseAsEnum v = do + (_, enumVal) <- asEnumVal v + parseOrderByEnum enumVal + parseOrderByEnum :: (MonadError QErr m) => G.EnumValue diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index 6aea90db905e7..c7406f8c14fe8 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -1076,6 +1076,57 @@ ordByEnumTy = ) ] +mkTabAggOpOrdByTy :: QualifiedTable -> G.Name -> G.NamedType +mkTabAggOpOrdByTy tn op = + G.NamedType $ qualTableToName tn <> "_" <> op <> "_order_by" + +{- +input table__order_by { + col1: order_by + . . + . . +} +-} + +mkTabAggOpOrdByInpObjs + :: QualifiedTable -> [PGCol] -> [PGCol] -> [InpObjTyInfo] +mkTabAggOpOrdByInpObjs tn numCols compCols = + map (mkInpObjTy numCols) numAggOps <> map (mkInpObjTy compCols) compAggOps + where + + mkDesc (G.Name op) = G.Description $ "order by " <> op <> "() on columns of table " <>> tn + mkInpObjTy cols op = InpObjTyInfo (Just $ mkDesc op) (mkTabAggOpOrdByTy tn op) $ + fromInpValL $ map mkColInpVal cols + mkColInpVal c = InpValInfo Nothing (mkColName c) $ G.toGT + ordByTy + +mkTabAggOrdByTy :: QualifiedTable -> G.NamedType +mkTabAggOrdByTy tn = + G.NamedType $ qualTableToName tn <> "_aggregate_order_by" + +{- +input table_aggregate_order_by { +count: order_by + : table__order_by +} +-} + +mkTabAggOrdByInpObj + :: QualifiedTable -> [PGCol] -> [PGCol] -> InpObjTyInfo +mkTabAggOrdByInpObj tn numCols compCols = + InpObjTyInfo (Just desc) (mkTabAggOrdByTy tn) $ fromInpValL $ + numOpOrdBys <> compOpOrdBys <> [countInpVal] + where + desc = G.Description $ + "order by aggregate values of table " <>> tn + + numOpOrdBys = bool (map mkInpValInfo numAggOps) [] $ null numCols + compOpOrdBys = bool (map mkInpValInfo compAggOps) [] $ null compCols + mkInpValInfo op = InpValInfo Nothing op $ G.toGT $ + mkTabAggOpOrdByTy tn op + + countInpVal = InpValInfo Nothing "count" $ G.toGT ordByTy + mkOrdByTy :: QualifiedTable -> G.NamedType mkOrdByTy tn = G.NamedType $ qualTableToName tn <> "_order_by" @@ -1098,14 +1149,17 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) inpObjTy = InpObjTyInfo (Just desc) namedTy $ fromInpValL $ map mkColOrdBy pgCols <> map mkObjRelOrdBy objRels + <> mapMaybe mkArrRelAggOrdBy arrRels namedTy = mkOrdByTy tn desc = G.Description $ "ordering options when selecting data from " <>> tn pgCols = lefts selFlds - objRels = flip filter (rights selFlds) $ \(ri, _, _, _, _) -> - riType ri == ObjRel + relFltr ty = flip filter (rights selFlds) $ \(ri, _, _, _, _) -> + riType ri == ty + objRels = relFltr ObjRel + arrRels = relFltr ArrRel mkColOrdBy ci = InpValInfo Nothing (mkColName $ pgiName ci) $ G.toGT ordByTy @@ -1113,8 +1167,13 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) InpValInfo Nothing (mkRelName $ riName ri) $ G.toGT $ mkOrdByTy $ riRTable ri + mkArrRelAggOrdBy (ri, isAggAllowed, _, _, _) = + let ivi = InpValInfo Nothing (mkAggRelName $ riName ri) $ + G.toGT $ mkTabAggOrdByTy $ riRTable ri + in bool Nothing (Just ivi) isAggAllowed + ordByCtx = Map.singleton namedTy $ Map.fromList $ - colOrdBys <> relOrdBys + colOrdBys <> relOrdBys <> arrRelOrdBys colOrdBys = flip map pgCols $ \ci -> ( mkColName $ pgiName ci , OBIPGCol ci @@ -1123,6 +1182,11 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) ( mkRelName $ riName ri , OBIRel ri fltr ) + arrRelOrdBys = flip mapMaybe arrRels $ \(ri, isAggAllowed, fltr, _, _) -> + let obItem = ( mkAggRelName $ riName ri + , OBIAgg ri fltr + ) + in bool Nothing (Just obItem) isAggAllowed newtype RootFlds = RootFlds @@ -1188,7 +1252,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM allC , TIInpObj <$> ordByInpObjM , TIObj <$> selObjM ] - aggQueryTypes = map TIObj aggObjs + aggQueryTypes = map TIObj aggObjs <> map TIInpObj aggOrdByInps mutationTypes = catMaybes [ TIInpObj <$> mutHelper viIsInsertable insInpObjM @@ -1268,15 +1332,19 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM allC -- table obj selObjM = mkTableObj tn <$> selFldsM - -- aggregate objs - aggObjs = case selPermM of + -- aggregate objs and order by inputs + (aggObjs, aggOrdByInps) = case selPermM of Just (True, selFlds) -> let numCols = (map pgiName . getNumCols) selFlds compCols = (map pgiName . getCompCols) selFlds - in [ mkTableAggObj tn - , mkTableAggFldsObj tn numCols compCols - ] <> mkColAggFldsObjs selFlds - _ -> [] + objs = [ mkTableAggObj tn + , mkTableAggFldsObj tn numCols compCols + ] <> mkColAggFldsObjs selFlds + ordByInps = mkTabAggOrdByInpObj tn numCols compCols + : mkTabAggOpOrdByInpObjs tn numCols compCols + in (objs, ordByInps) + _ -> ([], []) + getNumCols = onlyNumCols . lefts getCompCols = onlyComparableCols . lefts onlyFloat = const $ mkScalarTy PGFloat diff --git a/server/src-lib/Hasura/Prelude.hs b/server/src-lib/Hasura/Prelude.hs index 0a9c511e64acf..6050ce3fcbefc 100644 --- a/server/src-lib/Hasura/Prelude.hs +++ b/server/src-lib/Hasura/Prelude.hs @@ -13,7 +13,8 @@ import Data.Bool as M (bool) import Data.Either as M (lefts, partitionEithers, rights) import Data.Foldable as M (toList) import Data.Hashable as M (Hashable) -import Data.List as M (find, foldl', group, sort, sortBy) +import Data.List as M (find, foldl', group, sort, sortBy, + union) import Data.Maybe as M (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index 954ee0cdb9e20..bf7b07d7b5b04 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -17,6 +17,7 @@ import Hasura.Prelude import Hasura.RQL.DML.Internal import Hasura.RQL.GBoolExp import Hasura.RQL.Types +import Hasura.Server.Utils import Hasura.SQL.Types import qualified Hasura.SQL.DML as S @@ -53,9 +54,15 @@ instance FromJSON ExtCol where , "object (relationship)" ] +data AnnAggOrdBy + = AAOCount + | AAOOp !T.Text !PGCol + deriving (Show, Eq) + data AnnObCol = AOCPG !PGColInfo | AOCRel !RelInfo !S.BoolExp !AnnObCol + | AOCAgg !RelInfo !S.BoolExp !AnnAggOrdBy deriving (Show, Eq) type AnnOrderByItem = OrderByItemG AnnObCol @@ -195,10 +202,10 @@ asSingleRow col fromItem = , S.SEUnsafe "0" ] -aggNodeToSelect :: BaseNode -> S.Extractor -> S.BoolExp -> S.Select -aggNodeToSelect bn extr joinCond = +aggNodeToSelect :: BaseNode -> [S.Extractor] -> S.BoolExp -> S.Select +aggNodeToSelect bn extrs joinCond = S.mkSelect - { S.selExtr = [extr] + { S.selExtr = extrs , S.selFrom = Just $ S.FromExp [selFrom] } where @@ -246,6 +253,10 @@ mkAggAls :: Iden -> FieldName -> Iden mkAggAls pfx fldAls = pfx <> Iden ".agg." <> toIden fldAls +mkAggOrdByAls :: Iden -> RelName -> Iden +mkAggOrdByAls pfx relName = + pfx <> Iden ".agg." <> toIden relName <> Iden ".order_by" + mkBaseTableAls :: Iden -> Iden mkBaseTableAls pfx = pfx <> Iden ".base" @@ -301,6 +312,30 @@ withJsonBuildObj parAls exps = where jsonRow = S.applyJsonBuildObj exps +data OrderByNode + = OBNNothing + | OBNRelNode !RelName !RelNode + | OBNAggNode !S.Alias !AggNode + deriving (Show, Eq) + +mkAggObFld :: AnnAggOrdBy -> FieldName +mkAggObFld AAOCount = FieldName "count" +mkAggObFld (AAOOp op col) = + FieldName $ op <> "." <> getPGColTxt col + +mkAggObExtrAndFlds :: AnnAggOrdBy -> (S.Extractor, AggFlds) +mkAggObExtrAndFlds annAggOb = case annAggOb of + AAOCount -> + ( S.Extractor S.countStar als + , [("count", AFCount S.CTStar)] + ) + AAOOp op pgCol -> + ( S.Extractor (S.SEFnApp op [S.SEIden $ toIden pgCol] Nothing) als + , [(op, AFOp $ AggOp op [(getPGColTxt pgCol, PCFCol pgCol)])] + ) + where + als = Just $ S.toAlias $ mkAggObFld annAggOb + processAnnOrderByItem :: Iden -> AnnOrderByItem @@ -308,8 +343,8 @@ processAnnOrderByItem -> ( (S.Alias, S.SQLExp) -- the sql order by item that is attached to the final select , S.OrderByItem - -- optionally we may have to add an obj rel node - , Maybe (RelName, RelNode) + -- extra nodes for order by + , OrderByNode ) processAnnOrderByItem pfx (OrderByItemG obTyM annObCol obNullsM) = ( (obColAls, obColExp) @@ -327,8 +362,8 @@ processAnnOrderByCol -> AnnObCol -- the extractors which will select the needed columns -> ( (S.Alias, S.SQLExp) - -- optionally we may have to add an obj rel node - , Maybe (RelName, RelNode) + -- extra nodes for order by + , OrderByNode ) processAnnOrderByCol pfx = \case AOCPG colInfo -> @@ -336,22 +371,40 @@ processAnnOrderByCol pfx = \case qualCol = S.mkQIdenExp (mkBaseTableAls pfx) (toIden $ pgiName colInfo) obColAls = mkBaseTableColAls pfx $ pgiName colInfo in ( (S.Alias obColAls, qualCol) - , Nothing + , OBNNothing ) -- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest" AOCRel (RelInfo rn _ colMapping relTab _ _) relFltr rest -> let relPfx = mkObjRelTableAls pfx rn - ((nesAls, nesCol), nesNodeM) = processAnnOrderByCol relPfx rest + ((nesAls, nesCol), ordByNode) = processAnnOrderByCol relPfx rest + (objRelNodeM, aggNodeM) = case ordByNode of + OBNNothing -> (Nothing, Nothing) + OBNRelNode name node -> (Just (name, node), Nothing) + OBNAggNode als node -> (Nothing, Just (als, node)) qualCol = S.mkQIdenExp relPfx nesAls relBaseNode = BaseNode relPfx (S.FISimple relTab Nothing) relFltr Nothing Nothing Nothing (HM.singleton nesAls nesCol) - (maybe HM.empty (uncurry HM.singleton) nesNodeM) - HM.empty HM.empty + (maybe HM.empty (uncurry HM.singleton) objRelNodeM) + HM.empty + (maybe HM.empty (uncurry HM.singleton) aggNodeM) relNode = RelNode rn (fromRel rn) colMapping relBaseNode in ( (nesAls, qualCol) - , Just (rn, relNode) + , OBNRelNode rn relNode + ) + AOCAgg (RelInfo rn _ colMapping relTab _ _) relFltr annAggOb -> + let aggAls = mkAggOrdByAls pfx rn + fldName = mkAggObFld annAggOb + qOrdBy = S.mkQIdenExp aggAls $ toIden fldName + tabFrom = TableFrom relTab Nothing + tabPerm = TablePerm relFltr Nothing + (extr, aggFlds) = mkAggObExtrAndFlds annAggOb + selFld = TAFAgg aggFlds + bn = mkBaseNode pfx fldName selFld tabFrom tabPerm noTableArgs + aggNode = AggNode colMapping [extr] bn + in ( (S.Alias $ toIden fldName, qOrdBy) + , OBNAggNode (S.Alias aggAls) aggNode ) mkEmptyBaseNode :: Iden -> TableFrom -> BaseNode @@ -378,7 +431,7 @@ applyPermLimit mPermLimit mQueryLimit = aggSelToAggNode :: Iden -> FieldName -> AggSel -> AggNode aggSelToAggNode pfx als aggSel = - AggNode colMapping extr mergedBN + AggNode colMapping [extr] mergedBN where AggSel colMapping annSel = aggSel AnnSelG aggFlds tabFrm tabPerm tabArgs = annSel @@ -423,22 +476,19 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = (allObjs, allArrRels) = foldl' addRel (HM.empty, HM.empty) $ mapMaybe (\(als, f) -> (als,) <$> getAnnRel f) flds - allObjRelsWithOb = - foldl' (\objs (rn, relNode) -> HM.insertWith mergeRelNodes rn relNode objs) - allObjs $ catMaybes $ maybe [] _3 procOrdByM aggItems = HM.fromList $ map mkAggItem $ mapMaybe (\(als, f) -> (als,) <$> getAggFld f) flds in ( HM.fromList $ selExtr:obExtrs - , allObjRelsWithOb + , mkTotalObjRels allObjs , allArrRels - , aggItems + , aggItems `HM.union` aggItemsWithOb ) TAFAgg aggFlds -> let extrs = concatMap (fetchExtrFromAggFld . snd) aggFlds in ( HM.fromList $ extrs <> obExtrs + , mkTotalObjRels HM.empty , HM.empty - , HM.empty - , HM.empty + , aggItemsWithOb ) TAFExp _ -> (HM.fromList obExtrs, HM.empty, HM.empty, HM.empty) @@ -465,12 +515,13 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = fromItem = fromMaybe (S.FISimple tn Nothing) fromItemM - _1 (a, _, _) = a - _2 (_, b, _) = b - _3 (_, _, c) = c - procOrdByM = unzip3 . map (processAnnOrderByItem pfx) . toList <$> orderByM ordByExpM = S.OrderByExp . _2 <$> procOrdByM + mkTotalObjRels objRels = + foldl' (\objs (rn, relNode) -> HM.insertWith mergeRelNodes rn relNode objs) + objRels $ mapMaybe getOrdByRelNode $ maybe [] _3 procOrdByM + aggItemsWithOb = HM.fromListWith mergeAggNodes + $ mapMaybe getOrdByAggNode $ maybe [] _3 procOrdByM -- the columns needed for orderby obExtrs = maybe [] _1 procOrdByM @@ -507,6 +558,12 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = FAgg af -> Just af _ -> Nothing + getOrdByRelNode (OBNRelNode name node) = Just (name, node) + getOrdByRelNode _ = Nothing + + getOrdByAggNode (OBNAggNode als node) = Just (als, node) + getOrdByAggNode _ = Nothing + annSelToBaseNode :: Iden -> FieldName -> AnnSel -> BaseNode annSelToBaseNode pfx fldAls annSel = mkBaseNode pfx fldAls (TAFNodes selFlds) tabFrm tabPerm tabArgs @@ -547,10 +604,17 @@ mkRelNode pfx (relAls, AnnRel rn _ rMapn rAnnSel) = data AggNode = AggNode { _anColMapping :: ![(PGCol, PGCol)] - , _anExtr :: !S.Extractor + , _anExtr :: ![S.Extractor] , _anNodeDet :: !BaseNode } deriving (Show, Eq) +mergeAggNodes :: AggNode -> AggNode -> AggNode +mergeAggNodes lAggNode rAggNode = + AggNode colMapn (lExtrs `union` rExtrs) $ mergeBaseNodes lBN rBN + where + AggNode colMapn lExtrs lBN = lAggNode + AggNode _ rExtrs rBN = rAggNode + injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition -> S.WhereFrag -- ^ New where frag From e23dd0f96df508824e929f745b5cc1cc2a0de9ae Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Fri, 16 Nov 2018 12:54:13 +0530 Subject: [PATCH 2/5] add tests TODO:- add docs --- .../album_order_by_tracks_bytes_stddev.yaml | 33 ++++++++++ .../order_by/album_order_by_tracks_count.yaml | 18 +++++ .../album_order_by_tracks_duration_avg.yaml | 33 ++++++++++ .../album_order_by_tracks_max_name.yaml | 53 +++++++++++++++ .../queries/graphql_query/order_by/setup.yaml | 65 +++++++++++++++++++ .../graphql_query/order_by/teardown.yaml | 17 +++++ server/tests-py/test_graphql_queries.py | 12 ++++ 7 files changed, 231 insertions(+) create mode 100644 server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_bytes_stddev.yaml create mode 100644 server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_count.yaml create mode 100644 server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_duration_avg.yaml create mode 100644 server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_max_name.yaml diff --git a/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_bytes_stddev.yaml b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_bytes_stddev.yaml new file mode 100644 index 0000000000000..4bb4a701e0f5d --- /dev/null +++ b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_bytes_stddev.yaml @@ -0,0 +1,33 @@ +description: Fetch Albums order by their tracks' standard deviation on bytes +url: /v1alpha1/graphql +status: 200 +response: + data: + Album: + - album_id: 1 + title: Big Ones + Tracks_aggregate: + aggregate: + stddev: + bytes: 4460362.9621 + - album_id: 2 + title: Face Lift + Tracks_aggregate: + aggregate: + stddev: + bytes: 653697.39166707 +query: + query: | + query { + Album(order_by: {Tracks_aggregate: {stddev: {bytes: desc}}}){ + album_id + title + Tracks_aggregate{ + aggregate{ + stddev{ + bytes + } + } + } + } + } diff --git a/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_count.yaml b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_count.yaml new file mode 100644 index 0000000000000..991fd7eba23af --- /dev/null +++ b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_count.yaml @@ -0,0 +1,18 @@ +description: Fetch Albums order by their tracks count +url: /v1alpha1/graphql +status: 200 +response: + data: + Album: + - album_id: 2 + title: Face Lift + - album_id: 1 + title: Big Ones +query: + query: | + query { + Album(order_by: {Tracks_aggregate: {count: desc}}){ + album_id + title + } + } diff --git a/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_duration_avg.yaml b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_duration_avg.yaml new file mode 100644 index 0000000000000..0c38dc935d050 --- /dev/null +++ b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_duration_avg.yaml @@ -0,0 +1,33 @@ +description: Fetch Albums order by their average tracks duration +url: /v1alpha1/graphql +status: 200 +response: + data: + Album: + - album_id: 2 + title: Face Lift + Tracks_aggregate: + aggregate: + avg: + milliseconds: 574804.75 + - album_id: 1 + title: Big Ones + Tracks_aggregate: + aggregate: + avg: + milliseconds: 331883 +query: + query: | + query { + Album(order_by: {Tracks_aggregate: {avg: {milliseconds: desc}}}){ + album_id + title + Tracks_aggregate{ + aggregate{ + avg{ + milliseconds + } + } + } + } + } diff --git a/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_max_name.yaml b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_max_name.yaml new file mode 100644 index 0000000000000..4815c7fcbd6fe --- /dev/null +++ b/server/tests-py/queries/graphql_query/order_by/album_order_by_tracks_max_name.yaml @@ -0,0 +1,53 @@ +description: Fetch Album order by their tracks' maximum name +url: /v1alpha1/graphql +status: 200 +response: + data: + Album: + - album_id: 2 + title: Face Lift + Tracks_aggregate: + aggregate: + stddev: + bytes: 653697.39166707 + nodes: + - track_id: 5 + name: Random + - track_id: 7 + name: Mistress + - track_id: 6 + name: Good One + - track_id: 4 + name: Evil Walks + - album_id: 1 + title: Big Ones + Tracks_aggregate: + aggregate: + stddev: + bytes: 4460362.9621 + nodes: + - track_id: 1 + name: Restless + - track_id: 2 + name: Keepup + - track_id: 3 + name: Havana +query: + query: | + query { + Album(order_by: {Tracks_aggregate: {max: {name: asc}}}){ + album_id + title + Tracks_aggregate(order_by: {name: desc}){ + aggregate{ + stddev{ + bytes + } + } + nodes{ + track_id + name + } + } + } + } diff --git a/server/tests-py/queries/graphql_query/order_by/setup.yaml b/server/tests-py/queries/graphql_query/order_by/setup.yaml index 5a32a74a17335..21b04ce111779 100644 --- a/server/tests-py/queries/graphql_query/order_by/setup.yaml +++ b/server/tests-py/queries/graphql_query/order_by/setup.yaml @@ -111,3 +111,68 @@ args: 2, true ) + +# Album - Track Schema +- type: run_sql + args: + sql: | + create table "Album" ( + album_id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL + ) +- type: track_table + args: + schema: public + name: Album + +- type: run_sql + args: + sql: | + create table "Track" ( + track_id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + album_id INTEGER REFERENCES "Album"(album_id), + milliseconds INTEGER NOT NULL, + bytes INTEGER NOT NULL + ) +- type: track_table + args: + schema: public + name: Track + +- type: create_object_relationship + args: + table: Track + name: Album + using: + foreign_key_constraint_on: album_id + +- type: create_array_relationship + args: + table: Album + name: Tracks + using: + foreign_key_constraint_on: + table: Track + column: album_id + +# Insert values in Album and Track +- type: run_sql + args: + sql: | + insert into "Album" (album_id, title) + values ( 1, 'Big Ones' ), (2, 'Face Lift') + +- type: run_sql + args: + sql: | + insert into "Track" + (track_id, name, album_id, milliseconds, bytes) + values + ( 1, 'Restless', 1, 123654, 9836284), + ( 2, 'Keepup', 1, 637483, 6382913) , + ( 3, 'Havana', 1, 234512, 986384) , + ( 4, 'Evil Walks', 2, 437294, 6284302) , + ( 5, 'Random', 2, 1094732, 6032547) , + ( 6, 'Good One', 2, 346208, 6732987) , + ( 7, 'Mistress', 2, 420985, 7521946) diff --git a/server/tests-py/queries/graphql_query/order_by/teardown.yaml b/server/tests-py/queries/graphql_query/order_by/teardown.yaml index 843f55ab03649..cdabbb45bbffb 100644 --- a/server/tests-py/queries/graphql_query/order_by/teardown.yaml +++ b/server/tests-py/queries/graphql_query/order_by/teardown.yaml @@ -22,3 +22,20 @@ args: args: sql: | drop table contact + +- type: drop_relationship + args: + relationship: Tracks + table: + schema: public + name: Album + +- type: run_sql + args: + sql: | + drop table "Track" + +- type: run_sql + args: + sql: | + drop table "Album" diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 0f1ad537fc406..221de8e019cdb 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -259,6 +259,18 @@ def test_articles_order_by_rel_author_id(self, hge_ctx): def test_articles_order_by_rel_author_rel_contact_phone(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/articles_order_by_rel_author_rel_contact_phone.yaml') + def test_album_order_by_tracks_count(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/album_order_by_tracks_count.yaml') + + def test_album_order_by_tracks_duration_avg(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/album_order_by_tracks_duration_avg.yaml') + + def test_album_order_by_tracks_max_name(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/album_order_by_tracks_max_name.yaml') + + def test_album_order_by_tracks_bytes_stddev(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/album_order_by_tracks_bytes_stddev.yaml') + @classmethod def dir(cls): return 'queries/graphql_query/order_by' From da25f2925eef57ea625a660659ec4145cbbb46fc Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Fri, 16 Nov 2018 13:42:44 +0530 Subject: [PATCH 3/5] add docs --- docs/graphql/manual/api-reference/query.rst | 37 +++++++++++ docs/graphql/manual/queries/sorting.rst | 70 +++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/docs/graphql/manual/api-reference/query.rst b/docs/graphql/manual/api-reference/query.rst index d79718e5a5a66..9e00ecd73dbd9 100644 --- a/docs/graphql/manual/api-reference/query.rst +++ b/docs/graphql/manual/api-reference/query.rst @@ -349,6 +349,12 @@ or order_by: [{id: desc}, {author: {id: asc}}] +or + +.. parsed-literal:: + + order_by: {articles_aggregate: {count: asc}} + TableOrderBy ************ @@ -364,6 +370,11 @@ For object relations: .. parsed-literal:: {relation-name: TableOrderBy_} +For array relations aggregate: + +.. parsed-literal:: + {relation-name_aggregate: AggregateOrderBy_} + E.g. Order by type for "article" table: @@ -377,8 +388,34 @@ Order by type for "article" table: author_id: order_by #order by using "author" object relationship columns author: author_order_by + #order by using "likes" array relationship aggregates + likes_aggregate: likes_aggregate_order_by } +AggregateOrderBy +*************** + +Count aggregate + +.. parsed-literal:: + {count: OrderByEnum_} + +Operation aggregate + +.. parsed-literal:: + {op_name: TableAggOpOrderBy_} + +Available operations are ``sum``, ``avg``, ``max``, ``min``, ``stddev``, ``stddev_samp``, +``stddev_pop``, ``variance``, ``var_samp`` and ``var_pop`` + +TableAggOpOrderBy +***************** + +.. parsed-literal:: + {column: OrderByEnum_} + + + OrderByEnum *********** diff --git a/docs/graphql/manual/queries/sorting.rst b/docs/graphql/manual/queries/sorting.rst index f16b17658ee11..57344d0de18ae 100644 --- a/docs/graphql/manual/queries/sorting.rst +++ b/docs/graphql/manual/queries/sorting.rst @@ -25,6 +25,20 @@ The ``order_by`` argument takes an array of objects to allow sorting by multiple author_id: order_by #order by using "author" object relationship columns author: author_order_by + #order by using "likes" array relationship aggregates + likes_aggregate: likes_aggregate_order_by + } + + #the likes_aggregate_order_by type + input likes_aggregate_order_by { + count: order_by + op_name: likes_op_name_order_by + } + #Available "op_name"s are 'max', 'min', 'sum', 'avg', 'stddev', 'stddev_samp', 'stddev_pop', 'variance', 'var_samp' and 'var_pop' + + #the likes__order_by type + input likes_sum_order_by { + like_id: order_by } #the order_by enum type @@ -39,8 +53,10 @@ The ``order_by`` argument takes an array of objects to allow sorting by multiple desc_nulls_first } + .. Note:: Only columns from **object** relationships are allowed for sorting. + Only aggregates from **array** relationships are allowed for sorting. The following are example queries for different sorting use cases: @@ -226,6 +242,60 @@ Only columns in object relationships are allowed: } } +Sorting by array relationship aggregates +--------------------------------------- +Fetch a list of authors sorted by their article count. + +.. graphiql:: + :view_only: + :query: + query { + author(order_by: {articles_aggregate: {count: desc}}) { + id + name + articles_aggregate { + aggregate{ + count + } + } + } + } + :response: + { + "data": { + "author": [ + { + "id": 5, + "name": "Amii", + "articles_aggregate":{ + "aggregate": { + "count": 3 + } + } + }, + { + "id": 4, + "name": "Anjela", + "articles_aggregate":{ + "aggregate": { + "count": 2 + } + } + }, + { + "id": 8, + "name": "April", + "articles_aggregate":{ + "aggregate": { + "count": 2 + } + } + } + ] + } + } + + Sorting by multiple fields -------------------------- Fetch a list of articles that is sorted by their rating (descending) and then on their published date (ascending with From 0b2c360383cb386bed9e441694ac40ac8ec297de Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Fri, 23 Nov 2018 18:12:42 +0530 Subject: [PATCH 4/5] merge aggregate and array relation nodes if they have similar tableArgs --- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 6 +- server/src-lib/Hasura/RQL/DML/Select.hs | 4 +- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 249 ++++++++++++------ server/src-lib/Hasura/RQL/Types/Common.hs | 4 +- 4 files changed, 181 insertions(+), 82 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 76978baf846f6..216372b9c4d14 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -66,13 +66,13 @@ fromSelSet f fldTy flds = Right (relInfo, isAgg, tableFilter, tableLimit) -> do let relTN = riRTable relInfo colMapping = riMapping relInfo + rn = riName relInfo if isAgg then do aggSel <- fromAggField f relTN tableFilter tableLimit fld - return $ RS.FAgg $ RS.AggSel colMapping aggSel + return $ RS.FAgg $ RS.AggSel rn colMapping aggSel else do annSel <- fromField f relTN tableFilter tableLimit fld - let annRel = RS.AnnRel (riName relInfo) (riType relInfo) - colMapping annSel + let annRel = RS.AnnRel rn (riType relInfo) colMapping annSel return $ RS.FRel annRel fieldAsPath :: (MonadError QErr m) => Field -> m a -> m a diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 6841a84345a18..46fc091a2ffef 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -281,7 +281,7 @@ mkAggSelect :: AnnAggSel -> S.Select mkAggSelect annAggSel = prefixNumToAliases $ aggNodeToSelect bn extr $ S.BELit True where - aggSel = AggSel [] annAggSel + aggSel = AggSel (RelName "root") [] annAggSel AggNode _ extr bn = aggSelToAggNode (Iden "root") (FieldName "root") aggSel @@ -294,7 +294,7 @@ selectAggP2 (sel, p) = mkSQLSelect :: Bool -> AnnSel -> S.Select mkSQLSelect isSingleObject annSel = - prefixNumToAliases $ asJsonAggSel isSingleObject rootFldAls (S.BELit True) + prefixNumToAliases $ asJsonAggSel isSingleObject [rootFldAls] (S.BELit True) $ annSelToBaseNode (toIden rootFldName) rootFldName annSel where diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index 3e23301712de8..479189c33528a 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -1,11 +1,11 @@ {-# LANGUAGE DeriveLift #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} module Hasura.RQL.DML.Select.Internal where import Data.Aeson.Types +import Data.List (delete, sort) import Instances.TH.Lift () import Language.Haskell.TH.Syntax (Lift) @@ -75,11 +75,26 @@ data AnnRel , arAnnSel :: !AnnSel -- Current table. Almost ~ to SQL Select } deriving (Show, Eq) +data ArrRelNode + = ArrRelNode + { arnExtrAls :: ![S.Alias] + , arnNode :: !RelNode + } deriving (Show, Eq) + +mergeArrRelNodes :: ArrRelNode -> ArrRelNode -> ArrRelNode +mergeArrRelNodes lJAS rJAS = + ArrRelNode (lExtrs `union` rExtrs) $ mergeRelNodes lBN rBN + where + ArrRelNode lExtrs lBN = lJAS + ArrRelNode rExtrs rBN = rJAS + + type AnnAggSel = AnnSelG [(T.Text, TableAggFld)] data AggSel = AggSel - { agColMapping :: ![(PGCol, PGCol)] + { agRelName :: !RelName + , agColMapping :: ![(PGCol, PGCol)] , agAnnSel :: !AnnAggSel } deriving (Show, Eq) @@ -171,9 +186,8 @@ data BaseNode , _bnExtrs :: !(HM.HashMap S.Alias S.SQLExp) , _bnObjRels :: !(HM.HashMap RelName RelNode) - , _bnArrRels :: !(HM.HashMap S.Alias RelNode) + , _bnArrRels :: !(HM.HashMap S.Alias ArrRelNode) , _bnAggs :: !(HM.HashMap S.Alias AggNode) - } deriving (Show, Eq) txtToAlias :: Text -> S.Alias @@ -199,16 +213,17 @@ aggFldToExp aggFlds = jsonRow colFldsToExtr _ (t, PCFExp e) = [ S.SELit t , S.SELit e] -asSingleRow :: S.Alias -> S.FromItem -> S.Select -asSingleRow col fromItem = +asSingleRow :: [S.Alias] -> S.FromItem -> S.Select +asSingleRow cols fromItem = S.mkSelect - { S.selExtr = [S.Extractor extr $ Just col] + { S.selExtr = flip map cols $ + \c -> S.Extractor (mkExtr c) $ Just c , S.selFrom = Just $ S.FromExp [fromItem] } where - extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing - jsonAgg = S.SEOpApp (S.SQLOp "->") - [ S.SEFnApp "json_agg" [S.SEIden $ toIden col] Nothing + mkExtr c = S.SEFnApp "coalesce" [jsonAgg c, S.SELit "null"] Nothing + jsonAgg c = S.SEOpApp (S.SQLOp "->") + [ S.SEFnApp "json_agg" [S.SEIden $ toIden c] Nothing , S.SEUnsafe "0" ] @@ -222,17 +237,18 @@ aggNodeToSelect bn extrs joinCond = selFrom = S.mkSelFromItem (baseNodeToSel joinCond bn) $ S.Alias $ _bnPrefix bn -withJsonAgg :: Maybe S.OrderByExp -> S.Alias -> S.FromItem -> S.Select -withJsonAgg orderByM col fromItem = +withJsonAgg :: Maybe S.OrderByExp -> [S.Alias] -> S.FromItem -> S.Select +withJsonAgg orderByM cols fromItem = S.mkSelect - { S.selExtr = [S.Extractor extr $ Just col] + { S.selExtr = flip map cols $ + \c -> S.Extractor (mkExtr c) $ Just c , S.selFrom = Just $ S.FromExp [fromItem] } where - extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "[]"] Nothing - jsonAgg = S.SEFnApp "json_agg" [S.SEIden $ toIden col] orderByM + mkExtr c = S.SEFnApp "coalesce" [jsonAgg c, S.SELit "[]"] Nothing + jsonAgg c = S.SEFnApp "json_agg" [S.SEIden $ toIden c] orderByM -asJsonAggSel :: Bool -> S.Alias -> S.BoolExp -> BaseNode -> S.Select +asJsonAggSel :: Bool -> [S.Alias] -> S.BoolExp -> BaseNode -> S.Select asJsonAggSel singleObj als joinCond n = let ordByM = _bnOrderBy n fromItem = S.mkSelFromItem (baseNodeToSel joinCond n) $ @@ -244,16 +260,17 @@ asJsonAggSel singleObj als joinCond n = -- array relationships are not grouped, so have to be prefixed by -- parent's alias -mkUniqArrRelAls :: FieldName -> FieldName -> Iden -mkUniqArrRelAls parAls relAls = +mkUniqArrRelAls :: FieldName -> [FieldName] -> Iden +mkUniqArrRelAls parAls flds = Iden $ - getFieldNameTxt parAls <> "." <> getFieldNameTxt relAls + getFieldNameTxt parAls <> "." + <> T.intercalate "." (map getFieldNameTxt flds) -mkArrRelTableAls :: Iden -> FieldName -> FieldName -> Iden -mkArrRelTableAls pfx parAls relAls = +mkArrRelTableAls :: Iden -> FieldName -> [FieldName] -> Iden +mkArrRelTableAls pfx parAls flds = pfx <> Iden ".ar." <> uniqArrRelAls where - uniqArrRelAls = mkUniqArrRelAls parAls relAls + uniqArrRelAls = mkUniqArrRelAls parAls flds mkObjRelTableAls :: Iden -> RelName -> Iden mkObjRelTableAls pfx relName = @@ -263,9 +280,10 @@ mkAggAls :: Iden -> FieldName -> Iden mkAggAls pfx fldAls = pfx <> Iden ".agg." <> toIden fldAls -mkAggOrdByAls :: Iden -> RelName -> Iden -mkAggOrdByAls pfx relName = - pfx <> Iden ".agg." <> toIden relName <> Iden ".order_by" +mkMergedAggNodeAls :: Iden -> RelName -> [FieldName] -> Iden +mkMergedAggNodeAls pfx relName flds = + pfx <> Iden ".agg." <> toIden relName <> Iden "." + <> Iden (T.intercalate "." $ map getFieldNameTxt flds) mkBaseTableAls :: Iden -> Iden mkBaseTableAls pfx = @@ -280,9 +298,9 @@ mkBaseTableColAls pfx pgCol = -- json_build_object is slower than row_to_json hence it is only -- used when needed buildJsonObject - :: Iden -> FieldName + :: Iden -> FieldName -> AllAggCtx -> [(FieldName, AnnRel)] -> [(FieldName, AnnFld)] -> (S.Alias, S.SQLExp) -buildJsonObject pfx parAls flds = +buildJsonObject pfx parAls allAggCtx allArrRels flds = if any ( (> 63) . T.length . getFieldNameTxt . fst ) flds then withJsonBuildObj parAls jsonBuildObjExps else withRowToJSON parAls rowToJsonExtrs @@ -302,9 +320,13 @@ buildJsonObject pfx parAls flds = FRel annRel -> let qual = case arType annRel of ObjRel -> mkObjRelTableAls pfx $ arName annRel - ArrRel -> mkArrRelTableAls pfx parAls fldAls + ArrRel -> snd $ mkArrRelPfx pfx parAls + allArrRels (fldAls, annRel) in S.mkQIdenExp qual fldAls - FAgg _ -> S.mkQIdenExp (mkAggAls pfx fldAls) fldAls + FAgg aggSel -> + let aggPfx = mkAggNodePfx pfx allAggCtx $ + ANIField (fldAls, aggSel) + in S.mkQIdenExp aggPfx fldAls -- uses row_to_json to build a json object withRowToJSON @@ -329,9 +351,9 @@ data OrderByNode deriving (Show, Eq) mkAggObFld :: AnnAggOrdBy -> FieldName -mkAggObFld AAOCount = FieldName "count" -mkAggObFld (AAOOp op col) = - FieldName $ op <> "." <> getPGColTxt col +mkAggObFld = \case + AAOCount -> FieldName "count" + AAOOp op col -> FieldName $ op <> "." <> getPGColTxt col mkAggObExtrAndFlds :: AnnAggOrdBy -> (S.Extractor, AggFlds) mkAggObExtrAndFlds annAggOb = case annAggOb of @@ -348,6 +370,7 @@ mkAggObExtrAndFlds annAggOb = case annAggOb of processAnnOrderByItem :: Iden + -> AllAggCtx -> AnnOrderByItem -- the extractors which will select the needed columns -> ( (S.Alias, S.SQLExp) @@ -356,26 +379,27 @@ processAnnOrderByItem -- extra nodes for order by , OrderByNode ) -processAnnOrderByItem pfx (OrderByItemG obTyM annObCol obNullsM) = +processAnnOrderByItem pfx aggCtx (OrderByItemG obTyM annObCol obNullsM) = ( (obColAls, obColExp) , sqlOrdByItem , relNodeM ) where - ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx annObCol + ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx aggCtx annObCol sqlOrdByItem = S.OrderByItem (S.SEIden $ toIden obColAls) obTyM obNullsM processAnnOrderByCol :: Iden + -> AllAggCtx -> AnnObCol -- the extractors which will select the needed columns -> ( (S.Alias, S.SQLExp) -- extra nodes for order by , OrderByNode ) -processAnnOrderByCol pfx = \case +processAnnOrderByCol pfx aggCtx = \case AOCPG colInfo -> let qualCol = S.mkQIdenExp (mkBaseTableAls pfx) (toIden $ pgiName colInfo) @@ -386,7 +410,8 @@ processAnnOrderByCol pfx = \case -- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest" AOCRel (RelInfo rn _ colMapping relTab _) relFltr rest -> let relPfx = mkObjRelTableAls pfx rn - ((nesAls, nesCol), ordByNode) = processAnnOrderByCol relPfx rest + emptyAggCtx = AllAggCtx [] [] + ((nesAls, nesCol), ordByNode) = processAnnOrderByCol relPfx emptyAggCtx rest (objRelNodeM, aggNodeM) = case ordByNode of OBNNothing -> (Nothing, Nothing) OBNRelNode name node -> (Just (name, node), Nothing) @@ -405,17 +430,19 @@ processAnnOrderByCol pfx = \case , OBNRelNode rn relNode ) AOCAgg (RelInfo rn _ colMapping relTab _ ) relFltr annAggOb -> - let aggAls = mkAggOrdByAls pfx rn + let aggPfx = mkAggNodePfx pfx aggCtx $ ANIOrdBy rn fldName = mkAggObFld annAggOb - qOrdBy = S.mkQIdenExp aggAls $ toIden fldName + qOrdBy = S.mkQIdenExp aggPfx $ toIden fldName tabFrom = TableFrom relTab Nothing tabPerm = TablePerm relFltr Nothing (extr, aggFlds) = mkAggObExtrAndFlds annAggOb selFld = TAFAgg aggFlds - bn = mkBaseNode pfx fldName selFld tabFrom tabPerm noTableArgs - aggNode = AggNode colMapping [extr] bn - in ( (S.Alias $ toIden fldName, qOrdBy) - , OBNAggNode (S.Alias aggAls) aggNode + bn = mkBaseNode aggPfx fldName selFld tabFrom tabPerm noTableArgs + aggNode = AggNode colMapping [extr] $ mergeBaseNodes bn $ + mkEmptyBaseNode aggPfx tabFrom + obAls = aggPfx <> Iden "." <> toIden fldName + in ( (S.Alias obAls, qOrdBy) + , OBNAggNode (S.Alias aggPfx) aggNode ) mkEmptyBaseNode :: Iden -> TableFrom -> BaseNode @@ -443,7 +470,7 @@ aggSelToAggNode :: Iden -> FieldName -> AggSel -> AggNode aggSelToAggNode pfx als aggSel = AggNode colMapping [extr] mergedBN where - AggSel colMapping annSel = aggSel + AggSel _ colMapping annSel = aggSel AnnSelG aggFlds tabFrm tabPerm tabArgs = annSel fldAls = S.Alias $ toIden als @@ -469,6 +496,78 @@ aggSelToAggNode pfx als aggSel = S.SEFnApp "coalesce" [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing +data AllAggCtx + = AllAggCtx + { aacFields :: ![(FieldName, AggSel)] + , aacOrdBys :: ![RelName] + } deriving (Show, Eq) + +data AggNodeItem + = ANIField !(FieldName, AggSel) + | ANIOrdBy !RelName + deriving (Show, Eq) + +mkAggNodePfx + :: Iden + -> AllAggCtx + -> AggNodeItem + -> Iden +mkAggNodePfx pfx (AllAggCtx aggFlds obRels) = \case + ANIField aggFld@(fld, AggSel rn _ annAggSel) -> + let tabArgs = _asnArgs annAggSel + similarFlds = getSimilarAggFlds rn tabArgs $ delete aggFld + similarOrdByFound = rn `elem` obRels && tabArgs == noTableArgs + similarFound = not (null similarFlds) || similarOrdByFound + extraOrdByFlds = bool [] [ordByFld] similarOrdByFound + sortedFlds = sort $ fld : (similarFlds <> extraOrdByFlds) + in bool (mkAggAls pfx fld) (mkMergedAggNodeAls pfx rn sortedFlds) + similarFound + ANIOrdBy rn -> + let similarFlds = getSimilarAggFlds rn noTableArgs id + sortedFlds = sort $ ordByFld:similarFlds + in mkMergedAggNodeAls pfx rn sortedFlds + where + ordByFld = FieldName "order_by" + getSimilarAggFlds rn tabArgs f = map fst $ + flip filter (f aggFlds) $ \(_, AggSel arn _ annAggSel) -> + (rn == arn) && (tabArgs == _asnArgs annAggSel) + +mkAllAggCtx + :: TableAggFld + -> Maybe (NE.NonEmpty AnnOrderByItem) + -> AllAggCtx +mkAllAggCtx tabAggFld orderByM = + AllAggCtx aggFlds ordByAggRels + where + aggFlds = case tabAggFld of + TAFNodes flds -> mapMaybe getAggFld flds + _ -> [] + + ordByAggRels = maybe [] + (mapMaybe (fetchAggOrdByRels . obiColumn) . toList) orderByM + + getAggFld (f, annFld) = case annFld of + FAgg af -> Just (f, af) + _ -> Nothing + + fetchAggOrdByRels (AOCAgg ri _ _) = Just $ riName ri + fetchAggOrdByRels _ = Nothing + +mkArrRelPfx + :: Iden -> FieldName -> [(FieldName, AnnRel)] + -> (FieldName, AnnRel) -> (S.Alias, Iden) +mkArrRelPfx pfx parAls allArrRels arrRel@(fld, annRel) = + let getTabArgs = _asnArgs . arAnnSel + similarFlds = map fst $ + flip filter (delete arrRel allArrRels) $ + \(_, ar) -> (arName ar == arName annRel) + && (getTabArgs ar == getTabArgs annRel) + sortedFlds = sort $ fld:similarFlds + in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds + , mkArrRelTableAls pfx parAls sortedFlds + ) + + mkBaseNode :: Iden -> FieldName -> TableAggFld -> TableFrom -> TablePerm -> TableArgs -> BaseNode @@ -478,22 +577,25 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = where TablePerm fltr permLimitM = tablePerm TableArgs whereM orderByM limitM offsetM = tableArgs + allAggCtx@(AllAggCtx aggFlds _) = mkAllAggCtx annSelFlds orderByM + (allExtrs, allObjsWithOb, allArrs, aggs) = case annSelFlds of TAFNodes flds -> - let selExtr = buildJsonObject pfx fldAls flds + let selExtr = buildJsonObject pfx fldAls allAggCtx arrRels flds -- all the relationships + relFlds = mapMaybe getAnnRel flds + arrRels = flip filter relFlds $ \(_, annRel) -> + arType annRel == ArrRel (allObjs, allArrRels) = - foldl' addRel (HM.empty, HM.empty) $ - mapMaybe (\(als, f) -> (als,) <$> getAnnRel f) flds - aggItems = HM.fromList $ map mkAggItem $ - mapMaybe (\(als, f) -> (als,) <$> getAggFld f) flds + foldl' (addRel arrRels) (HM.empty, HM.empty) relFlds + aggItems = HM.fromListWith mergeAggNodes $ map mkAggItem aggFlds in ( HM.fromList $ selExtr:obExtrs , mkTotalObjRels allObjs , allArrRels - , aggItems `HM.union` aggItemsWithOb + , HM.unionWith mergeAggNodes aggItems aggItemsWithOb ) - TAFAgg aggFlds -> - let extrs = concatMap (fetchExtrFromAggFld . snd) aggFlds + TAFAgg tabAggs -> + let extrs = concatMap (fetchExtrFromAggFld . snd) tabAggs in ( HM.fromList $ extrs <> obExtrs , mkTotalObjRels HM.empty , HM.empty @@ -525,7 +627,7 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = tableQual = tableFromToQual tableFrom finalLimit = applyPermLimit permLimitM limitM - procOrdByM = unzip3 . map (processAnnOrderByItem pfx) . toList <$> orderByM + procOrdByM = unzip3 . map (processAnnOrderByItem pfx allAggCtx) . toList <$> orderByM ordByExpM = S.OrderByExp . _2 <$> procOrdByM mkTotalObjRels objRels = foldl' (\objs (rn, relNode) -> HM.insertWith mergeRelNodes rn relNode objs) @@ -536,37 +638,34 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = -- the columns needed for orderby obExtrs = maybe [] _1 procOrdByM - mkRelPfx rTy rn relAls = case rTy of - ObjRel -> mkObjRelTableAls pfx rn - ArrRel -> mkArrRelTableAls pfx fldAls relAls - -- process a relationship - addRel (objs, arrs) (relAls, annRel) = + addRel allArrRels (objs, arrs) (relAls, annRel) = let relName = arName annRel - relNodePfx = mkRelPfx (arType annRel) relName relAls - relNode = mkRelNode relNodePfx (relAls, annRel) in case arType annRel of -- in case of object relationships, we merge ObjRel -> - (HM.insertWith mergeRelNodes relName relNode objs, arrs) + let relNodePfx = mkObjRelTableAls pfx relName + relNode = mkRelNode relNodePfx (relAls, annRel) + in (HM.insertWith mergeRelNodes relName relNode objs, arrs) ArrRel -> - let arrRelTableAls = S.Alias $ mkUniqArrRelAls fldAls relAls - in (objs, HM.insert arrRelTableAls relNode arrs) + let --arrRelTableAls = S.Alias $ mkUniqArrRelAls fldAls relAls + (arrRelTableAls, relNodePfx) = + mkArrRelPfx pfx fldAls allArrRels (relAls, annRel) + relNode = mkRelNode relNodePfx (relAls, annRel) + jsonAggNode = ArrRelNode [S.Alias $ toIden relAls] relNode + in (objs, HM.insertWith mergeArrRelNodes arrRelTableAls jsonAggNode arrs) + -- process agg field mkAggItem (f, aggSel) = - let aggPfx = mkAggAls pfx f + let aggPfx = mkAggNodePfx pfx allAggCtx $ ANIField (f, aggSel) aggAls = S.Alias aggPfx aggNode = aggSelToAggNode aggPfx f aggSel in (aggAls, aggNode) - getAnnRel = \case - FRel ar -> Just ar - _ -> Nothing - - getAggFld = \case - FAgg af -> Just af - _ -> Nothing + getAnnRel (f, annFld) = case annFld of + FRel ar -> Just (f, ar) + _ -> Nothing getOrdByRelNode (OBNRelNode name node) = Just (name, node) getOrdByRelNode _ = Nothing @@ -674,11 +773,11 @@ baseNodeToSel joinCond (BaseNode pfx fromItem whr ordByM limitM offsetM extrs ob sel = baseNodeToSel (mkJoinCond baseSelAls relMapn) relBaseNode in S.mkLateralFromItem sel als - arrRelToFromItem :: RelNode -> S.FromItem - arrRelToFromItem (RelNode _ relFld relMapn relBaseNode) = - let als = S.Alias $ _bnPrefix relBaseNode - fldAls = S.Alias $ toIden relFld - sel = asJsonAggSel False fldAls (mkJoinCond baseSelAls relMapn) relBaseNode + arrRelToFromItem :: ArrRelNode -> S.FromItem + arrRelToFromItem (ArrRelNode alses relNode) = + let RelNode _ _ relMapn relBaseNode = relNode + als = S.Alias $ _bnPrefix relBaseNode + sel = asJsonAggSel False alses (mkJoinCond baseSelAls relMapn) relBaseNode in S.mkLateralFromItem sel als aggToFromItem :: (S.Alias, AggNode) -> S.FromItem diff --git a/server/src-lib/Hasura/RQL/Types/Common.hs b/server/src-lib/Hasura/RQL/Types/Common.hs index 3743adc29ea88..ed03d4f7d67bd 100644 --- a/server/src-lib/Hasura/RQL/Types/Common.hs +++ b/server/src-lib/Hasura/RQL/Types/Common.hs @@ -28,8 +28,8 @@ import Hasura.Prelude import Hasura.SQL.Types import Data.Aeson -import Data.Aeson.TH import Data.Aeson.Casing +import Data.Aeson.TH import qualified Data.Text as T import qualified Database.PG.Query as Q import Instances.TH.Lift () @@ -91,7 +91,7 @@ $(deriveToJSON (aesonDrop 2 snakeCase) ''RelInfo) newtype FieldName = FieldName { getFieldNameTxt :: T.Text } - deriving (Show, Eq, Hashable, FromJSON, ToJSON, FromJSONKey, ToJSONKey, Lift) + deriving (Show, Eq, Ord, Hashable, FromJSON, ToJSON, FromJSONKey, ToJSONKey, Lift) instance IsIden FieldName where toIden (FieldName f) = Iden f From b856090b4cddc7cf933730b1e20198beed25ff19 Mon Sep 17 00:00:00 2001 From: rakeshkky Date: Tue, 27 Nov 2018 15:00:28 +0530 Subject: [PATCH 5/5] improve query performance by merging similar array & array agg nodes -> Move all types from '..DML/Select/Internal.hs' to '../DML/Select/Types.hs' -> Merge array relation items and array relation aggregate items in 'BaseNode' Type --- server/graphql-engine.cabal | 1 + .../src-lib/Hasura/GraphQL/Resolve/BoolExp.hs | 18 +- .../src-lib/Hasura/GraphQL/Resolve/Context.hs | 5 +- .../src-lib/Hasura/GraphQL/Resolve/Select.hs | 39 +- server/src-lib/Hasura/RQL/DML/Select.hs | 59 +- .../src-lib/Hasura/RQL/DML/Select/Internal.hs | 702 +++++++----------- server/src-lib/Hasura/RQL/DML/Select/Types.hs | 220 ++++++ 7 files changed, 526 insertions(+), 518 deletions(-) create mode 100644 server/src-lib/Hasura/RQL/DML/Select/Types.hs diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 020f31a0b3c88..ef3478c13cc0d 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -171,6 +171,7 @@ library , Hasura.RQL.DML.Insert , Hasura.RQL.DML.Returning , Hasura.RQL.DML.Select.Internal + , Hasura.RQL.DML.Select.Types , Hasura.RQL.DML.Select , Hasura.RQL.DML.Update , Hasura.RQL.DML.Count diff --git a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs index d90a0c858da71..3d64904f047c4 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs @@ -16,8 +16,6 @@ import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Language.GraphQL.Draft.Syntax as G -import qualified Hasura.SQL.DML as S - import Hasura.GraphQL.Resolve.Context import Hasura.GraphQL.Resolve.InputValue import Hasura.GraphQL.Validate.Types @@ -87,12 +85,8 @@ parseAsEqOp annVal = do parseColExp :: (MonadError QErr m, MonadReader r m, Has FieldMap r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> G.NamedType - -> G.Name - -> AnnGValue - -> (AnnGValue -> m [OpExp]) - -> m AnnBoolExpFldSQL + => PrepFn m -> G.NamedType -> G.Name -> AnnGValue + -> (AnnGValue -> m [OpExp]) -> m AnnBoolExpFldSQL parseColExp f nt n val expParser = do fldInfo <- getFldInfo nt n case fldInfo of @@ -105,9 +99,7 @@ parseColExp f nt n val expParser = do parseBoolExp :: (MonadError QErr m, MonadReader r m, Has FieldMap r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> AnnGValue - -> m AnnBoolExpSQL + => PrepFn m -> AnnGValue -> m AnnBoolExpSQL parseBoolExp f annGVal = do boolExpsM <- flip withObjectM annGVal @@ -124,9 +116,7 @@ type PGColValMap = Map.HashMap G.Name AnnGValue pgColValToBoolExp :: (MonadError QErr m, MonadReader r m, Has FieldMap r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> PGColValMap - -> m AnnBoolExpSQL + => PrepFn m -> PGColValMap -> m AnnBoolExpSQL pgColValToBoolExp f colValMap = do colExps <- forM colVals $ \(name, val) -> do (ty, _) <- asPGColVal val diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index 7bfc546fb2e1c..a3c0100d9fc3a 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -14,6 +14,7 @@ module Hasura.GraphQL.Resolve.Context , InsCtx(..) , InsCtxMap , RespTx + , PrepFn , InsertTxConflictCtx(..) , getFldInfo , getPGColInfo @@ -73,6 +74,7 @@ $(J.deriveJSON (J.aesonDrop 3 J.snakeCase) ''InsResp) -- deriving (Show, Eq) type RespTx = Q.TxE QErr BL.ByteString +type PrepFn m = (PGColType, PGColValue) -> m S.SQLExp -- -- order by context -- data OrdByItem @@ -162,8 +164,7 @@ type Convert = StateT PrepArgs (ReaderT (FieldMap, OrdByCtx, InsCtxMap) (Except QErr)) prepare - :: (MonadState PrepArgs m) - => (PGColType, PGColValue) -> m S.SQLExp + :: (MonadState PrepArgs m) => PrepFn m prepare (colTy, colVal) = do preparedArgs <- get put (preparedArgs Seq.|> binEncoder colVal) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs index 27577d7f07c99..1424cbae175dc 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Select.hs @@ -19,12 +19,14 @@ module Hasura.GraphQL.Resolve.Select , fromAggField ) where +import Control.Arrow (first) import Data.Has import Hasura.Prelude import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T import qualified Language.GraphQL.Draft.Syntax as G import qualified Hasura.RQL.DML.Select as RS @@ -49,10 +51,7 @@ withSelSet selSet f = fromSelSet :: (MonadError QErr m, MonadReader r m, Has FieldMap r, Has OrdByCtx r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> G.NamedType - -> SelSet - -> m [(FieldName, RS.AnnFld)] + => PrepFn m -> G.NamedType -> SelSet -> m [(FieldName, RS.AnnFld)] fromSelSet f fldTy flds = forM (toList flds) $ \fld -> do let fldName = _fName fld @@ -69,20 +68,20 @@ fromSelSet f fldTy flds = rn = riName relInfo if isAgg then do aggSel <- fromAggField f relTN tableFilter tableLimit fld - return $ RS.FAgg $ RS.AggSel rn colMapping aggSel + return $ RS.FArr $ RS.ASAgg $ RS.AnnRelG rn colMapping aggSel else do annSel <- fromField f relTN tableFilter tableLimit fld - let annRel = RS.AnnRel rn (riType relInfo) colMapping annSel - return $ RS.FRel annRel + let annRel = RS.AnnRelG rn colMapping annSel + return $ case riType relInfo of + ObjRel -> RS.FObj annRel + ArrRel -> RS.FArr $ RS.ASSimple annRel fieldAsPath :: (MonadError QErr m) => Field -> m a -> m a fieldAsPath = nameAsPath . _fName - parseTableArgs :: (MonadError QErr m, MonadReader r m, Has FieldMap r, Has OrdByCtx r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> ArgsMap -> m RS.TableArgs + => PrepFn m -> ArgsMap -> m RS.TableArgs parseTableArgs f args = do whereExpM <- withArgM args "where" $ parseBoolExp f ordByExpML <- withArgM args "order_by" parseOrderBy @@ -110,8 +109,8 @@ parseTableArgs f args = do fromField :: (MonadError QErr m, MonadReader r m, Has FieldMap r, Has OrdByCtx r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> QualifiedTable -> AnnBoolExpSQL -> Maybe Int -> Field -> m RS.AnnSel + => PrepFn m -> QualifiedTable -> AnnBoolExpSQL + -> Maybe Int -> Field -> m RS.AnnSel fromField f tn permFilter permLimitM fld = fieldAsPath fld $ do tableArgs <- parseTableArgs f args @@ -166,7 +165,7 @@ getAnnObItems f nt obj = do return [OrderByItemG (Just ordTy) aobCol (Just nullsOrd)] OBIRel ri fltr -> do - let annObColFn = f . RS.AOCRel ri fltr + let annObColFn = f . RS.AOCObj ri fltr withObject (getAnnObItems annObColFn) v OBIAgg ri fltr -> do @@ -272,9 +271,12 @@ convertCount args = do mkCType isDistinct cols = return $ bool (S.CTSimple cols) (S.CTDistinct cols) isDistinct +toFields :: [(T.Text, a)] -> [(FieldName, a)] +toFields = map (first FieldName) + convertColFlds :: Monad m => G.NamedType -> SelSet -> m RS.ColFlds -convertColFlds ty selSet = +convertColFlds ty selSet = fmap toFields $ withSelSet selSet $ \fld -> case _fName fld of "__typename" -> return $ RS.PCFExp $ G.unName $ G.unNamedType ty @@ -283,7 +285,7 @@ convertColFlds ty selSet = convertAggFld :: (Monad m, MonadError QErr m) => G.NamedType -> SelSet -> m RS.AggFlds -convertAggFld ty selSet = +convertAggFld ty selSet = fmap toFields $ withSelSet selSet $ \fld -> do let fType = _fType fld fSelSet = _fSelSet fld @@ -300,11 +302,12 @@ convertAggFld ty selSet = fromAggField :: (MonadError QErr m, MonadReader r m, Has FieldMap r, Has OrdByCtx r) - => ((PGColType, PGColValue) -> m S.SQLExp) - -> QualifiedTable -> AnnBoolExpSQL -> Maybe Int -> Field -> m RS.AnnAggSel + => PrepFn m -> QualifiedTable -> AnnBoolExpSQL + -> Maybe Int -> Field -> m RS.AnnAggSel fromAggField fn tn permFilter permLimitM fld = fieldAsPath fld $ do tableArgs <- parseTableArgs fn args - aggSelFlds <- fromAggSel (_fType fld) $ _fSelSet fld + aggSelFlds <- toFields <$> + fromAggSel (_fType fld) (_fSelSet fld) let tabFrom = RS.TableFrom tn Nothing tabPerm = RS.TablePerm permFilter permLimitM return $ RS.AnnSelG aggSelFlds tabFrom tabPerm tableArgs diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 5cd0220f659ed..a6272e05131a8 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -7,8 +7,6 @@ module Hasura.RQL.DML.Select ( selectP2 , selectAggP2 - , mkSQLSelect - , mkAggSelect , convSelectQuery , getSelectDeps , module Hasura.RQL.DML.Select.Internal @@ -29,7 +27,6 @@ import Hasura.RQL.DML.Internal import Hasura.RQL.DML.Select.Internal import Hasura.RQL.GBoolExp import Hasura.RQL.Types -import Hasura.SQL.Rewrite (prefixNumToAliases) import Hasura.SQL.Types import qualified Database.PG.Query as Q @@ -144,7 +141,7 @@ convOrderByElem (flds, spi) = \case ," and can't be used in 'order_by'" ] (relFim, relSpi) <- fetchRelDet (riName relInfo) (riRTable relInfo) - AOCRel relInfo (spiFilter relSpi) <$> + AOCObj relInfo (spiFilter relSpi) <$> convOrderByElem (relFim, relSpi) rest convSelectQ @@ -163,7 +160,9 @@ convSelectQ fieldInfoMap selPermInfo selQ prepValBuilder = do return (fromPGCol pgCol, FCol colInfo) (ECRel relName mAlias relSelQ) -> do annRel <- convExtRel fieldInfoMap relName mAlias relSelQ prepValBuilder - return (fromRel $ fromMaybe relName mAlias, FRel annRel) + return ( fromRel $ fromMaybe relName mAlias + , either FObj FArr annRel + ) -- let spiT = spiTable selPermInfo @@ -212,17 +211,21 @@ convExtRel -> Maybe RelName -> SelectQExt -> (PGColType -> Value -> m S.SQLExp) - -> m AnnRel + -> m (Either ObjSel ArrSel) convExtRel fieldInfoMap relName mAlias selQ prepValBuilder = do -- Point to the name key relInfo <- withPathK "name" $ askRelType fieldInfoMap relName pgWhenRelErr let (RelInfo _ relTy colMapping relTab _) = relInfo (relCIM, relSPI) <- fetchRelDet relName relTab - when (relTy == ObjRel && misused) $ - throw400 UnexpectedPayload objRelMisuseMsg annSel <- convSelectQ relCIM relSPI selQ prepValBuilder - return $ AnnRel (fromMaybe relName mAlias) relTy colMapping annSel + case relTy of + ObjRel -> do + when misused $ throw400 UnexpectedPayload objRelMisuseMsg + return $ Left $ AnnRelG (fromMaybe relName mAlias) colMapping annSel + ArrRel -> + return $ Right $ ASSimple $ AnnRelG (fromMaybe relName mAlias) + colMapping annSel where pgWhenRelErr = "only relationships can be expanded" misused = @@ -238,12 +241,13 @@ convExtRel fieldInfoMap relName mAlias selQ prepValBuilder = do ] partAnnFlds - :: [AnnFld] -> ([(PGCol, PGColType)], [AnnRel]) + :: [AnnFld] + -> ([(PGCol, PGColType)], [Either ObjSel ArrSel]) partAnnFlds flds = partitionEithers $ catMaybes $ flip map flds $ \case FCol c -> Just $ Left (pgiName c, pgiType c) - FRel r -> Just $ Right r - FAgg _ -> Nothing + FObj o -> Just $ Right $ Left o + FArr a -> Just $ Right $ Right a FExp _ -> Nothing getSelectDeps @@ -259,13 +263,23 @@ getSelectDeps (AnnSelG flds tabFrm _ tableArgs) = TableFrom tn _ = tabFrm annWc = _taWhere tableArgs (sCols, rCols) = partAnnFlds $ map snd flds + (objSels, arrSels) = partitionEithers rCols colDeps = map (mkColDep "untyped" tn . fst) sCols - relDeps = map (mkRelDep . arName) rCols - nestedDeps = concatMap (getSelectDeps . arAnnSel) rCols + relDeps = map mkRelDep $ map aarName objSels + <> mapMaybe getRelName arrSels + nestedDeps = concatMap getSelectDeps $ map aarAnnSel objSels + <> mapMaybe getAnnSel arrSels whereDeps = getBoolExpDeps tn <$> annWc mkRelDep rn = SchemaDependency (SOTableObj tn (TORel rn)) "untyped" + -- ignore aggregate selections to calculate schema deps + getRelName (ASSimple aar) = Just $ aarName aar + getRelName (ASAgg _) = Nothing + + getAnnSel (ASSimple aar) = Just $ aarAnnSel aar + getAnnSel (ASAgg _) = Nothing + convSelectQuery :: (P1C m) => (PGColType -> Value -> m S.SQLExp) @@ -278,14 +292,6 @@ convSelectQuery prepArgBuilder (DMLQuery qt selQ) = do validateHeaders $ spiRequiredHeaders selPermInfo convSelectQ (tiFieldInfoMap tabInfo) selPermInfo extSelQ prepArgBuilder -mkAggSelect :: AnnAggSel -> S.Select -mkAggSelect annAggSel = - prefixNumToAliases $ aggNodeToSelect bn extr $ S.BELit True - where - aggSel = AggSel (RelName "root") [] annAggSel - AggNode _ extr bn = - aggSelToAggNode (Iden "root") (FieldName "root") aggSel - selectAggP2 :: (AnnAggSel, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody selectAggP2 (sel, p) = runIdentity . Q.getRow @@ -293,15 +299,6 @@ selectAggP2 (sel, p) = where selectSQL = toSQL $ mkAggSelect sel -mkSQLSelect :: Bool -> AnnSel -> S.Select -mkSQLSelect isSingleObject annSel = - prefixNumToAliases $ asJsonAggSel isSingleObject [rootFldAls] (S.BELit True) - $ annSelToBaseNode (toIden rootFldName) - rootFldName annSel - where - rootFldName = FieldName "root" - rootFldAls = S.Alias $ toIden rootFldName - -- selectP2 :: (QErrM m, CacheRWM m, MonadTx m, MonadIO m) => (SelectQueryP1, DS.Seq Q.PrepArg) -> m RespBody selectP2 :: Bool -> (AnnSel, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody diff --git a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs index fb98db73f8521..b602a4c7b364c 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -1,156 +1,36 @@ -{-# LANGUAGE DeriveLift #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -module Hasura.RQL.DML.Select.Internal where +module Hasura.RQL.DML.Select.Internal + ( mkSQLSelect + , mkAggSelect + , module Hasura.RQL.DML.Select.Types + ) +where -import Control.Arrow ((&&&)) -import Data.Aeson.Types -import Data.List (delete, sort) -import Instances.TH.Lift () -import Language.Haskell.TH.Syntax (Lift) +import Control.Arrow ((&&&)) +import Data.List (delete, sort) +import Instances.TH.Lift () -import qualified Data.HashMap.Strict as HM -import qualified Data.List.NonEmpty as NE -import qualified Data.Text as T +import qualified Data.HashMap.Strict as HM +import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T import Hasura.Prelude import Hasura.RQL.DML.Internal +import Hasura.RQL.DML.Select.Types import Hasura.RQL.GBoolExp import Hasura.RQL.Types import Hasura.Server.Utils +import Hasura.SQL.Rewrite (prefixNumToAliases) import Hasura.SQL.Types -import qualified Hasura.SQL.DML as S +import qualified Hasura.SQL.DML as S -- Conversion of SelectQ happens in 2 Stages. -- Stage 1 : Convert input query into an annotated AST -- Stage 2 : Convert annotated AST to SQL Select -type SelectQExt = SelectG ExtCol BoolExp Int --- Columns in RQL -data ExtCol - = ECSimple !PGCol - | ECRel !RelName !(Maybe RelName) !SelectQExt - deriving (Show, Eq, Lift) - -instance ToJSON ExtCol where - toJSON (ECSimple s) = toJSON s - toJSON (ECRel rn mrn selq) = - object $ [ "name" .= rn - , "alias" .= mrn - ] ++ selectGToPairs selq - -instance FromJSON ExtCol where - parseJSON v@(Object o) = - ECRel - <$> o .: "name" - <*> o .:? "alias" - <*> parseJSON v - parseJSON (String s) = - return $ ECSimple $ PGCol s - parseJSON _ = - fail $ mconcat - [ "A column should either be a string or an " - , "object (relationship)" - ] - -data AnnAggOrdBy - = AAOCount - | AAOOp !T.Text !PGCol - deriving (Show, Eq) - -data AnnObCol - = AOCPG !PGColInfo - | AOCRel !RelInfo !AnnBoolExpSQL !AnnObCol - | AOCAgg !RelInfo !AnnBoolExpSQL !AnnAggOrdBy - deriving (Show, Eq) - -type AnnOrderByItem = OrderByItemG AnnObCol - -data AnnRel - = AnnRel - { arName :: !RelName -- Relationship name - , arType :: !RelType -- Relationship type (ObjRel, ArrRel) - , arMapping :: ![(PGCol, PGCol)] -- Column of the left table to join with - , arAnnSel :: !AnnSel -- Current table. Almost ~ to SQL Select - } deriving (Show, Eq) - -data ArrRelNode - = ArrRelNode - { arnExtrAls :: ![S.Alias] - , arnNode :: !RelNode - } deriving (Show, Eq) - -mergeArrRelNodes :: ArrRelNode -> ArrRelNode -> ArrRelNode -mergeArrRelNodes lJAS rJAS = - ArrRelNode (lExtrs `union` rExtrs) $ mergeRelNodes lBN rBN - where - ArrRelNode lExtrs lBN = lJAS - ArrRelNode rExtrs rBN = rJAS - - -type AnnAggSel = AnnSelG [(T.Text, TableAggFld)] - -data AggSel - = AggSel - { agRelName :: !RelName - , agColMapping :: ![(PGCol, PGCol)] - , agAnnSel :: !AnnAggSel - } deriving (Show, Eq) - -data AnnFld - = FCol !PGColInfo - | FExp !T.Text - | FRel !AnnRel - | FAgg !AggSel - deriving (Show, Eq) - -data TableArgs - = TableArgs - { _taWhere :: !(Maybe AnnBoolExpSQL) - , _taOrderBy :: !(Maybe (NE.NonEmpty AnnOrderByItem)) - , _taLimit :: !(Maybe Int) - , _taOffset :: !(Maybe S.SQLExp) - , _taDistCols :: !(Maybe (NE.NonEmpty PGCol)) - } deriving (Show, Eq) - -noTableArgs :: TableArgs -noTableArgs = TableArgs Nothing Nothing Nothing Nothing Nothing - -data PGColFld - = PCFCol !PGCol - | PCFExp !T.Text - deriving (Show, Eq) - -type ColFlds = [(T.Text, PGColFld)] - -data AggOp - = AggOp - { _aoOp :: !T.Text - , _aoFlds :: !ColFlds - } deriving (Show, Eq) - -data AggFld - = AFCount !S.CountType - | AFOp !AggOp - | AFExp !T.Text - deriving (Show, Eq) - -type AggFlds = [(T.Text, AggFld)] - -data TableAggFld - = TAFAgg !AggFlds - | TAFNodes ![(FieldName, AnnFld)] - | TAFExp !T.Text - deriving (Show, Eq) - -data TableFrom - = TableFrom - { _tfTable :: !QualifiedTable - , _tfIden :: !(Maybe Iden) - } deriving (Show, Eq) - tableFromToFromItem :: TableFrom -> S.FromItem tableFromToFromItem = \case TableFrom tn Nothing -> S.FISimple tn Nothing @@ -161,47 +41,12 @@ tableFromToQual = \case TableFrom tn Nothing -> S.QualTable tn TableFrom _ (Just i) -> S.QualIden i -data TablePerm - = TablePerm - { _tpFilter :: !AnnBoolExpSQL - , _tpLimit :: !(Maybe Int) - } deriving (Eq, Show) - -data AnnSelG a - = AnnSelG - { _asnFields :: !a - , _asnFrom :: !TableFrom - , _asnPerm :: !TablePerm - , _asnArgs :: !TableArgs - } deriving (Show, Eq) - -type AnnSel = AnnSelG [(FieldName, AnnFld)] - -data BaseNode - = BaseNode - { _bnPrefix :: !Iden - , _bnDistinct :: !(Maybe S.DistinctExpr) - , _bnFrom :: !S.FromItem - , _bnWhere :: !S.BoolExp - , _bnOrderBy :: !(Maybe S.OrderByExp) - , _bnLimit :: !(Maybe Int) - , _bnOffset :: !(Maybe S.SQLExp) - - , _bnExtrs :: !(HM.HashMap S.Alias S.SQLExp) - , _bnObjRels :: !(HM.HashMap RelName RelNode) - , _bnArrRels :: !(HM.HashMap S.Alias ArrRelNode) - , _bnAggs :: !(HM.HashMap S.Alias AggNode) - } deriving (Show, Eq) - -txtToAlias :: Text -> S.Alias -txtToAlias = S.Alias . Iden - aggFldToExp :: AggFlds -> S.SQLExp aggFldToExp aggFlds = jsonRow where jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds) withAls fldName sqlExp = [S.SELit fldName, sqlExp] - aggToFlds (t, fld) = withAls t $ case fld of + aggToFlds (FieldName t, fld) = withAls t $ case fld of AFCount cty -> S.SECount cty AFOp aggOp -> aggOpToObj aggOp AFExp e -> S.SELit e @@ -209,29 +54,15 @@ aggFldToExp aggFlds = jsonRow aggOpToObj (AggOp op flds) = S.applyJsonBuildObj $ concatMap (colFldsToExtr op) flds - colFldsToExtr op (t, PCFCol col) = + colFldsToExtr op (FieldName t, PCFCol col) = [ S.SELit t , S.SEFnApp op [S.SEIden $ toIden col] Nothing ] - colFldsToExtr _ (t, PCFExp e) = + colFldsToExtr _ (FieldName t, PCFExp e) = [ S.SELit t , S.SELit e] -asSingleRow :: [S.Alias] -> S.FromItem -> S.Select -asSingleRow cols fromItem = - S.mkSelect - { S.selExtr = flip map cols $ - \c -> S.Extractor (mkExtr c) $ Just c - , S.selFrom = Just $ S.FromExp [fromItem] - } - where - mkExtr c = S.SEFnApp "coalesce" [jsonAgg c, S.SELit "null"] Nothing - jsonAgg c = S.SEOpApp (S.SQLOp "->") - [ S.SEFnApp "json_agg" [S.SEIden $ toIden c] Nothing - , S.SEUnsafe "0" - ] - -aggNodeToSelect :: BaseNode -> [S.Extractor] -> S.BoolExp -> S.Select -aggNodeToSelect bn extrs joinCond = +arrNodeToSelect :: BaseNode -> [S.Extractor] -> S.BoolExp -> S.Select +arrNodeToSelect bn extrs joinCond = S.mkSelect { S.selExtr = extrs , S.selFrom = Just $ S.FromExp [selFrom] @@ -240,26 +71,25 @@ aggNodeToSelect bn extrs joinCond = selFrom = S.mkSelFromItem (baseNodeToSel joinCond bn) $ S.Alias $ _bnPrefix bn -withJsonAgg :: Maybe S.OrderByExp -> [S.Alias] -> S.FromItem -> S.Select -withJsonAgg orderByM cols fromItem = - S.mkSelect - { S.selExtr = flip map cols $ - \c -> S.Extractor (mkExtr c) $ Just c - , S.selFrom = Just $ S.FromExp [fromItem] - } +asSingleRowExtr :: S.Alias -> S.Extractor +asSingleRowExtr col = S.Extractor extr $ Just col + where + extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing + jsonAgg = S.SEOpApp (S.SQLOp "->") + [ S.SEFnApp "json_agg" [S.SEIden $ toIden col] Nothing + , S.SEUnsafe "0" + ] + +withJsonAggExtr :: Maybe S.OrderByExp -> S.Alias -> S.Extractor +withJsonAggExtr orderByM col = + S.Extractor extr $ Just col where - mkExtr c = S.SEFnApp "coalesce" [jsonAgg c, S.SELit "[]"] Nothing - jsonAgg c = S.SEFnApp "json_agg" [S.SEIden $ toIden c] orderByM - -asJsonAggSel :: Bool -> [S.Alias] -> S.BoolExp -> BaseNode -> S.Select -asJsonAggSel singleObj als joinCond n = - let ordByM = _bnOrderBy n - fromItem = S.mkSelFromItem (baseNodeToSel joinCond n) $ - S.Alias $ _bnPrefix n - in bool - (withJsonAgg ordByM als fromItem) - (asSingleRow als fromItem) - singleObj + extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "[]"] Nothing + jsonAgg = S.SEFnApp "json_agg" [S.SEIden $ toIden col] orderByM + +asJsonAggExtr :: Bool -> S.Alias -> Maybe S.OrderByExp -> S.Extractor +asJsonAggExtr singleObj als ordByExpM = + bool (withJsonAggExtr ordByExpM als) (asSingleRowExtr als) singleObj -- array relationships are not grouped, so have to be prefixed by -- parent's alias @@ -279,15 +109,6 @@ mkObjRelTableAls :: Iden -> RelName -> Iden mkObjRelTableAls pfx relName = pfx <> Iden ".or." <> toIden relName -mkAggAls :: Iden -> FieldName -> Iden -mkAggAls pfx fldAls = - pfx <> Iden ".agg." <> toIden fldAls - -mkMergedAggNodeAls :: Iden -> RelName -> [FieldName] -> Iden -mkMergedAggNodeAls pfx relName flds = - pfx <> Iden ".agg." <> toIden relName <> Iden "." - <> Iden (T.intercalate "." $ map getFieldNameTxt flds) - mkBaseTableAls :: Iden -> Iden mkBaseTableAls pfx = pfx <> Iden ".base" @@ -296,14 +117,17 @@ mkBaseTableColAls :: Iden -> PGCol -> Iden mkBaseTableColAls pfx pgCol = pfx <> Iden ".pg." <> toIden pgCol +ordByFldName :: FieldName +ordByFldName = FieldName "order_by" + -- posttgres ignores anything beyond 63 chars for an iden -- in this case, we'll need to use json_build_object function -- json_build_object is slower than row_to_json hence it is only -- used when needed buildJsonObject - :: Iden -> FieldName -> AllAggCtx -> [(FieldName, AnnRel)] + :: Iden -> FieldName -> ArrRelCtx -> [(FieldName, AnnFld)] -> (S.Alias, S.SQLExp) -buildJsonObject pfx parAls allAggCtx allArrRels flds = +buildJsonObject pfx parAls arrRelCtx flds = if any ( (> 63) . T.length . getFieldNameTxt . fst ) flds then withJsonBuildObj parAls jsonBuildObjExps else withRowToJSON parAls rowToJsonExtrs @@ -320,16 +144,13 @@ buildJsonObject pfx parAls allAggCtx allArrRels flds = FCol col -> toJSONableExp (pgiType col) $ S.mkQIdenExp (mkBaseTableAls pfx) $ pgiName col FExp e -> S.SELit e - FRel annRel -> - let qual = case arType annRel of - ObjRel -> mkObjRelTableAls pfx $ arName annRel - ArrRel -> snd $ mkArrRelPfx pfx parAls - allArrRels (fldAls, annRel) + FObj objSel -> + let qual = mkObjRelTableAls pfx $ aarName objSel in S.mkQIdenExp qual fldAls - FAgg aggSel -> - let aggPfx = mkAggNodePfx pfx allAggCtx $ - ANIField (fldAls, aggSel) - in S.mkQIdenExp aggPfx fldAls + FArr arrSel -> + let arrPfx = snd $ mkArrNodePfx pfx parAls arrRelCtx $ + ANIField (fldAls, arrSel) + in S.mkQIdenExp arrPfx fldAls -- uses row_to_json to build a json object withRowToJSON @@ -347,12 +168,6 @@ withJsonBuildObj parAls exps = where jsonRow = S.applyJsonBuildObj exps -data OrderByNode - = OBNNothing - | OBNRelNode !RelName !RelNode - | OBNAggNode !S.Alias !AggNode - deriving (Show, Eq) - mkAggObFld :: AnnAggOrdBy -> FieldName mkAggObFld = \case AAOCount -> FieldName "count" @@ -362,18 +177,19 @@ mkAggObExtrAndFlds :: AnnAggOrdBy -> (S.Extractor, AggFlds) mkAggObExtrAndFlds annAggOb = case annAggOb of AAOCount -> ( S.Extractor S.countStar als - , [("count", AFCount S.CTStar)] + , [(FieldName "count", AFCount S.CTStar)] ) AAOOp op pgCol -> ( S.Extractor (S.SEFnApp op [S.SEIden $ toIden pgCol] Nothing) als - , [(op, AFOp $ AggOp op [(getPGColTxt pgCol, PCFCol pgCol)])] + , [(FieldName op, AFOp $ AggOp op [(fromPGCol pgCol, PCFCol pgCol)])] ) where als = Just $ S.toAlias $ mkAggObFld annAggOb processAnnOrderByItem :: Iden - -> AllAggCtx + -> FieldName + -> ArrRelCtx -> AnnOrderByItem -- the extractors which will select the needed columns -> ( (S.Alias, S.SQLExp) @@ -382,27 +198,28 @@ processAnnOrderByItem -- extra nodes for order by , OrderByNode ) -processAnnOrderByItem pfx aggCtx (OrderByItemG obTyM annObCol obNullsM) = +processAnnOrderByItem pfx parAls arrRelCtx (OrderByItemG obTyM annObCol obNullsM) = ( (obColAls, obColExp) , sqlOrdByItem , relNodeM ) where - ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx aggCtx annObCol + ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx parAls arrRelCtx annObCol sqlOrdByItem = S.OrderByItem (S.SEIden $ toIden obColAls) obTyM obNullsM processAnnOrderByCol :: Iden - -> AllAggCtx + -> FieldName + -> ArrRelCtx -> AnnObCol -- the extractors which will select the needed columns -> ( (S.Alias, S.SQLExp) -- extra nodes for order by , OrderByNode ) -processAnnOrderByCol pfx aggCtx = \case +processAnnOrderByCol pfx parAls arrRelCtx = \case AOCPG colInfo -> let qualCol = S.mkQIdenExp (mkBaseTableAls pfx) (toIden $ pgiName colInfo) @@ -411,41 +228,41 @@ processAnnOrderByCol pfx aggCtx = \case , OBNNothing ) -- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest" - AOCRel (RelInfo rn _ colMapping relTab _) relFltr rest -> + AOCObj (RelInfo rn _ colMapping relTab _) relFltr rest -> let relPfx = mkObjRelTableAls pfx rn - emptyAggCtx = AllAggCtx [] [] - ((nesAls, nesCol), ordByNode) = processAnnOrderByCol relPfx emptyAggCtx rest - (objRelNodeM, aggNodeM) = case ordByNode of + ((nesAls, nesCol), ordByNode) = + processAnnOrderByCol relPfx ordByFldName emptyArrRelCtx rest + (objNodeM, arrNodeM) = case ordByNode of OBNNothing -> (Nothing, Nothing) - OBNRelNode name node -> (Just (name, node), Nothing) - OBNAggNode als node -> (Nothing, Just (als, node)) + OBNObjNode name node -> (Just (name, node), Nothing) + OBNArrNode als node -> (Nothing, Just (als, node)) qualCol = S.mkQIdenExp relPfx nesAls relBaseNode = BaseNode relPfx Nothing (S.FISimple relTab Nothing) (toSQLBoolExp (S.QualTable relTab) relFltr) Nothing Nothing Nothing (HM.singleton nesAls nesCol) - (maybe HM.empty (uncurry HM.singleton) objRelNodeM) - HM.empty - (maybe HM.empty (uncurry HM.singleton) aggNodeM) - relNode = RelNode rn (fromRel rn) colMapping relBaseNode + (maybe HM.empty (uncurry HM.singleton) objNodeM) + (maybe HM.empty (uncurry HM.singleton) arrNodeM) + relNode = ObjNode colMapping relBaseNode in ( (nesAls, qualCol) - , OBNRelNode rn relNode + , OBNObjNode rn relNode ) AOCAgg (RelInfo rn _ colMapping relTab _ ) relFltr annAggOb -> - let aggPfx = mkAggNodePfx pfx aggCtx $ ANIOrdBy rn + let (arrAls, arrPfx) = + mkArrNodePfx pfx parAls arrRelCtx $ ANIAggOrdBy rn fldName = mkAggObFld annAggOb - qOrdBy = S.mkQIdenExp aggPfx $ toIden fldName + qOrdBy = S.mkQIdenExp arrPfx $ toIden fldName tabFrom = TableFrom relTab Nothing tabPerm = TablePerm relFltr Nothing - (extr, aggFlds) = mkAggObExtrAndFlds annAggOb - selFld = TAFAgg aggFlds - bn = mkBaseNode aggPfx fldName selFld tabFrom tabPerm noTableArgs - aggNode = AggNode colMapping [extr] $ mergeBaseNodes bn $ - mkEmptyBaseNode aggPfx tabFrom - obAls = aggPfx <> Iden "." <> toIden fldName + (extr, arrFlds) = mkAggObExtrAndFlds annAggOb + selFld = TAFAgg arrFlds + bn = mkBaseNode arrPfx fldName selFld tabFrom tabPerm noTableArgs + aggNode = ArrNode [extr] colMapping $ mergeBaseNodes bn $ + mkEmptyBaseNode arrPfx tabFrom + obAls = arrPfx <> Iden "." <> toIden fldName in ( (S.Alias obAls, qOrdBy) - , OBNAggNode (S.Alias aggPfx) aggNode + , OBNArrNode arrAls aggNode ) processDistinctOnCol @@ -466,8 +283,8 @@ processDistinctOnCol pfx neCols = (distOnExp, colExtrs) mkEmptyBaseNode :: Iden -> TableFrom -> BaseNode mkEmptyBaseNode pfx tableFrom = - BaseNode pfx Nothing fromItem (S.BELit True) Nothing Nothing Nothing - selOne HM.empty HM.empty HM.empty + BaseNode pfx Nothing fromItem (S.BELit True) Nothing Nothing + Nothing selOne HM.empty HM.empty where selOne = HM.singleton (S.Alias $ pfx <> Iden "__one") (S.SEUnsafe "1") fromItem = tableFromToFromItem tableFrom @@ -485,11 +302,11 @@ applyPermLimit mPermLimit mQueryLimit = compareLimits pLimit qLimit = Just $ if qLimit > pLimit then pLimit else qLimit -aggSelToAggNode :: Iden -> FieldName -> AggSel -> AggNode -aggSelToAggNode pfx als aggSel = - AggNode colMapping [extr] mergedBN +aggSelToArrNode :: Iden -> FieldName -> ArrRelAgg -> ArrNode +aggSelToArrNode pfx als aggSel = + ArrNode [extr] colMapping mergedBN where - AggSel _ colMapping annSel = aggSel + AnnRelG _ colMapping annSel = aggSel AnnSelG aggFlds tabFrm tabPerm tabArgs = annSel fldAls = S.Alias $ toIden als @@ -502,10 +319,10 @@ aggSelToAggNode pfx als aggSel = emptyBN = mkEmptyBaseNode pfx tabFrm mergedBN = foldr mergeBaseNodes emptyBN allBNs - mkAggBaseNode (t, selFld) = - mkBaseNode pfx (FieldName t) selFld tabFrm tabPerm tabArgs + mkAggBaseNode (fn, selFld) = + mkBaseNode pfx fn selFld tabFrm tabPerm tabArgs - selFldToExtr (t, fld) = (:) (S.SELit t) $ pure $ case fld of + selFldToExtr (FieldName t, fld) = (:) (S.SELit t) $ pure $ case fld of TAFAgg flds -> aggFldToExp flds TAFNodes _ -> let jsonAgg = S.SEFnApp "json_agg" [S.SEIden $ Iden t] ordBy @@ -515,111 +332,132 @@ aggSelToAggNode pfx als aggSel = S.SEFnApp "coalesce" [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing -data AllAggCtx - = AllAggCtx - { aacFields :: ![(FieldName, AggSel)] - , aacOrdBys :: ![RelName] - } deriving (Show, Eq) - -data AggNodeItem - = ANIField !(FieldName, AggSel) - | ANIOrdBy !RelName - deriving (Show, Eq) - -mkAggNodePfx +mkArrNodePfx :: Iden - -> AllAggCtx - -> AggNodeItem - -> Iden -mkAggNodePfx pfx (AllAggCtx aggFlds obRels) = \case - ANIField aggFld@(fld, AggSel rn _ annAggSel) -> - let tabArgs = _asnArgs annAggSel + -> FieldName + -> ArrRelCtx + -> ArrNodeItem + -> (S.Alias, Iden) +mkArrNodePfx pfx parAls (ArrRelCtx arrFlds obRels) = \case + ANIField aggFld@(fld, annArrSel) -> + let (rn, tabArgs) = fetchRNAndTArgs annArrSel similarFlds = getSimilarAggFlds rn tabArgs $ delete aggFld similarOrdByFound = rn `elem` obRels && tabArgs == noTableArgs - similarFound = not (null similarFlds) || similarOrdByFound - extraOrdByFlds = bool [] [ordByFld] similarOrdByFound + extraOrdByFlds = bool [] [ordByFldName] similarOrdByFound sortedFlds = sort $ fld : (similarFlds <> extraOrdByFlds) - in bool (mkAggAls pfx fld) (mkMergedAggNodeAls pfx rn sortedFlds) - similarFound - ANIOrdBy rn -> + in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds + , mkArrRelTableAls pfx parAls sortedFlds + ) + ANIAggOrdBy rn -> let similarFlds = getSimilarAggFlds rn noTableArgs id - sortedFlds = sort $ ordByFld:similarFlds - in mkMergedAggNodeAls pfx rn sortedFlds + sortedFlds = sort $ ordByFldName:similarFlds + in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds + , mkArrRelTableAls pfx parAls sortedFlds + ) where - ordByFld = FieldName "order_by" getSimilarAggFlds rn tabArgs f = map fst $ - flip filter (f aggFlds) $ \(_, AggSel arn _ annAggSel) -> - (rn == arn) && (tabArgs == _asnArgs annAggSel) + flip filter (f arrFlds) $ \(_, annArrSel) -> + let (lrn, lTabArgs) = fetchRNAndTArgs annArrSel + in (lrn == rn) && (lTabArgs == tabArgs) + + fetchRNAndTArgs (ASSimple (AnnRelG rn _ annSel)) = + (rn, _asnArgs annSel) + fetchRNAndTArgs (ASAgg (AnnRelG rn _ annSel)) = + (rn, _asnArgs annSel) + +fetchOrdByAggRels + :: Maybe (NE.NonEmpty AnnOrderByItem) + -> [RelName] +fetchOrdByAggRels orderByM = fromMaybe [] relNamesM + where + relNamesM = + mapMaybe (fetchAggOrdByRels . obiColumn) . toList <$> orderByM -mkAllAggCtx - :: TableAggFld + fetchAggOrdByRels (AOCAgg ri _ _) = Just $ riName ri + fetchAggOrdByRels _ = Nothing + +mkOrdByItems + :: Iden -> FieldName -> Maybe (NE.NonEmpty AnnOrderByItem) - -> AllAggCtx -mkAllAggCtx tabAggFld orderByM = - AllAggCtx aggFlds ordByAggRels + -> ArrRelCtx + -- extractors + -> ( [(S.Alias, S.SQLExp)] + -- object relation nodes + , HM.HashMap RelName ObjNode + -- array relation aggregate nodes + , HM.HashMap S.Alias ArrNode + -- final order by expression + , Maybe S.OrderByExp + ) +mkOrdByItems pfx fldAls orderByM arrRelCtx = + (obExtrs, ordByObjsMap, ordByArrsMap, ordByExpM) where - aggFlds = case tabAggFld of - TAFNodes flds -> mapMaybe getAggFld flds - _ -> [] + procAnnOrdBy' = processAnnOrderByItem pfx fldAls arrRelCtx + procOrdByM = + unzip3 . map procAnnOrdBy' . toList <$> orderByM - ordByAggRels = maybe [] - (mapMaybe (fetchAggOrdByRels . obiColumn) . toList) orderByM + obExtrs = maybe [] _1 procOrdByM + ordByExpM = S.OrderByExp . _2 <$> procOrdByM - getAggFld (f, annFld) = case annFld of - FAgg af -> Just (f, af) - _ -> Nothing + ordByObjs = mapMaybe getOrdByRelNode $ maybe [] _3 procOrdByM + ordByObjsMap = HM.fromListWith mergeObjNodes ordByObjs - fetchAggOrdByRels (AOCAgg ri _ _) = Just $ riName ri - fetchAggOrdByRels _ = Nothing + ordByAggArrs = mapMaybe getOrdByAggNode $ maybe [] _3 procOrdByM + ordByArrsMap = HM.fromListWith mergeArrNodes ordByAggArrs -mkArrRelPfx - :: Iden -> FieldName -> [(FieldName, AnnRel)] - -> (FieldName, AnnRel) -> (S.Alias, Iden) -mkArrRelPfx pfx parAls allArrRels arrRel@(fld, annRel) = - let getTabArgs = _asnArgs . arAnnSel - similarFlds = map fst $ - flip filter (delete arrRel allArrRels) $ - \(_, ar) -> (arName ar == arName annRel) - && (getTabArgs ar == getTabArgs annRel) - sortedFlds = sort $ fld:similarFlds - in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds - , mkArrRelTableAls pfx parAls sortedFlds - ) + getOrdByRelNode (OBNObjNode name node) = Just (name, node) + getOrdByRelNode _ = Nothing + + getOrdByAggNode (OBNArrNode als node) = Just (als, node) + getOrdByAggNode _ = Nothing mkBaseNode :: Iden -> FieldName -> TableAggFld -> TableFrom -> TablePerm -> TableArgs -> BaseNode mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = BaseNode pfx distExprM fromItem finalWhere ordByExpM finalLimit offsetM - allExtrs allObjsWithOb allArrs aggs + allExtrs allObjsWithOb allArrsWithOb where TablePerm fltr permLimitM = tablePerm TableArgs whereM orderByM limitM offsetM distM = tableArgs - allAggCtx@(AllAggCtx aggFlds _) = mkAllAggCtx annSelFlds orderByM - - (allExtrs, allObjsWithOb, allArrs, aggs) = case annSelFlds of - TAFNodes flds -> - let selExtr = buildJsonObject pfx fldAls allAggCtx arrRels flds - -- all the relationships - relFlds = mapMaybe getAnnRel flds - arrRels = flip filter relFlds $ \(_, annRel) -> - arType annRel == ArrRel - (allObjs, allArrRels) = - foldl' (addRel arrRels) (HM.empty, HM.empty) relFlds - aggItems = HM.fromListWith mergeAggNodes $ map mkAggItem aggFlds - in ( HM.fromList $ selExtr:obExtrs <> distExtrs - , mkTotalObjRels allObjs - , allArrRels - , HM.unionWith mergeAggNodes aggItems aggItemsWithOb - ) - TAFAgg tabAggs -> - let extrs = concatMap (fetchExtrFromAggFld . snd) tabAggs - in ( HM.fromList $ extrs <> obExtrs <> distExtrs - , mkTotalObjRels HM.empty - , HM.empty - , aggItemsWithOb - ) - TAFExp _ -> (HM.fromList obExtrs, HM.empty, HM.empty, HM.empty) + aggOrdByRelNames = fetchOrdByAggRels orderByM + + (allExtrs, allObjsWithOb, allArrsWithOb, ordByExpM) = + case annSelFlds of + TAFNodes flds -> + let arrFlds = mapMaybe getAnnArr flds + arrRelCtx = mkArrRelCtx arrFlds + selExtr = buildJsonObject pfx fldAls arrRelCtx flds + -- all object relationships + objNodes = HM.fromListWith mergeObjNodes $ + map mkObjItem (mapMaybe getAnnObj flds) + -- all array items (array relationships + aggregates) + arrNodes = HM.fromListWith mergeArrNodes $ + map (mkArrItem arrRelCtx) arrFlds + + (obExtrs, ordByObjs, ordByArrs, obeM) + = mkOrdByItems' arrRelCtx + allObjs = HM.unionWith mergeObjNodes objNodes ordByObjs + allArrs = HM.unionWith mergeArrNodes arrNodes ordByArrs + + in ( HM.fromList $ selExtr:obExtrs <> distExtrs + , allObjs + , allArrs + , obeM + ) + TAFAgg tabAggs -> + let extrs = concatMap (fetchExtrFromAggFld . snd) tabAggs + (obExtrs, ordByObjs, ordByArrs, obeM) + = mkOrdByItems' emptyArrRelCtx + in ( HM.fromList $ extrs <> obExtrs <> distExtrs + , ordByObjs + , ordByArrs + , obeM + ) + TAFExp _ -> + let (obExtrs, ordByObjs, ordByArrs, obeM) + = mkOrdByItems' emptyArrRelCtx + in (HM.fromList obExtrs, ordByObjs, ordByArrs, obeM) fetchExtrFromAggFld (AFCount cty) = countTyToExps cty fetchExtrFromAggFld (AFOp aggOp) = aggOpToExps aggOp @@ -645,56 +483,35 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = tableQual = tableFromToQual tableFrom finalLimit = applyPermLimit permLimitM limitM - procOrdByM = unzip3 . map (processAnnOrderByItem pfx allAggCtx) . toList <$> orderByM + mkArrRelCtx arrSels = ArrRelCtx arrSels aggOrdByRelNames + + mkOrdByItems' = mkOrdByItems pfx fldAls orderByM distItemsM = processDistinctOnCol pfx <$> distM distExprM = fst <$> distItemsM distExtrs = fromMaybe [] (snd <$> distItemsM) - ordByExpM = S.OrderByExp . _2 <$> procOrdByM - mkTotalObjRels objRels = - foldl' (\objs (rn, relNode) -> HM.insertWith mergeRelNodes rn relNode objs) - objRels $ mapMaybe getOrdByRelNode $ maybe [] _3 procOrdByM - aggItemsWithOb = HM.fromListWith mergeAggNodes - $ mapMaybe getOrdByAggNode $ maybe [] _3 procOrdByM - - -- the columns needed for orderby - obExtrs = maybe [] _1 procOrdByM - - -- process a relationship - addRel allArrRels (objs, arrs) (relAls, annRel) = - let relName = arName annRel - in case arType annRel of - -- in case of object relationships, we merge - ObjRel -> - let relNodePfx = mkObjRelTableAls pfx relName - relNode = mkRelNode relNodePfx (relAls, annRel) - in (HM.insertWith mergeRelNodes relName relNode objs, arrs) - ArrRel -> - let --arrRelTableAls = S.Alias $ mkUniqArrRelAls fldAls relAls - (arrRelTableAls, relNodePfx) = - mkArrRelPfx pfx fldAls allArrRels (relAls, annRel) - relNode = mkRelNode relNodePfx (relAls, annRel) - jsonAggNode = ArrRelNode [S.Alias $ toIden relAls] relNode - in (objs, HM.insertWith mergeArrRelNodes arrRelTableAls jsonAggNode arrs) - - - -- process agg field - mkAggItem (f, aggSel) = - let aggPfx = mkAggNodePfx pfx allAggCtx $ ANIField (f, aggSel) - aggAls = S.Alias aggPfx - aggNode = aggSelToAggNode aggPfx f aggSel - in (aggAls, aggNode) - - getAnnRel (f, annFld) = case annFld of - FRel ar -> Just (f, ar) + -- process an object relationship + mkObjItem (fld, objSel) = + let relName = aarName objSel + objNodePfx = mkObjRelTableAls pfx $ aarName objSel + objNode = mkObjNode objNodePfx (fld, objSel) + in (relName, objNode) + + -- process an array/array-aggregate item + mkArrItem arrRelCtx (fld, arrSel) = + let (arrAls, arrPfx) = mkArrNodePfx pfx fldAls arrRelCtx $ + ANIField (fld, arrSel) + arrNode = mkArrNode arrPfx (fld, arrSel) + in (arrAls, arrNode) + + getAnnObj (f, annFld) = case annFld of + FObj ob -> Just (f, ob) _ -> Nothing - getOrdByRelNode (OBNRelNode name node) = Just (name, node) - getOrdByRelNode _ = Nothing - - getOrdByAggNode (OBNAggNode als node) = Just (als, node) - getOrdByAggNode _ = Nothing + getAnnArr (f, annFld) = case annFld of + FArr ar -> Just (f, ar) + _ -> Nothing annSelToBaseNode :: Iden -> FieldName -> AnnSel -> BaseNode annSelToBaseNode pfx fldAls annSel = @@ -702,50 +519,19 @@ annSelToBaseNode pfx fldAls annSel = where AnnSelG selFlds tabFrm tabPerm tabArgs = annSel -mergeBaseNodes :: BaseNode -> BaseNode -> BaseNode -mergeBaseNodes lNodeDet rNodeDet = - BaseNode pfx dExp f whr ordBy limit offset - (HM.union lExtrs rExtrs) - (HM.unionWith mergeRelNodes lObjs rObjs) - (HM.union lArrs rArrs) - (HM.union lAggs rAggs) - where - (BaseNode pfx dExp f whr ordBy limit offset lExtrs lObjs lArrs lAggs) = lNodeDet - (BaseNode _ _ _ _ _ _ _ rExtrs rObjs rArrs rAggs) = rNodeDet +mkObjNode :: Iden -> (FieldName, ObjSel) -> ObjNode +mkObjNode pfx (fldName, AnnRelG _ rMapn rAnnSel) = + ObjNode rMapn $ annSelToBaseNode pfx fldName rAnnSel --- should only be used to merge obj rel nodes -mergeRelNodes :: RelNode -> RelNode -> RelNode -mergeRelNodes lNode rNode = - RelNode rn rAls rMapn $ mergeBaseNodes lNodeDet rNodeDet - where - (RelNode rn rAls rMapn lNodeDet) = lNode - (RelNode _ _ _ rNodeDet) = rNode - -data RelNode - = RelNode - { _rnRelName :: !RelName - , _rnRelAlias :: !FieldName - , _rnRelMapping :: ![(PGCol, PGCol)] - , _rnNodeDet :: !BaseNode - } deriving (Show, Eq) - -mkRelNode :: Iden -> (FieldName, AnnRel) -> RelNode -mkRelNode pfx (relAls, AnnRel rn _ rMapn rAnnSel) = - RelNode rn relAls rMapn $ annSelToBaseNode pfx relAls rAnnSel - -data AggNode - = AggNode - { _anColMapping :: ![(PGCol, PGCol)] - , _anExtr :: ![S.Extractor] - , _anNodeDet :: !BaseNode - } deriving (Show, Eq) - -mergeAggNodes :: AggNode -> AggNode -> AggNode -mergeAggNodes lAggNode rAggNode = - AggNode colMapn (lExtrs `union` rExtrs) $ mergeBaseNodes lBN rBN - where - AggNode colMapn lExtrs lBN = lAggNode - AggNode _ rExtrs rBN = rAggNode +mkArrNode :: Iden -> (FieldName, ArrSel) -> ArrNode +mkArrNode pfx (fldName, annArrSel) = case annArrSel of + ASSimple annArrRel -> + let bn = annSelToBaseNode pfx fldName $ aarAnnSel annArrRel + extr = asJsonAggExtr False (S.toAlias fldName) $ + _bnOrderBy bn + in ArrNode [extr] (aarMapping annArrRel) bn + + ASAgg annAggSel -> aggSelToArrNode pfx fldName annAggSel injectJoinCond :: S.BoolExp -- ^ Join condition -> S.BoolExp -- ^ Where condition @@ -771,7 +557,7 @@ baseNodeToSel joinCond baseNode = } where BaseNode pfx dExp fromItem whr ordByM limitM - offsetM extrs objRels arrRels aggs + offsetM extrs objRels arrRels = baseNode -- this is the table which is aliased as "pfx.base" baseSel = S.mkSelect @@ -790,24 +576,34 @@ baseNodeToSel joinCond baseNode = -- this is the from eexp for the final select joinedFrom :: S.FromItem joinedFrom = foldl' leftOuterJoin baseFromItem $ - map objRelToFromItem (HM.elems objRels) <> - map arrRelToFromItem (HM.elems arrRels) <> - map aggToFromItem (HM.toList aggs) + map objNodeToFromItem (HM.elems objRels) <> + map arrNodeToFromItem (HM.elems arrRels) - objRelToFromItem :: RelNode -> S.FromItem - objRelToFromItem (RelNode _ _ relMapn relBaseNode) = + objNodeToFromItem :: ObjNode -> S.FromItem + objNodeToFromItem (ObjNode relMapn relBaseNode) = let als = S.Alias $ _bnPrefix relBaseNode sel = baseNodeToSel (mkJoinCond baseSelAls relMapn) relBaseNode in S.mkLateralFromItem sel als - arrRelToFromItem :: ArrRelNode -> S.FromItem - arrRelToFromItem (ArrRelNode alses relNode) = - let RelNode _ _ relMapn relBaseNode = relNode - als = S.Alias $ _bnPrefix relBaseNode - sel = asJsonAggSel False alses (mkJoinCond baseSelAls relMapn) relBaseNode + arrNodeToFromItem :: ArrNode -> S.FromItem + arrNodeToFromItem (ArrNode es colMapn bn) = + let sel = arrNodeToSelect bn es (mkJoinCond baseSelAls colMapn) + als = S.Alias $ _bnPrefix bn in S.mkLateralFromItem sel als - aggToFromItem :: (S.Alias, AggNode) -> S.FromItem - aggToFromItem (als, AggNode colMapn extr bn) = - let sel = aggNodeToSelect bn extr (mkJoinCond baseSelAls colMapn) - in S.mkLateralFromItem sel als +mkAggSelect :: AnnAggSel -> S.Select +mkAggSelect annAggSel = + prefixNumToAliases $ arrNodeToSelect bn extr $ S.BELit True + where + aggSel = AnnRelG (RelName "root") [] annAggSel + ArrNode extr _ bn = + aggSelToArrNode (Iden "root") (FieldName "root") aggSel + +mkSQLSelect :: Bool -> AnnSel -> S.Select +mkSQLSelect isSingleObject annSel = + prefixNumToAliases $ arrNodeToSelect baseNode extrs $ S.BELit True + where + extrs = pure $ asJsonAggExtr isSingleObject rootFldAls $ _bnOrderBy baseNode + baseNode = annSelToBaseNode (toIden rootFldName) rootFldName annSel + rootFldName = FieldName "root" + rootFldAls = S.Alias $ toIden rootFldName diff --git a/server/src-lib/Hasura/RQL/DML/Select/Types.hs b/server/src-lib/Hasura/RQL/DML/Select/Types.hs new file mode 100644 index 0000000000000..a69d4d10f91ae --- /dev/null +++ b/server/src-lib/Hasura/RQL/DML/Select/Types.hs @@ -0,0 +1,220 @@ +{-# LANGUAGE DeriveLift #-} +{-# LANGUAGE OverloadedStrings #-} + +module Hasura.RQL.DML.Select.Types where + +import Data.Aeson.Types +import Language.Haskell.TH.Syntax (Lift) + +import qualified Data.HashMap.Strict as HM +import qualified Data.List.NonEmpty as NE +import qualified Data.Text as T + +import Hasura.Prelude +import Hasura.RQL.Types +import Hasura.SQL.Types + +import qualified Hasura.SQL.DML as S + +type SelectQExt = SelectG ExtCol BoolExp Int +-- Columns in RQL +data ExtCol + = ECSimple !PGCol + | ECRel !RelName !(Maybe RelName) !SelectQExt + deriving (Show, Eq, Lift) + +instance ToJSON ExtCol where + toJSON (ECSimple s) = toJSON s + toJSON (ECRel rn mrn selq) = + object $ [ "name" .= rn + , "alias" .= mrn + ] ++ selectGToPairs selq + +instance FromJSON ExtCol where + parseJSON v@(Object o) = + ECRel + <$> o .: "name" + <*> o .:? "alias" + <*> parseJSON v + parseJSON (String s) = + return $ ECSimple $ PGCol s + parseJSON _ = + fail $ mconcat + [ "A column should either be a string or an " + , "object (relationship)" + ] + +data AnnAggOrdBy + = AAOCount + | AAOOp !T.Text !PGCol + deriving (Show, Eq) + +data AnnObCol + = AOCPG !PGColInfo + | AOCObj !RelInfo !AnnBoolExpSQL !AnnObCol + | AOCAgg !RelInfo !AnnBoolExpSQL !AnnAggOrdBy + deriving (Show, Eq) + +type AnnOrderByItem = OrderByItemG AnnObCol + +data AnnRelG a + = AnnRelG + { aarName :: !RelName -- Relationship name + , aarMapping :: ![(PGCol, PGCol)] -- Column of left table to join with + , aarAnnSel :: !a -- Current table. Almost ~ to SQL Select + } deriving (Show, Eq) + +type ObjSel = AnnRelG AnnSel +type ArrRel = AnnRelG AnnSel +type ArrRelAgg = AnnRelG AnnAggSel + +data ArrSel + = ASSimple !ArrRel + | ASAgg !ArrRelAgg + deriving (Show, Eq) + +data AnnFld + = FCol !PGColInfo + | FObj !ObjSel + | FArr !ArrSel + | FExp !T.Text + deriving (Show, Eq) + +data TableArgs + = TableArgs + { _taWhere :: !(Maybe AnnBoolExpSQL) + , _taOrderBy :: !(Maybe (NE.NonEmpty AnnOrderByItem)) + , _taLimit :: !(Maybe Int) + , _taOffset :: !(Maybe S.SQLExp) + , _taDistCols :: !(Maybe (NE.NonEmpty PGCol)) + } deriving (Show, Eq) + +noTableArgs :: TableArgs +noTableArgs = TableArgs Nothing Nothing Nothing Nothing Nothing + +data PGColFld + = PCFCol !PGCol + | PCFExp !T.Text + deriving (Show, Eq) + +type ColFlds = [(FieldName, PGColFld)] + +data AggOp + = AggOp + { _aoOp :: !T.Text + , _aoFlds :: !ColFlds + } deriving (Show, Eq) + +data AggFld + = AFCount !S.CountType + | AFOp !AggOp + | AFExp !T.Text + deriving (Show, Eq) + +type AggFlds = [(FieldName, AggFld)] + +data TableAggFld + = TAFAgg !AggFlds + | TAFNodes ![(FieldName, AnnFld)] + | TAFExp !T.Text + deriving (Show, Eq) + +data TableFrom + = TableFrom + { _tfTable :: !QualifiedTable + , _tfIden :: !(Maybe Iden) + } deriving (Show, Eq) + +data TablePerm + = TablePerm + { _tpFilter :: !AnnBoolExpSQL + , _tpLimit :: !(Maybe Int) + } deriving (Eq, Show) + +data AnnSelG a + = AnnSelG + { _asnFields :: !a + , _asnFrom :: !TableFrom + , _asnPerm :: !TablePerm + , _asnArgs :: !TableArgs + } deriving (Show, Eq) + +type AnnSel = AnnSelG [(FieldName, AnnFld)] +type AnnAggSel = AnnSelG [(FieldName, TableAggFld)] + +data BaseNode + = BaseNode + { _bnPrefix :: !Iden + , _bnDistinct :: !(Maybe S.DistinctExpr) + , _bnFrom :: !S.FromItem + , _bnWhere :: !S.BoolExp + , _bnOrderBy :: !(Maybe S.OrderByExp) + , _bnLimit :: !(Maybe Int) + , _bnOffset :: !(Maybe S.SQLExp) + + , _bnExtrs :: !(HM.HashMap S.Alias S.SQLExp) + , _bnObjs :: !(HM.HashMap RelName ObjNode) + , _bnArrs :: !(HM.HashMap S.Alias ArrNode) + } deriving (Show, Eq) + +mergeBaseNodes :: BaseNode -> BaseNode -> BaseNode +mergeBaseNodes lNodeDet rNodeDet = + BaseNode pfx dExp f whr ordBy limit offset + (HM.union lExtrs rExtrs) + (HM.unionWith mergeObjNodes lObjs rObjs) + (HM.unionWith mergeArrNodes lArrs rArrs) + where + BaseNode pfx dExp f whr ordBy limit offset lExtrs lObjs lArrs + = lNodeDet + BaseNode _ _ _ _ _ _ _ rExtrs rObjs rArrs + = rNodeDet + +data OrderByNode + = OBNNothing + | OBNObjNode !RelName !ObjNode + | OBNArrNode !S.Alias !ArrNode + deriving (Show, Eq) + +data ArrRelCtx + = ArrRelCtx + { aacFields :: ![(FieldName, ArrSel)] + , aacAggOrdBys :: ![RelName] + } deriving (Show, Eq) + +emptyArrRelCtx :: ArrRelCtx +emptyArrRelCtx = ArrRelCtx [] [] + +data ArrNodeItem + = ANIField !(FieldName, ArrSel) + | ANIAggOrdBy !RelName + deriving (Show, Eq) + +data ObjNode + = ObjNode + { _rnRelMapping :: ![(PGCol, PGCol)] + , _rnNodeDet :: !BaseNode + } deriving (Show, Eq) + +mergeObjNodes :: ObjNode -> ObjNode -> ObjNode +mergeObjNodes lNode rNode = + ObjNode colMapping $ mergeBaseNodes lBN rBN + where + ObjNode colMapping lBN = lNode + ObjNode _ rBN = rNode + +-- simple array select, aggregate select and order by +-- nodes differ in extractors +data ArrNode + = ArrNode + { _anExtr :: ![S.Extractor] + , _anRelMapping :: ![(PGCol, PGCol)] + , _anNodeDet :: !BaseNode + } deriving (Show, Eq) + +mergeArrNodes :: ArrNode -> ArrNode -> ArrNode +mergeArrNodes lNode rNode = + ArrNode (lExtrs `union` rExtrs) colMapping $ + mergeBaseNodes lBN rBN + where + ArrNode lExtrs colMapping lBN = lNode + ArrNode rExtrs _ rBN = rNode