Skip to content

Commit

Permalink
add more SQL aggregate functions min, sum, count
Browse files Browse the repository at this point in the history
  • Loading branch information
agentm committed May 7, 2024
1 parent 95d5f66 commit 9c8d986
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 160 deletions.
1 change: 0 additions & 1 deletion src/bin/benchmark/Handles.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ProjectM36.Persist
import Options.Applicative
import TutorialD.Interpreter
import ProjectM36.Interpreter hiding (Parser)
import ProjectM36.DatabaseContext
import TutorialD.Interpreter.Base hiding (option)
import qualified Data.Text as T
#if __GLASGOW_HASKELL__ < 804
Expand Down
38 changes: 34 additions & 4 deletions src/lib/ProjectM36/DataTypes/SQL/Null.hs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ nullAtomFunctions = HS.fromList [
funcName = "sql_max",
funcType = foldAtomFuncType (TypeVariableType "a") (nullAtomType IntegerAtomType),
funcBody = FunctionBuiltInBody sqlMax
},
Function {
funcName = "sql_min",
funcType = foldAtomFuncType (TypeVariableType "a") (nullAtomType IntegerAtomType),
funcBody = FunctionBuiltInBody sqlMin
},
Function {
funcName = "sql_count",
funcType = foldAtomFuncType (TypeVariableType "a") IntegerAtomType,
funcBody = FunctionBuiltInBody sqlCount
},
Function {
funcName = "sql_sum",
funcType = foldAtomFuncType (TypeVariableType "a") (nullAtomType IntegerAtomType),
funcBody = FunctionBuiltInBody sqlSum
}
] <> sqlBooleanIntegerFunctions

Expand Down Expand Up @@ -190,6 +205,12 @@ sqlIntegerUnaryFunction expectedAtomType op [x]
_other -> Left AtomFunctionTypeMismatchError
sqlIntegerUnaryFunction _ _ _ = Left AtomFunctionTypeMismatchError

sqlCount :: [Atom] -> Either AtomFunctionError Atom
sqlCount [RelationAtom relIn] =
case cardinality relIn of
Finite c -> pure $ IntegerAtom (toInteger c)
Countable -> Left AtomFunctionTypeMismatchError
sqlCount _ = Left AtomFunctionTypeMismatchError

sqlAbs :: [Atom] -> Either AtomFunctionError Atom
sqlAbs [IntegerAtom val] = pure $ IntegerAtom (abs val)
Expand All @@ -201,9 +222,18 @@ sqlAbs [ConstructedAtom "SQLJust" aType [IntegerAtom val]]
sqlAbs _other = Left AtomFunctionTypeMismatchError

sqlMax :: [Atom] -> Either AtomFunctionError Atom
sqlMax [RelationAtom relIn] =
sqlMax = sqlIntegerAgg max

sqlMin :: [Atom] -> Either AtomFunctionError Atom
sqlMin = sqlIntegerAgg min

sqlSum :: [Atom] -> Either AtomFunctionError Atom
sqlSum = sqlIntegerAgg (+)

sqlIntegerAgg :: (Integer -> Integer -> Integer) -> [Atom] -> Either AtomFunctionError Atom
sqlIntegerAgg op [RelationAtom relIn] =
case oneTuple relIn of
Nothing -> pure $ nullAtom IntegerAtomType Nothing -- SQL max of empty table is NULL
Nothing -> pure $ nullAtom IntegerAtomType Nothing -- SQL max/min of empty table is NULL
Just oneTup ->
if atomTypeForAtom (newVal oneTup) /= IntegerAtomType then
Left AtomFunctionTypeMismatchError
Expand All @@ -214,12 +244,12 @@ sqlMax [RelationAtom relIn] =
nullMax acc nextVal =
let mNextVal = sqlNullableIntegerToMaybe nextVal
mOldVal = sqlNullableIntegerToMaybe acc
mResult = max <$> mNextVal <*> mOldVal
mResult = op <$> mNextVal <*> mOldVal
in
nullAtom IntegerAtomType (case mResult of
Nothing -> Nothing
Just v -> Just (IntegerAtom v))
sqlMax _ = Left AtomFunctionTypeMismatchError
sqlIntegerAgg _ _ = Left AtomFunctionTypeMismatchError


