add statistical aggregate operations and count on columns (close #1028) (#1029)

This commit is contained in:
Rakesh Emmadi 2018-11-14 18:29:59 +05:30 committed by Vamshi Surabhi
parent e663089946
commit b719e82e89
9 changed files with 373 additions and 151 deletions

View File

@ -154,7 +154,7 @@ parseOnConflict inpCols val = withPathK "on_conflict" $
flip withObject val $ \_ obj -> do
actionM <- forM (OMap.lookup "action" obj) parseAction
constraint <- parseConstraint obj
updColsM <- forM (OMap.lookup "update_columns" obj) parseUpdCols
updColsM <- forM (OMap.lookup "update_columns" obj) parseColumns
-- consider "action" if "update_columns" is not mentioned
return $ mkConflictClause $ case (updColsM, actionM) of
(Just [], _) -> RI.CCDoNothing $ Just constraint
@ -177,11 +177,6 @@ parseOnConflict inpCols val = withPathK "on_conflict" $
(_, enumVal) <- asEnumVal v
return $ ConstraintName $ G.unName $ G.unEnumValue enumVal
parseUpdCols v = flip withArray v $ \_ enumVals ->
forM enumVals $ \eVal -> do
(_, ev) <- asEnumVal eVal
return $ PGCol $ G.unName $ G.unEnumValue ev
mkConflictClause (RI.CCDoNothing constrM) =
RI.CP1DoNothing $ fmap RI.Constraint constrM
mkConflictClause (RI.CCUpdate constr updCols) =

View File

@ -10,6 +10,7 @@ module Hasura.GraphQL.Resolve.Select
( convertSelect
, convertSelectByPKey
, convertAggSelect
, parseColumns
, withSelSet
, fromSelSet
, fieldAsPath
@ -32,6 +33,7 @@ import qualified Hasura.SQL.DML as S
import Hasura.GraphQL.Resolve.BoolExp
import Hasura.GraphQL.Resolve.Context
import Hasura.GraphQL.Resolve.InputValue
import Hasura.GraphQL.Schema (isAggFld)
import Hasura.GraphQL.Validate.Field
import Hasura.GraphQL.Validate.Types
import Hasura.RQL.DML.Internal (onlyPositiveInt)
@ -199,6 +201,29 @@ convertSelectByPKey qt permFilter fld = do
return $ RS.selectP2 True (selData, prepArgs)
-- agg select related
parseColumns :: MonadError QErr m => AnnGValue -> m [PGCol]
parseColumns val =
flip withArray val $ \_ vals ->
forM vals $ \v -> do
(_, enumVal) <- asEnumVal v
return $ PGCol $ G.unName $ G.unEnumValue enumVal
convertCount :: MonadError QErr m => ArgsMap -> m S.CountType
convertCount args = do
columnsM <- withArgM args "columns" parseColumns
isDistinct <- or <$> withArgM args "distinct" parseDistinct
maybe (return S.CTStar) (mkCType isDistinct) columnsM
where
parseDistinct v = do
(_, val) <- asPGColVal v
case val of
PGValBoolean b -> return b
_ ->
throw500 "expecting Boolean for \"distinct\""
mkCType isDistinct cols = return $
bool (S.CTSimple cols) (S.CTDistinct cols) isDistinct
convertColFlds
:: Monad m => G.NamedType -> SelSet -> m RS.ColFlds
convertColFlds ty selSet =
@ -216,12 +241,14 @@ convertAggFld ty selSet =
fSelSet = _fSelSet fld
case _fName fld of
"__typename" -> return $ RS.AFExp $ G.unName $ G.unNamedType ty
"count" -> return RS.AFCount
"sum" -> RS.AFSum <$> convertColFlds fType fSelSet
"avg" -> RS.AFAvg <$> convertColFlds fType fSelSet
"max" -> RS.AFMax <$> convertColFlds fType fSelSet
"min" -> RS.AFMin <$> convertColFlds fType fSelSet
G.Name t -> throw500 $ "unexpected field in _agg node: " <> t
"count" -> RS.AFCount <$> convertCount (_fArguments fld)
n -> do
colFlds <- convertColFlds fType fSelSet
unless (isAggFld n) $ throwInvalidFld n
return $ RS.AFOp $ RS.AggOp (G.unName n) colFlds
where
throwInvalidFld (G.Name t) =
throw500 $ "unexpected field in _aggregate node: " <> t
fromAggField
:: (MonadError QErr m, MonadReader r m, Has FieldMap r, Has OrdByCtx r)

View File

@ -17,6 +17,7 @@ module Hasura.GraphQL.Schema
, InsCtx(..)
, InsCtxMap
, RelationInfoMap
, isAggFld
) where
import Data.Has
@ -153,6 +154,17 @@ isRelNullable fim ri = isNullable
lColInfos = getColInfos lCols allCols
isNullable = any pgiIsNullable lColInfos
numAggOps :: [G.Name]
numAggOps = [ "sum", "avg", "stddev", "stddev_samp", "stddev_pop"
, "variance", "var_samp", "var_pop"
]
compAggOps :: [G.Name]
compAggOps = ["max", "min"]
isAggFld :: G.Name -> Bool
isAggFld = flip elem (numAggOps <> compAggOps)
mkColName :: PGCol -> G.Name
mkColName (PGCol n) = G.Name n
@ -378,7 +390,14 @@ mkTableAggObj tn =
{-
type table_aggregate_fields{
count: Int
sum: table_num_fields
sum: table_sum_fields
avg: table_avg_fields
stddev: table_stddev_fields
stddev_pop: table_stddev_pop_fields
variance: table_variance_fields
var_pop: table_var_pop_fields
max: table_max_fields
min: table_min_fields
}
-}
mkTableAggFldsObj
@ -390,22 +409,24 @@ mkTableAggFldsObj tn numCols compCols =
desc = G.Description $
"aggregate fields of " <>> tn
countFld = ObjFldInfo Nothing "count" Map.empty $ G.toGT $
countFld = ObjFldInfo Nothing "count" countParams $ G.toGT $
mkScalarTy PGInteger
numFlds = bool [sumFld, avgFld] [] $ null numCols
compFlds = bool [maxFld, minFld] [] $ null compCols
countParams = fromInpValL [countColInpVal, distinctInpVal]
sumFld = mkColOpFld "sum"
avgFld = mkColOpFld "avg"
maxFld = mkColOpFld "max"
minFld = mkColOpFld "min"
countColInpVal = InpValInfo Nothing "columns" $ G.toGT $
G.toLT $ G.toNT $ mkSelColumnInpTy tn
distinctInpVal = InpValInfo Nothing "distinct" $ G.toGT $
mkScalarTy PGBoolean
numFlds = bool (map mkColOpFld numAggOps) [] $ null numCols
compFlds = bool (map mkColOpFld compAggOps) [] $ null compCols
mkColOpFld op = ObjFldInfo Nothing op Map.empty $ G.toGT $
mkTableColAggFldsTy op tn
{-
type table_sum_fields{
type table_<agg-op>_fields{
num_col: Int
. .
. .
@ -840,10 +861,15 @@ mkConstraintInpTy :: QualifiedTable -> G.NamedType
mkConstraintInpTy tn =
G.NamedType $ qualTableToName tn <> "_constraint"
-- table_column
mkColumnInpTy :: QualifiedTable -> G.NamedType
mkColumnInpTy tn =
G.NamedType $ qualTableToName tn <> "_column"
-- table_update_column
mkUpdColumnInpTy :: QualifiedTable -> G.NamedType
mkUpdColumnInpTy tn =
G.NamedType $ qualTableToName tn <> "_update_column"
--table_select_column
mkSelColumnInpTy :: QualifiedTable -> G.NamedType
mkSelColumnInpTy tn =
G.NamedType $ qualTableToName tn <> "_select_column"
{-
input table_obj_rel_insert_input {
data: table_insert_input!
@ -946,7 +972,7 @@ mkOnConflictInp tn =
G.toGT $ G.toNT $ mkConstraintInpTy tn
updateColumnsInpVal = InpValInfo Nothing (G.Name "update_columns") $
G.toGT $ G.toLT $ G.toNT $ mkColumnInpTy tn
G.toGT $ G.toLT $ G.toNT $ mkUpdColumnInpTy tn
{-
insert_table(
@ -991,17 +1017,27 @@ mkConstriantTy tn cons = enumTyInfo
EnumValInfo (Just "unique or primary key constraint")
(G.EnumValue $ G.Name n) False
mkColumnTy :: QualifiedTable -> [PGCol] -> EnumTyInfo
mkColumnTy tn cols = enumTyInfo
mkColumnEnumVal :: PGCol -> EnumValInfo
mkColumnEnumVal (PGCol col) =
EnumValInfo (Just "column name") (G.EnumValue $ G.Name col) False
mkUpdColumnTy :: QualifiedTable -> [PGCol] -> EnumTyInfo
mkUpdColumnTy tn cols = enumTyInfo
where
enumTyInfo = EnumTyInfo (Just desc) (mkColumnInpTy tn) $
enumTyInfo = EnumTyInfo (Just desc) (mkUpdColumnInpTy tn) $
mapFromL _eviVal $ map mkColumnEnumVal cols
desc = G.Description $
"columns of table " <>> tn
"update columns of table " <>> tn
mkColumnEnumVal (PGCol col) =
EnumValInfo (Just "column name") (G.EnumValue $ G.Name col) False
mkSelColumnTy :: QualifiedTable -> [PGCol] -> EnumTyInfo
mkSelColumnTy tn cols = enumTyInfo
where
enumTyInfo = EnumTyInfo (Just desc) (mkSelColumnInpTy tn) $
mapFromL _eviVal $ map mkColumnEnumVal cols
desc = G.Description $
"select columns of table " <>> tn
mkConflictActionTy :: EnumTyInfo
mkConflictActionTy = EnumTyInfo (Just desc) ty $ mapFromL _eviVal
@ -1108,7 +1144,7 @@ mkOnConflictTypes tn c cols =
where
tyInfos = [ TIEnum mkConflictActionTy
, TIEnum $ mkConstriantTy tn constraints
, TIEnum $ mkColumnTy tn cols
, TIEnum $ mkUpdColumnTy tn cols
, TIInpObj $ mkOnConflictInp tn
]
constraints = filter isUniqueOrPrimary c
@ -1159,6 +1195,7 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM allC
, TIInpObj <$> mutHelper viIsUpdatable updSetInpObjM
, TIInpObj <$> mutHelper viIsUpdatable updIncInpObjM
, TIObj <$> mutRespObjM
, TIEnum <$> selColInpTyM
]
mutHelper f objM = bool Nothing objM $ isMutable f viM
@ -1190,6 +1227,8 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM allC
updSetInpObjFldsM = mkColFldMap (mkUpdSetTy tn) <$> updColsM
selFldsM = snd <$> selPermM
selColsM = (map pgiName . lefts) <$> selFldsM
selColInpTyM = mkSelColumnTy tn <$> selColsM
-- boolexp input type
boolExpInpObjM = case selFldsM of
Just selFlds -> Just $ mkBoolExpInp tn selFlds
@ -1240,15 +1279,18 @@ mkGCtxRole' tn insPermM selPermM updColsM delPermM pkeyCols constraints viM allC
_ -> []
getNumCols = onlyNumCols . lefts
getCompCols = onlyComparableCols . lefts
onlyFloat = const $ mkScalarTy PGFloat
mkTypeMaker "sum" = mkScalarTy
mkTypeMaker _ = onlyFloat
mkColAggFldsObjs flds =
let numCols = getNumCols flds
compCols = getCompCols flds
sumFldsObj = mkTableColAggFldsObj tn "sum" mkScalarTy numCols
avgFldsObj = mkTableColAggFldsObj tn "avg" (const $ mkScalarTy PGFloat) numCols
maxFldsObj = mkTableColAggFldsObj tn "max" mkScalarTy compCols
minFldsObj = mkTableColAggFldsObj tn "min" mkScalarTy compCols
numFldsObjs = bool [sumFldsObj, avgFldsObj] [] $ null numCols
compFldsObjs = bool [maxFldsObj, minFldsObj] [] $ null compCols
mkNumObjFld n = mkTableColAggFldsObj tn n (mkTypeMaker n) numCols
mkCompObjFld n = mkTableColAggFldsObj tn n mkScalarTy compCols
numFldsObjs = bool (map mkNumObjFld numAggOps) [] $ null numCols
compFldsObjs = bool (map mkCompObjFld compAggOps) [] $ null compCols
in numFldsObjs <> compFldsObjs
-- the fields used in table object
selObjFldsM = mkFldMap (mkTableTy tn) <$> selFldsM

View File

@ -41,7 +41,7 @@ mkSQLCount
:: CountQueryP1 -> S.Select
mkSQLCount (CountQueryP1 tn (permFltr, mWc) mDistCols) =
S.mkSelect
{ S.selExtr = [S.Extractor (S.SEFnApp "count" [S.SEStar] Nothing) Nothing]
{ S.selExtr = [S.Extractor S.countStar Nothing]
, S.selFrom = Just $ S.FromExp
[S.mkSelFromExp False innerSel $ TableName "r"]
}

View File

@ -54,7 +54,7 @@ mkMutFldExp :: QualifiedTable -> Bool -> MutFld -> S.SQLExp
mkMutFldExp qt singleObj = \case
MCount -> S.SESelect $
S.mkSelect
{ S.selExtr = [S.Extractor (S.SEUnsafe "count(*)") Nothing]
{ S.selExtr = [S.Extractor S.countStar Nothing]
, S.selFrom = Just $ S.FromExp $ pure frmItem
}
MExp t -> S.SELit t

View File

@ -101,12 +101,15 @@ data PGColFld
type ColFlds = [(T.Text, PGColFld)]
data AggOp
= AggOp
{ _aoOp :: !T.Text
, _aoFlds :: !ColFlds
} deriving (Show, Eq)
data AggFld
= AFCount
| AFSum !ColFlds
| AFAvg !ColFlds
| AFMax !ColFlds
| AFMin !ColFlds
= AFCount !S.CountType
| AFOp !AggOp
| AFExp !T.Text
deriving (Show, Eq)
@ -165,14 +168,11 @@ aggFldToExp aggFlds = jsonRow
jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds)
withAls fldName sqlExp = [S.SELit fldName, sqlExp]
aggToFlds (t, fld) = withAls t $ case fld of
AFCount -> S.SEUnsafe "count(*)"
AFSum sumFlds -> colFldsToObj "sum" sumFlds
AFAvg avgFlds -> colFldsToObj "avg" avgFlds
AFMax maxFlds -> colFldsToObj "max" maxFlds
AFMin minFlds -> colFldsToObj "min" minFlds
AFExp e -> S.SELit e
AFCount cty -> S.SECount cty
AFOp aggOp -> aggOpToObj aggOp
AFExp e -> S.SELit e
colFldsToObj op flds =
aggOpToObj (AggOp op flds) =
S.applyJsonBuildObj $ concatMap (colFldsToExtr op) flds
colFldsToExtr op (t, PCFCol col) =
@ -442,14 +442,17 @@ mkBaseNode pfx fldAls annSelFlds tableFrom tablePerm tableArgs =
)
TAFExp _ -> (HM.fromList obExtrs, HM.empty, HM.empty, HM.empty)
fetchExtrFromAggFld AFCount = []
fetchExtrFromAggFld (AFSum sumFlds) = colFldsToExps sumFlds
fetchExtrFromAggFld (AFAvg avgFlds) = colFldsToExps avgFlds
fetchExtrFromAggFld (AFMax maxFlds) = colFldsToExps maxFlds
fetchExtrFromAggFld (AFMin minFlds) = colFldsToExps minFlds
fetchExtrFromAggFld (AFExp _) = []
fetchExtrFromAggFld (AFCount cty) = countTyToExps cty
fetchExtrFromAggFld (AFOp aggOp) = aggOpToExps aggOp
fetchExtrFromAggFld (AFExp _) = []
colFldsToExps = mapMaybe (mkColExp . snd)
countTyToExps S.CTStar = []
countTyToExps (S.CTSimple cols) = colsToExps cols
countTyToExps (S.CTDistinct cols) = colsToExps cols
colsToExps = mapMaybe (mkColExp . PCFCol)
aggOpToExps = mapMaybe (mkColExp . snd) . _aoFlds
mkColExp (PCFCol c) =
let qualCol = S.mkQIdenExp (mkBaseTableAls pfx) (toIden c)

View File

@ -238,6 +238,19 @@ jsonType = AnnType "json"
jsonbType :: AnnType
jsonbType = AnnType "jsonb"
data CountType
= CTStar
| CTSimple ![PGCol]
| CTDistinct ![PGCol]
deriving(Show, Eq)
instance ToSQL CountType where
toSQL CTStar = "*"
toSQL (CTSimple cols) =
paren $ ", " <+> cols
toSQL (CTDistinct cols) =
"DISTINCT" <-> paren (", " <+> cols)
data SQLExp
= SEPrep !Int
| SELit !T.Text
@ -255,6 +268,7 @@ data SQLExp
| SEBool !BoolExp
| SEExcluded !T.Text
| SEArray ![SQLExp]
| SECount !CountType
deriving (Show, Eq)
newtype Alias
@ -270,6 +284,9 @@ instance ToSQL Alias where
toAlias :: (IsIden a) => a -> Alias
toAlias = Alias . toIden
countStar :: SQLExp
countStar = SECount CTStar
instance ToSQL SQLExp where
toSQL (SEPrep argNumber) =
TB.char '$' <> fromString (show argNumber)
@ -304,6 +321,7 @@ instance ToSQL SQLExp where
<> toSQL (PGCol t)
toSQL (SEArray exps) = "ARRAY" <> TB.char '['
<> (", " <+> exps) <> TB.char ']'
toSQL (SECount ty) = "COUNT" <> paren (toSQL ty)
intToSQLExp :: Int -> SQLExp
intToSQLExp =

View File

@ -173,6 +173,7 @@ uSqlExp = restoringIdens . \case
S.SEExcluded <$> return t
S.SEArray l ->
S.SEArray <$> mapM uSqlExp l
S.SECount cty -> return $ S.SECount cty
where
uQual = \case
S.QualIden iden -> S.QualIden <$> getIden iden

View File

@ -6,6 +6,8 @@ response:
article_aggregate:
aggregate:
count: 2
count_columns_id: 2
count_columns_id_distinct: 2
sum:
id: 5
id_sum: 5
@ -26,6 +28,66 @@ response:
id_avg: 2.5
author_id: 1.5
author_id_avg: 1.5
stddev:
id: 0.7071067811865476
id_stddev: 0.7071067811865476
author_id: 0.7071067811865476
author_id_stddev: 0.7071067811865476
stddev_fields:
id: 0.7071067811865476
id_stddev: 0.7071067811865476
author_id: 0.7071067811865476
author_id_stddev: 0.7071067811865476
stddev_samp:
id: 0.7071067811865476
id_stddev_samp: 0.7071067811865476
author_id: 0.7071067811865476
author_id_stddev_samp: 0.7071067811865476
stddev_samp_fields:
id: 0.7071067811865476
id_stddev_samp: 0.7071067811865476
author_id: 0.7071067811865476
author_id_stddev_samp: 0.7071067811865476
stddev_pop:
id: 0.5
id_stddev_pop: 0.5
author_id: 0.5
author_id_stddev_pop: 0.5
stddev_pop_fields:
id: 0.5
id_stddev_pop: 0.5
author_id: 0.5
author_id_stddev_pop: 0.5
variance:
id: 0.5
id_variance: 0.5
author_id: 0.5
author_id_variance: 0.5
variance_fields:
id: 0.5
id_variance: 0.5
author_id: 0.5
author_id_variance: 0.5
var_samp:
id: 0.5
id_var_samp: 0.5
author_id: 0.5
author_id_var_samp: 0.5
var_samp_fields:
id: 0.25
id_var_pop: 0.25
author_id: 0.25
author_id_var_pop: 0.25
var_pop:
id: 0.25
id_var_pop: 0.25
author_id: 0.25
author_id_var_pop: 0.25
var_pop_fields:
id: 0.25
id_var_pop: 0.25
author_id: 0.25
author_id_var_pop: 0.25
max:
id: 3
id_max: 3
@ -95,94 +157,168 @@ response:
query:
query: |
query{
article_aggregate(where: {id: {_gt: 1}}){
aggregate{
count
sum{
id
id_sum: id
author_id
author_id_sum: author_id
}
sum_fields: sum{
id
id_sum: id
author_id
author_id_sum: author_id
}
avg{
id
id_avg: id
author_id
author_id_avg: author_id
}
avg_fields: avg{
id
id_avg: id
author_id
author_id_avg: author_id
}
max{
id
id_max: id
title
title_max: title
content
content_max: content
author_id
author_id_max: author_id
}
max_fields: max{
id
id_max: id
title
title_max: title
content
content_max: content
author_id
author_id_max: author_id
}
min{
id
id_min: id
title
title_min: title
content
content_min: content
author_id
author_id_min: author_id
}
min_fields: min{
id
id_min: id
title
title_min: title
content
content_min: content
author_id
author_id_min: author_id
}
}
nodes{
id
title
content
is_published
author{
id
name
}
}
articles: nodes{
id
title
content
is_published
author{
id
name
}
}
}
}
query {
article_aggregate(where: {id: {_gt: 1}}) {
aggregate {
count
count_columns_id: count(columns: [id])
count_columns_id_distinct: count(columns: [id], distinct: true)
sum {
id
id_sum: id
author_id
author_id_sum: author_id
}
sum_fields: sum {
id
id_sum: id
author_id
author_id_sum: author_id
}
avg {
id
id_avg: id
author_id
author_id_avg: author_id
}
avg_fields: avg {
id
id_avg: id
author_id
author_id_avg: author_id
}
stddev {
id
id_stddev: id
author_id
author_id_stddev: author_id
}
stddev_fields: stddev {
id
id_stddev: id
author_id
author_id_stddev: author_id
}
stddev_samp {
id
id_stddev_samp: id
author_id
author_id_stddev_samp: author_id
}
stddev_samp_fields: stddev_samp {
id
id_stddev_samp: id
author_id
author_id_stddev_samp: author_id
}
stddev_pop {
id
id_stddev_pop: id
author_id
author_id_stddev_pop: author_id
}
stddev_pop_fields: stddev_pop {
id
id_stddev_pop: id
author_id
author_id_stddev_pop: author_id
}
variance {
id
id_variance: id
author_id
author_id_variance: author_id
}
variance_fields: variance {
id
id_variance: id
author_id
author_id_variance: author_id
}
var_samp {
id
id_var_samp: id
author_id
author_id_var_samp: author_id
}
var_samp_fields: var_pop {
id
id_var_pop: id
author_id
author_id_var_pop: author_id
}
var_pop {
id
id_var_pop: id
author_id
author_id_var_pop: author_id
}
var_pop_fields: var_pop {
id
id_var_pop: id
author_id
author_id_var_pop: author_id
}
max {
id
id_max: id
title
title_max: title
content
content_max: content
author_id
author_id_max: author_id
}
max_fields: max {
id
id_max: id
title
title_max: title
content
content_max: content
author_id
author_id_max: author_id
}
min {
id
id_min: id
title
title_min: title
content
content_min: content
author_id
author_id_min: author_id
}
min_fields: min {
id
id_min: id
title
title_min: title
content
content_min: content
author_id
author_id_min: author_id
}
}
nodes {
id
title
content
is_published
author {
id
name
}
}
articles: nodes {
id
title
content
is_published
author {
id
name
}
}
}
}