diff --git a/rel8.cabal b/rel8.cabal index bb28fb36..f1c1f14e 100644 --- a/rel8.cabal +++ b/rel8.cabal @@ -59,6 +59,7 @@ library other-modules: Rel8.Aggregate + Rel8.Aggregate.Fold Rel8.Column Rel8.Column.ADT diff --git a/src/Rel8.hs b/src/Rel8.hs index be3f7265..93c8be95 100644 --- a/src/Rel8.hs +++ b/src/Rel8.hs @@ -135,9 +135,6 @@ module Rel8 , BuildADT, buildADT , ConstructADT, constructADT - -- *** Other ADT operations - , AggregateADT, aggregateADT - -- *** Miscellaneous notes -- $misc-notes @@ -147,7 +144,6 @@ module Rel8 , ConstructHKD, constructHKD , DeconstructHKD, deconstructHKD , NameHKD, nameHKD - , AggregateHKD, aggregateHKD -- ** Table schemas , TableSchema(..) @@ -253,24 +249,34 @@ module Rel8 , loop -- ** Aggregation - , Aggregate - , Aggregates + , Aggregator + , Aggregator1 + , Aggregator' + , Fold (Semi, Full) + , semi + , full , aggregate + , aggregate1 + , filterWhere + , filterWhereOptional + , distinctAggregate + , orderAggregateBy + , optionalAggregate , countRows - , groupBy - , listAgg, listAggExpr + , groupBy, groupByOn + , listAgg, listAggOn, listAggExpr, listAggExprOn , mode - , nonEmptyAgg, nonEmptyAggExpr - , DBMax, max - , DBMin, min - , DBSum, sum, sumWhere, avg + , nonEmptyAgg, nonEmptyAggOn, nonEmptyAggExpr, nonEmptyAggExprOn + , DBMax, max, maxOn + , DBMin, min, minOn + , DBSum, sum, sumOn, sumWhere, avg, avgOn , DBString, stringAgg - , count + , count, countOn , countStar - , countDistinct - , countWhere - , and - , or + , countDistinct, countDistinctOn + , countWhere, countWhereOn + , and, andOn + , or, orOn -- ** Ordering , orderBy @@ -288,7 +294,6 @@ module Rel8 , partitionBy , orderPartitionBy , cumulative - , cumulative_ , currentRow , rowNumber , rank @@ -296,11 +301,11 @@ module Rel8 , percentRank , cumeDist , ntile - , lag - , lead - , firstValue - , lastValue - , nthValue + , lag, lagOn + , lead, leadOn + , firstValue, firstValueOn + , lastValue, lastValueOn + , nthValue, nthValueOn , indexed -- ** Bindings @@ -352,6 +357,7 @@ import Prelude () -- rel8 import Rel8.Aggregate +import Rel8.Aggregate.Fold import Rel8.Column import Rel8.Column.ADT import Rel8.Column.Either @@ -374,7 +380,7 @@ import Rel8.Expr.Order import Rel8.Expr.Serialize import Rel8.Expr.Sequence import Rel8.Expr.Text ( like, ilike ) -import Rel8.Expr.Window hiding ( cumulative ) +import Rel8.Expr.Window import Rel8.Generic.Rel8able ( KRel8able, Rel8able ) import Rel8.Order import Rel8.Query diff --git a/src/Rel8/Aggregate.hs b/src/Rel8/Aggregate.hs index eebee190..fd5f7e7f 100644 --- a/src/Rel8/Aggregate.hs +++ b/src/Rel8/Aggregate.hs @@ -1,83 +1,197 @@ {-# language DataKinds #-} -{-# language FlexibleContexts #-} -{-# language FlexibleInstances #-} -{-# language MultiParamTypeClasses #-} -{-# language RankNTypes #-} +{-# language GADTs #-} +{-# language KindSignatures #-} +{-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-} -{-# language TypeFamilies #-} -{-# language UndecidableInstances #-} module Rel8.Aggregate - ( Aggregate(..), zipOutputs - , unsafeMakeAggregate - , Aggregates + ( Aggregator' (Aggregator) + , Aggregator + , Aggregator1 + , semi + , full + , filterWhereExplicit + , unsafeMakeAggregator ) where -- base -import Control.Applicative ( liftA2 ) -import Data.Functor.Identity ( Identity( Identity ) ) -import Data.Kind ( Constraint, Type ) +import Control.Applicative (liftA2) +import Data.Kind (Type) import Prelude --- profunctors -import Data.Profunctor ( dimap ) - -- opaleye import qualified Opaleye.Aggregate as Opaleye -import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye -import qualified Opaleye.Internal.Column as Opaleye +import qualified Opaleye.Internal.MaybeFields as Opaleye +import qualified Opaleye.Internal.Operators as Opaleye --- rel8 -import Rel8.Expr ( Expr ) -import Rel8.Schema.HTable.Identity ( HIdentity(..) ) -import qualified Rel8.Schema.Kind as K -import Rel8.Schema.Null ( Sql ) -import Rel8.Table - ( Table, Columns, Context, fromColumns, toColumns - , FromExprs, fromResult, toResult - , Transpose +-- product-profunctor +import Data.Profunctor.Product + ( ProductProfunctor, purePP, (****) + , SumProfunctor, (+++!) ) -import Rel8.Table.Transpose ( Transposes ) -import Rel8.Type ( DBType ) - - --- | 'Aggregate' is a special context used by 'Rel8.aggregate'. -type Aggregate :: K.Context -newtype Aggregate a = Aggregate (Opaleye.Aggregator () (Expr a)) - - -instance Sql DBType a => Table Aggregate (Aggregate a) where - type Columns (Aggregate a) = HIdentity a - type Context (Aggregate a) = Aggregate - type FromExprs (Aggregate a) = a - type Transpose to (Aggregate a) = to a - - toColumns = HIdentity - fromColumns (HIdentity a) = a - toResult = HIdentity . Identity - fromResult (HIdentity (Identity a)) = a - - --- | @Aggregates a b@ means that the columns in @a@ are all 'Aggregate's --- for the 'Expr' columns in @b@. -type Aggregates :: Type -> Type -> Constraint -class Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs -instance Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs - - -zipOutputs :: () - => (Expr a -> Expr b -> Expr c) -> Aggregate a -> Aggregate b -> Aggregate c -zipOutputs f (Aggregate a) (Aggregate b) = Aggregate (liftA2 f a b) +-- profunctors +import Data.Profunctor (Profunctor, dimap) -unsafeMakeAggregate :: forall (input :: Type) (output :: Type) n n' a a'. () - => (Expr input -> Opaleye.PrimExpr) - -> (Opaleye.PrimExpr -> Expr output) - -> Opaleye.Aggregator (Opaleye.Field_ n a) (Opaleye.Field_ n' a') - -> Expr input - -> Aggregate output -unsafeMakeAggregate input output aggregator expr = - Aggregate $ dimap in_ out aggregator - where out = output . Opaleye.unColumn - in_ = Opaleye.Column . input . const expr +-- rel8 +import Rel8.Expr (Expr) +import Rel8.Expr.Opaleye (toPrimExpr, toColumn) +import Rel8.Aggregate.Fold (Fallback (Empty, Fallback), Fold (Full, Semi)) + +-- semigroupoids +import Data.Functor.Apply (Apply, liftF2) + + +-- | 'Aggregator'' is the most general form of \"aggregator\", of which +-- 'Aggregator' and 'Aggregator1' are special cases. 'Aggregator''s are +-- comprised of aggregation functions and/or @GROUP BY@ clauses. +-- +-- Aggregation functions operating on individual 'Rel8.Expr's such as +-- 'Rel8.sum' can be combined into 'Aggregator's operating on larger types +-- using the 'Applicative', 'Profunctor' and 'ProductProfunctor' interfaces. +-- Working with 'Profunctor's can sometimes be awkward so for every 'Rel8.sum' +-- we also provide a 'Rel8.sumOn' which bundles an 'Data.Profunctor.lmap'. For +-- complex aggregations, we recommend using these functions along with +-- @ApplicativeDo@, @BlockArguments@, @OverloadedRecordDot@ and +-- @RecordWildCards@: +-- +-- @ +-- +-- data Input f = Input +-- { orderId :: Column f OrderId +-- , customerId :: Column f CustomerId +-- , productId :: Column f ProductId +-- , quantity :: Column f Int64 +-- , price :: Column f Scientific +-- } +-- deriving (Generic, Rel8able) +-- +-- +-- totalPrice :: Input Expr -> Expr Scientific +-- totalPrice input = fromIntegral input.quantity * input.price +-- +-- +-- data Result f = Result +-- { customerId :: Column f CustomerId +-- , totalOrders :: Column f Int64 +-- , productsOrdered :: Column f Int64 +-- , totalPrice :: Column Scientific +-- } +-- deriving (Generic, Rel8able) +-- +-- +-- allResults :: Query (Result Expr) +-- allResults = +-- aggregate +-- do +-- customerId <- groupByOn (.customerId) +-- totalOrders <- countDistinctOn (.orderId) +-- productsOrdered <- countDistinctOn (.productId) +-- totalPrice <- sumOn totalPrice +-- pure Result {..} +-- do +-- order <- each orderSchema +-- orderLine <- each orderLineSchema +-- where_ $ order.id ==. orderLine.orderId +-- pure +-- Input +-- { orderId = order.id +-- , customerId = order.customerId +-- , productId = orderLine.productId +-- , quantity = orderLine.quantity +-- , price = orderLine.price +-- } +-- @ +type Aggregator' :: Fold -> Type -> Type -> Type +data Aggregator' fold i a = Aggregator !(Fallback fold a) !(Opaleye.Aggregator i a) + + +instance Profunctor (Aggregator' fold) where + dimap f g (Aggregator fallback a) = + Aggregator (fmap g fallback) (dimap f g a) + + +instance ProductProfunctor (Aggregator' fold) where + purePP = pure + (****) = (<*>) + + +instance SumProfunctor (Aggregator' fold) where + Aggregator fallback a +++! Aggregator fallback' b = + flip Aggregator (a +++! b) $ case fallback of + Empty -> case fallback' of + Empty -> Empty + Fallback x -> Fallback (Right x) + Fallback x -> Fallback (Left x) + + +instance Functor (Aggregator' fold i) where + fmap = dimap id + + +instance Apply (Aggregator' fold i) where + liftF2 f (Aggregator fallback a) (Aggregator fallback' b) = + Aggregator (liftF2 f fallback fallback') (liftA2 f a b) + + +instance Applicative (Aggregator' fold i) where + pure a = Aggregator (pure a) (pure a) + liftA2 = liftF2 + + +-- | An 'Aggregator' takes a 'Rel8.Query' producing a collection of rows of +-- type @a@ and transforms it into a 'Rel8.Query' producing a single row of +-- type @b@. If the given 'Rel8.Query' produces an empty collection of rows, +-- then the single row in the resulting 'Rel8.Query' contains the identity +-- values of the aggregation functions comprising the 'Aggregator' (i.e., +-- @0@ for 'Rel8.sum', 'Rel8.false' for 'Rel8.or', etc.). +-- +-- 'Aggregator' is a special form of 'Aggregator'' parameterised by 'Full'. +type Aggregator :: Type -> Type -> Type +type Aggregator = Aggregator' 'Full + + +-- | An 'Aggregator1' takes a collection of rows of type @a@, groups them, and +-- transforms each group into a single row of type @b@. This corresponds to +-- aggregators using @GROUP BY@ in SQL. If given an empty collection of rows, +-- 'Aggregator1' will have no groups and will therefore also return an empty +-- collection of rows. +-- +-- 'Aggregator1' is a special form of 'Aggregator'' parameterised by 'Semi'. +type Aggregator1 :: Type -> Type -> Type +type Aggregator1 = Aggregator' 'Semi + + +-- | 'semi' turns an 'Aggregator' into an 'Aggregator1'. +semi :: Aggregator' fold i a -> Aggregator1 i a +semi (Aggregator _ a) = Aggregator Empty a + + +-- | Given a value to fall back on if given an empty collection of rows, 'full' +-- turns an 'Aggregator1' into an 'Aggregator'. +full :: a -> Aggregator' fold i a -> Aggregator' fold' i a +full fallback (Aggregator _ a) = Aggregator (Fallback fallback) a + + +filterWhereExplicit :: () + => Opaleye.IfPP a a + -> (i -> Expr Bool) + -> Aggregator i a + -> Aggregator' fold i a +filterWhereExplicit ifPP f (Aggregator (Fallback fallback) aggregator) = + Aggregator (Fallback fallback) aggregator' + where + aggregator' = + Opaleye.fromMaybeFieldsExplicit ifPP fallback + <$> Opaleye.filterWhere (toColumn . toPrimExpr . f) aggregator + + +unsafeMakeAggregator :: forall (i :: Type) (o :: Type) (fold :: Fold) i' o'. () + => (i -> i') + -> (o' -> o) + -> Fallback fold o + -> Opaleye.Aggregator i' o' + -> Aggregator' fold i o +unsafeMakeAggregator input output fallback = + Aggregator fallback . dimap input output diff --git a/src/Rel8/Aggregate/Fold.hs b/src/Rel8/Aggregate/Fold.hs new file mode 100644 index 00000000..8cedca4b --- /dev/null +++ b/src/Rel8/Aggregate/Fold.hs @@ -0,0 +1,52 @@ +{-# language DataKinds #-} +{-# language GADTs #-} +{-# language LambdaCase #-} +{-# language StandaloneKindSignatures #-} + +module Rel8.Aggregate.Fold + ( Fallback (Empty, Fallback) + , Fold (Semi, Full) + ) +where + +-- base +import Control.Applicative (liftA2) +import Data.Kind (Type) +import Prelude + +-- semigroupoids +import Data.Functor.Apply (Apply, liftF2) + + +-- | 'Fold' is a kind that parameterises aggregations. Aggregations +-- parameterised by 'Semi' are analogous to 'Data.Semigroup.Foldable.foldMap1' +-- (i.e, they can only produce results on a non-empty 'Rel8.Query') whereas +-- aggregations parameterised by 'Full' are analagous to 'foldMap' (given a +-- non-empty) query, they return the identity values of the aggregation +-- functions. +type Fold :: Type +data Fold = Semi | Full + + +type Fallback :: Fold -> Type -> Type +data Fallback fold a where + Fallback :: !a -> Fallback fold a + Empty :: Fallback 'Semi a + + +instance Functor (Fallback fold) where + fmap f = \case + Fallback a -> Fallback (f a) + Empty -> Empty + + +instance Apply (Fallback fold) where + liftF2 f (Fallback a) (Fallback b) = Fallback (f a b) + liftF2 _ (Fallback _) Empty = Empty + liftF2 _ Empty (Fallback _) = Empty + liftF2 _ Empty Empty = Empty + + +instance Applicative (Fallback fold) where + pure = Fallback + liftA2 = liftF2 diff --git a/src/Rel8/Expr/Aggregate.hs b/src/Rel8/Expr/Aggregate.hs index b949f04c..5b87fdb7 100644 --- a/src/Rel8/Expr/Aggregate.hs +++ b/src/Rel8/Expr/Aggregate.hs @@ -1,19 +1,24 @@ {-# language DataKinds #-} {-# language FlexibleContexts #-} +{-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-} {-# language TypeFamilies #-} {-# options_ghc -fno-warn-redundant-constraints #-} module Rel8.Expr.Aggregate - ( count, countDistinct, countStar, countWhere - , and, or - , min, max - , sum, sumWhere - , avg - , stringAgg - , groupByExpr - , listAggExpr, nonEmptyAggExpr + ( count, countOn, countStar + , countDistinct, countDistinctOn + , countWhere, countWhereOn + , and, andOn, or, orOn + , min, minOn, max, maxOn + , sum, sumOn, sumWhere + , avg, avgOn + , stringAgg, stringAggOn + , groupByExpr, groupByExprOn + , distinctAggregate + , filterWhereExplicit + , listAggExpr, listAggExprOn, nonEmptyAggExpr, nonEmptyAggExprOn , slistAggExpr, snonEmptyAggExpr ) where @@ -21,25 +26,35 @@ where -- base import Data.Int ( Int64 ) import Data.List.NonEmpty ( NonEmpty ) +import Data.String (IsString) import Prelude hiding ( and, max, min, null, or, sum ) -- opaleye -import qualified Opaleye.Internal.Aggregate as Opaleye -import Opaleye.Internal.Column ( Field_( Column ) ) import qualified Opaleye.Aggregate as Opaleye +import qualified Opaleye.Internal.Aggregate as Opaleye +import qualified Opaleye.Internal.Operators as Opaleye + +-- profunctors +import Data.Profunctor (dimap, lmap) -- rel8 -import Rel8.Aggregate ( Aggregate, unsafeMakeAggregate ) +import Rel8.Aggregate + ( Aggregator' (Aggregator) + , Aggregator1 + , filterWhereExplicit + , unsafeMakeAggregator + ) +import Rel8.Aggregate.Fold (Fallback (Empty, Fallback)) import Rel8.Expr ( Expr ) -import Rel8.Expr.Bool ( caseExpr ) +import Rel8.Expr.Array (sempty) +import Rel8.Expr.Bool (false, true) import Rel8.Expr.Opaleye ( castExpr + , fromColumn , fromPrimExpr - , fromPrimExpr + , toColumn , toPrimExpr ) -import Rel8.Expr.Null ( null ) -import Rel8.Expr.Serialize ( litExpr ) import Rel8.Schema.Null ( Sql, Unnullify ) import Rel8.Type ( DBType, typeInformation ) import Rel8.Type.Array ( encodeArrayElement ) @@ -52,52 +67,132 @@ import Rel8.Type.Sum ( DBSum ) -- | Count the occurances of a single column. Corresponds to @COUNT(a)@ -count :: Expr a -> Aggregate Int64 -count = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.count +count :: Aggregator' fold (Expr a) (Expr Int64) +count = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + (Fallback 0) + Opaleye.count + --- | Count the number of distinct occurances of a single column. Corresponds to +-- | Applies 'count' to the column selected by the given function. +countOn :: (i -> Expr a) -> Aggregator' fold i (Expr Int64) +countOn f = lmap f count + + +-- | Count the number of distinct occurrences of a single column. Corresponds to -- @COUNT(DISTINCT a)@ -countDistinct :: Sql DBEq a => Expr a -> Aggregate Int64 -countDistinct = unsafeMakeAggregate toPrimExpr fromPrimExpr $ - Opaleye.distinctAggregator Opaleye.count +countDistinct :: Sql DBEq a + => Aggregator' fold (Expr a) (Expr Int64) +countDistinct = distinctAggregate count + + +-- | Applies 'countDistinct' to the column selected by the given function. +countDistinctOn :: Sql DBEq a + => (i -> Expr a) -> Aggregator' fold i (Expr Int64) +countDistinctOn f = lmap f countDistinct -- | Corresponds to @COUNT(*)@. -countStar :: Aggregate Int64 -countStar = count (litExpr True) +countStar :: Aggregator' fold i (Expr Int64) +countStar = lmap (const true) count -- | A count of the number of times a given expression is @true@. -countWhere :: Expr Bool -> Aggregate Int64 -countWhere condition = count (caseExpr [(condition, litExpr (Just True))] null) +countWhere :: Aggregator' fold (Expr Bool) (Expr Int64) +countWhere = filterWhereExplicit ifPP id countStar + + +-- | Applies 'countWhere' to the column selected by the given function. +countWhereOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Int64) +countWhereOn f = lmap f countWhere -- | Corresponds to @bool_and@. -and :: Expr Bool -> Aggregate Bool -and = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.boolAnd +and :: Aggregator' fold (Expr Bool) (Expr Bool) +and = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + (Fallback true) + Opaleye.boolAnd + + +-- | Applies 'and' to the column selected by the given function. +andOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Bool) +andOn f = lmap f and -- | Corresponds to @bool_or@. -or :: Expr Bool -> Aggregate Bool -or = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.boolOr +or :: Aggregator' fold (Expr Bool) (Expr Bool) +or = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + (Fallback false) + Opaleye.boolOr + + +-- | Applies 'or' to the column selected by the given function. +orOn :: (i -> Expr Bool) -> Aggregator' fold i (Expr Bool) +orOn f = lmap f or -- | Produce an aggregation for @Expr a@ using the @max@ function. -max :: Sql DBMax a => Expr a -> Aggregate a -max = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMax +max :: Sql DBMax a => Aggregator1 (Expr a) (Expr a) +max = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + Empty + Opaleye.unsafeMax + + +-- | Applies 'max' to the column selected by the given function. +maxOn :: Sql DBMax a => (i -> Expr a) -> Aggregator1 i (Expr a) +maxOn f = lmap f max -- | Produce an aggregation for @Expr a@ using the @max@ function. -min :: Sql DBMin a => Expr a -> Aggregate a -min = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMin +min :: Sql DBMin a => Aggregator1 (Expr a) (Expr a) +min = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + Empty + Opaleye.unsafeMin + + +-- | Applies 'min' to the column selected by the given function. +minOn :: Sql DBMin a => (i -> Expr a) -> Aggregator1 i (Expr a) +minOn f = lmap f min + -- | Corresponds to @sum@. Note that in SQL, @sum@ is type changing - for -- example the @sum@ of @integer@ returns a @bigint@. Rel8 doesn't support -- this, and will add explicit casts back to the original input type. This can -- lead to overflows, and if you anticipate very large sums, you should upcast -- your input. -sum :: Sql DBSum a => Expr a -> Aggregate a -sum = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) Opaleye.unsafeSum +sum :: (Sql DBNum a, Sql DBSum a) => Aggregator' fold (Expr a) (Expr a) +sum = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + (Fallback 0) + Opaleye.unsafeSum + + +-- | Applies 'sum' to the column selected by the given fucntion. +sumOn :: (Sql DBNum a, Sql DBSum a) + => (i -> Expr a) -> Aggregator' fold i (Expr a) +sumOn f = lmap f sum + + +-- | 'sumWhere' is a combination of 'Rel8.filterWhere' and 'sumOn'. +sumWhere :: (Sql DBNum a, Sql DBSum a) + => (i -> Expr Bool) -> (i -> Expr a) -> Aggregator' fold i (Expr a) +sumWhere condition = filterWhereExplicit ifPP condition . sumOn -- | Corresponds to @avg@. Note that in SQL, @avg@ is type changing - for @@ -105,46 +200,103 @@ sum = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) Opaleye.unsafeSum -- this, and will add explicit casts back to the original input type. If you -- need a fractional result on an integral column, you should cast your input -- to 'Double' or 'Data.Scientific.Scientific' before calling 'avg'. -avg :: Sql DBSum a => Expr a -> Aggregate a -avg = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) Opaleye.unsafeAvg +avg :: Sql DBSum a => Aggregator1 (Expr a) (Expr a) +avg = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + Empty + Opaleye.unsafeAvg --- | Take the sum of all expressions that satisfy a predicate. -sumWhere :: (Sql DBNum a, Sql DBSum a) - => Expr Bool -> Expr a -> Aggregate a -sumWhere condition a = sum (caseExpr [(condition, a)] 0) + +-- | Applies 'avg' to the column selected by the given fucntion. +avgOn :: Sql DBSum a => (i -> Expr a) -> Aggregator1 i (Expr a) +avgOn f = lmap f avg -- | Corresponds to @string_agg()@. -stringAgg :: Sql DBString a - => Expr db -> Expr a -> Aggregate a +stringAgg :: (Sql IsString a, Sql DBString a) + => Expr a -> Aggregator' fold (Expr a) (Expr a) stringAgg delimiter = - unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) (Opaleye.stringAgg (Column (toPrimExpr delimiter))) + unsafeMakeAggregator + (toColumn . toPrimExpr) + (castExpr . fromPrimExpr . fromColumn) + (Fallback "") + (Opaleye.stringAgg (toColumn (toPrimExpr delimiter))) + + +-- | Applies 'stringAgg' to the column selected by the given function. +stringAggOn :: (Sql IsString a, Sql DBString a) + => Expr a -> (i -> Expr a) -> Aggregator' fold i (Expr a) +stringAggOn delimiter f = lmap f (stringAgg delimiter) -- | Aggregate a value by grouping by it. -groupByExpr :: Sql DBEq a => Expr a -> Aggregate a -groupByExpr = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.groupBy +groupByExpr :: Sql DBEq a => Aggregator1 (Expr a) (Expr a) +groupByExpr = + unsafeMakeAggregator + (toColumn . toPrimExpr) + (fromPrimExpr . fromColumn) + Empty + Opaleye.groupBy + + +-- | Applies 'groupByExprOn' to the column selected by the given function. +groupByExprOn :: Sql DBEq a => (i -> Expr a) -> Aggregator1 i (Expr a) +groupByExprOn f = lmap f groupByExpr -- | Collect expressions values as a list. -listAggExpr :: Sql DBType a => Expr a -> Aggregate [a] +listAggExpr :: Sql DBType a => Aggregator' fold (Expr a) (Expr [a]) listAggExpr = slistAggExpr typeInformation +-- | Applies 'listAggExpr' to the column selected by the given function. +listAggExprOn :: Sql DBType a => (i -> Expr a) -> Aggregator' fold i (Expr [a]) +listAggExprOn f = lmap f listAggExpr + + -- | Collect expressions values as a non-empty list. -nonEmptyAggExpr :: Sql DBType a => Expr a -> Aggregate (NonEmpty a) +nonEmptyAggExpr :: Sql DBType a => Aggregator1 (Expr a) (Expr (NonEmpty a)) nonEmptyAggExpr = snonEmptyAggExpr typeInformation +-- | Applies 'nonEmptyAggExpr' to the column selected by the given function. +nonEmptyAggExprOn :: Sql DBType a + => (i -> Expr a) -> Aggregator1 i (Expr (NonEmpty a)) +nonEmptyAggExprOn f = lmap f nonEmptyAggExpr + + +-- | 'distinctAggregate' modifies an 'Aggregator' to consider only distinct +-- values of a particular column. +distinctAggregate :: Sql DBEq a + => Aggregator' fold i (Expr a) -> Aggregator' fold i (Expr a) +distinctAggregate (Aggregator fallback a) = + Aggregator fallback (Opaleye.distinctAggregator a) + + slistAggExpr :: () - => TypeInformation (Unnullify a) -> Expr a -> Aggregate [a] -slistAggExpr info = unsafeMakeAggregate to fromPrimExpr Opaleye.arrayAgg - where - to = encodeArrayElement info . toPrimExpr + => TypeInformation (Unnullify a) -> Aggregator' fold (Expr a) (Expr [a]) +slistAggExpr info = + unsafeMakeAggregator + (toColumn . encodeArrayElement info . toPrimExpr) + (fromPrimExpr . fromColumn) + (Fallback (sempty info)) + Opaleye.arrayAgg snonEmptyAggExpr :: () - => TypeInformation (Unnullify a) -> Expr a -> Aggregate (NonEmpty a) -snonEmptyAggExpr info = unsafeMakeAggregate to fromPrimExpr Opaleye.arrayAgg + => TypeInformation (Unnullify a) -> Aggregator1 (Expr a) (Expr (NonEmpty a)) +snonEmptyAggExpr info = + unsafeMakeAggregator + (toColumn . encodeArrayElement info . toPrimExpr) + (fromPrimExpr . fromColumn) + Empty + Opaleye.arrayAgg + + +ifPP :: Opaleye.IfPP (Expr a) (Expr a) +ifPP = dimap from to Opaleye.ifPPField where - to = encodeArrayElement info . toPrimExpr + from = toColumn . toPrimExpr + to = fromPrimExpr . fromColumn diff --git a/src/Rel8/Expr/Window.hs b/src/Rel8/Expr/Window.hs index 76f869f0..ad18f028 100644 --- a/src/Rel8/Expr/Window.hs +++ b/src/Rel8/Expr/Window.hs @@ -6,11 +6,11 @@ module Rel8.Expr.Window , percentRank , cumeDist , ntile - , lag - , lead - , firstValue - , lastValue - , nthValue + , lag, lagOn + , lead, leadOn + , firstValue, firstValueOn + , lastValue, lastValueOn + , nthValue, nthValueOn ) where @@ -25,48 +25,52 @@ import qualified Opaleye.Internal.Window as Opaleye import qualified Opaleye.Window as Opaleye -- profunctors -import Data.Profunctor (dimap) +import Data.Profunctor (dimap, lmap) -- rel8 -import Rel8.Aggregate ( Aggregate( Aggregate ) ) +import Rel8.Aggregate (Aggregator' (Aggregator)) import Rel8.Expr ( Expr ) import Rel8.Expr.Opaleye ( fromColumn, fromPrimExpr, toColumn, toPrimExpr ) import Rel8.Schema.Null ( Nullify ) import Rel8.Window ( Window( Window ) ) -cumulative :: (a -> Aggregate b) -> Window a (Expr b) +-- | 'cumulative' allows the use of aggregation functions in 'Window' +-- expressions. In particular, @'cumulative' 'Rel8.sum'@ +-- (when combined with 'Rel8.Window.orderPartitionBy') gives a running total, +-- also known as a \"cumulative sum\", hence the name @cumulative@. +cumulative :: Aggregator' fold i a -> Window i a cumulative f = fromWindowFunction $ Opaleye.aggregatorWindowFunction (fromAggregate f) id -- | [@row_number()@](https://www.postgresql.org/docs/current/functions-window.html) -rowNumber :: Window a (Expr Int64) +rowNumber :: Window i (Expr Int64) rowNumber = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.rowNumber -- | [@rank()@](https://www.postgresql.org/docs/current/functions-window.html) -rank :: Window a (Expr Int64) +rank :: Window i (Expr Int64) rank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.rank -- | [@dense_rank()@](https://www.postgresql.org/docs/current/functions-window.html) -denseRank :: Window a (Expr Int64) +denseRank :: Window i (Expr Int64) denseRank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.denseRank -- | [@percent_rank()@](https://www.postgresql.org/docs/current/functions-window.html) -percentRank :: Window a (Expr Double) +percentRank :: Window i (Expr Double) percentRank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.percentRank -- | [@cume_dist()@](https://www.postgresql.org/docs/current/functions-window.html) -cumeDist :: Window a (Expr Double) +cumeDist :: Window i (Expr Double) cumeDist = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.cumeDist -- | [@ntile(num_buckets)@](https://www.postgresql.org/docs/current/functions-window.html) -ntile :: Expr Int32 -> Window a (Expr Int32) +ntile :: Expr Int32 -> Window i (Expr Int32) ntile buckets = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.ntile (toColumn (toPrimExpr buckets)) @@ -79,6 +83,11 @@ lag offset def = Opaleye.lag (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def)) +-- | Applies 'lag' to the column selected by the given function. +lagOn :: Expr Int32 -> Expr a -> (i -> Expr a) -> Window i (Expr a) +lagOn offset def f = lmap f (lag offset def) + + -- | [@lead(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html) lead :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a) lead offset def = @@ -87,6 +96,11 @@ lead offset def = Opaleye.lead (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def)) +-- | Applies 'lead' to the column selected by the given function. +leadOn :: Expr Int32 -> Expr a -> (i -> Expr a) -> Window i (Expr a) +leadOn offset def f = lmap f (lead offset def) + + -- | [@first_value(value)@](https://www.postgresql.org/docs/current/functions-window.html) firstValue :: Window (Expr a) (Expr a) firstValue = @@ -95,6 +109,11 @@ firstValue = Opaleye.firstValue +-- | Applies 'firstValue' to the column selected by the given function. +firstValueOn :: (i -> Expr a) -> Window i (Expr a) +firstValueOn f = lmap f firstValue + + -- | [@last_value(value)@](https://www.postgresql.org/docs/current/functions-window.html) lastValue :: Window (Expr a) (Expr a) lastValue = @@ -103,6 +122,11 @@ lastValue = Opaleye.lastValue +-- | Applies 'lastValue' to the column selected by the given function. +lastValueOn :: (i -> Expr a) -> Window i (Expr a) +lastValueOn f = lmap f lastValue + + -- | [@nth_value(value, n)@](https://www.postgresql.org/docs/current/functions-window.html) nthValue :: Expr Int32 -> Window (Expr a) (Expr (Nullify a)) nthValue n = @@ -111,11 +135,15 @@ nthValue n = Opaleye.nthValue (toColumn (toPrimExpr n)) -fromAggregate :: (a -> Aggregate b) -> Opaleye.Aggregator a (Expr b) -fromAggregate f = Opaleye.Aggregator $ Opaleye.PackMap $ \w a -> case f a of - Aggregate (Opaleye.Aggregator (Opaleye.PackMap x)) -> x w () +-- | [@nth_value(value, n)@](https://www.postgresql.org/docs/current/functions-window.html) +nthValueOn :: Expr Int32 -> (i -> Expr a) -> Window i (Expr (Nullify a)) +nthValueOn n f = lmap f (nthValue n) + + +fromAggregate :: Aggregator' fold i a -> Opaleye.Aggregator i a +fromAggregate (Aggregator _ a) = a -fromWindowFunction :: Opaleye.WindowFunction a b -> Window a b +fromWindowFunction :: Opaleye.WindowFunction i a -> Window i a fromWindowFunction (Opaleye.WindowFunction (Opaleye.PackMap w)) = Window $ Opaleye.Windows $ Opaleye.PackMap $ \f -> w $ \o -> f (o, mempty) diff --git a/src/Rel8/Generic/Construction.hs b/src/Rel8/Generic/Construction.hs index 36598613..cec92825 100644 --- a/src/Rel8/Generic/Construction.hs +++ b/src/Rel8/Generic/Construction.hs @@ -16,23 +16,21 @@ module Rel8.Generic.Construction , GGBuild, ggbuild , GGConstructable , GGConstruct, ggconstruct - , GGDeconstruct, ggdeconstruct + , GGDeconstruct, ggdeconstruct, ggdeconstructA , GGName, ggname - , GGAggregate, ggaggregate ) where -- base import Data.Bifunctor ( first ) +import Data.Functor ((<&>)) import Data.Kind ( Constraint, Type ) import Data.List.NonEmpty ( NonEmpty( (:|) ) ) import GHC.TypeLits ( Symbol ) import Prelude -- rel8 -import Rel8.Aggregate ( Aggregate( Aggregate ) ) import Rel8.Expr ( Expr ) -import Rel8.Expr.Aggregate ( groupByExpr ) import Rel8.Expr.Eq ( (==.) ) import Rel8.Expr.Null ( nullify, snull, unsafeUnnullify ) import Rel8.Expr.Serialize ( litExpr ) @@ -40,10 +38,10 @@ import Rel8.FCF ( Eval, Exp, Id ) import Rel8.Generic.Construction.ADT ( GConstructorADT, GMakeableADT, gmakeADT , GConstructableADT - , GBuildADT, gbuildADT, gunbuildADT + , GBuildADT, gbuildADT , GConstructADT, gconstructADT, gdeconstructADT , RepresentableConstructors, GConstructors, gcindex, gctabulate - , RepresentableFields, gfindex, gftabulate + , RepresentableFields, gftabulate ) import Rel8.Generic.Construction.Record ( GConstructor @@ -56,7 +54,6 @@ import Rel8.Kind.Algebra , KnownAlgebra, algebraSing ) import qualified Rel8.Kind.Algebra as K -import Rel8.Schema.Context.Nullify ( sguard, snullify ) import Rel8.Schema.HTable ( HTable ) import Rel8.Schema.HTable.Identity ( HIdentity( HIdentity ) ) import qualified Rel8.Schema.Kind as K @@ -70,11 +67,14 @@ import Rel8.Table import Rel8.Table.Bool ( case_ ) import Rel8.Type.Tag ( Tag ) +-- semigroupoids +import Data.Functor.Apply (Apply) +import Data.Semigroup.Traversable (sequence1, traverse1) + type GGBuildable :: K.Algebra -> Symbol -> (K.Context -> Exp (Type -> Type)) -> Constraint type GGBuildable algebra name rep = ( KnownAlgebra algebra - , Eval (GGColumns algebra TColumns (Eval (rep Aggregate))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , Eval (GGColumns algebra TColumns (Eval (rep Expr))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , Eval (GGColumns algebra TColumns (Eval (rep Name))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , HTable (Eval (GGColumns algebra TColumns (Eval (rep Expr)))) @@ -138,7 +138,6 @@ ggbuild gfromColumns = case algebraSing @algebra of type GGConstructable :: K.Algebra -> (K.Context -> Exp (Type -> Type)) -> Constraint type GGConstructable algebra rep = ( KnownAlgebra algebra - , Eval (GGColumns algebra TColumns (Eval (rep Aggregate))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , Eval (GGColumns algebra TColumns (Eval (rep Expr))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , Eval (GGColumns algebra TColumns (Eval (rep Name))) ~ Eval (GGColumns algebra TColumns (Eval (rep Expr))) , HTable (Eval (GGColumns algebra TColumns (Eval (rep Expr)))) @@ -149,20 +148,16 @@ type GGConstructable algebra rep = type GGConstructable' :: K.Algebra -> (K.Context -> Exp (Type -> Type)) -> Constraint type family GGConstructable' algebra rep where GGConstructable' 'K.Product rep = - ( Representable Id (Eval (rep Aggregate)) - , Representable Id (Eval (rep Expr)) + ( Representable Id (Eval (rep Expr)) , Representable Id (Eval (rep Name)) - , GConstructable (TTable Aggregate) TColumns Id Aggregate (Eval (rep Aggregate)) , GConstructable (TTable Expr) TColumns Id Expr (Eval (rep Expr)) , GConstructable (TTable Name) TColumns Id Name (Eval (rep Name)) ) GGConstructable' 'K.Sum rep = ( RepresentableConstructors Id (Eval (rep Expr)) - , RepresentableFields Id (Eval (rep Aggregate)) , RepresentableFields Id (Eval (rep Expr)) , RepresentableFields Id (Eval (rep Name)) , Functor (GConstructors Id (Eval (rep Expr))) - , GConstructableADT (TTable Aggregate) TColumns Id Aggregate (Eval (rep Aggregate)) , GConstructableADT (TTable Expr) TColumns Id Expr (Eval (rep Expr)) , GConstructableADT (TTable Name) TColumns Id Name (Eval (rep Name)) ) @@ -250,6 +245,43 @@ ggdeconstruct gtoColumns = case algebraSing @algebra of case_ cases' r +ggdeconstructA :: forall algebra rep a f r. (GGConstructable algebra rep, Apply f, Table Expr r) + => (a -> Eval (GGColumns algebra TColumns (Eval (rep Expr))) Expr) + -> GGDeconstruct algebra rep a (f r) +ggdeconstructA gtoColumns = case algebraSing @algebra of + SProduct -> \build -> + gindex @Id @(Eval (rep Expr)) @(f r) build . + gdeconstruct + @(TTable Expr) + @TColumns + @Id + @Expr + @(Eval (rep Expr)) + (const fromColumns) . + gtoColumns + SSum -> + gctabulate @Id @(Eval (rep Expr)) @(f r) @(a -> f r) $ \constructors as -> + let + (HIdentity tag, cases) = + gdeconstructADT + @(TTable Expr) + @TColumns + @Id + @Expr + @(Eval (rep Expr)) + (const fromColumns) + (\Spec {nullity} -> case nullity of + Null -> id + NotNull -> unsafeUnnullify) + constructors $ + gtoColumns as + fcases = traverse1 sequence1 cases + in + fcases + <&> \((_, r) :| (map (first ((tag ==.) . litExpr)) -> cases')) -> + case_ cases' r + + type GGName :: K.Algebra -> (K.Context -> Exp (Type -> Type)) -> Type -> Type type family GGName algebra rep a where GGName 'K.Product rep a = GConstruct Id (Eval (rep Name)) a @@ -282,72 +314,3 @@ ggname gfromColumns = case algebraSing @algebra of (const toColumns) (\_ _ (Name a) -> Name a) (HIdentity tag) - - -type GGAggregate :: K.Algebra -> (K.Context -> Exp (Type -> Type)) -> Type -> Type -type family GGAggregate algebra rep r where - GGAggregate 'K.Product rep r = - GConstruct Id (Eval (rep Aggregate)) r -> - GConstruct Id (Eval (rep Expr)) r - GGAggregate 'K.Sum rep r = - GBuildADT Id (Eval (rep Aggregate)) r -> - GBuildADT Id (Eval (rep Expr)) r - - -ggaggregate :: forall algebra rep exprs agg. GGConstructable algebra rep - => (Eval (GGColumns algebra TColumns (Eval (rep Expr))) Aggregate -> agg) - -> (exprs -> Eval (GGColumns algebra TColumns (Eval (rep Expr))) Expr) - -> GGAggregate algebra rep agg -> exprs -> agg -ggaggregate gfromColumns gtoColumns agg es = case algebraSing @algebra of - SProduct -> flip f exprs $ - gfromColumns . - gconstruct - @(TTable Aggregate) - @TColumns - @Id - @Aggregate - @(Eval (rep Aggregate)) - (const toColumns) - where - f = - gindex @Id @(Eval (rep Expr)) @agg . - agg . - gtabulate @Id @(Eval (rep Aggregate)) @agg - exprs = - gdeconstruct - @(TTable Expr) - @TColumns - @Id - @Expr - @(Eval (rep Expr)) - (const fromColumns) $ - gtoColumns es - SSum -> flip f exprs $ - gfromColumns . - gbuildADT - @(TTable Aggregate) - @TColumns - @Id - @Aggregate - @(Eval (rep Aggregate)) - (const toColumns) - (\tag' Spec {nullity} (Aggregate a) -> - Aggregate $ sguard (tag ==. litExpr tag') . snullify nullity <$> a) - (HIdentity (groupByExpr tag)) - where - f = - gfindex @Id @(Eval (rep Expr)) @agg . - agg . - gftabulate @Id @(Eval (rep Aggregate)) @agg - (HIdentity tag, exprs) = - gunbuildADT - @(TTable Expr) - @TColumns - @Id - @Expr - @(Eval (rep Expr)) - (const fromColumns) - (\Spec {nullity} -> case nullity of - Null -> id - NotNull -> unsafeUnnullify) $ - gtoColumns es diff --git a/src/Rel8/Generic/Rel8able.hs b/src/Rel8/Generic/Rel8able.hs index a5dc687b..25faaead 100644 --- a/src/Rel8/Generic/Rel8able.hs +++ b/src/Rel8/Generic/Rel8able.hs @@ -33,7 +33,6 @@ import GHC.Generics ( Generic, Rep, from, to ) import Prelude -- rel8 -import Rel8.Aggregate ( Aggregate ) import Rel8.Expr ( Expr ) import Rel8.FCF ( Exp, Eval ) import Rel8.Generic.Record ( Record(..) ) @@ -170,30 +169,26 @@ class HTable (GColumns t) => Rel8able t where type GFromExprs t = t Result default gfromColumns :: forall context. - ( SRel8able t Aggregate - , SRel8able t Expr + ( SRel8able t Expr , forall table. SRel8able t (Field table) , SRel8able t Name , SSerialize t ) => SContext context -> GColumns t context -> t context gfromColumns = \case - SAggregate -> sfromColumns SExpr -> sfromColumns SField -> sfromColumns SName -> sfromColumns SResult -> sfromResult default gtoColumns :: forall context. - ( SRel8able t Aggregate - , SRel8able t Expr + ( SRel8able t Expr , forall table. SRel8able t (Field table) , SRel8able t Name , SSerialize t ) => SContext context -> t context -> GColumns t context gtoColumns = \case - SAggregate -> stoColumns SExpr -> stoColumns SField -> stoColumns SName -> stoColumns diff --git a/src/Rel8/Kind/Context.hs b/src/Rel8/Kind/Context.hs index 08c1c78a..048f096f 100644 --- a/src/Rel8/Kind/Context.hs +++ b/src/Rel8/Kind/Context.hs @@ -14,7 +14,6 @@ import Data.Kind ( Constraint, Type ) import Prelude () -- rel8 -import Rel8.Aggregate ( Aggregate ) import Rel8.Expr ( Expr ) import Rel8.Schema.Field ( Field ) import Rel8.Schema.Kind ( Context ) @@ -24,7 +23,6 @@ import Rel8.Schema.Result ( Result ) type SContext :: Context -> Type data SContext context where - SAggregate :: SContext Aggregate SExpr :: SContext Expr SField :: SContext (Field table) SName :: SContext Name @@ -36,10 +34,6 @@ class Reifiable context where contextSing :: SContext context -instance Reifiable Aggregate where - contextSing = SAggregate - - instance Reifiable Expr where contextSing = SExpr diff --git a/src/Rel8/Query/Aggregate.hs b/src/Rel8/Query/Aggregate.hs index 2f1e6094..2e3a215d 100644 --- a/src/Rel8/Query/Aggregate.hs +++ b/src/Rel8/Query/Aggregate.hs @@ -1,16 +1,17 @@ {-# language FlexibleContexts #-} {-# language MonoLocalBinds #-} {-# language ScopedTypeVariables #-} -{-# language TypeApplications #-} module Rel8.Query.Aggregate ( aggregate + , aggregate1 , countRows , mode ) where -- base +import Control.Applicative (liftA2) import Data.Functor.Contravariant ( (>$<) ) import Data.Int ( Int64 ) import Prelude @@ -19,7 +20,8 @@ import Prelude import qualified Opaleye.Aggregate as Opaleye -- rel8 -import Rel8.Aggregate ( Aggregates ) +import Rel8.Aggregate (Aggregator' (Aggregator), Aggregator) +import Rel8.Aggregate.Fold (Fallback (Fallback)) import Rel8.Expr ( Expr ) import Rel8.Expr.Aggregate ( countStar ) import Rel8.Expr.Order ( desc ) @@ -28,29 +30,36 @@ import Rel8.Query.Limit ( limit ) import Rel8.Query.Maybe ( optional ) import Rel8.Query.Opaleye ( mapOpaleye ) import Rel8.Query.Order ( orderBy ) -import Rel8.Table ( toColumns ) -import Rel8.Table.Aggregate ( hgroupBy ) -import Rel8.Table.Cols ( Cols( Cols ), fromCols ) -import Rel8.Table.Eq ( EqTable, eqTable ) -import Rel8.Table.Opaleye ( aggregator ) -import Rel8.Table.Maybe ( maybeTable ) +import Rel8.Table (Table) +import Rel8.Table.Aggregate (groupBy) +import Rel8.Table.Eq (EqTable) +import Rel8.Table.Maybe (fromMaybeTable) --- | Apply an aggregation to all rows returned by a 'Query'. -aggregate :: Aggregates aggregates exprs => Query aggregates -> Query exprs -aggregate = mapOpaleye (Opaleye.aggregate aggregator) +-- | Apply an 'Aggregator' to all rows returned by a 'Query'. If the 'Query' +-- is empty, then a single \"fallback\" row is returned, composed of the +-- identity elements of the constituent aggregation functions. +aggregate :: Table Expr a => Aggregator i a -> Query i -> Query a +aggregate aggregator@(Aggregator (Fallback fallback) _) = + fmap (fromMaybeTable fallback) . optional . aggregate1 aggregator + + +-- | Apply an 'Rel8.Aggregator1' to all rows returned by a 'Query'. If +-- the 'Query' is empty, then zero rows are returned. +aggregate1 :: Aggregator' fold i a -> Query i -> Query a +aggregate1 (Aggregator _ aggregator) = mapOpaleye (Opaleye.aggregate aggregator) -- | Count the number of rows returned by a query. Note that this is different -- from @countStar@, as even if the given query yields no rows, @countRows@ -- will return @0@. countRows :: Query a -> Query (Expr Int64) -countRows = fmap (maybeTable 0 id) . optional . aggregate . fmap (const countStar) +countRows = aggregate countStar -- | Return the most common row in a query. mode :: forall a. EqTable a => Query a -> Query a -mode rows = limit 1 $ fmap (fromCols . snd) $ orderBy (fst >$< desc) $ do - aggregate $ do - row <- toColumns <$> rows - pure (countStar, Cols $ hgroupBy (eqTable @a) row) +mode rows = + limit 1 $ fmap snd $ + orderBy (fst >$< desc) $ do + aggregate1 (liftA2 (,) countStar groupBy) rows diff --git a/src/Rel8/Query/List.hs b/src/Rel8/Query/List.hs index fc0e8ee0..ab4e906c 100644 --- a/src/Rel8/Query/List.hs +++ b/src/Rel8/Query/List.hs @@ -23,17 +23,14 @@ import Rel8.Expr ( Expr ) import Rel8.Expr.Aggregate ( listAggExpr, nonEmptyAggExpr ) import Rel8.Expr.Opaleye ( mapPrimExpr ) import Rel8.Query ( Query ) -import Rel8.Query.Aggregate ( aggregate ) -import Rel8.Query.Maybe ( optional ) +import Rel8.Query.Aggregate (aggregate, aggregate1) import Rel8.Query.Rebind ( rebind ) import Rel8.Schema.HTable.Vectorize ( hunvectorize ) import Rel8.Schema.Null ( Sql, Unnullify ) import Rel8.Schema.Spec ( Spec( Spec, info ) ) import Rel8.Table ( Table, fromColumns ) -import Rel8.Table.Cols ( toCols ) import Rel8.Table.Aggregate ( listAgg, nonEmptyAgg ) import Rel8.Table.List ( ListTable( ListTable ) ) -import Rel8.Table.Maybe ( maybeTable ) import Rel8.Table.NonEmpty ( NonEmptyTable( NonEmptyTable ) ) import Rel8.Type ( DBType, typeInformation ) import Rel8.Type.Array ( extractArrayElement ) @@ -48,11 +45,7 @@ import Rel8.Type.Information ( TypeInformation ) -- @many@ is analogous to 'Control.Applicative.many' from -- @Control.Applicative@. many :: Table Expr a => Query a -> Query (ListTable Expr a) -many = - fmap (maybeTable mempty (\(ListTable a) -> ListTable a)) . - optional . - aggregate . - fmap (listAgg . toCols) +many = aggregate listAgg -- | Aggregate a 'Query' into a 'NonEmptyTable'. If the supplied query returns @@ -64,20 +57,17 @@ many = -- @some@ is analogous to 'Control.Applicative.some' from -- @Control.Applicative@. some :: Table Expr a => Query a -> Query (NonEmptyTable Expr a) -some = - fmap (\(NonEmptyTable a) -> NonEmptyTable a) . - aggregate . - fmap (nonEmptyAgg . toCols) +some = aggregate1 nonEmptyAgg -- | A version of 'many' specialised to single expressions. manyExpr :: Sql DBType a => Query (Expr a) -> Query (Expr [a]) -manyExpr = fmap (maybeTable mempty id) . optional . aggregate . fmap listAggExpr +manyExpr = aggregate listAggExpr -- | A version of 'many' specialised to single expressions. someExpr :: Sql DBType a => Query (Expr a) -> Query (Expr (NonEmpty a)) -someExpr = aggregate . fmap nonEmptyAggExpr +someExpr = aggregate1 nonEmptyAggExpr -- | Expand a 'ListTable' into a 'Query', where each row in the query is an diff --git a/src/Rel8/Query/Materialize.hs b/src/Rel8/Query/Materialize.hs index 506bab4f..9e010d10 100644 --- a/src/Rel8/Query/Materialize.hs +++ b/src/Rel8/Query/Materialize.hs @@ -20,10 +20,10 @@ import Rel8.Table.Opaleye ( unpackspec ) -- | 'materialize' takes a 'Query' and fully evaluates it and caches the --- results thereof, and returns a new 'Query' that simply looks up these --- cached results. It's usually best not to use this and to let the Postgres --- optimizer decide for itself what's best, but if you know what you're doing --- this can sometimes help to nudge it in a particular direction. +-- results thereof, and passes to a continuation a new 'Query' that simply +-- looks up these cached results. It's usually best not to use this and to let +-- the Postgres optimizer decide for itself what's best, but if you know what +-- you're doing this can sometimes help to nudge it in a particular direction. -- -- 'materialize' is currently implemented in terms of Postgres' -- [@WITH](https://www.postgresql.org/docs/current/queries-with.html) syntax. diff --git a/src/Rel8/Query/Maybe.hs b/src/Rel8/Query/Maybe.hs index 28062222..ba272651 100644 --- a/src/Rel8/Query/Maybe.hs +++ b/src/Rel8/Query/Maybe.hs @@ -21,11 +21,10 @@ import qualified Opaleye.Internal.MaybeFields as Opaleye -- rel8 import Rel8.Expr ( Expr ) import Rel8.Expr.Eq ( (==.) ) -import Rel8.Expr.Opaleye ( fromColumn, fromPrimExpr ) import Rel8.Query ( Query ) import Rel8.Query.Filter ( where_ ) import Rel8.Query.Opaleye ( mapOpaleye ) -import Rel8.Table.Maybe ( MaybeTable(..), isJustTable ) +import Rel8.Table.Maybe (MaybeTable(..), isJustTable, makeMaybeTable) -- | Convert a query that might return zero rows to a query that always returns @@ -34,10 +33,7 @@ import Rel8.Table.Maybe ( MaybeTable(..), isJustTable ) -- To speak in more concrete terms, 'optional' is most useful to write @LEFT -- JOIN@s. optional :: Query a -> Query (MaybeTable Expr a) -optional = mapOpaleye $ Opaleye.optionalInternal $ \tag a -> MaybeTable - { tag = fromPrimExpr $ fromColumn tag - , just = pure a - } +optional = mapOpaleye $ Opaleye.optionalInternal makeMaybeTable -- | Filter out 'MaybeTable's, returning only the tables that are not-null. diff --git a/src/Rel8/Schema/Context/Nullify.hs b/src/Rel8/Schema/Context/Nullify.hs index 1c6f8e10..d980a1a9 100644 --- a/src/Rel8/Schema/Context/Nullify.hs +++ b/src/Rel8/Schema/Context/Nullify.hs @@ -10,7 +10,6 @@ module Rel8.Schema.Context.Nullify ( Nullifiability(..), NonNullifiability(..), nullifiableOrNot, absurd , Nullifiable, nullifiability , guarder, nullifier, unnullifier - , sguard, snullify ) where @@ -24,7 +23,6 @@ import Prelude hiding ( null ) import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye -- rel8 -import Rel8.Aggregate ( Aggregate(..), zipOutputs ) import Rel8.Expr ( Expr ) import Rel8.Expr.Bool ( boolExpr ) import Rel8.Expr.Null ( nullify, unsafeUnnullify ) @@ -40,7 +38,6 @@ import Rel8.Schema.Spec ( Spec(..) ) type Nullifiability :: K.Context -> Type data Nullifiability context where - NAggregate :: Nullifiability Aggregate NExpr :: Nullifiability Expr NName :: Nullifiability Name @@ -50,10 +47,6 @@ class Nullifiable context where nullifiability :: Nullifiability context -instance Nullifiable Aggregate where - nullifiability = NAggregate - - instance Nullifiable Expr where nullifiability = NExpr @@ -72,7 +65,6 @@ nullifiableOrNot :: () => SContext context -> Either (NonNullifiability context) (Nullifiability context) nullifiableOrNot = \case - SAggregate -> Right NAggregate SExpr -> Right NExpr SField -> Left NField SName -> Right NName @@ -81,7 +73,6 @@ nullifiableOrNot = \case absurd :: Nullifiability context -> NonNullifiability context -> a absurd = \case - NAggregate -> \case NExpr -> \case NName -> \case @@ -94,7 +85,6 @@ guarder :: () -> context (Maybe a) -> context (Maybe a) guarder = \case - SAggregate -> \tag _ isNonNull -> zipOutputs (sguard . isNonNull) tag SExpr -> \tag _ isNonNull -> sguard (isNonNull tag) SField -> \_ _ _ -> id SName -> \_ _ _ -> id @@ -108,8 +98,6 @@ nullifier :: () -> context a -> context (Nullify a) nullifier = \case - NAggregate -> \Spec {nullity} (Aggregate a) -> - Aggregate $ snullify nullity <$> a NExpr -> \Spec {nullity} a -> snullify nullity a NName -> \_ (Name a) -> Name a @@ -120,8 +108,6 @@ unnullifier :: () -> context (Nullify a) -> context a unnullifier = \case - NAggregate -> \Spec {nullity} (Aggregate a) -> - Aggregate $ sunnullify nullity <$> a NExpr -> \Spec {nullity} a -> sunnullify nullity a NName -> \_ (Name a) -> Name a diff --git a/src/Rel8/Schema/HTable/Vectorize.hs b/src/Rel8/Schema/HTable/Vectorize.hs index e77b7d45..3a4c1c35 100644 --- a/src/Rel8/Schema/HTable/Vectorize.hs +++ b/src/Rel8/Schema/HTable/Vectorize.hs @@ -19,7 +19,7 @@ module Rel8.Schema.HTable.Vectorize ( HVectorize - , hvectorize, hunvectorize + , hvectorize, hvectorizeA, hunvectorize , happend, hempty , hproject , hcolumn @@ -36,7 +36,9 @@ import Prelude import Rel8.FCF ( Eval, Exp ) import Rel8.Schema.Dict ( Dict( Dict ) ) import qualified Rel8.Schema.Kind as K -import Rel8.Schema.HTable ( HTable, hfield, htabulate, htabulateA, hspecs ) +import Rel8.Schema.HTable + ( HField, HTable, hfield, htabulate, htabulateA, hspecs + ) import Rel8.Schema.HTable.Identity ( HIdentity( HIdentity ) ) import Rel8.Schema.HTable.MapTable ( HMapTable( HMapTable ), HMapTableField( HMapTableField ) @@ -52,6 +54,9 @@ import Rel8.Type.Information ( TypeInformation ) -- semialign import Data.Zip ( Unzip, Zip, Zippy(..) ) +-- semigroupoids +import Data.Functor.Apply (Apply) + type Vector :: (Type -> Type) -> Constraint class Vector list where @@ -104,6 +109,16 @@ hvectorize vectorizer as = HVectorize $ htabulate $ \(HMapTableField field) -> {-# INLINABLE hvectorize #-} +hvectorizeA :: (HTable t, Apply f, Vector list) + => (forall a. Spec a -> HField t a -> f (context' (list a))) + -> f (HVectorize list t context') +hvectorizeA vectorizer = fmap HVectorize $ + htabulateA $ \(HMapTableField field) -> + case hfield hspecs field of + spec -> vectorizer spec field +{-# INLINABLE hvectorizeA #-} + + hunvectorize :: (HTable t, Zip f, Vector list) => (forall a. Spec a -> context (list a) -> f (context' a)) -> HVectorize list t context diff --git a/src/Rel8/Table/ADT.hs b/src/Rel8/Table/ADT.hs index dbda8716..dfd4f237 100644 --- a/src/Rel8/Table/ADT.hs +++ b/src/Rel8/Table/ADT.hs @@ -18,9 +18,8 @@ module Rel8.Table.ADT , BuildADT, buildADT , ConstructableADT , ConstructADT, constructADT - , DeconstructADT, deconstructADT + , DeconstructADT, deconstructADT, deconstructAADT , NameADT, nameADT - , AggregateADT, aggregateADT , ADTRep ) where @@ -32,7 +31,6 @@ import GHC.TypeLits ( Symbol ) import Prelude -- rel8 -import Rel8.Aggregate ( Aggregate ) import Rel8.Expr ( Expr ) import Rel8.FCF ( Eval, Exp ) import Rel8.Generic.Construction @@ -40,9 +38,8 @@ import Rel8.Generic.Construction , GGBuild, ggbuild , GGConstructable , GGConstruct, ggconstruct - , GGDeconstruct, ggdeconstruct + , GGDeconstruct, ggdeconstruct, ggdeconstructA , GGName, ggname - , GGAggregate, ggaggregate ) import Rel8.Generic.Record ( Record( Record ), unrecord ) import Rel8.Generic.Rel8able @@ -59,6 +56,9 @@ import Rel8.Schema.Name ( Name ) import Rel8.Schema.Result ( Result ) import Rel8.Table ( Table, TColumns ) +-- semigroupoids +import Data.Functor.Apply (Apply) + type ADT :: K.Rel8able -> K.Rel8able newtype ADT t context = ADT (GColumnsADT t context) @@ -146,6 +146,12 @@ deconstructADT = ggdeconstruct @'K.Sum @(ADTRep t) @(ADT t Expr) @r (\(ADT a) -> a) +deconstructAADT :: forall t f r. (ConstructableADT t, Apply f, Table Expr r) + => DeconstructADT t (f r) +deconstructAADT = + ggdeconstructA @'K.Sum @(ADTRep t) @(ADT t Expr) @f @r (\(ADT a) -> a) + + type NameADT :: K.Rel8able -> Type type NameADT t = GGName 'K.Sum (ADTRep t) (ADT t Name) @@ -154,17 +160,6 @@ nameADT :: forall t. ConstructableADT t => NameADT t nameADT = ggname @'K.Sum @(ADTRep t) @(ADT t Name) ADT -type AggregateADT :: K.Rel8able -> Type -type AggregateADT t = forall r. GGAggregate 'K.Sum (ADTRep t) r - - -aggregateADT :: forall t. ConstructableADT t - => AggregateADT t -> ADT t Expr -> ADT t Aggregate -aggregateADT f = - ggaggregate @'K.Sum @(ADTRep t) @(ADT t Expr) @(ADT t Aggregate) ADT (\(ADT a) -> a) - (f @(ADT t Aggregate)) - - data ADTRep :: K.Rel8able -> K.Context -> Exp (Type -> Type) type instance Eval (ADTRep t context) = GRep t context diff --git a/src/Rel8/Table/Aggregate.hs b/src/Rel8/Table/Aggregate.hs index b37a3a57..15f4aeae 100644 --- a/src/Rel8/Table/Aggregate.hs +++ b/src/Rel8/Table/Aggregate.hs @@ -1,37 +1,51 @@ +{-# language BlockArguments #-} {-# language FlexibleContexts #-} {-# language NamedFieldPuns #-} {-# language ScopedTypeVariables #-} {-# language TypeApplications #-} {-# language TypeFamilies #-} -{-# language ViewPatterns #-} module Rel8.Table.Aggregate - ( groupBy, hgroupBy - , listAgg, nonEmptyAgg + ( groupBy, groupByOn + , listAgg, listAggOn, nonEmptyAgg, nonEmptyAggOn + , filterWhere, filterWhereOptional + , orderAggregateBy + , optionalAggregate ) where -- base -import Data.Functor.Identity ( Identity( Identity ) ) import Prelude +-- opaleye +import qualified Opaleye.Internal.Aggregate as Opaleye + +-- profunctors +import Data.Profunctor (dimap, lmap) + -- rel8 -import Rel8.Aggregate ( Aggregate, Aggregates ) +import Rel8.Aggregate (Aggregator, Aggregator' (Aggregator), Aggregator1, full) +import Rel8.Aggregate.Fold (Fallback (Fallback)) import Rel8.Expr ( Expr ) import Rel8.Expr.Aggregate - ( groupByExpr + ( filterWhereExplicit + , groupByExprOn , slistAggExpr , snonEmptyAggExpr ) +import Rel8.Expr.Opaleye (toColumn, toPrimExpr) +import Rel8.Order (Order (Order)) import Rel8.Schema.Dict ( Dict( Dict ) ) -import Rel8.Schema.HTable ( HTable, hfield, htabulate ) -import Rel8.Schema.HTable.Vectorize ( hvectorize ) +import Rel8.Schema.HTable (HTable, hfield, htabulateA) +import Rel8.Schema.HTable.Vectorize (hvectorizeA) import Rel8.Schema.Null ( Sql ) import Rel8.Schema.Spec ( Spec( Spec, info ) ) -import Rel8.Table ( toColumns, fromColumns ) +import Rel8.Table (Table, toColumns, fromColumns) import Rel8.Table.Eq ( EqTable, eqTable ) import Rel8.Table.List ( ListTable ) +import Rel8.Table.Maybe (MaybeTable, makeMaybeTable, justTable, nothingTable) import Rel8.Table.NonEmpty ( NonEmptyTable ) +import Rel8.Table.Opaleye (ifPP) import Rel8.Type.Eq ( DBEq ) @@ -43,22 +57,48 @@ import Rel8.Type.Eq ( DBEq ) -- -- @ -- itemsByOrder :: Query (OrderId Expr, ListTable Expr (Item Expr)) --- itemsByOrder = aggregate $ do --- item <- each itemSchema --- let orderId = groupBy (itemOrderId item) --- let orderItems = listAgg item --- pure (orderId, orderItems) +-- itemsByOrder = +-- aggregate +-- do +-- orderId <- groupByOn (.orderId) +-- items <- listAgg +-- pure (orderId, items) +-- do +-- each itemSchema -- @ -groupBy :: forall exprs aggregates. (EqTable exprs, Aggregates aggregates exprs) - => exprs -> aggregates -groupBy = fromColumns . hgroupBy (eqTable @exprs) . toColumns +groupBy :: forall a. EqTable a => Aggregator1 a a +groupBy = dimap toColumns fromColumns (hgroupBy (eqTable @a)) + + +-- | Applies 'groupBy' to the columns selected by the given function. +groupByOn :: EqTable a => (i -> a) -> Aggregator1 i a +groupByOn f = lmap f groupBy + + +hgroupBy :: HTable t => t (Dict (Sql DBEq)) -> Aggregator1 (t Expr) (t Expr) +hgroupBy eqs = htabulateA $ \field -> case hfield eqs field of + Dict -> groupByExprOn (`hfield` field) + + +-- | 'filterWhere' allows an 'Aggregator' to filter out rows from the input +-- query before considering them for aggregation. Note that because the +-- predicate supplied to 'filterWhere' could return 'Rel8.false' for every +-- row, 'filterWhere' needs an 'Aggregator' as opposed to an 'Aggregator1', so +-- that it can return a default value in such a case. For a variant of +-- 'filterWhere' that can work with 'Aggregator1's, see 'filterWhereOptional'. +filterWhere :: Table Expr a + => (i -> Expr Bool) -> Aggregator i a -> Aggregator' fold i a +filterWhere = filterWhereExplicit ifPP -hgroupBy :: HTable t => t (Dict (Sql DBEq)) -> t Expr -> t Aggregate -hgroupBy eqs exprs = htabulate $ \field -> - case hfield eqs field of - Dict -> case hfield exprs field of - expr -> groupByExpr expr +-- | A variant of 'filterWhere' that can be used with an 'Aggregator1' +-- (upgrading it to an 'Aggregator' in the process). It returns +-- 'nothingTable' in the case where the predicate matches zero rows. +filterWhereOptional :: Table Expr a + => (i -> Expr Bool) -> Aggregator' fold i a -> Aggregator' fold' i (MaybeTable Expr a) +filterWhereOptional f (Aggregator _ aggregator) = + Aggregator (Fallback nothingTable) $ + Opaleye.filterWhereInternal makeMaybeTable (toColumn . toPrimExpr . f) aggregator -- | Aggregate rows into a single row containing an array of all aggregated @@ -74,19 +114,46 @@ hgroupBy eqs exprs = htabulate $ \field -> -- ordersWithItems :: Query (Order Expr, ListTable Expr (Item Expr)) -- ordersWithItems = do -- order <- each orderSchema --- items <- aggregate $ listAgg <$> itemsFromOrder order +-- items <- aggregate listAgg (itemsFromOrder order) -- return (order, items) -- @ -listAgg :: Aggregates aggregates exprs => exprs -> ListTable Aggregate aggregates -listAgg (toColumns -> exprs) = fromColumns $ - hvectorize - (\Spec {info} (Identity a) -> slistAggExpr info a) - (pure exprs) +listAgg :: Table Expr a => Aggregator' fold a (ListTable Expr a) +listAgg = + fromColumns <$> + hvectorizeA \Spec {info} field -> + lmap ((`hfield` field) . toColumns) $ slistAggExpr info + + +-- | Applies 'listAgg' to the columns selected by the given function. +listAggOn :: Table Expr a => (i -> a) -> Aggregator' fold i (ListTable Expr a) +listAggOn f = lmap f listAgg -- | Like 'listAgg', but the result is guaranteed to be a non-empty list. -nonEmptyAgg :: Aggregates aggregates exprs => exprs -> NonEmptyTable Aggregate aggregates -nonEmptyAgg (toColumns -> exprs) = fromColumns $ - hvectorize - (\Spec {info} (Identity a) -> snonEmptyAggExpr info a) - (pure exprs) +nonEmptyAgg :: Table Expr a => Aggregator1 a (NonEmptyTable Expr a) +nonEmptyAgg = + fromColumns <$> + hvectorizeA \Spec {info} field -> + lmap ((`hfield` field) . toColumns) $ snonEmptyAggExpr info + + +-- | Applies 'nonEmptyAgg' to the columns selected by the given function. +nonEmptyAggOn :: Table Expr a + => (i -> a) -> Aggregator1 i (NonEmptyTable Expr a) +nonEmptyAggOn f = lmap f nonEmptyAgg + + +-- | Order the values within each aggregation in an `Aggregator` using the +-- given ordering. This is only relevant for aggregations that depend on the +-- order they get their elements, like `Rel8.listAgg` and `Rel8.stringAgg`. +orderAggregateBy :: Order i -> Aggregator' fold i a -> Aggregator' fold i a +orderAggregateBy (Order order) (Aggregator fallback aggregator) = + Aggregator fallback $ Opaleye.orderAggregate order aggregator + + +-- | 'optionalAggregate' upgrades an 'Aggregator1' into an 'Aggregator' by +-- having it return 'nothingTable' when aggregating over an empty collection +-- of rows. +optionalAggregate :: Table Expr a + => Aggregator' fold i a -> Aggregator' fold' i (MaybeTable Expr a) +optionalAggregate = full nothingTable . fmap justTable diff --git a/src/Rel8/Table/Either.hs b/src/Rel8/Table/Either.hs index 63fa4243..d4f9a721 100644 --- a/src/Rel8/Table/Either.hs +++ b/src/Rel8/Table/Either.hs @@ -33,10 +33,13 @@ import Prelude hiding ( undefined ) -- comonad import Control.Comonad ( extract ) +-- profunctors +import Data.Profunctor (lmap) + -- rel8 -import Rel8.Aggregate ( Aggregate ) +import Rel8.Aggregate (Aggregator', Aggregator1, semi) import Rel8.Expr ( Expr ) -import Rel8.Expr.Aggregate ( groupByExpr ) +import Rel8.Expr.Aggregate (groupByExprOn) import Rel8.Expr.Serialize ( litExpr ) import Rel8.Kind.Context ( Reifiable ) import Rel8.Schema.Context.Nullify ( Nullifiable ) @@ -215,18 +218,17 @@ rightTable :: Table Expr a => b -> EitherTable Expr a b rightTable = EitherTable (litExpr IsRight) undefined . pure --- | Lift a pair of aggregating functions to operate on an 'EitherTable'. --- @leftTable@s and @rightTable@s are grouped separately. +-- | Lift a pair aggregators to operate on an 'EitherTable'. @leftTable@s and +-- @rightTable@s are grouped separately. aggregateEitherTable :: () - => (exprs -> aggregates) - -> (exprs' -> aggregates') - -> EitherTable Expr exprs exprs' - -> EitherTable Aggregate aggregates aggregates' -aggregateEitherTable f g (EitherTable tag a b) = EitherTable - { tag = groupByExpr tag - , left = aggregateNullify f a - , right = aggregateNullify g b - } + => Aggregator' fold i a + -> Aggregator' fold' i' b + -> Aggregator1 (EitherTable Expr i i') (EitherTable Expr a b) +aggregateEitherTable a b = + EitherTable + <$> groupByExprOn tag + <*> lmap left (semi (aggregateNullify a)) + <*> lmap right (semi (aggregateNullify b)) -- | Construct a 'EitherTable' in the 'Name' context. This can be useful if you diff --git a/src/Rel8/Table/HKD.hs b/src/Rel8/Table/HKD.hs index 57903005..c10500de 100644 --- a/src/Rel8/Table/HKD.hs +++ b/src/Rel8/Table/HKD.hs @@ -19,9 +19,8 @@ module Rel8.Table.HKD , BuildHKD, buildHKD , ConstructableHKD , ConstructHKD, constructHKD - , DeconstructHKD, deconstructHKD + , DeconstructHKD, deconstructHKD, deconstructAHKD , NameHKD, nameHKD - , AggregateHKD, aggregateHKD , HKDRep ) where @@ -33,7 +32,6 @@ import GHC.TypeLits ( Symbol ) import Prelude -- rel8 -import Rel8.Aggregate ( Aggregate ) import Rel8.Column ( TColumn ) import Rel8.Expr ( Expr ) import Rel8.FCF ( Eval, Exp ) @@ -43,9 +41,8 @@ import Rel8.Generic.Construction , GGBuild, ggbuild , GGConstructable , GGConstruct, ggconstruct - , GGDeconstruct, ggdeconstruct + , GGDeconstruct, ggdeconstruct, ggdeconstructA , GGName, ggname - , GGAggregate, ggaggregate ) import Rel8.Generic.Map ( GMap ) import Rel8.Generic.Record @@ -72,6 +69,9 @@ import Rel8.Table , TSerialize ) +-- semigroupoids +import Data.Functor.Apply (Apply) + type GColumnsHKD :: Type -> K.HTable type GColumnsHKD a = @@ -205,6 +205,11 @@ deconstructHKD :: forall a r. (ConstructableHKD a, Table Expr r) deconstructHKD = ggdeconstruct @(GAlgebra (Rep a)) @(HKDRep a) @(HKD a Expr) @r (\(HKD a) -> a) +deconstructAHKD :: forall a f r. (ConstructableHKD a, Apply f, Table Expr r) + => DeconstructHKD a (f r) +deconstructAHKD = ggdeconstructA @(GAlgebra (Rep a)) @(HKDRep a) @(HKD a Expr) @f @r (\(HKD a) -> a) + + type NameHKD :: Type -> Type type NameHKD a = GGName (GAlgebra (Rep a)) (HKDRep a) (HKD a Name) @@ -213,17 +218,6 @@ nameHKD :: forall a. ConstructableHKD a => NameHKD a nameHKD = ggname @(GAlgebra (Rep a)) @(HKDRep a) @(HKD a Name) HKD -type AggregateHKD :: Type -> Type -type AggregateHKD a = forall r. GGAggregate (GAlgebra (Rep a)) (HKDRep a) r - - -aggregateHKD :: forall a. ConstructableHKD a - => AggregateHKD a -> HKD a Expr -> HKD a Aggregate -aggregateHKD f = - ggaggregate @(GAlgebra (Rep a)) @(HKDRep a) @(HKD a Expr) @(HKD a Aggregate) HKD (\(HKD a) -> a) - (f @(HKD a Aggregate)) - - data HKDRep :: Type -> K.Context -> Exp (Type -> Type) type instance Eval (HKDRep a context) = GRecord (GMap (TColumn context) (Rep a)) diff --git a/src/Rel8/Table/Maybe.hs b/src/Rel8/Table/Maybe.hs index 1300c4df..e95caccf 100644 --- a/src/Rel8/Table/Maybe.hs +++ b/src/Rel8/Table/Maybe.hs @@ -20,6 +20,7 @@ module Rel8.Table.Maybe , ($?) , aggregateMaybeTable , nameMaybeTable + , makeMaybeTable ) where @@ -33,12 +34,20 @@ import Prelude hiding ( null, undefined ) -- comonad import Control.Comonad ( extract ) +-- opaleye +import qualified Opaleye.Field as Opaleye +import qualified Opaleye.SqlTypes as Opaleye + +-- profunctors +import Data.Profunctor (lmap) + -- rel8 -import Rel8.Aggregate ( Aggregate ) +import Rel8.Aggregate (Aggregator', Aggregator1, semi) import Rel8.Expr ( Expr ) -import Rel8.Expr.Aggregate ( groupByExpr ) +import Rel8.Expr.Aggregate (groupByExprOn) import Rel8.Expr.Bool ( boolExpr ) import Rel8.Expr.Null ( isNull, isNonNull, null, nullify ) +import Rel8.Expr.Opaleye (fromColumn, fromPrimExpr) import Rel8.Kind.Context ( Reifiable ) import Rel8.Schema.Dict ( Dict( Dict ) ) import qualified Rel8.Schema.Kind as K @@ -224,14 +233,15 @@ f $? ma@(MaybeTable _ a) = case nullable @b of infixl 4 $? --- | Lift an aggregating function to operate on a 'MaybeTable'. --- @nothingTable@s and @justTable@s are grouped separately. +-- | Lift an aggregator to operate on a 'MaybeTable'. @nothingTable@s and +-- @justTable@s are grouped separately. aggregateMaybeTable :: () - => (exprs -> aggregates) - -> MaybeTable Expr exprs - -> MaybeTable Aggregate aggregates -aggregateMaybeTable f (MaybeTable tag a) = - MaybeTable (groupByExpr tag) (aggregateNullify f a) + => Aggregator' fold i a + -> Aggregator1 (MaybeTable Expr i) (MaybeTable Expr a) +aggregateMaybeTable aggregator = + MaybeTable + <$> groupByExprOn tag + <*> lmap just (semi (aggregateNullify aggregator)) -- | Construct a 'MaybeTable' in the 'Name' context. This can be useful if you @@ -245,3 +255,10 @@ nameMaybeTable -- ^ Names of the columns in @a@. -> MaybeTable Name a nameMaybeTable tag = MaybeTable tag . pure + + +makeMaybeTable :: Opaleye.FieldNullable Opaleye.SqlBool -> a -> MaybeTable Expr a +makeMaybeTable tag a = MaybeTable + { tag = fromPrimExpr $ fromColumn tag + , just = pure a + } diff --git a/src/Rel8/Table/Nullify.hs b/src/Rel8/Table/Nullify.hs index 2c406d77..c86738d1 100644 --- a/src/Rel8/Table/Nullify.hs +++ b/src/Rel8/Table/Nullify.hs @@ -29,14 +29,17 @@ import Prelude -- comonad import Control.Comonad ( Comonad, duplicate, extract, ComonadApply, (<@>) ) +-- profunctors +import Data.Profunctor (dimap) + -- rel8 -import Rel8.Aggregate ( Aggregate ) +import Rel8.Aggregate (Aggregator') import Rel8.Expr ( Expr ) import Rel8.Expr.Bool ( (||.), false ) import qualified Rel8.Expr.Null as Expr import Rel8.Kind.Context ( Reifiable, contextSing ) import Rel8.Schema.Context.Nullify - ( Nullifiability( NAggregate, NExpr ) + ( Nullifiability( NExpr ) , NonNullifiability , Nullifiable, nullifiability , nullifiableOrNot, absurd @@ -175,12 +178,14 @@ instance (OrdTable a, context ~ Expr) => OrdTable (Nullify context a) where aggregateNullify :: () - => (exprs -> aggregates) - -> Nullify Expr exprs - -> Nullify Aggregate aggregates -aggregateNullify f = \case - Table _ a -> Table NAggregate (f a) - Fields notNullifiable _ -> absurd NExpr notNullifiable + => Aggregator' fold i a + -> Aggregator' fold (Nullify Expr i) (Nullify Expr a) +aggregateNullify = dimap from to + where + from = \case + Table _ a -> a + Fields notNullifiable _ -> absurd NExpr notNullifiable + to = Table NExpr guard :: (Reifiable context, HTable t) diff --git a/src/Rel8/Table/Opaleye.hs b/src/Rel8/Table/Opaleye.hs index 7a908c6e..269c8cf9 100644 --- a/src/Rel8/Table/Opaleye.hs +++ b/src/Rel8/Table/Opaleye.hs @@ -10,12 +10,12 @@ {-# options_ghc -Wno-deprecations #-} module Rel8.Table.Opaleye - ( aggregator - , attributes + ( attributes , binaryspec , distinctspec , exprs , exprsWithNames + , ifPP , table , tableFields , unpackspec @@ -33,7 +33,6 @@ import Prelude -- opaleye import qualified Opaleye.Adaptors as Opaleye import qualified Opaleye.Field as Opaleye ( Field_ ) -import qualified Opaleye.Internal.Aggregate as Opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye import qualified Opaleye.Internal.Values as Opaleye import qualified Opaleye.Table as Opaleye @@ -42,7 +41,6 @@ import qualified Opaleye.Table as Opaleye import Data.Profunctor ( dimap, lmap ) -- rel8 -import Rel8.Aggregate ( Aggregate( Aggregate ), Aggregates ) import Rel8.Expr ( Expr ) import Rel8.Expr.Opaleye ( fromPrimExpr, toPrimExpr @@ -61,12 +59,6 @@ import Data.Functor.Apply ( WrappedApplicative(..) ) import Data.Profunctor.Product ( ProductProfunctor ) -aggregator :: Aggregates aggregates exprs => Opaleye.Aggregator aggregates exprs -aggregator = dimap toColumns fromColumns $ - htraverseP $ - lmap (\(Aggregate a) -> (a, ())) Opaleye.aggregatorApply - - attributes :: Selects names exprs => TableSchema names -> exprs attributes schema@TableSchema {columns} = fromColumns $ htabulate $ \field -> case hfield (toColumns columns) field of @@ -103,6 +95,10 @@ exprsWithNames names as = getConst $ htabulateA $ \field -> (Name name, expr) -> Const (pure (name, toPrimExpr expr)) +ifPP :: Table Expr a => Opaleye.IfPP a a +ifPP = fromOpaleyespec Opaleye.ifPPField + + table :: Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs table (TableSchema name schema columns) = case schema of diff --git a/src/Rel8/Table/These.hs b/src/Rel8/Table/These.hs index b5ff2ef3..4c2bd2bd 100644 --- a/src/Rel8/Table/These.hs +++ b/src/Rel8/Table/These.hs @@ -33,8 +33,11 @@ import Data.Kind ( Type ) import Data.Maybe ( isJust ) import Prelude hiding ( null, undefined ) +-- profunctors +import Data.Profunctor (lmap) + -- rel8 -import Rel8.Aggregate ( Aggregate ) +import Rel8.Aggregate (Aggregator', Aggregator1) import Rel8.Expr ( Expr ) import Rel8.Expr.Bool ( (&&.), (||.), boolExpr, not_ ) import Rel8.Expr.Null ( null, isNonNull ) @@ -325,17 +328,16 @@ theseTable f g h TheseTable {here, there} = there --- | Lift a pair of aggregating functions to operate on an 'TheseTable'. --- @thisTable@s, @thatTable@s and @thoseTable@s are grouped separately. +-- | Lift a pair aggregators to operate on a 'TheseTable'. @thisTable@s, +-- @thatTable@s are @thoseTable@s are grouped separately. aggregateTheseTable :: () - => (exprs -> aggregates) - -> (exprs' -> aggregates') - -> TheseTable Expr exprs exprs' - -> TheseTable Aggregate aggregates aggregates' -aggregateTheseTable f g (TheseTable here there) = TheseTable - { here = aggregateMaybeTable f here - , there = aggregateMaybeTable g there - } + => Aggregator' fold i a + -> Aggregator' fold' i' b + -> Aggregator1 (TheseTable Expr i i') (TheseTable Expr a b) +aggregateTheseTable a b = + TheseTable + <$> lmap here (aggregateMaybeTable a) + <*> lmap there (aggregateMaybeTable b) -- | Construct a 'TheseTable' in the 'Name' context. This can be useful if you diff --git a/src/Rel8/Table/Window.hs b/src/Rel8/Table/Window.hs index ca155170..f2fa860f 100644 --- a/src/Rel8/Table/Window.hs +++ b/src/Rel8/Table/Window.hs @@ -1,9 +1,7 @@ {-# language MonoLocalBinds #-} module Rel8.Table.Window - ( cumulative - , cumulative_ - , currentRow + ( currentRow ) where @@ -14,28 +12,7 @@ import Prelude import qualified Opaleye.Window as Opaleye -- rel8 -import Rel8.Aggregate ( Aggregates ) -import qualified Rel8.Expr.Window as Expr -import Rel8.Schema.HTable ( hfield, htabulateA ) -import Rel8.Table ( fromColumns, toColumns ) -import Rel8.Window ( Window( Window ) ) - - --- | 'cumulative' allows the use of aggregation functions in 'Window' --- expressions. In particular, @'cumulative' 'Rel8.sum'@ --- (when combined with 'Rel8.Window.orderPartitionBy') gives a running total, --- also known as a \"cumulative sum\", hence the name @cumulative@. -cumulative :: Aggregates aggregates exprs => (a -> aggregates) -> Window a exprs -cumulative f = - fmap fromColumns $ - htabulateA $ \field -> - Expr.cumulative ((`hfield` field) . toColumns . f) - - --- | A version of 'cumulative' for use with nullary aggregators like --- 'Rel8.Expr.Aggregate.countStar'. -cumulative_ :: Aggregates aggregates exprs => aggregates -> Window a exprs -cumulative_ = cumulative . const +import Rel8.Window (Window (Window)) -- | Return every column of the current row of a window query. diff --git a/src/Rel8/Tabulate.hs b/src/Rel8/Tabulate.hs index 666ac3e4..a2f0f1da 100644 --- a/src/Rel8/Tabulate.hs +++ b/src/Rel8/Tabulate.hs @@ -22,6 +22,7 @@ module Rel8.Tabulate -- * Aggregation and Ordering , aggregate + , aggregate1 , distinct , order @@ -71,7 +72,6 @@ import Data.Bifunctor.Clown ( Clown( Clown ), runClown ) import Control.Comonad ( extract ) -- opaleye -import qualified Opaleye.Aggregate as Opaleye import qualified Opaleye.Order as Opaleye ( orderBy, distinctOnExplicit ) -- profunctors @@ -85,12 +85,14 @@ import Data.Profunctor.Product import qualified Data.Profunctor.Product as PP -- rel8 -import Rel8.Aggregate ( Aggregates ) +import Rel8.Aggregate (Aggregator' (Aggregator), Aggregator, semi) +import Rel8.Aggregate.Fold (Fallback (Fallback)) import Rel8.Expr ( Expr ) -import Rel8.Expr.Aggregate ( countStar ) +import Rel8.Expr.Aggregate (countStar) import Rel8.Expr.Bool ( true ) import Rel8.Order ( Order( Order ) ) import Rel8.Query ( Query ) +import qualified Rel8.Query.Aggregate as Q import qualified Rel8.Query.Exists as Q ( exists, present, absent ) import Rel8.Query.Filter ( where_ ) import Rel8.Query.List ( catNonEmptyTable ) @@ -100,17 +102,16 @@ import Rel8.Query.Opaleye ( mapOpaleye, unsafePeekQuery ) import Rel8.Query.Rebind ( rebind ) import Rel8.Query.These ( alignBy ) import Rel8.Table ( Table, fromColumns, toColumns ) -import Rel8.Table.Aggregate ( hgroupBy, listAgg, nonEmptyAgg ) +import Rel8.Table.Aggregate (groupBy, listAgg, nonEmptyAgg) import Rel8.Table.Alternative ( AltTable, (<|>:) , AlternativeTable, emptyTable ) -import Rel8.Table.Cols ( fromCols, toCols ) -import Rel8.Table.Eq ( EqTable, (==:), eqTable ) -import Rel8.Table.List ( ListTable( ListTable ) ) -import Rel8.Table.Maybe ( MaybeTable( MaybeTable ), maybeTable ) -import Rel8.Table.NonEmpty ( NonEmptyTable( NonEmptyTable ) ) -import Rel8.Table.Opaleye ( aggregator, unpackspec ) +import Rel8.Table.Eq (EqTable, (==:)) +import Rel8.Table.List (ListTable) +import Rel8.Table.Maybe (MaybeTable (MaybeTable), fromMaybeTable) +import Rel8.Table.NonEmpty (NonEmptyTable) +import Rel8.Table.Opaleye ( unpackspec ) import Rel8.Table.Ord ( OrdTable ) import Rel8.Table.Order ( ascTable ) import Rel8.Table.Projection @@ -333,19 +334,23 @@ lookup k (Tabulation f) = do p = match (pure k) --- | 'aggregate' aggregates the values within each key of a +-- | 'aggregate' produces a \"magic\" 'Tabulation' whereby the values within +-- each group of keys in the given 'Tabulation' is aggregated according to +-- the given aggregator, and every other possible key contains a single +-- \"fallback\" row is returned, composed of the identity elements of the +-- constituent aggregation functions. +aggregate :: (EqTable k, Table Expr a) + => Aggregator i a -> Tabulation k i -> Tabulation k a +aggregate aggregator@(Aggregator (Fallback fallback) _) = + fmap (fromMaybeTable fallback) . optional . aggregate1 aggregator + + +-- | 'aggregate1' aggregates the values within each key of a -- 'Tabulation'. There is an implicit @GROUP BY@ on all the key columns. -aggregate :: forall k aggregates exprs. - ( EqTable k - , Aggregates aggregates exprs - ) - => Tabulation k aggregates -> Tabulation k exprs -aggregate (Tabulation f) = Tabulation $ - mapOpaleye (Opaleye.aggregate (keyed haggregator aggregator)) . - fmap (first (fmap (hgroupBy (eqTable @k) . toColumns))) . - f - where - haggregator = dimap fromColumns fromCols aggregator +aggregate1 :: EqTable k + => Aggregator' fold i a -> Tabulation k i -> Tabulation k a +aggregate1 aggregator (Tabulation f) = + Tabulation $ Q.aggregate1 (keyed groupBy (semi aggregator)) . f -- | 'distinct' ensures a 'Tabulation' has at most one value for @@ -412,11 +417,7 @@ order ordering (Tabulation f) = -- The resulting 'Tabulation' is \"magic\" in that the value @0@ exists at -- every possible key that wasn't in the given 'Tabulation'. count :: EqTable k => Tabulation k a -> Tabulation k (Expr Int64) -count = - fmap (maybeTable 0 id) . - optional . - aggregate . - fmap (const countStar) +count = aggregate countStar -- | 'optional' produces a \"magic\" 'Tabulation' whereby each @@ -443,11 +444,7 @@ optional (Tabulation f) = Tabulation $ \p -> case p of -- 'Tabulation'. many :: (EqTable k, Table Expr a) => Tabulation k a -> Tabulation k (ListTable Expr a) -many = - fmap (maybeTable mempty (\(ListTable a) -> ListTable a)) . - optional . - aggregate . - fmap (listAgg . toCols) +many = aggregate listAgg -- | 'some' aggregates each entry with a particular key into a @@ -456,10 +453,7 @@ many = -- 'order' can be used to give this 'NonEmptyTable' a defined order. some :: (EqTable k, Table Expr a) => Tabulation k a -> Tabulation k (NonEmptyTable Expr a) -some = - fmap (\(NonEmptyTable a) -> NonEmptyTable a) . - aggregate . - fmap (nonEmptyAgg . toCols) +some = aggregate1 nonEmptyAgg -- | 'exists' produces a \"magic\" 'Tabulation' which contains the