sqlNullableIntegerToMaybe :: Atom -> Maybe Integer
Expand Down
165 changes: 14 additions & 151 deletions src/lib/ProjectM36/SQL/Convert.hs
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,13 @@ prettyColumnAliasRemapper :: ColumnAliasRemapper -> String
prettyColumnAliasRemapper cAMap = intercalate ", " $ map (\(realAttr, (attrAlias, colNameSet)) -> "real->" <> T.unpack realAttr <> ":alias->" <> T.unpack attrAlias <> ":alts->{" <> show colNameSet <> "}") (M.toList cAMap)



{-
traceStateM :: ConvertM ()
traceStateM = do
s <- get
traceM (prettyTableContext s)

-}

-- key: alias value: real column attribute name
type ColumnAliasMap = M.Map ColumnAlias AttributeName

Expand Down Expand Up @@ -175,10 +176,6 @@ withSubSelect m = do

diff <- foldM tableDiffFolder mempty (M.toList postSub)

{- let diff = M.differenceWith tctxDiff postSub orig
tctxDiff (rexprA, attrsA, colAliasMapA) (_, _, colAliasMapB) =
Just (rexprA, attrsA, M.difference colAliasMapB colAliasMapA)-}
-- traceShowM ("subselect diff"::String, diff)
pure (ret, diff)

-- if we find a column naming conflict, generate a non-conflicting name for insertion into the column alias map
Expand Down Expand Up @@ -217,22 +214,6 @@ noteColumnMention mTblAlias colName mColAlias = do
-- traceShowM ("noteColumnMention"::String, mTblAlias, colName)
-- traceStateM
tc@(TableContext tcontext) <- get
{- tblAlias' <- case mTblAlias of
Just tblAlias -> do
void $ lookupTable tblAlias
pure tblAlias
Nothing ->do
-- scan column names for match- if there are multiple matches, return a column ambiguity error
ret <- findOneColumn colName
-- traceShowM ("insertColumn2", colName)
pure ret-}
-- insert into the column alias map
{- let colAttr = case colName of
ColumnName [c] -> c
ColumnName [t,c] ->
origAttrName = case colName of
ColumnName [c] -> c
ColumnName [_,c] -> c-}
-- check if we already have a mention mapping
let lookupWithTableAlias (TableAlias tAlias) colAttr = do
when (isJust mTblAlias && Just (TableAlias tAlias) /= mTblAlias) (throwSQLE (TableAliasMismatchError (TableAlias tAlias)))
Expand Down Expand Up @@ -298,93 +279,13 @@ noteColumnMention mTblAlias colName mColAlias = do
throwSQLE (AmbiguousColumnResolutionError colName)
other@ColumnName{} -> throwSQLE (UnexpectedColumnNameError other)


