diff --git a/docs/graphql/manual/api-reference/query.rst b/docs/graphql/manual/api-reference/query.rst index a6ce2395fd7a5..3450609c3f32d 100644 --- a/docs/graphql/manual/api-reference/query.rst +++ b/docs/graphql/manual/api-reference/query.rst @@ -379,6 +379,12 @@ or order_by: [{id: desc}, {author: {id: asc}}] +or + +.. parsed-literal:: + + order_by: {articles_aggregate: {count: asc}} + TableOrderBy ************ @@ -394,6 +400,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: @@ -407,8 +418,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 c30b0b57e892d..4783f4b8d1a86 100644 --- a/docs/graphql/manual/queries/sorting.rst +++ b/docs/graphql/manual/queries/sorting.rst @@ -32,6 +32,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 @@ -50,8 +64,10 @@ The ``order_by`` argument takes an array of objects to allow sorting by multiple desc_nulls_last } + .. 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: @@ -238,6 +254,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 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 67977dcbbece9..847c99a40f6a1 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,11 +74,13 @@ $(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 -- = OBIPGCol !PGColInfo -- | OBIRel !RelInfo !AnnBoolExpSQL +-- | OBIAgg !RelInfo !AnnBoolExpSQL -- deriving (Show, Eq) -- type OrdByItemMap = Map.HashMap G.Name OrdByItem @@ -163,8 +166,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/ContextTypes.hs b/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs index 44e8980cd8697..5406eac613b27 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/ContextTypes.hs @@ -19,6 +19,7 @@ type FieldMap data OrdByItem = OBIPGCol !PGColInfo | OBIRel !RelInfo !AnnBoolExpSQL + | OBIAgg !RelInfo !AnnBoolExpSQL 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 603ee1a362331..54b20e4c299ac 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 @@ -66,23 +65,23 @@ 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.FArr $ RS.ASAgg $ RS.AnnRelG rn colMapping aggSel else do annSel <- fromField f relTN tableFilter tableLimit fld - let annRel = RS.AnnRel (riName relInfo) (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 @@ -164,10 +163,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 + let annObColFn = f . RS.AOCObj 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 @@ -244,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 @@ -255,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 @@ -272,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/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index de0bef7f777c2..330e8f92dab40 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -1135,6 +1135,57 @@ mkConflictActionTy = mkHsraEnumTyInfo (Just desc) ty $ mapFromL _eviVal -- ) -- ] +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 = mkHsraInpTyInfo (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 = + mkHsraInpTyInfo (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" @@ -1157,14 +1208,17 @@ mkOrdByInpObj tn selFlds = (inpObjTy, ordByCtx) inpObjTy = mkHsraInpTyInfo (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 @@ -1172,8 +1226,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 @@ -1182,6 +1241,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 @@ -1247,7 +1311,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 @@ -1327,15 +1391,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/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index da4730a448e84..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 [] 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 5f2e9606782cc..b602a4c7b364c 100644 --- a/server/src-lib/Hasura/RQL/DML/Select/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Select/Internal.hs @@ -1,134 +1,36 @@ -{-# LANGUAGE DeriveLift #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TupleSections #-} -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 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 AnnObCol - = AOCPG !PGColInfo - | AOCRel !RelInfo !AnnBoolExpSQL !AnnObCol - 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) - -type AnnAggSel = AnnSelG [(T.Text, TableAggFld)] - -data AggSel - = AggSel - { 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 @@ -139,48 +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 RelNode) - , _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 @@ -188,19 +54,25 @@ 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 col fromItem = +arrNodeToSelect :: BaseNode -> [S.Extractor] -> S.BoolExp -> S.Select +arrNodeToSelect bn extrs joinCond = S.mkSelect - { S.selExtr = [S.Extractor extr $ Just col] - , S.selFrom = Just $ S.FromExp [fromItem] - } + { S.selExtr = extrs + , S.selFrom = Just $ S.FromExp [selFrom] + } + where + selFrom = S.mkSelFromItem (baseNodeToSel joinCond bn) $ S.Alias $ + _bnPrefix bn + +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 "->") @@ -208,57 +80,35 @@ asSingleRow col fromItem = , S.SEUnsafe "0" ] -aggNodeToSelect :: BaseNode -> S.Extractor -> S.BoolExp -> S.Select -aggNodeToSelect bn extr joinCond = - S.mkSelect - { S.selExtr = [extr] - , S.selFrom = Just $ S.FromExp [selFrom] - } - where - selFrom = S.mkSelFromItem (baseNodeToSel joinCond bn) $ S.Alias $ - _bnPrefix bn - -withJsonAgg :: Maybe S.OrderByExp -> S.Alias -> S.FromItem -> S.Select -withJsonAgg orderByM col fromItem = - S.mkSelect - { S.selExtr = [S.Extractor extr $ Just col] - , S.selFrom = Just $ S.FromExp [fromItem] - } +withJsonAggExtr :: Maybe S.OrderByExp -> S.Alias -> S.Extractor +withJsonAggExtr orderByM col = + S.Extractor extr $ Just col where - extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "[]"] Nothing + extr = S.SEFnApp "coalesce" [jsonAgg, S.SELit "[]"] Nothing jsonAgg = S.SEFnApp "json_agg" [S.SEIden $ toIden col] 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 +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 -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 = pfx <> Iden ".or." <> toIden relName -mkAggAls :: Iden -> FieldName -> Iden -mkAggAls pfx fldAls = - pfx <> Iden ".agg." <> toIden fldAls - mkBaseTableAls :: Iden -> Iden mkBaseTableAls pfx = pfx <> Iden ".base" @@ -267,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 + :: Iden -> FieldName -> ArrRelCtx -> [(FieldName, AnnFld)] -> (S.Alias, S.SQLExp) -buildJsonObject pfx parAls flds = +buildJsonObject pfx parAls arrRelCtx flds = if any ( (> 63) . T.length . getFieldNameTxt . fst ) flds then withJsonBuildObj parAls jsonBuildObjExps else withRowToJSON parAls rowToJsonExtrs @@ -291,12 +144,13 @@ buildJsonObject pfx parAls 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 -> mkArrRelTableAls pfx parAls fldAls + FObj objSel -> + let qual = mkObjRelTableAls pfx $ aarName objSel in S.mkQIdenExp qual fldAls - FAgg _ -> S.mkQIdenExp (mkAggAls pfx fldAls) 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 @@ -314,58 +168,101 @@ withJsonBuildObj parAls exps = where jsonRow = S.applyJsonBuildObj exps +mkAggObFld :: AnnAggOrdBy -> FieldName +mkAggObFld = \case + AAOCount -> FieldName "count" + AAOOp op col -> FieldName $ op <> "." <> getPGColTxt col + +mkAggObExtrAndFlds :: AnnAggOrdBy -> (S.Extractor, AggFlds) +mkAggObExtrAndFlds annAggOb = case annAggOb of + AAOCount -> + ( S.Extractor S.countStar als + , [(FieldName "count", AFCount S.CTStar)] + ) + AAOOp op pgCol -> + ( S.Extractor (S.SEFnApp op [S.SEIden $ toIden pgCol] Nothing) als + , [(FieldName op, AFOp $ AggOp op [(fromPGCol pgCol, PCFCol pgCol)])] + ) + where + als = Just $ S.toAlias $ mkAggObFld annAggOb + processAnnOrderByItem :: Iden + -> FieldName + -> ArrRelCtx -> AnnOrderByItem -- the extractors which will select the needed columns -> ( (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) = +processAnnOrderByItem pfx parAls arrRelCtx (OrderByItemG obTyM annObCol obNullsM) = ( (obColAls, obColExp) , sqlOrdByItem , relNodeM ) where - ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx annObCol + ((obColAls, obColExp), relNodeM) = processAnnOrderByCol pfx parAls arrRelCtx annObCol sqlOrdByItem = S.OrderByItem (S.SEIden $ toIden obColAls) obTyM obNullsM processAnnOrderByCol :: Iden + -> FieldName + -> ArrRelCtx -> 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 +processAnnOrderByCol pfx parAls arrRelCtx = \case AOCPG colInfo -> let 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 -> + AOCObj (RelInfo rn _ colMapping relTab _) relFltr rest -> let relPfx = mkObjRelTableAls pfx rn - ((nesAls, nesCol), nesNodeM) = processAnnOrderByCol relPfx rest + ((nesAls, nesCol), ordByNode) = + processAnnOrderByCol relPfx ordByFldName emptyArrRelCtx rest + (objNodeM, arrNodeM) = case ordByNode of + OBNNothing -> (Nothing, Nothing) + 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) nesNodeM) - HM.empty HM.empty - 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) - , Just (rn, relNode) + , OBNObjNode rn relNode + ) + AOCAgg (RelInfo rn _ colMapping relTab _ ) relFltr annAggOb -> + let (arrAls, arrPfx) = + mkArrNodePfx pfx parAls arrRelCtx $ ANIAggOrdBy rn + fldName = mkAggObFld annAggOb + qOrdBy = S.mkQIdenExp arrPfx $ toIden fldName + tabFrom = TableFrom relTab Nothing + tabPerm = TablePerm relFltr Nothing + (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) + , OBNArrNode arrAls aggNode ) processDistinctOnCol @@ -386,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 @@ -405,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 @@ -422,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 @@ -435,40 +332,132 @@ aggSelToAggNode pfx als aggSel = S.SEFnApp "coalesce" [ S.SELit e , S.SEUnsafe "bool_or('true')::text"] Nothing +mkArrNodePfx + :: Iden + -> 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 + extraOrdByFlds = bool [] [ordByFldName] similarOrdByFound + sortedFlds = sort $ fld : (similarFlds <> extraOrdByFlds) + in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds + , mkArrRelTableAls pfx parAls sortedFlds + ) + ANIAggOrdBy rn -> + let similarFlds = getSimilarAggFlds rn noTableArgs id + sortedFlds = sort $ ordByFldName:similarFlds + in ( S.Alias $ mkUniqArrRelAls parAls sortedFlds + , mkArrRelTableAls pfx parAls sortedFlds + ) + where + getSimilarAggFlds rn tabArgs f = map fst $ + 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 + + fetchAggOrdByRels (AOCAgg ri _ _) = Just $ riName ri + fetchAggOrdByRels _ = Nothing + +mkOrdByItems + :: Iden -> FieldName + -> Maybe (NE.NonEmpty AnnOrderByItem) + -> 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 + procAnnOrdBy' = processAnnOrderByItem pfx fldAls arrRelCtx + procOrdByM = + unzip3 . map procAnnOrdBy' . toList <$> orderByM + + obExtrs = maybe [] _1 procOrdByM + ordByExpM = S.OrderByExp . _2 <$> procOrdByM + + ordByObjs = mapMaybe getOrdByRelNode $ maybe [] _3 procOrdByM + ordByObjsMap = HM.fromListWith mergeObjNodes ordByObjs + + ordByAggArrs = mapMaybe getOrdByAggNode $ maybe [] _3 procOrdByM + ordByArrsMap = HM.fromListWith mergeArrNodes ordByAggArrs + + 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 - (allExtrs, allObjsWithOb, allArrs, aggs) = case annSelFlds of - TAFNodes flds -> - let selExtr = buildJsonObject pfx fldAls flds - -- all the relationships - (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 <> distExtrs - , allObjRelsWithOb - , allArrRels - , aggItems - ) - TAFAgg aggFlds -> - let extrs = concatMap (fetchExtrFromAggFld . snd) aggFlds - in ( HM.fromList $ extrs <> obExtrs <> distExtrs - , HM.empty - , HM.empty - , HM.empty - ) - 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 @@ -494,51 +483,35 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs = tableQual = tableFromToQual tableFrom finalLimit = applyPermLimit permLimitM limitM - _1 (a, _, _) = a - _2 (_, b, _) = b - _3 (_, _, c) = c + mkArrRelCtx arrSels = ArrRelCtx arrSels aggOrdByRelNames + + mkOrdByItems' = mkOrdByItems pfx fldAls orderByM distItemsM = processDistinctOnCol pfx <$> distM distExprM = fst <$> distItemsM distExtrs = fromMaybe [] (snd <$> distItemsM) - procOrdByM = unzip3 . map (processAnnOrderByItem pfx) . toList <$> orderByM - ordByExpM = S.OrderByExp . _2 <$> procOrdByM + -- process an object relationship + mkObjItem (fld, objSel) = + let relName = aarName objSel + objNodePfx = mkObjRelTableAls pfx $ aarName objSel + objNode = mkObjNode objNodePfx (fld, objSel) + in (relName, objNode) - -- the columns needed for orderby - obExtrs = maybe [] _1 procOrdByM + -- 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) - mkRelPfx rTy rn relAls = case rTy of - ObjRel -> mkObjRelTableAls pfx rn - ArrRel -> mkArrRelTableAls pfx fldAls relAls - - -- process a relationship - addRel (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) - ArrRel -> - let arrRelTableAls = S.Alias $ mkUniqArrRelAls fldAls relAls - in (objs, HM.insert arrRelTableAls relNode arrs) - - -- process agg field - mkAggItem (f, aggSel) = - let aggPfx = mkAggAls pfx f - 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 + getAnnObj (f, annFld) = case annFld of + FObj ob -> Just (f, ob) + _ -> Nothing + + getAnnArr (f, annFld) = case annFld of + FArr ar -> Just (f, ar) + _ -> Nothing annSelToBaseNode :: Iden -> FieldName -> AnnSel -> BaseNode annSelToBaseNode pfx fldAls annSel = @@ -546,43 +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) +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 @@ -608,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 @@ -627,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 :: 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 + 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 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 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 224d8c662140a..23fabc966e0cf 100644 --- a/server/tests-py/queries/graphql_query/order_by/setup.yaml +++ b/server/tests-py/queries/graphql_query/order_by/setup.yaml @@ -112,6 +112,71 @@ args: 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) + # Create employee table - type: run_sql args: @@ -156,4 +221,3 @@ args: - name: Bent department: Engineering salary: 4122 - 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 f81e4070aa992..80524b8a740bc 100644 --- a/server/tests-py/queries/graphql_query/order_by/teardown.yaml +++ b/server/tests-py/queries/graphql_query/order_by/teardown.yaml @@ -23,6 +23,23 @@ 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" + - type: run_sql args: sql: | diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index e13f0aa967956..ea867063dc600 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -274,6 +274,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') + def test_employee_distinct_department_order_by_salary_desc(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/employee_distinct_department_order_by_salary_desc.yaml')