------
{- case findNotedColumn' colName tc of
Right [] -> do
-- no match found, so we can insert this as a new column alias
let colAlias = case mColAlias of
Just al -> al
Nothing -> --ColumnAlias (unTableAlias tblAlias' <> "." <> origAttrName)
ColumnAlias origAttrName
insertColumnAlias tblAlias' origAttrName colAlias colName
pure colAlias
Right [match] ->
-- one match found- error
throwSQLE (AmbiguousColumnResolutionError colName)
Right (match:_) ->
-- multiple matches found- error
throwSQLE (AmbiguousColumnResolutionError colName)
Left (ColumnResolutionError{}) ->
throwSQLE err-}
{- case M.lookup tblAlias' tcontext of
Nothing -> throwSQLE (MissingTableReferenceError tblAlias')
Just (_,_,colAliasRemapper) -> do
case attributeNameForAttributeAlias colAttr colAliasRemapper of
Right _ -> pure (ColumnAlias colAttr)
Left _ -> do -- no match previously recorded, so add it-}
{- when (newAlias `elem` allColumnAliases tcontext) $ do
traceShowM ("gonk error",
"colName", colName,
"mTblAlias", mTblAlias,
"mColAlias", mColAlias,
p tmap)
throwSQLE (DuplicateColumnAliasError newAlias)-} --duplicate column aliases are OK
--verify that the alias is not duplicated
{- let colAlias = case mColAlias of
Just al -> al
Nothing -> --ColumnAlias (unTableAlias tblAlias' <> "." <> origAttrName)
ColumnAlias origAttrName
insertColumnAlias tblAlias' origAttrName colAlias colName
pure colAlias
-}
{-
-- | Add a column alias for a column which has already been inserted into the TableContext.
addColumnAlias' :: TableContext -> TableAlias -> ColumnAlias -> AttributeName -> Either SQLError TableContext
addColumnAlias' (TableContext tctx) tAlias colAlias@(ColumnAlias colText) attr = do
case M.lookup tAlias tctx of
Nothing -> Left (ColumnAliasResolutionError colAlias)
Just (rvexpr, attrs, colMap) ->
--check that the attribute is present in attributes, then plop it into the colMap and return the updated TableContext
if attr `A.isAttributeNameContained` attrs then do
insertColumnAlias
let newColMap = M.insert colAlias attr colMap
newTContext = M.insert tAlias (rvexpr, attrs, newColMap) tctx
pure (TableContext newTContext)
else do
traceShow "addColAlias'" $ Left (ColumnResolutionError (ColumnName [attr]))
addColumnAlias :: TableAlias -> ColumnAlias -> AttributeName -> ConvertM ()
addColumnAlias tAlias colAlias attrName = do
tctx <- get
case addColumnAlias' tctx tAlias colAlias attrName of
Left err -> throwSQLE err
Right tctx' -> put tctx'
allColumnAliases :: TableContext -> [ColumnAlias]
allColumnAliases (TableContext tmap) =
foldl' folder [] tmap
where
folder acc (_,_,colmap) = M.keys colmap <> acc
-}
lookupTable :: TableAlias -> ConvertM (RelationalExpr, Attributes, ColumnAliasRemapper)
lookupTable ta = do
(TableContext map') <- get
case M.lookup ta map' of
Nothing -> throwSQLE (MissingTableReferenceError ta)
Just res -> pure res

{-
-- | Merge table contexts (used in subselects)
mergeContext :: TableContext -> ConvertM ColumnAliasMap
mergeContext (TableContext ctxB) = do
(TableContext tMapA) <- get
foldM folder mempty (M.toList tMapA)
where
folder acc (tAlias, (re,attrs, _)) = do
colMap <- insertTable tAlias re attrs
pure (M.union acc colMap)
-}
-- | Find a column name or column alias in the underlying table context. Returns key into table context.
findColumn :: ColumnName -> ConvertM [TableAlias]
findColumn targetCol = do
Expand Down Expand Up @@ -493,51 +394,14 @@ attributeNameForColumnName colName = do
ColumnName [_, col] | col `A.isAttributeNameContained` rvattrs ->
-- the column has not been aliased, so we presume it can be use the column name directly
pure col
_ -> throwSQLE $ traceShow ("attrnameforcolname"::String, rvattrs, colName) $ ColumnResolutionError colName
{-
attributeNameForColumnName :: ColumnName -> ConvertM AttributeName
attributeNameForColumnName colName = do
s <- get
case attributeNameForColumnName' colName s of
Left err -> throwSQLE err
Right al -> do
traceStateM
traceShowM ("attributeNameForColumnName"::String, colName, "->"::String, al)
pure al
-}
_ -> throwSQLE $ ColumnResolutionError colName

wrapTypeF :: TypeForRelExprF -> RelationalExpr -> ConvertM Relation
wrapTypeF typeF relExpr =
case typeF relExpr of
Left relError -> throwSQLE (SQLRelationalError relError)
Right v -> pure v


-- | Return the table alias for the column name iff the attribute is unique. Used for attribute resolution.
{-
tableAliasForColumnName :: TypeForRelExprF -> ColumnName -> TableContext -> Either SQLError TableAlias
-- the table alias is included
tableAliasForColumnName typeF cn@(ColumnName [tAlias, _]) (TableContext tMap) = do
if M.member (TableAlias tAlias) tMap then
pure (TableAlias tAlias)
else
Left (ColumnResolutionError cn)
tableAliasForColumnName typeF qn@(ColumnName [colName]) (TableContext tMap) = do
--look up the column name in all possible tables
res <- foldM folder Nothing (M.toList tMap)
case res of
Just res -> pure res
Nothing -> Left (ColumnResolutionError qn)
where
folder :: Maybe ColumnName -> (TableAlias, RelationalExpr) -> _
folder Just{} _ = Left (AmbiguousColumnResolutionError qn)
folder Nothing (TableAlias tableAlias, (rvExpr,_)) = do
tRel <- wrapTypeF typeF rvExpr -- we could cache this in the table alias map ADT
--traceShowM ("findColName", rvExpr, tRel)
if colName `S.member` attributeNameSet (attributes tRel) then
pure (Just (ColumnName [tableAlias, colName]))
else pure Nothing
-}
baseDFExpr :: DataFrameExpr
baseDFExpr = DataFrameExpr { convertExpr = MakeRelationFromExprs (Just []) (TupleExprs () [TupleExpr mempty]), --relationTrue if the table expression is empty "SELECT 1"
orderExprs = [],
Expand Down Expand Up @@ -695,12 +559,6 @@ convertSelectItem typeF acc (c,selItem) =
colinfo (ColumnProjectionName [ProjectionName name]) = do
findOneColumn (ColumnName [name])
colinfo colProjName = throwSQLE $ UnexpectedColumnProjectionName colProjName
{- processGroupBy e@(sexpr, alias) = (replaceProjScalarExpr groupByReplacer sexpr, alias)
groupByReplacer expr =
case expr of
FunctionApplication "sql_max" [targetColumn] -> FunctionApplication "sql_max" [
_ -> expr-}


convertProjection :: TypeForRelExprF -> [SelectItem] -> [GroupByExpr] -> Maybe HavingExpr -> ConvertM (RelationalExpr -> RelationalExpr)
convertProjection typeF selItems groupBys havingExpr = do
Expand Down Expand Up @@ -752,6 +610,7 @@ convertProjection typeF selItems groupBys havingExpr = do
-- let fAggregates
-- apply rename
renamesSet <- foldM (\acc (qProjName, (ColumnAlias newName)) -> do
traceShowM ("renamesSet"::String, qProjName, newName)
oldName <- convertColumnProjectionName qProjName
pure $ S.insert (oldName, newName) acc) S.empty (taskRenames task)
let fRenames = if S.null renamesSet then id else Rename renamesSet
Expand Down Expand Up @@ -1126,6 +985,8 @@ lookupFunc qname =
other -> throwSQLE $ NotSupportedError ("function name: " <> T.pack (show other))
where
f n args = FunctionAtomExpr n args ()
aggMapper (FuncName [nam], nam') = (nam, f nam')
aggMapper (FuncName other,_) = error ("unexpected multi-component SQL aggregate function: " <> show other)
sqlFuncs = [(">",f "sql_gt"),
("<",f "sql_lt"),
(">=",f "sql_gte"),
Expand All @@ -1136,9 +997,8 @@ lookupFunc qname =
("+", f "sql_add"),
("and", f "sql_and"),
("or", f "sql_or"),
("abs", f "sql_abs"),
("max", f "sql_max")
]
("abs", f "sql_abs")
] <> map aggMapper aggregateFunctions


-- | Used in join condition detection necessary for renames to enable natural joins.
Expand Down Expand Up @@ -1307,7 +1167,7 @@ convertInsert typeF ins = do
else
Rename (S.fromList (filter rendundantRename (zip rvExprAttrNames insAttrNames))) (convertExpr dfExpr)
rendundantRename (a,b) = a /= b
traceShowM ("ins"::String, insExpr)
--traceShowM ("ins"::String, insExpr)
pure $ B.Insert rvTarget insExpr

convertDelete :: TypeForRelExprF -> Delete.Delete -> ConvertM DatabaseContextExpr
Expand Down Expand Up @@ -1399,7 +1259,9 @@ convertGroupBy _typeF groupBys mHavingExpr sqlProjection = do
AggGroupByItem pe _gb ->
pure $ info { aggregates = pe : aggregates info }
NonAggGroupByItem (Identifier colName) gb -> do
traceShowM ("convertGroupBy"::String, colName)
aname <- convertColumnProjectionName colName
traceShowM ("convertGroupBy2"::String, "done")
pure $ info { nonAggregates = (aname, gb) : nonAggregates info }
NonAggGroupByItem pe _ -> do
throwSQLE (UnsupportedGroupByProjectionError pe)
Expand Down Expand Up @@ -1445,7 +1307,8 @@ emptyGroupByInfo = GroupByInfo { aggregates = [], nonAggregates = [], havingRest
aggregateFunctions :: [(FuncName, FunctionName)]
aggregateFunctions = [(FuncName ["max"], "sql_max"),
(FuncName ["min"], "sql_min"),
(FuncName ["sum"], "sql_sum")]
(FuncName ["sum"], "sql_sum"),
(FuncName ["count"], "sql_count")]

isAggregateFunction :: FuncName -> Bool
isAggregateFunction fname = fname `elem` map fst aggregateFunctions
Expand Down
10 changes: 6 additions & 4 deletions test/SQL/InterpreterTest.hs
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,20 @@ testSelect = TestCase $ do
"(relation{tuple{attr_1 SQLJust 4}})"
),
-- where not exists
-- group by
-- group by with max aggregate
("SELECT city,max(status) FROM s GROUP BY city",
"((s group ({all but city} as `_sql_aggregate`) : {attr_2:=sql_max(@`_sql_aggregate`{status})}){city,attr_2})",
"(relation{city Text, attr_2 SQLNullable Integer}{tuple{city \"London\", attr_2 SQLJust 20}, tuple{city \"Paris\", attr_2 SQLJust 30}, tuple{city \"Athens\", attr_2 SQLJust 30}})"
),
-- group by with aggregate column alias
-- group by with aggregate max column alias
("SELECT city,max(status) as status FROM s GROUP BY city",
"((s group ({all but city} as `_sql_aggregate`) : {status:=sql_max(@`_sql_aggregate`{status})}){city,status})",
"(relation{city Text, status SQLNullable Integer}{tuple{city \"London\", status SQLJust 20}, tuple{city \"Paris\", status SQLJust 30}, tuple{city \"Athens\", status SQLJust 30}})"),
-- aggregate without grouping
-- aggregate max without grouping
("SELECT max(status) as status FROM s",
"(((s group ({all but } as `_sql_aggregate`)):{status:=sql_max( (@`_sql_aggregate`){ status } )}){ status })",
"(relation{status SQLNullable Integer}{tuple{status SQLJust 30}})"),
-- group by having
-- group by having max
("select city,max(status) as status from s group by city having max(status)=30",
"((((s group ({all but city} as `_sql_aggregate`)):{status:=sql_max( (@`_sql_aggregate`){ status } ), `_sql_having`:=sql_coalesce_bool( sql_equals( sql_max( (@`_sql_aggregate`){ status } ), 30 ) )}){ city, status }) where `_sql_having`=True)",
"(relation{city Text,status SQLNullable Integer}{tuple{city \"Athens\",status SQLJust 30},tuple{city \"Paris\",status SQLJust 30}})"),
Expand All @@ -184,6 +184,8 @@ testSelect = TestCase $ do
"(relation{tuple{city \"London\"},tuple{city \"New York\"}, tuple{city \"Athens\"}, tuple{city \"Paris\"}})"
),
-- except
("select city from s except select 'London' as city",
),
-- limit
("SELECT * FROM s LIMIT 10",
"(s) limit 10",
Expand Down

0 comments on commit 9c8d986

Please sign in to comment.