packages feed

rel8 1.4.1.0 → 1.5.0.0

raw patch · 93 files changed

+4095/−1049 lines, 93 filesdep +attoparsecdep +attoparsec-aesondep +base-compatdep ~basedep ~hasqldep ~hedgehog

Dependencies added: attoparsec, attoparsec-aeson, base-compat, base16, data-dword, data-textual, network-ip, utf8-string, vector

Dependency ranges changed: base, hasql, hedgehog, opaleye

Files

Changelog.md view
@@ -1,3 +1,134 @@++<a id='changelog-1.5.0.0'></a>+# 1.5.0.0 — 2024-03-19++## Removed++- Removed `nullaryFunction`. Instead `function` can be called with `()`. ([#258](https://github.com/circuithub/rel8/pull/258))++## Added++- Support PostgreSQL's `inet` type (which maps to the Haskell `NetAddr IP` type). ([#227](https://github.com/circuithub/rel8/pull/227))++- `Rel8.materialize` and `Rel8.Tabulate.materialize`, which add a materialization/optimisation fence to `SELECT` statements by binding a query to a `WITH` subquery. Note that explicitly materialized common table expressions are only supported in PostgreSQL 12 an higher. ([#180](https://github.com/circuithub/rel8/pull/180)) ([#284](https://github.com/circuithub/rel8/pull/284))++- `Rel8.head`, `Rel8.headExpr`, `Rel8.last`, `Rel8.lastExpr` for accessing the first/last elements of `ListTable`s and arrays. We have also added variants for `NonEmptyTable`s/non-empty arrays with the `1` suffix (e.g., `head1`). ([#245](https://github.com/circuithub/rel8/pull/245))++- Rel8 now has extensive support for `WITH` statements and data-modifying statements (https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING).++  This work offers a lot of new power to Rel8. One new possibility is "moving" rows between tables, for example to archive rows in one table into a log table:++  ```haskell+  import Rel8++  archive :: Statement ()+  archive = do+    deleted <-+      delete Delete+        { from = mainTable+        , using = pure ()+        , deleteWhere = \foo -> fooId foo ==. lit 123+        , returning = Returning id+        }++    insert Insert+      { into = archiveTable+      , rows = deleted+      , onConflict = DoNothing+      , returning = NoReturninvg+      }+  ```++  This `Statement` will compile to a single SQL statement - essentially:++  ```sql+  WITH deleted_rows (DELETE FROM main_table WHERE id = 123 RETURNING *)+  INSERT INTO archive_table SELECT * FROM deleted_rows+  ```++  This feature is a significant performant improvement, as it avoids an entire roundtrip.++  This change has necessitated a change to how a `SELECT` statement is ran: `select` now will now produce a `Rel8.Statement`, which you have to `run` to turn it into a Hasql `Statement`. Rel8 offers a variety of `run` functions depending on how many rows need to be returned - see the various family of `run` functions in Rel8's documentation for more.++  [#250](https://github.com/circuithub/rel8/pull/250)++- `Rel8.loop` and `Rel8.loopDistinct`, which allow writing `WITH .. RECURSIVE` queries. ([#180](https://github.com/circuithub/rel8/pull/180))++- Added the `QualifiedName` type for named PostgreSQL objects (tables, views, functions, operators, sequences, etc.) that can optionally be qualified by a schema, including an `IsString` instance. ([#257](https://github.com/circuithub/rel8/pull/257)) ([#263](https://github.com/circuithub/rel8/pull/263))++- Added `queryFunction` for `SELECT`ing from table-returning functions such as `jsonb_to_recordset`. ([#241](https://github.com/circuithub/rel8/pull/241))++- `TypeName` record, which gives a richer representation of the components of a PostgreSQL type name (name, schema, modifiers, scalar/array). ([#263](https://github.com/circuithub/rel8/pull/263))++- `Rel8.length` and `Rel8.lengthExpr` for getting the length `ListTable`s and arrays. We have also added variants for `NonEmptyTable`s/non-empty arrays with the `1` suffix (e.g., `length1`). ([#268](https://github.com/circuithub/rel8/pull/268))++- Added aggregators `listCat` and `nonEmptyCat` for folding a collection of lists into a single list by concatenation. ([#270](https://github.com/circuithub/rel8/pull/270))++- `DBType` instance for `Fixed` that would map (e.g.) `Micro` to `numeric(1000, 6)` and `Pico` to `numeric(1000, 12)`. ([#280](https://github.com/circuithub/rel8/pull/280))++- `aggregationFunction`, which allows custom aggregation functions to be used. ([#283](https://github.com/circuithub/rel8/pull/283))++- Add support for ordered-set aggregation functions, including `mode`, `percentile`, `percentileContinuous`, `hypotheticalRank`, `hypotheticalDenseRank`, `hypotheticalPercentRank` and `hypotheticalCumeDist`. ([#282](https://github.com/circuithub/rel8/pull/282))++- Added `index`, `index1`, `indexExpr`, and `index1Expr` functions for extracting individual elements from `ListTable`s and `NonEmptyTable`s. ([#285](https://github.com/circuithub/rel8/pull/285))++- Rel8 now supports GHC 9.8. ([#299](https://github.com/circuithub/rel8/pull/299))++## Changed++- Rel8's API regarding aggregation has changed significantly, and is now a closer match to Opaleye.++  The previous aggregation API had `aggregate` transform a `Table` from the `Aggregate` context back into the `Expr` context:++  ```haskell+  myQuery = aggregate do+    a <- each tableA+    return $ liftF2 (,) (sum (foo a)) (countDistinct (bar a))+  ```++  This API seemed convenient, but has some significant shortcomings. The new API requires an explicit `Aggregator` be passed to `aggregate`:++  ```haskell+  myQuery = aggregate (liftA2 (,) (sumOn foo) (countDistinctOn bar)) do+    each tableA+  ```++  For more details, see [#235](https://github.com/circuithub/rel8/pull/235)++- `TypeInformation`'s `decoder` field has changed. Instead of taking a `Hasql.Decoder`, it now takes a `Rel8.Decoder`, which itself is comprised of a `Hasql.Decoder` and an `attoparsec` `Parser`. This is necessitated by the fix for [#168](https://github.com/circuithub/rel8/issues/168); we generally decode things in PostgreSQL's binary format (using a `Hasql.Decoder`), but for nested arrays we now get things in PostgreSQL's text format (for which we need an `attoparsec` `Parser`), so must have both. Most `DBType` instances that use `mapTypeInformation` or `ParseTypeInformation`, or `DerivingVia` helpers like `ReadShow`, `JSONBEncoded`, `Enum` and `Composite` are unaffected by this change. ([#243](https://github.com/circuithub/rel8/pull/243))++- The `schema` field from `TableSchema` has been removed and the name field changed from `String` to `QualifiedName`. ([#257](https://github.com/circuithub/rel8/pull/257))++- `nextval`, `function` and `binaryOperator` now take a `QualifiedName` instead of a `String`. ([#262](https://github.com/circuithub/rel8/pull/262))++- `function` has been changed to accept a single argument (as opposed to variadic arguments). ([#258](https://github.com/circuithub/rel8/pull/258))++- `TypeInformation`'s `typeName` parameter from `String` to `TypeName`. ([#263](https://github.com/circuithub/rel8/pull/263))++- `DBEnum`'s `enumTypeName` method from `String` to `QualifiedName`. ([#263](https://github.com/circuithub/rel8/pull/263))++- `DBComposite`'s `compositeTypeName` method from `String` to `QualifiedName`. ([#263](https://github.com/circuithub/rel8/pull/263))++- Changed `Upsert` by adding a `predicate` field, which allows partial indexes to be specified as conflict targets. ([#264](https://github.com/circuithub/rel8/pull/264))++- The window functions `lag`, `lead`, `firstValue`, `lastValue` and `nthValue` can now operate on entire rows at once as opposed to just single columns. ([#281](https://github.com/circuithub/rel8/pull/281))++## Fixed++- Fixed a bug with `catListTable` and `catNonEmptyTable` where invalid SQL could be produced. ([#240](https://github.com/circuithub/rel8/pull/240))++- A fix for [#168](https://github.com/circuithub/rel8/issues/168), which prevented using `catListTable` on arrays of arrays. To achieve this we had to coerce arrays of arrays to text internally, which unfortunately isn't completely transparent; you can oberve it if you write something like `listTable [listTable [10]] > listTable [listTable [9]]`: previously that would be `false`, but now it's `true`. Arrays of non-arrays are unaffected by this.++- Fixes [#228](https://github.com/circuithub/rel8/issues/228) where it was impossible to call `nextval` with a qualified sequence name.++- Fixes [#71](https://github.com/circuithub/rel8/issues/71).++- Fixed a typo in the documentation for `/=.`. ([#312](https://github.com/circuithub/rel8/pull/312))++- Fixed a bug where `fromRational` could crash with repeating fractions. ([#309](https://github.com/circuithub/rel8/pull/309))++- Fixed a typo in the documentation for `min`. ([#306](https://github.com/circuithub/rel8/pull/306))+ # 1.4.1.0 (2023-01-19)  ## New features
rel8.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.0 name:                rel8-version:             1.4.1.0+version:             1.5.0.0 synopsis:            Hey! Hey! Can u rel8? license:             BSD3 license-file:        LICENSE@@ -20,14 +20,20 @@ library   build-depends:       aeson-    , base ^>= 4.14 || ^>=4.15 || ^>=4.16 || ^>=4.17+    , attoparsec+    , attoparsec-aeson+    , base ^>= 4.16 || ^>= 4.17 || ^>= 4.18 || ^>= 4.19+    , base16 >= 1.0+    , base-compat ^>= 0.11 || ^>= 0.12 || ^>= 0.13     , bifunctors     , bytestring     , case-insensitive     , comonad     , contravariant-    , hasql ^>= 1.4.5.1 || ^>= 1.5.0.0 || ^>= 1.6.0.0-    , opaleye ^>= 0.9.6.1+    , data-textual+    , hasql ^>= 1.6.1.2+    , network-ip+    , opaleye ^>= 0.10.2.1     , pretty     , profunctors     , product-profunctors@@ -37,7 +43,11 @@     , text     , these     , time+    , transformers+    , utf8-string     , uuid+    , vector+   default-language:     Haskell2010   ghc-options:@@ -51,6 +61,7 @@     src   exposed-modules:     Rel8+    Rel8.Array     Rel8.Expr.Num     Rel8.Expr.Text     Rel8.Expr.Time@@ -58,6 +69,8 @@    other-modules:     Rel8.Aggregate+    Rel8.Aggregate.Fold+    Rel8.Aggregate.Function      Rel8.Column     Rel8.Column.ADT@@ -76,12 +89,16 @@     Rel8.Expr.Default     Rel8.Expr.Eq     Rel8.Expr.Function+    Rel8.Expr.List+    Rel8.Expr.NonEmpty     Rel8.Expr.Null     Rel8.Expr.Opaleye     Rel8.Expr.Ord     Rel8.Expr.Order+    Rel8.Expr.Read     Rel8.Expr.Sequence     Rel8.Expr.Serialize+    Rel8.Expr.Show     Rel8.Expr.Window      Rel8.FCF@@ -109,9 +126,12 @@     Rel8.Query.Evaluate     Rel8.Query.Exists     Rel8.Query.Filter+    Rel8.Query.Function     Rel8.Query.Indexed     Rel8.Query.Limit     Rel8.Query.List+    Rel8.Query.Loop+    Rel8.Query.Materialize     Rel8.Query.Maybe     Rel8.Query.Null     Rel8.Query.Opaleye@@ -125,6 +145,7 @@      Rel8.Schema.Context.Nullify     Rel8.Schema.Dict+    Rel8.Schema.Escape     Rel8.Schema.Field     Rel8.Schema.HTable     Rel8.Schema.HTable.Either@@ -141,14 +162,18 @@     Rel8.Schema.Kind     Rel8.Schema.Name     Rel8.Schema.Null+    Rel8.Schema.QualifiedName     Rel8.Schema.Result     Rel8.Schema.Spec     Rel8.Schema.Table +    Rel8.Statement     Rel8.Statement.Delete     Rel8.Statement.Insert     Rel8.Statement.OnConflict     Rel8.Statement.Returning+    Rel8.Statement.Rows+    Rel8.Statement.Run     Rel8.Statement.Select     Rel8.Statement.Set     Rel8.Statement.SQL@@ -186,14 +211,20 @@     Rel8.Type     Rel8.Type.Array     Rel8.Type.Composite+    Rel8.Type.Decimal+    Rel8.Type.Decoder     Rel8.Type.Eq     Rel8.Type.Enum     Rel8.Type.Information     Rel8.Type.JSONEncoded     Rel8.Type.JSONBEncoded     Rel8.Type.Monoid+    Rel8.Type.Name     Rel8.Type.Num     Rel8.Type.Ord+    Rel8.Type.Parser+    Rel8.Type.Parser.ByteString+    Rel8.Type.Parser.Time     Rel8.Type.ReadShow     Rel8.Type.Semigroup     Rel8.Type.String@@ -210,10 +241,12 @@     , bytestring     , case-insensitive     , containers+    , data-dword     , hasql     , hasql-transaction-    , hedgehog          ^>= 1.0 || ^>= 1.1+    , hedgehog          ^>= 1.0 || ^>= 1.1 || ^>= 1.2 || ^>= 1.3 || ^>= 1.4 || ^>= 1.5     , mmorph+    , network-ip     , rel8     , scientific     , tasty@@ -235,3 +268,4 @@     -Wno-missing-import-lists -Wno-prepositive-qualified-module     -Wno-deprecations -Wno-monomorphism-restriction     -Wno-missing-local-signatures -Wno-implicit-prelude+    -Wno-missing-kind-signatures
src/Rel8.hs view
@@ -19,9 +19,13 @@      -- *** @TypeInformation@   , TypeInformation(..)+  , TypeName(..)   , mapTypeInformation   , parseTypeInformation +    -- *** @Decoder@+  , Decoder(..)+     -- ** The @DBType@ hierarchy   , DBSemigroup(..)   , DBMonoid(..)@@ -135,9 +139,6 @@   , BuildADT, buildADT   , ConstructADT, constructADT -    -- *** Other ADT operations-  , AggregateADT, aggregateADT-     -- *** Miscellaneous notes     -- $misc-notes @@ -147,10 +148,10 @@   , ConstructHKD, constructHKD   , DeconstructHKD, deconstructHKD   , NameHKD, nameHKD-  , AggregateHKD, aggregateHKD      -- ** Table schemas   , TableSchema(..)+  , QualifiedName(..)   , Name   , namesFromLabels   , namesFromLabelsWith@@ -193,10 +194,10 @@   , leastExpr, greatestExpr      -- ** Functions-  , Function+  , Arguments   , function-  , nullaryFunction   , binaryOperator+  , queryFunction      -- * Queries   , Query@@ -246,26 +247,53 @@   , without   , withoutBy +    -- ** @WITH@+  , materialize++    -- ** @WITH RECURSIVE@+  , loop+  , loopDistinct+     -- ** Aggregation-  , Aggregate-  , Aggregates+  , Aggregator+  , Aggregator1+  , Aggregator'+  , Fold (Semi, Full)+  , toAggregator+  , toAggregator1   , aggregate+  , aggregate1+  , filterWhere+  , filterWhereOptional+  , distinctAggregate+  , orderAggregateBy+  , optionalAggregate   , countRows-  , groupBy-  , listAgg, listAggExpr-  , mode-  , nonEmptyAgg, nonEmptyAggExpr-  , DBMax, max-  , DBMin, min-  , DBSum, sum, sumWhere, avg+  , groupBy, groupByOn+  , listAgg, listAggOn, listAggExpr, listAggExprOn+  , listCat, listCatOn, listCatExpr, listCatExprOn+  , nonEmptyAgg, nonEmptyAggOn, nonEmptyAggExpr, nonEmptyAggExprOn+  , nonEmptyCat, nonEmptyCatOn, nonEmptyCatExpr, nonEmptyCatExprOn+  , 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+  , aggregateFunction +  , mode, modeOn+  , percentile, percentileOn+  , percentileContinuous, percentileContinuousOn+  , hypotheticalRank+  , hypotheticalDenseRank+  , hypotheticalPercentRank+  , hypotheticalCumeDist+     -- ** Ordering   , orderBy   , Order@@ -282,7 +310,6 @@   , partitionBy   , orderPartitionBy   , cumulative-  , cumulative_   , currentRow   , rowNumber   , rank@@ -290,11 +317,11 @@   , percentRank   , cumeDist   , ntile-  , lag-  , lead-  , firstValue-  , lastValue-  , nthValue+  , lag, lagOn+  , lead, leadOn+  , firstValue, firstValueOn+  , lastValue, lastValueOn+  , nthValue, nthValueOn   , indexed      -- ** Bindings@@ -307,6 +334,12 @@      -- * Running statements     -- $running+  , run+  , run_+  , runN+  , run1+  , runMaybe+  , runVector      -- ** @SELECT@   , select@@ -332,6 +365,10 @@     -- ** @.. RETURNING@   , Returning(..) +    -- ** @WITH@+  , Statement+  , showStatement+     -- ** @CREATE VIEW@   , createView   , createOrReplaceView@@ -346,6 +383,8 @@  -- rel8 import Rel8.Aggregate+import Rel8.Aggregate.Fold+import Rel8.Aggregate.Function import Rel8.Column import Rel8.Column.ADT import Rel8.Column.Either@@ -368,7 +407,7 @@ 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@@ -379,9 +418,12 @@ import Rel8.Query.Evaluate import Rel8.Query.Exists import Rel8.Query.Filter+import Rel8.Query.Function import Rel8.Query.Indexed import Rel8.Query.Limit import Rel8.Query.List+import Rel8.Query.Loop+import Rel8.Query.Materialize import Rel8.Query.Maybe import Rel8.Query.Null import Rel8.Query.Order@@ -395,12 +437,15 @@ import Rel8.Schema.HTable import Rel8.Schema.Name import Rel8.Schema.Null hiding ( nullable )+import Rel8.Schema.QualifiedName import Rel8.Schema.Result ( Result ) import Rel8.Schema.Table+import Rel8.Statement import Rel8.Statement.Delete import Rel8.Statement.Insert import Rel8.Statement.OnConflict import Rel8.Statement.Returning+import Rel8.Statement.Run import Rel8.Statement.Select import Rel8.Statement.SQL import Rel8.Statement.Update@@ -429,12 +474,14 @@ import Rel8.Table.Window import Rel8.Type import Rel8.Type.Composite+import Rel8.Type.Decoder import Rel8.Type.Eq import Rel8.Type.Enum import Rel8.Type.Information import Rel8.Type.JSONBEncoded import Rel8.Type.JSONEncoded import Rel8.Type.Monoid+import Rel8.Type.Name import Rel8.Type.Num import Rel8.Type.Ord import Rel8.Type.ReadShow@@ -446,9 +493,17 @@  -- $running -- To run queries and otherwise interact with a PostgreSQL database, Rel8--- provides 'select', 'insert', 'update' and 'delete' functions. Note that--- 'insert', 'update' and 'delete' will generally need the--- `DuplicateRecordFields` language extension enabled.+-- provides the @run@ functions. These produce a 'Hasql.Statement.Statement's+-- which can be passed to 'Hasql.Session.statement' to execute the statement+-- against a PostgreSQL 'Hasql.Connection.Connection'.+--+-- 'run' takes a 'Statement', which can be constructed using either 'select',+-- 'insert', 'update' or 'delete'. It decodes the rows returned by the+-- statement as a list of Haskell of values. See 'run_', 'runN', 'run1',+-- 'runMaybe' and 'runVector' for other variations.+--+-- Note that constructing an 'Insert', 'Update' or 'Delete' will require the+-- @DisambiguateRecordFields@ language extension to be enabled.  -- $adts -- Algebraic data types can be modelled between Haskell and SQL.@@ -508,9 +563,9 @@ -- query = do --   thingExpr <- each thingSchema --   where_ $---     deconstructADT @Thing---       (\employer -> employerName employer ==. lit \"Mary\")---       (\potato -> grower potato ==. lit \"Mary\")+--     deconstructADT \@Thing+--       (\\employer -> employerName employer ==. lit \"Mary\")+--       (\\potato -> grower potato ==. lit \"Mary\") --       (lit False) -- Nullary case --       thingExpr --   pure thingExpr@@ -554,10 +609,10 @@ -- instantiations of 'buildADT' for 'Task': -- -- @--- > :t buildADT @Task @\"Pending\"--- buildADT @Task @\"Pending\" :: ADT Task Expr--- > :t buildADT @Task @\"Complete\"--- buildADT @Task @\"Complete\" :: CompletedTask Expr -> ADT Task Expr+-- > :t buildADT \@Task \@\"Pending\"+-- buildADT \@Task \@\"Pending\" :: ADT Task Expr+-- > :t buildADT \@Task @\"Complete\"+-- buildADT \@Task \@\"Complete\" :: CompletedTask Expr -> ADT Task Expr -- @ -- -- Note that as the "Pending" constructor has no fields, @buildADT@@ -580,8 +635,8 @@ -- @ -- > :{ -- showQuery $ values---   [ buildADT @Task @\"Pending\"---   , buildADT @Task @\"Complete\" CompletedTask {date = Rel8.Expr.Time.now}+--   [ buildADT \@Task \@\"Pending\"+--   , buildADT \@Task \@\"Complete\" CompletedTask {date = Rel8.Expr.Time.now} --   ] -- :} -- @@@ -630,10 +685,10 @@ -- @ -- let --   pending :: ADT Task Expr---   pending = constructADT @Task $ \pending _complete -> pending+--   pending = constructADT \@Task $ \\pending _complete -> pending -- --   complete :: ADT Task Expr---   complete = constructADT @Task $ \_pending complete -> complete CompletedTask {date = Rel8.Expr.Time.now}+--   complete = constructADT \@Task $ \\_pending complete -> complete CompletedTask {date = Rel8.Expr.Time.now} -- @ -- -- These values are otherwise identical to the ones we saw above with
src/Rel8/Aggregate.hs view
@@ -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+  , toAggregator+  , toAggregator1+  , 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 ) +-- profunctors+import Data.Profunctor (Profunctor, dimap) --- | 'Aggregate' is a special context used by 'Rel8.aggregate'.-type Aggregate :: K.Context-newtype Aggregate a = Aggregate (Opaleye.Aggregator () (Expr a))+-- 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) -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+-- | '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)  --- | @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+instance Profunctor (Aggregator' fold) where+  dimap f g (Aggregator fallback a) =+    Aggregator (fmap g fallback) (dimap f g a)  -zipOutputs :: ()-  => (Expr a -> Expr b -> Expr c) -> Aggregate a -> Aggregate b -> Aggregate c-zipOutputs f (Aggregate a) (Aggregate b) = Aggregate (liftA2 f a b)+instance ProductProfunctor (Aggregator' fold) where+  purePP = pure+  (****) = (<*>)  -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+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+++-- | 'toAggregator1' turns an 'Aggregator' into an 'Aggregator1'.+toAggregator1 :: Aggregator' fold i a -> Aggregator1 i a+toAggregator1 (Aggregator _ a) = Aggregator Empty a+++-- | Given a value to fall back on if given an empty collection of rows,+-- 'toAggregator' turns an 'Aggregator1' into an 'Aggregator'.+toAggregator :: a -> Aggregator' fold i a -> Aggregator' fold' i a+toAggregator 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
+ src/Rel8/Aggregate/Fold.hs view
@@ -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
+ src/Rel8/Aggregate/Function.hs view
@@ -0,0 +1,40 @@+{-# language FlexibleContexts #-}+{-# language MonoLocalBinds #-}++module Rel8.Aggregate.Function (+  aggregateFunction,+) where++-- base+import Prelude++-- opaleye+import qualified Opaleye.Internal.Aggregate as Opaleye+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye++-- rel8+import Rel8.Aggregate (Aggregator1, unsafeMakeAggregator)+import Rel8.Aggregate.Fold (Fallback (Empty))+import Rel8.Expr (Expr)+import Rel8.Expr.Opaleye (castExpr, fromColumn, fromPrimExpr)+import Rel8.Schema.Null (Sql)+import Rel8.Schema.QualifiedName (QualifiedName, showQualifiedName)+import Rel8.Table (Table)+import Rel8.Table.Opaleye (unpackspec)+import Rel8.Type (DBType)+++-- | 'aggregateFunction' allows the use use of custom aggregation functions+-- or PostgreSQL aggregation functions which are not otherwise supported by+-- Rel8.+aggregateFunction ::+  (Table Expr i, Sql DBType a) =>+  QualifiedName ->+  Aggregator1 i (Expr a)+aggregateFunction name =+  unsafeMakeAggregator+    id+    (castExpr . fromPrimExpr . fromColumn)+    Empty+    (Opaleye.makeAggrExplicit unpackspec+      (Opaleye.AggrOther (showQualifiedName name)))
+ src/Rel8/Array.hs view
@@ -0,0 +1,26 @@+module Rel8.Array+  (+    -- ** @ListTable@+    ListTable+  , head, headExpr+  , index, indexExpr+  , last, lastExpr+  , length, lengthExpr++    -- ** @NonEmptyTable@+  , NonEmptyTable+  , head1, head1Expr+  , index1, index1Expr+  , last1, last1Expr+  , length1, length1Expr+  )+where++-- base+import Prelude hiding (head, last, length)++-- rel8+import Rel8.Expr.List+import Rel8.Expr.NonEmpty+import Rel8.Table.List+import Rel8.Table.NonEmpty
src/Rel8/Expr.hs view
@@ -3,6 +3,7 @@ {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language MultiParamTypeClasses #-}+{-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-}@@ -16,6 +17,7 @@  -- base import Data.Functor.Identity ( Identity( Identity ) )+import Data.Ratio (denominator, numerator) import Data.String ( IsString, fromString ) import Prelude hiding ( null ) @@ -23,7 +25,7 @@ import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye  -- rel8-import Rel8.Expr.Function ( function, nullaryFunction )+import Rel8.Expr.Function (function) import Rel8.Expr.Null ( liftOpNull, nullify ) import Rel8.Expr.Opaleye   ( castExpr@@ -45,7 +47,10 @@ import Rel8.Type.Num ( DBFloating, DBFractional, DBNum ) import Rel8.Type.Semigroup ( DBSemigroup, (<>.) ) +-- scientific+import Data.Scientific (fromRationalRepetendLimited) + -- | Typed SQL expressions. type Expr :: K.Context newtype Expr a = Expr Opaleye.PrimExpr@@ -88,17 +93,24 @@ instance Sql DBFractional a => Fractional (Expr a) where   (/) = zipPrimExprsWith (Opaleye.BinExpr (Opaleye.:/)) -  fromRational =-    castExpr . Expr . Opaleye.ConstExpr . Opaleye.NumericLit . realToFrac+  fromRational = castExpr . Expr . toScientific+    where+      toScientific r = case fromRationalRepetendLimited 20 r of+        Right (s, Nothing) -> Opaleye.ConstExpr (Opaleye.NumericLit s)+        _ -> Opaleye.BinExpr (Opaleye.:/) (int n) (int d)+          where+            int = Opaleye.ConstExpr . Opaleye.NumericLit . fromInteger+            n = numerator r+            d = denominator r   instance Sql DBFloating a => Floating (Expr a) where-  pi = nullaryFunction "PI"+  pi = function "pi" ()   exp = function "exp"   log = function "ln"   sqrt = function "sqrt"   (**) = zipPrimExprsWith (Opaleye.BinExpr (Opaleye.:^))-  logBase = function "log"+  logBase a b = function "log" (a, b)   sin = function "sin"   cos = function "cos"   tan = function "tan"
src/Rel8/Expr/Aggregate.hs view
@@ -1,150 +1,495 @@ {-# language DataKinds #-}+{-# language DisambiguateRecordFields #-} {-# language FlexibleContexts #-}+{-# language NamedFieldPuns #-}+{-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-}+{-# language TypeApplications #-} {-# 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+  , mode, modeOn+  , percentile, percentileOn+  , percentileContinuous, percentileContinuousOn+  , hypotheticalRank+  , hypotheticalDenseRank+  , hypotheticalPercentRank+  , hypotheticalCumeDist+  , groupByExpr, groupByExprOn+  , distinctAggregate+  , filterWhereExplicit+  , listAggExpr, listAggExprOn, nonEmptyAggExpr, nonEmptyAggExprOn+  , listCatExpr, listCatExprOn, nonEmptyCatExpr, nonEmptyCatExprOn   , slistAggExpr, snonEmptyAggExpr+  , slistCatExpr, snonEmptyCatExpr   ) where  -- base+import Data.Functor.Contravariant ((>$<)) import Data.Int ( Int64 ) import Data.List.NonEmpty ( NonEmpty )-import Prelude hiding ( and, max, min, null, or, sum )+import Data.String (IsString)+import Prelude hiding (and, max, min, null, or, show, 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.HaskellDB.PrimQuery 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.Eq ((/=.)) import Rel8.Expr.Opaleye   ( castExpr-  , fromPrimExpr+  , fromColumn   , fromPrimExpr+  , toColumn   , toPrimExpr+  , unsafeCastExpr   )-import Rel8.Expr.Null ( null )-import Rel8.Expr.Serialize ( litExpr )+import Rel8.Expr.Order (asc)+import Rel8.Expr.Read (sread)+import Rel8.Expr.Show (show)+import qualified Rel8.Expr.Text as Text+import Rel8.Order (Order (Order)) import Rel8.Schema.Null ( Sql, Unnullify )+import Rel8.Table.Opaleye (fromOrder, unpackspec)+import Rel8.Table.Order (ascTable) import Rel8.Type ( DBType, typeInformation )-import Rel8.Type.Array ( encodeArrayElement )+import Rel8.Type.Array (arrayTypeName, encodeArrayElement) import Rel8.Type.Eq ( DBEq )-import Rel8.Type.Information ( TypeInformation )-import Rel8.Type.Num ( DBNum )-import Rel8.Type.Ord ( DBMax, DBMin )+import Rel8.Type.Information (TypeInformation)+import Rel8.Type.Num (DBFractional, DBNum)+import Rel8.Type.Ord (DBMax, DBMin, DBOrd) import Rel8.Type.String ( DBString ) 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  --- | Produce an aggregation for @Expr a@ using the @max@ function.-max :: Sql DBMax a => Expr a -> Aggregate a-max = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMax+-- | 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.-min :: Sql DBMin a => Expr a -> Aggregate a-min = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMin+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 @min@ function.+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 -- example, the @avg@ of @integer@ returns a @numeric@. Rel8 doesn't support -- 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)+++-- | Corresponds to @mode() WITHIN GROUP (ORDER BY _)@.+mode :: Sql DBOrd a => Aggregator1 (Expr a) (Expr a)+mode =+  unsafeMakeAggregator+    id+    (fromPrimExpr . fromColumn)+    Empty+    (Opaleye.withinGroup ((\(Order o) -> o) ascTable)+      (Opaleye.makeAggrExplicit (pure ()) (Opaleye.AggrOther "mode")))+++-- | Applies 'mode' to the column selected by the given function.+modeOn :: Sql DBOrd a => (i -> Expr a) -> Aggregator1 i (Expr a)+modeOn f = lmap f mode+++-- | Corresponds to @percentile_disc(_) WITHIN GROUP (ORDER BY _)@.+percentile :: Sql DBOrd a => Expr Double -> Aggregator1 (Expr a) (Expr a)+percentile fraction = +  unsafeMakeAggregator+    (\a -> (fraction, a))+    (castExpr . fromPrimExpr . fromColumn)+    Empty+    (Opaleye.withinGroup ((\(Order o) -> o) (snd >$< ascTable))+      (Opaleye.makeAggrExplicit+        (lmap fst unpackspec)+        (Opaleye.AggrOther "percentile_disc")))+++-- | Applies 'percentile' to the column selected by the given function.+percentileOn ::+  Sql DBOrd a =>+  Expr Double ->+  (i -> Expr a) ->+  Aggregator1 i (Expr a)+percentileOn fraction f = lmap f (percentile fraction)+++-- | Corresponds to @percentile_cont(_) WITHIN GROUP (ORDER BY _)@.+percentileContinuous ::+  Sql DBFractional a =>+  Expr Double ->+  Aggregator1 (Expr a) (Expr a)+percentileContinuous fraction = +  unsafeMakeAggregator+    (\a -> (fraction, a))+    (castExpr . fromPrimExpr . fromColumn)+    Empty+    (Opaleye.withinGroup ((\(Order o) -> o) (unsafeCastExpr @Double . snd >$< asc))+      (Opaleye.makeAggrExplicit+        (lmap fst unpackspec)+        (Opaleye.AggrOther "percentile_disc")))++++-- | Applies 'percentileContinuous' to the column selected by the given+-- function.+percentileContinuousOn ::+  Sql DBFractional a =>+  Expr Double ->+  (i -> Expr a) ->+  Aggregator1 i (Expr a)+percentileContinuousOn fraction f = lmap f (percentileContinuous fraction)+++-- | Corresponds to @rank(_) WITHIN GROUP (ORDER BY _)@.+hypotheticalRank ::+  Order a ->+  a ->+  Aggregator' fold a (Expr Int64)+hypotheticalRank (Order order) args = +  unsafeMakeAggregator+    (\a -> (args, a))+    (castExpr . fromPrimExpr . fromColumn)+    (Fallback 1)+    (Opaleye.withinGroup (snd >$< order)+      (Opaleye.makeAggrExplicit+        (fromOrder (fst >$< order))+        (Opaleye.AggrOther "rank")))+++-- | Corresponds to @dense_rank(_) WITHIN GROUP (ORDER BY _)@.+hypotheticalDenseRank ::+  Order a ->+  a ->+  Aggregator' fold a (Expr Int64)+hypotheticalDenseRank (Order order) args = +  unsafeMakeAggregator+    (const args)+    (castExpr . fromPrimExpr . fromColumn)+    (Fallback 1)+    (Opaleye.withinGroup order+      (Opaleye.makeAggrExplicit (fromOrder order)+        (Opaleye.AggrOther "dense_rank")))+++-- | Corresponds to @percent_rank(_) WITHIN GROUP (ORDER BY _)@.+hypotheticalPercentRank ::+  Order a ->+  a ->+  Aggregator' fold a (Expr Double)+hypotheticalPercentRank (Order order) args = +  unsafeMakeAggregator+    (const args)+    (castExpr . fromPrimExpr . fromColumn)+    (Fallback 0)+    (Opaleye.withinGroup order+      (Opaleye.makeAggrExplicit (fromOrder order)+        (Opaleye.AggrOther "percent_rank")))+++-- | Corresponds to @cume_dist(_) WITHIN GROUP (ORDER BY _)@.+hypotheticalCumeDist ::+  Order a ->+  a ->+  Aggregator' fold a (Expr Double)+hypotheticalCumeDist (Order order) args = +  unsafeMakeAggregator+    (const args)+    (castExpr . fromPrimExpr . fromColumn)+    (Fallback 1)+    (Opaleye.withinGroup order+      (Opaleye.makeAggrExplicit (fromOrder order)+        (Opaleye.AggrOther "cume_dist")))++ -- | 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 'groupByExpr' 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+++-- | Concatenate lists into a single list.+listCatExpr :: Sql DBType a => Aggregator' fold (Expr [a]) (Expr [a])+listCatExpr = slistCatExpr typeInformation+++-- | Applies 'listCatExpr' to the column selected by the given function.+listCatExprOn :: Sql DBType a+  => (i -> Expr [a]) -> Aggregator' fold i (Expr [a])+listCatExprOn f = lmap f listCatExpr+++-- | Concatenate non-empty lists into a single non-empty list.+nonEmptyCatExpr :: Sql DBType a+  => Aggregator1 (Expr (NonEmpty a)) (Expr (NonEmpty a))+nonEmptyCatExpr = snonEmptyCatExpr typeInformation+++-- | Applies 'nonEmptyCatExpr' to the column selected by the given function.+nonEmptyCatExprOn :: Sql DBType a+  => (i -> Expr (NonEmpty a)) -> Aggregator1 i (Expr (NonEmpty a))+nonEmptyCatExprOn f = lmap f nonEmptyCatExpr+++-- | 'distinctAggregate' modifies an 'Aggregator' to consider only distinct+-- values of each particular column. Note that this "distinction" only happens+-- within each column individually, not across all columns simultaneously.+distinctAggregate :: Aggregator' fold i a -> Aggregator' fold i 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+++slistCatExpr :: ()+  => TypeInformation (Unnullify a) -> Aggregator' fold (Expr [a]) (Expr [a])+slistCatExpr info = dimap (unbracket . show) (sread name . bracket) agg   where-    to = encodeArrayElement info . toPrimExpr+    bracket a = "{" <> a <> "}"+    unbracket a = Text.substr a 2 (Just (Text.length a - 2))+    agg = filterWhereExplicit ifPP (/=. "") (stringAgg ",")+    name = arrayTypeName info+++snonEmptyCatExpr :: ()+  => TypeInformation (Unnullify a)+  -> Aggregator1 (Expr (NonEmpty a)) (Expr (NonEmpty a))+snonEmptyCatExpr info = dimap (unbracket . show) (sread name . bracket) agg+  where+    bracket a = "{" <> a <> "}"+    unbracket a = Text.substr a 2 (Just (Text.length a - 2))+    agg = filterWhereExplicit ifPP (/=. "") (stringAgg ",")+    name = arrayTypeName info+++ifPP :: Opaleye.IfPP (Expr a) (Expr a)+ifPP = dimap from to Opaleye.ifPPField+  where+    from = toColumn . toPrimExpr+    to = fromPrimExpr . fromColumn
src/Rel8/Expr/Array.hs view
@@ -12,7 +12,7 @@ where  -- base-import Data.List.NonEmpty ( NonEmpty )+import Data.List.NonEmpty (NonEmpty) import Prelude  -- opaleye@@ -20,14 +20,11 @@  -- rel8 import {-# SOURCE #-} Rel8.Expr ( Expr )-import Rel8.Expr.Opaleye-  ( fromPrimExpr, toPrimExpr-  , zipPrimExprsWith-  )+import Rel8.Expr.Opaleye (fromPrimExpr, toPrimExpr, zipPrimExprsWith) import Rel8.Type ( DBType, typeInformation )-import Rel8.Type.Array ( array )+import Rel8.Type.Array (array) import Rel8.Type.Information ( TypeInformation(..) )-import Rel8.Schema.Null ( Unnullify, Sql )+import Rel8.Schema.Null (Unnullify, Sql)   sappend :: Expr [a] -> Expr [a] -> Expr [a]
src/Rel8/Expr/Default.hs view
@@ -27,6 +27,11 @@ -- 3. @DEFAULT@ values can not be transformed. For example, the innocuous Rel8 -- code @unsafeDefault + 1@ will crash, despite type checking. --+-- Also note, PostgreSQL's syntax rules mean that @DEFAULT@ can only appear in+-- @INSERT@ expressions whose rows are specified using @VALUES@. This means+-- that if the @rows@ field of your 'Rel8.Insert' record doesn\'t look like+-- @values [..]@, then @unsafeDefault@ won't work.+-- -- Given all these caveats, we suggest avoiding the use of default values where -- possible, instead being explicit. A common scenario where default values are -- used is with auto-incrementing identifier columns. In this case, we suggest
src/Rel8/Expr/Eq.hs view
@@ -55,9 +55,9 @@ -- | Test if two expressions are different (not equal). -- -- This corresponds to the SQL @IS DISTINCT FROM@ operator, and will return--- @false@ when comparing two @null@ values. This differs from ordinary @=@--- which would return @null@. This operator is closer to Haskell's '=='--- operator. For an operator identical to SQL @=@, see '/=?'.+-- @false@ when comparing two @null@ values. This differs from ordinary @<>@+-- which would return @null@. This operator is closer to Haskell's '/='+-- operator. For an operator identical to SQL @<>@, see '/=?'. (/=.) :: forall a. Sql DBEq a => Expr a -> Expr a -> Expr Bool (/=.) = case nullable @a of   Null -> \ma mb -> isNull ma `ne` isNull mb ||. ma /=? mb
src/Rel8/Expr/Function.hs view
@@ -1,13 +1,16 @@ {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language MultiParamTypeClasses #-}+{-# language RecordWildCards #-} {-# language StandaloneKindSignatures #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Expr.Function-  ( Function, function-  , nullaryFunction+  ( Arguments+  , function+  , primFunction   , binaryOperator   ) where@@ -19,45 +22,64 @@ -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye +-- pretty+import Text.PrettyPrint (parens, text)+ -- rel8-import {-# SOURCE #-} Rel8.Expr ( Expr( Expr ) )+import {-# SOURCE #-} Rel8.Expr (Expr) import Rel8.Expr.Opaleye   ( castExpr   , fromPrimExpr, toPrimExpr, zipPrimExprsWith   )+import Rel8.Schema.Escape (escape)+import Rel8.Schema.HTable (hfoldMap) import Rel8.Schema.Null ( Sql )+import Rel8.Schema.QualifiedName (QualifiedName (..), showQualifiedName)+import Rel8.Table (Table, toColumns) import Rel8.Type ( DBType )  --- | This type class exists to allow 'function' to have arbitrary arity. It's--- mostly an implementation detail, and typical uses of 'Function' shouldn't--- need this to be specified.-type Function :: Type -> Type -> Constraint-class Function arg res where-  applyArgument :: ([Opaleye.PrimExpr] -> Opaleye.PrimExpr) -> arg -> res+-- | This type class is basically @'Table' 'Expr'@, where each column of the+-- 'Table' is an argument to the function, but it also has an additional+-- instance for @()@ for calling functions with no arguments.+type Arguments :: Type -> Constraint+class Arguments a where+  arguments :: a -> [Opaleye.PrimExpr]  -instance (arg ~ Expr a, Sql DBType b) => Function arg (Expr b) where-  applyArgument f a = castExpr $ fromPrimExpr $ f [toPrimExpr a]+instance Table Expr a => Arguments a where+  arguments = hfoldMap (pure . toPrimExpr) . toColumns  -instance (arg ~ Expr a, Function args res) => Function arg (args -> res) where-  applyArgument f a = applyArgument (f . (toPrimExpr a :))+instance {-# OVERLAPS #-} Arguments () where+  arguments _ = []  --- | Construct an n-ary function that produces an 'Expr' that when called runs--- a SQL function.-function :: Function args result => String -> args -> result-function = applyArgument . Opaleye.FunExpr+-- | @'function' name arguments@ runs the PostgreSQL function @name@ with+-- the arguments @arguments@ returning an @'Expr' a@.+function :: (Arguments arguments, Sql DBType a)+  => QualifiedName -> arguments -> Expr a+function qualified = castExpr . fromPrimExpr . primFunction qualified  --- | Construct a function call for functions with no arguments.-nullaryFunction :: Sql DBType a => String -> Expr a-nullaryFunction name = castExpr $ Expr (Opaleye.FunExpr name [])+primFunction :: Arguments arguments+  => QualifiedName -> arguments -> Opaleye.PrimExpr+primFunction qualified = Opaleye.FunExpr name . arguments+  where+    name = showQualifiedName qualified   -- | Construct an expression by applying an infix binary operator to two -- operands.-binaryOperator :: Sql DBType c => String -> Expr a -> Expr b -> Expr c+binaryOperator :: Sql DBType c => QualifiedName -> Expr a -> Expr b -> Expr c binaryOperator operator a b =-  castExpr $ zipPrimExprsWith (Opaleye.BinExpr (Opaleye.OpOther operator)) a b+  castExpr $ zipPrimExprsWith (Opaleye.BinExpr (Opaleye.OpOther name)) a b+  where+    name = showQualifiedOperator operator+++showQualifiedOperator :: QualifiedName -> String+showQualifiedOperator QualifiedName {schema = mschema, ..} = case mschema of+  Nothing -> name+  Just schema ->+    show $ text "OPERATOR" <> parens (escape schema <> text "." <> text name)
+ src/Rel8/Expr/List.hs view
@@ -0,0 +1,52 @@+{-# language FlexibleContexts #-}+{-# language MonoLocalBinds #-}++module Rel8.Expr.List (+  headExpr,+  indexExpr,+  lastExpr,+  sheadExpr,+  sindexExpr,+  slastExpr,+  lengthExpr,+) where++-- base+import Data.Int (Int32)+import Prelude++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Opaleye (mapPrimExpr, toPrimExpr)+import Rel8.Schema.Null (Nullify, Sql, Unnullify)+import Rel8.Type (DBType, typeInformation)+import Rel8.Type.Information (TypeInformation)+import qualified Rel8.Type.Array as Prim+++headExpr :: Sql DBType a => Expr [a] -> Expr (Nullify a)+headExpr = sheadExpr typeInformation+++indexExpr :: Sql DBType a => Expr Int32 -> Expr [a] -> Expr (Nullify a)+indexExpr = sindexExpr typeInformation+++lastExpr :: Sql DBType a => Expr [a] -> Expr (Nullify a)+lastExpr = slastExpr typeInformation+++sheadExpr :: TypeInformation (Unnullify a) -> Expr [a] -> Expr (Nullify a)+sheadExpr info = mapPrimExpr (Prim.head info)+++sindexExpr :: TypeInformation (Unnullify a) -> Expr Int32 -> Expr [a] -> Expr (Nullify a)+sindexExpr info i = mapPrimExpr (Prim.index info (toPrimExpr i))+++slastExpr :: TypeInformation (Unnullify a) -> Expr [a] -> Expr (Nullify a)+slastExpr info = mapPrimExpr (Prim.last info)+++lengthExpr :: Expr [a] -> Expr Int32+lengthExpr = mapPrimExpr (Prim.length)
+ src/Rel8/Expr/NonEmpty.hs view
@@ -0,0 +1,53 @@+{-# language FlexibleContexts #-}+{-# language MonoLocalBinds #-}++module Rel8.Expr.NonEmpty (+  head1Expr,+  index1Expr,+  last1Expr,+  shead1Expr,+  sindex1Expr,+  slast1Expr,+  length1Expr,+) where++-- base+import Data.Int (Int32)+import Data.List.NonEmpty (NonEmpty)+import Prelude++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Opaleye (mapPrimExpr, toPrimExpr)+import Rel8.Schema.Null (Nullify, Sql, Unnullify)+import Rel8.Type (DBType, typeInformation)+import Rel8.Type.Information (TypeInformation)+import qualified Rel8.Type.Array as Prim+++head1Expr :: Sql DBType a => Expr (NonEmpty a) -> Expr a+head1Expr = shead1Expr typeInformation+++index1Expr :: Sql DBType a => Expr Int32 -> Expr (NonEmpty a) -> Expr (Nullify a)+index1Expr = sindex1Expr typeInformation+++last1Expr :: Sql DBType a => Expr (NonEmpty a) -> Expr a+last1Expr = slast1Expr typeInformation+++shead1Expr :: TypeInformation (Unnullify a) -> Expr (NonEmpty a) -> Expr a+shead1Expr info = mapPrimExpr (Prim.head info)+++sindex1Expr :: TypeInformation (Unnullify a) -> Expr Int32 -> Expr (NonEmpty a) -> Expr (Nullify a)+sindex1Expr info i = mapPrimExpr (Prim.index info (toPrimExpr i))+++slast1Expr :: TypeInformation (Unnullify a) -> Expr (NonEmpty a) -> Expr a+slast1Expr info = mapPrimExpr (Prim.last info)+++length1Expr :: Expr (NonEmpty a) -> Expr Int32+length1Expr = mapPrimExpr (Prim.length)
src/Rel8/Expr/Num.hs view
@@ -1,4 +1,5 @@ {-# language FlexibleContexts #-}+{-# language OverloadedStrings #-} {-# language TypeFamilies #-}  {-# options_ghc -fno-warn-redundant-constraints #-}@@ -18,7 +19,7 @@ -- rel import Rel8.Expr ( Expr( Expr ) ) import Rel8.Expr.Eq ( (==.) )-import Rel8.Expr.Function ( function )+import Rel8.Expr.Function (function) import Rel8.Expr.Opaleye ( castExpr ) import Rel8.Schema.Null ( Homonullable, Sql ) import Rel8.Table.Bool ( bool )@@ -32,7 +33,7 @@ fromIntegral (Expr a) = castExpr (Expr a)  --- | Cast 'DBNum' types to 'DBFractional' types. For example, his can be useful+-- | Cast 'DBNum' types to 'DBFractional' types. For example, this can be useful -- to convert @Expr Float@ to @Expr Double@. realToFrac :: (Sql DBNum a, Sql DBFractional b, Homonullable a b)   => Expr a -> Expr b@@ -71,13 +72,13 @@ -- PostgreSQL, which behaves like Haskell's 'Prelude.quot' rather than -- 'Prelude.div'. quot :: Sql DBIntegral a => Expr a -> Expr a -> Expr a-quot = function "div"+quot n d = function "div" (n, d)   -- | Corresponds to the @mod()@ function in PostgreSQL, which behaves like -- Haskell's 'Prelude.rem' rather than 'Prelude.mod'. rem :: Sql DBIntegral a => Expr a -> Expr a -> Expr a-rem = function "mod"+rem n d = function "mod" (n, d)   -- | Simultaneous 'quot' and 'rem'.
src/Rel8/Expr/Opaleye.hs view
@@ -1,6 +1,7 @@ {-# language FlexibleContexts #-} {-# language NamedFieldPuns #-} {-# language ScopedTypeVariables #-}+{-# language TypeApplications #-} {-# language TypeFamilies #-}  {-# options_ghc -fno-warn-redundant-constraints #-}@@ -26,6 +27,7 @@ import Rel8.Schema.Null ( Unnullify, Sql ) import Rel8.Type ( DBType, typeInformation ) import Rel8.Type.Information ( TypeInformation(..) )+import Rel8.Type.Name (TypeName, showTypeName)  -- profunctors import Data.Profunctor ( Profunctor, dimap )@@ -37,18 +39,19 @@  -- | Cast an expression to a different type. Corresponds to a @CAST()@ function -- call.-unsafeCastExpr :: Sql DBType b => Expr a -> Expr b-unsafeCastExpr = sunsafeCastExpr typeInformation+unsafeCastExpr :: forall b a. Sql DBType b => Expr a -> Expr b+unsafeCastExpr = case typeInformation @(Unnullify b) of+  TypeInformation {typeName} -> sunsafeCastExpr typeName   scastExpr :: TypeInformation (Unnullify a) -> Expr a -> Expr a-scastExpr = sunsafeCastExpr+scastExpr TypeInformation {typeName} = sunsafeCastExpr typeName   sunsafeCastExpr :: ()-  => TypeInformation (Unnullify b) -> Expr a -> Expr b-sunsafeCastExpr TypeInformation {typeName} =-  fromPrimExpr . Opaleye.CastExpr typeName . toPrimExpr+  => TypeName -> Expr a -> Expr b+sunsafeCastExpr name =+  fromPrimExpr . Opaleye.CastExpr (showTypeName name) . toPrimExpr   -- | Unsafely construct an expression from literal SQL.
+ src/Rel8/Expr/Read.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MonoLocalBinds #-}++module Rel8.Expr.Read+  ( read+  , sread+  )+where++-- base+import Prelude ()++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Opaleye (unsafeCastExpr, sunsafeCastExpr)+import Rel8.Schema.Null (Sql)+import Rel8.Type (DBType)+import Rel8.Type.Name (TypeName)++-- text+import Data.Text (Text)+++read :: Sql DBType a => Expr Text -> Expr a+read = unsafeCastExpr+++sread :: TypeName -> Expr Text -> Expr a+sread = sunsafeCastExpr
src/Rel8/Expr/Sequence.hs view
@@ -7,16 +7,19 @@ import Data.Int ( Int64 ) import Prelude +-- opaleye+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye+ -- rel8 import Rel8.Expr ( Expr )-import Rel8.Expr.Function ( function )-import Rel8.Expr.Serialize ( litExpr )-import Rel8.Expr.Text ( quoteIdent )---- text-import Data.Text ( pack )+import Rel8.Expr.Opaleye (fromPrimExpr)+import Rel8.Schema.QualifiedName (QualifiedName, showQualifiedName)   -- | See https://www.postgresql.org/docs/current/functions-sequence.html-nextval :: String -> Expr Int64-nextval = function "nextval" . quoteIdent . litExpr . pack+nextval :: QualifiedName -> Expr Int64+nextval name =+  fromPrimExpr $+    Opaleye.FunExpr "nextval"+      [ Opaleye.ConstExpr (Opaleye.StringLit (showQualifiedName name))+      ]
src/Rel8/Expr/Serialize.hs view
@@ -23,6 +23,7 @@ import Rel8.Expr.Opaleye ( scastExpr ) import Rel8.Schema.Null ( Unnullify, Nullity( Null, NotNull ), Sql, nullable ) import Rel8.Type ( DBType, typeInformation )+import Rel8.Type.Decoder (Decoder (..)) import Rel8.Type.Information ( TypeInformation(..) )  @@ -44,6 +45,6 @@   sparseValue :: Nullity a -> TypeInformation (Unnullify a) -> Hasql.Row a-sparseValue nullity TypeInformation {decode} = case nullity of-  Null -> Hasql.column $ Hasql.nullable decode-  NotNull -> Hasql.column $ Hasql.nonNullable decode+sparseValue nullity TypeInformation {decode = Decoder {binary}} = case nullity of+  Null -> Hasql.column $ Hasql.nullable binary+  NotNull -> Hasql.column $ Hasql.nonNullable binary
+ src/Rel8/Expr/Show.hs view
@@ -0,0 +1,18 @@+module Rel8.Expr.Show+  ( show+  )+where++-- base+import Prelude ()++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Opaleye (unsafeCastExpr)++-- text+import Data.Text (Text)+++show :: Expr a -> Expr Text+show = unsafeCastExpr
src/Rel8/Expr/Text.hs view
@@ -1,4 +1,5 @@ {-# language DataKinds #-}+{-# language OverloadedStrings #-}  module Rel8.Expr.Text   (@@ -32,9 +33,13 @@ -- bytestring import Data.ByteString ( ByteString ) +-- opaleye+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye+ -- rel8 import Rel8.Expr ( Expr )-import Rel8.Expr.Function ( binaryOperator, function, nullaryFunction )+import Rel8.Expr.Function (binaryOperator, function)+import Rel8.Expr.Opaleye (zipPrimExprsWith)  -- text import Data.Text (Text)@@ -120,7 +125,7 @@  -- | Corresponds to the @btrim@ function. btrim :: Expr Text -> Maybe (Expr Text) -> Expr Text-btrim a (Just b) = function "btrim" a b+btrim a (Just b) = function "btrim" (a, b) btrim a Nothing = function "btrim" a  @@ -131,27 +136,27 @@  -- | Corresponds to the @convert@ function. convert :: Expr ByteString -> Expr Text -> Expr Text -> Expr ByteString-convert = function "convert"+convert a b c = function "convert" (a, b, c)   -- | Corresponds to the @convert_from@ function. convertFrom :: Expr ByteString -> Expr Text -> Expr Text-convertFrom = function "convert_from"+convertFrom a b = function "convert_from" (a, b)   -- | Corresponds to the @convert_to@ function. convertTo :: Expr Text -> Expr Text -> Expr ByteString-convertTo = function "convert_to"+convertTo a b = function "convert_to" (a, b)   -- | Corresponds to the @decode@ function. decode :: Expr Text -> Expr Text -> Expr ByteString-decode = function "decode"+decode a b = function "decode" (a, b)   -- | Corresponds to the @encode@ function. encode :: Expr ByteString -> Expr Text -> Expr Text-encode = function "encode"+encode a b = function "encode" (a, b)   -- | Corresponds to the @initcap@ function.@@ -161,7 +166,7 @@  -- | Corresponds to the @left@ function. left :: Expr Text -> Expr Int32 -> Expr Text-left = function "left"+left a b = function "left" (a, b)   -- | Corresponds to the @length@ function.@@ -171,18 +176,18 @@  -- | Corresponds to the @length@ function. lengthEncoding :: Expr ByteString -> Expr Text -> Expr Int32-lengthEncoding = function "length"+lengthEncoding a b = function "length" (a, b)   -- | Corresponds to the @lpad@ function. lpad :: Expr Text -> Expr Int32 -> Maybe (Expr Text) -> Expr Text-lpad a b (Just c) = function "lpad" a b c-lpad a b Nothing = function "lpad" a b+lpad a b (Just c) = function "lpad" (a, b, c)+lpad a b Nothing = function "lpad" (a, b)   -- | Corresponds to the @ltrim@ function. ltrim :: Expr Text -> Maybe (Expr Text) -> Expr Text-ltrim a (Just b) = function "ltrim" a b+ltrim a (Just b) = function "ltrim" (a, b) ltrim a Nothing = function "ltrim" a  @@ -193,7 +198,7 @@  -- | Corresponds to the @pg_client_encoding()@ expression. pgClientEncoding :: Expr Text-pgClientEncoding = nullaryFunction "pg_client_encoding"+pgClientEncoding = function "pg_client_encoding" ()   -- | Corresponds to the @quote_ident@ function.@@ -214,25 +219,25 @@ -- | Corresponds to the @regexp_replace@ function. regexpReplace :: ()   => Expr Text -> Expr Text -> Expr Text -> Maybe (Expr Text) -> Expr Text-regexpReplace a b c (Just d) = function "regexp_replace" a b c d-regexpReplace a b c Nothing = function "regexp_replace" a b c+regexpReplace a b c (Just d) = function "regexp_replace" (a, b, c, d)+regexpReplace a b c Nothing = function "regexp_replace" (a, b, c)   -- | Corresponds to the @regexp_split_to_array@ function. regexpSplitToArray :: ()   => Expr Text -> Expr Text -> Maybe (Expr Text) -> Expr [Text]-regexpSplitToArray a b (Just c) = function "regexp_split_to_array" a b c-regexpSplitToArray a b Nothing = function "regexp_split_to_array" a b+regexpSplitToArray a b (Just c) = function "regexp_split_to_array" (a, b, c)+regexpSplitToArray a b Nothing = function "regexp_split_to_array" (a, b)   -- | Corresponds to the @repeat@ function. repeat :: Expr Text -> Expr Int32 -> Expr Text-repeat = function "repeat"+repeat a b = function "repeat" (a, b)   -- | Corresponds to the @replace@ function. replace :: Expr Text -> Expr Text -> Expr Text -> Expr Text-replace = function "replace"+replace a b c = function "replace" (a, b, c)   -- | Corresponds to the @reverse@ function.@@ -242,40 +247,40 @@  -- | Corresponds to the @right@ function. right :: Expr Text -> Expr Int32 -> Expr Text-right = function "right"+right a b = function "right" (a, b)   -- | Corresponds to the @rpad@ function. rpad :: Expr Text -> Expr Int32 -> Maybe (Expr Text) -> Expr Text-rpad a b (Just c) = function "rpad" a b c-rpad a b Nothing = function "rpad" a b+rpad a b (Just c) = function "rpad" (a, b, c)+rpad a b Nothing = function "rpad" (a, b)   -- | Corresponds to the @rtrim@ function. rtrim :: Expr Text -> Maybe (Expr Text) -> Expr Text-rtrim a (Just b) = function "rtrim" a b+rtrim a (Just b) = function "rtrim" (a, b) rtrim a Nothing = function "rtrim" a   -- | Corresponds to the @split_part@ function. splitPart :: Expr Text -> Expr Text -> Expr Int32 -> Expr Text-splitPart = function "split_part"+splitPart a b c = function "split_part" (a, b, c)   -- | Corresponds to the @strpos@ function. strpos :: Expr Text -> Expr Text -> Expr Int32-strpos = function "strpos"+strpos a b = function "strpos" (a, b)   -- | Corresponds to the @substr@ function. substr :: Expr Text -> Expr Int32 -> Maybe (Expr Int32) -> Expr Text-substr a b (Just c) = function "substr" a b c-substr a b Nothing = function "substr" a b+substr a b (Just c) = function "substr" (a, b, c)+substr a b Nothing = function "substr" (a, b)   -- | Corresponds to the @translate@ function. translate :: Expr Text -> Expr Text -> Expr Text -> Expr Text-translate = function "translate"+translate a b c = function "translate" (a, b, c)   -- | @like x y@ corresponds to the expression @y LIKE x@.@@ -284,7 +289,7 @@ -- you can write expressions like -- @filter (like "Rel%" . packageName) =<< each haskellPackages@ like :: Expr Text -> Expr Text -> Expr Bool-like = flip (binaryOperator "LIKE")+like = flip (zipPrimExprsWith (Opaleye.BinExpr Opaleye.OpLike))   -- | @ilike x y@ corresponds to the expression @y ILIKE x@.@@ -293,4 +298,4 @@ -- you can write expressions like -- @filter (ilike "Rel%" . packageName) =<< each haskellPackages@ ilike :: Expr Text -> Expr Text -> Expr Bool-ilike = flip (binaryOperator "ILIKE")+ilike = flip (zipPrimExprsWith (Opaleye.BinExpr Opaleye.OpILike))
src/Rel8/Expr/Time.hs view
@@ -1,3 +1,5 @@+{-# language OverloadedStrings #-}+ module Rel8.Expr.Time   ( -- * Working with @Day@     today@@ -30,7 +32,7 @@  -- rel8 import Rel8.Expr ( Expr )-import Rel8.Expr.Function ( binaryOperator, nullaryFunction )+import Rel8.Expr.Function (binaryOperator, function) import Rel8.Expr.Opaleye ( castExpr, unsafeCastExpr, unsafeLiteral )  -- time@@ -71,7 +73,7 @@  -- | Corresponds to @now()@. now :: Expr UTCTime-now = nullaryFunction "now"+now = function "now" ()   -- | Add a time interval to a point in time, yielding a new point in time.
src/Rel8/Expr/Window.hs view
@@ -6,11 +6,11 @@   , percentRank   , cumeDist   , ntile-  , lag-  , lead-  , firstValue-  , lastValue-  , nthValue+  , lagExpr, lagExprOn+  , leadExpr, leadExprOn+  , firstValueExpr, firstValueExprOn+  , lastValueExpr, lastValueExprOn+  , nthValueExpr, nthValueExprOn   ) where @@ -25,97 +25,125 @@ 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))   -- | [@lag(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html)-lag :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a)-lag offset def =+lagExpr :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a)+lagExpr offset def =   fromWindowFunction $     dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $       Opaleye.lag (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def))  +-- | Applies 'lag' to the column selected by the given function.+lagExprOn :: Expr Int32 -> Expr a -> (i -> Expr a) -> Window i (Expr a)+lagExprOn offset def f = lmap f (lagExpr 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 =+leadExpr :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a)+leadExpr offset def =   fromWindowFunction $     dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $       Opaleye.lead (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def))  +-- | Applies 'lead' to the column selected by the given function.+leadExprOn :: Expr Int32 -> Expr a -> (i -> Expr a) -> Window i (Expr a)+leadExprOn offset def f = lmap f (leadExpr offset def)++ -- | [@first_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)-firstValue :: Window (Expr a) (Expr a)-firstValue =+firstValueExpr :: Window (Expr a) (Expr a)+firstValueExpr =   fromWindowFunction $     dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn)       Opaleye.firstValue  +-- | Applies 'firstValue' to the column selected by the given function.+firstValueExprOn :: (i -> Expr a) -> Window i (Expr a)+firstValueExprOn f = lmap f firstValueExpr++ -- | [@last_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)-lastValue :: Window (Expr a) (Expr a)-lastValue =+lastValueExpr :: Window (Expr a) (Expr a)+lastValueExpr =   fromWindowFunction $     dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn)       Opaleye.lastValue  +-- | Applies 'lastValue' to the column selected by the given function.+lastValueExprOn :: (i -> Expr a) -> Window i (Expr a)+lastValueExprOn f = lmap f lastValueExpr++ -- | [@nth_value(value, n)@](https://www.postgresql.org/docs/current/functions-window.html)-nthValue :: Expr Int32 -> Window (Expr a) (Expr (Nullify a))-nthValue n =+nthValueExpr :: Expr Int32 -> Window (Expr a) (Expr (Nullify a))+nthValueExpr n =   fromWindowFunction $     dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $       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)+nthValueExprOn :: Expr Int32 -> (i -> Expr a) -> Window i (Expr (Nullify a))+nthValueExprOn n f = lmap f (nthValueExpr n)  -fromWindowFunction :: Opaleye.WindowFunction a b -> Window a b+fromAggregate :: Aggregator' fold i a -> Opaleye.Aggregator i a+fromAggregate (Aggregator _ a) = a+++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)
src/Rel8/Generic/Construction.hs view
@@ -7,6 +7,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-} {-# language ViewPatterns #-} @@ -15,23 +16,21 @@   , 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 )@@ -39,10 +38,10 @@ 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@@ -55,7 +54,6 @@   , 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@@ -69,11 +67,14 @@ 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))))@@ -137,7 +138,6 @@ 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))))@@ -148,20 +148,16 @@ 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))     )@@ -249,6 +245,43 @@             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@@ -281,72 +314,3 @@       (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
src/Rel8/Generic/Rel8able.hs view
@@ -33,7 +33,6 @@ 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 @@   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
src/Rel8/Kind/Context.hs view
@@ -14,7 +14,6 @@ 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 @@  type SContext :: Context -> Type data SContext context where-  SAggregate :: SContext Aggregate   SExpr :: SContext Expr   SField :: SContext (Field table)   SName :: SContext Name@@ -34,10 +32,6 @@ type Reifiable :: Context -> Constraint class Reifiable context where   contextSing :: SContext context---instance Reifiable Aggregate where-  contextSing = SAggregate   instance Reifiable Expr where
src/Rel8/Query/Aggregate.hs view
@@ -1,56 +1,59 @@ {-# language FlexibleContexts #-} {-# language MonoLocalBinds #-} {-# language ScopedTypeVariables #-}-{-# language TypeApplications #-}  module Rel8.Query.Aggregate   ( aggregate+  , aggregate1+  , aggregateU   , countRows-  , mode   ) where  -- base-import Data.Functor.Contravariant ( (>$<) )+import Control.Applicative (liftA2) import Data.Int ( Int64 ) import Prelude  -- opaleye+import qualified Opaleye.Adaptors as Opaleye 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 )+import Rel8.Expr.Bool (true) import Rel8.Query ( Query )-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.Maybe (fromMaybeTable)+import Rel8.Table.Opaleye (unpackspec)  --- | 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 i, 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 :: Table Expr i => Aggregator' fold i a -> Query i -> Query a+aggregate1 = aggregateU unpackspec+++aggregateU :: Opaleye.Unpackspec i i -> Aggregator' fold i a -> Query i -> Query a+aggregateU unpack (Aggregator _ aggregator) =+  mapOpaleye (Opaleye.aggregateExplicit unpack 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)----- | 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)+countRows = aggregate countStar . (true <$)
src/Rel8/Query/Distinct.hs view
@@ -17,13 +17,13 @@ import Rel8.Query ( Query ) import Rel8.Query.Opaleye ( mapOpaleye ) import Rel8.Table.Eq ( EqTable )-import Rel8.Table.Opaleye ( distinctspec, unpackspec )+import Rel8.Table.Opaleye (distinctspec, unpackspec)   -- | Select all distinct rows from a query, removing duplicates.  @distinct q@ -- is equivalent to the SQL statement @SELECT DISTINCT q@. distinct :: EqTable a => Query a -> Query a-distinct = mapOpaleye (Opaleye.distinctExplicit distinctspec)+distinct = mapOpaleye (Opaleye.distinctExplicit unpackspec distinctspec)   -- | Select all distinct rows from a query, where rows are equivalent according
+ src/Rel8/Query/Function.hs view
@@ -0,0 +1,32 @@+{-# language FlexibleContexts #-}+{-# language MonoLocalBinds #-}++module Rel8.Query.Function+  ( queryFunction+  )+where++-- base+import Prelude++-- opaleye+import qualified Opaleye.Internal.Operators as Opaleye++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Function (Arguments, primFunction)+import Rel8.Query (Query)+import Rel8.Query.Opaleye (fromOpaleye)+import Rel8.Schema.QualifiedName (QualifiedName)+import Rel8.Table (Table)+import Rel8.Table.Opaleye (castTable, relExprPP)+++-- | Select each row from a function that returns a relation. This is+-- equivalent to @FROM function(input)@.+queryFunction :: (Arguments input, Table Expr output)+  => QualifiedName -> input -> Query output+queryFunction name input = fmap castTable $ fromOpaleye $+  Opaleye.relationValuedExprExplicit relExprPP (const expr)+  where+    expr = primFunction name input
src/Rel8/Query/List.hs view
@@ -11,6 +11,7 @@ where  -- base+import Control.Monad ((>=>)) import Data.Functor.Identity ( runIdentity ) import Data.List.NonEmpty ( NonEmpty ) import Prelude@@ -23,21 +24,18 @@ 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.Rebind ( rebind )+import Rel8.Query.Aggregate (aggregate, aggregate1)+import Rel8.Query.Rebind (hrebind, rebind)+import Rel8.Schema.HTable (HTable, hfield, hspecs, htabulate) import Rel8.Schema.HTable.Vectorize ( hunvectorize )-import Rel8.Schema.Null ( Sql, Unnullify )+import Rel8.Schema.Null ( Sql ) import Rel8.Schema.Spec ( Spec( Spec, info ) )-import Rel8.Table ( Table, fromColumns )-import Rel8.Table.Cols ( toCols )+import Rel8.Table (Table, fromColumns, toColumns) 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 ( DBType ) import Rel8.Type.Array ( extractArrayElement )-import Rel8.Type.Information ( TypeInformation )   -- | Aggregate a 'Query' into a 'ListTable'. If the supplied query returns 0@@ -48,11 +46,7 @@ -- @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 +58,17 @@ -- @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@@ -86,8 +77,8 @@ -- @catListTable@ is an inverse to 'many'. catListTable :: Table Expr a => ListTable Expr a -> Query a catListTable (ListTable as) =-  rebind "unnest" $ fromColumns $ runIdentity $-    hunvectorize (\Spec {info} -> pure . sunnest info) as+  fmap fromColumns $ (hrebind "unnest" >=> hextract) $ runIdentity $+    hunvectorize (\_ -> pure . unnest) as   -- | Expand a 'NonEmptyTable' into a 'Query', where each row in the query is an@@ -96,8 +87,8 @@ -- @catNonEmptyTable@ is an inverse to 'some'. catNonEmptyTable :: Table Expr a => NonEmptyTable Expr a -> Query a catNonEmptyTable (NonEmptyTable as) =-  rebind "unnest" $ fromColumns $ runIdentity $-    hunvectorize (\Spec {info} -> pure . sunnest info) as+  fmap fromColumns $ (hrebind "unnest" >=> hextract) $ runIdentity $+    hunvectorize (\_ -> pure . unnest) as   -- | Expand an expression that contains a list into a 'Query', where each row@@ -105,7 +96,7 @@ -- -- @catList@ is an inverse to 'manyExpr'. catList :: Sql DBType a => Expr [a] -> Query (Expr a)-catList = rebind "unnest" . sunnest typeInformation+catList = rebind "unnest" . unnest >=> extract   -- | Expand an expression that contains a non-empty list into a 'Query', where@@ -113,10 +104,21 @@ -- -- @catNonEmpty@ is an inverse to 'someExpr'. catNonEmpty :: Sql DBType a => Expr (NonEmpty a) -> Query (Expr a)-catNonEmpty = rebind "unnest" . sunnest typeInformation+catNonEmpty = rebind "unnest" . unnest >=> extract  -sunnest :: TypeInformation (Unnullify a) -> Expr (list a) -> Expr a-sunnest info = mapPrimExpr $-  extractArrayElement info .-  Opaleye.UnExpr (Opaleye.UnOpOther "UNNEST")+unnest :: Expr (list a) -> Expr a+unnest = mapPrimExpr $ Opaleye.UnExpr (Opaleye.UnOpOther "UNNEST")+++extract :: Table Expr a => a -> Query a+extract = fmap fromColumns . hextract . toColumns+++hextract :: HTable t => t Expr -> Query (t Expr)+hextract = hrebind "extract" . go+  where+    go as = htabulate $ \field ->+      case hfield as field of+        a -> case hfield hspecs field of+          Spec {info} -> mapPrimExpr (extractArrayElement info) a
+ src/Rel8/Query/Loop.hs view
@@ -0,0 +1,73 @@+{-# language FlexibleContexts #-}++module Rel8.Query.Loop+  ( loop+  , loopDistinct+  ) where++-- base+import Prelude++-- opaleye+import Opaleye.With (withRecursiveExplicit, withRecursiveDistinctExplicit)++-- rel8+import Rel8.Expr ( Expr )+import Rel8.Query ( Query )+import Rel8.Query.Opaleye ( fromOpaleye, toOpaleye )+import Rel8.Table ( Table )+import Rel8.Table.Opaleye ( binaryspec )+++-- | 'loop' allows the construction of recursive queries, using Postgres'+-- [@WITH RECURSIVE@](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE)+-- under the hood. The first argument to 'loop' is what the Postgres+-- documentation refers to as the \"non-recursive term\" and the second+-- argument is the \"recursive term\", which is defined in terms of the result+-- of the \"non-recursive term\". 'loop' uses @UNION ALL@ to combine the+-- recursive and non-recursive terms.+--+-- Denotionally, @'loop' s f@ is the smallest set of rows @r@ such+-- that+--+-- @+-- r == s \`'Rel8.unionAll'\` (r >>= f)+-- @+--+-- Operationally, @'loop' s f@ takes each row in an initial set @s@ and+-- supplies it to @f@, resulting in a new generation of rows which are added+-- to the result set. Each row from this new generation is then fed back to+-- @f@, and this process is repeated until a generation comes along for which+-- @f@ returns an empty set for each row therein.+loop :: Table Expr a => Query a -> (a -> Query a) -> Query a+loop base recurse =+  fromOpaleye $ withRecursiveExplicit binaryspec base' recurse'+  where+    base' = toOpaleye base+    recurse' = toOpaleye . recurse+++-- | 'loopDistinct' is like 'loop' but uses @UNION@ instead of @UNION ALL@ to+-- combine the recursive and non-recursive terms.+--+-- Denotationally, @'loopDistinct' s f@ is the smallest set of rows+-- @r@ such that+--+-- @+-- r == s \`'Rel8.union'\` (r >>= f)+-- @+--+-- Operationally, @'loopDistinct' s f@ takes each /distinct/ row in an+-- initial set @s@ and supplies it to @f@, resulting in a new generation of+-- rows. Any rows returned by @f@ that already exist in the result set are not+-- considered part of this new generation by 'loopDistinct' (in contrast to+-- 'loop'). This new generation is then added to the result set, and each row+-- therein is then fed back to @f@, and this process is repeated until a+-- generation comes along for which @f@ returns no rows that don't already+-- exist in the result set.+loopDistinct :: Table Expr a => Query a -> (a -> Query a) -> Query a+loopDistinct base recurse =+  fromOpaleye $ withRecursiveDistinctExplicit binaryspec base' recurse'+  where+    base' = toOpaleye base+    recurse' = toOpaleye . recurse
+ src/Rel8/Query/Materialize.hs view
@@ -0,0 +1,42 @@+{-# language FlexibleContexts #-}++module Rel8.Query.Materialize+  ( materialize+  )+where++-- base+import Prelude++-- opaleye+import Opaleye.With ( withMaterializedExplicit )++-- rel8+import Rel8.Expr ( Expr )+import Rel8.Query ( Query )+import Rel8.Query.Opaleye ( fromOpaleye, toOpaleye )+import Rel8.Query.Rebind ( rebind )+import Rel8.Table ( Table )+import Rel8.Table.Opaleye ( unpackspec )+++-- | 'materialize' takes a 'Query' and fully evaluates it and caches the+-- 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,+-- specifically the @WITH _ AS MATERIALIZED (_)@ form introduced in PostgreSQL+-- 12. This means that 'materialize' can only be used with PostgreSQL 12 or+-- newer.+materialize :: (Table Expr a, Table Expr b)+  => Query a -> (Query a -> Query b) -> Query b+materialize query f =+  (>>= rebind "with") . fromOpaleye $+    withMaterializedExplicit unpackspec+      (toOpaleye query')+      (toOpaleye . f . fromOpaleye)+  where+    query' = query
src/Rel8/Query/Maybe.hs view
@@ -21,11 +21,10 @@ -- 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 @@ -- 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.
src/Rel8/Query/Rebind.hs view
@@ -2,6 +2,7 @@  module Rel8.Query.Rebind   ( rebind+  , hrebind   ) where @@ -15,7 +16,10 @@ -- rel8 import Rel8.Expr ( Expr ) import Rel8.Query ( Query )+import Rel8.Query.Limit (offset)+import Rel8.Schema.HTable (HTable) import Rel8.Table ( Table )+import Rel8.Table.Cols (Cols (Cols)) import Rel8.Table.Opaleye ( unpackspec ) import Rel8.Query.Opaleye (fromOpaleye) @@ -24,4 +28,9 @@ -- to a new variable in the SQL. The @a@ returned consists only of these -- variables. It's essentially a @let@ binding for Postgres expressions. rebind :: Table Expr a => String -> a -> Query a-rebind prefix a = fromOpaleye (Opaleye.rebindExplicitPrefix prefix unpackspec <<< pure a)+rebind prefix a = offset 0 $+  fromOpaleye (Opaleye.rebindExplicitPrefix prefix unpackspec <<< pure a)+++hrebind :: HTable t => String -> t Expr -> Query (t Expr)+hrebind prefix = fmap (\(Cols a) -> a) . rebind prefix . Cols
src/Rel8/Query/SQL.hs view
@@ -9,13 +9,19 @@ -- base import Prelude +-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye+ -- rel8 import Rel8.Expr ( Expr ) import Rel8.Query ( Query ) import Rel8.Statement.Select ( ppSelect ) import Rel8.Table ( Table ) +-- transformers+import Control.Monad.Trans.State.Strict (evalState) + -- | Convert a 'Query' to a 'String' containing a @SELECT@ statement. showQuery :: Table Expr a => Query a -> String-showQuery = show . ppSelect+showQuery = show . (`evalState` Opaleye.start) . ppSelect
src/Rel8/Schema/Context/Nullify.hs view
@@ -10,7 +10,6 @@   ( Nullifiability(..), NonNullifiability(..), nullifiableOrNot, absurd   , Nullifiable, nullifiability   , guarder, nullifier, unnullifier-  , sguard, snullify   ) where @@ -24,7 +23,6 @@ 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 @@  type Nullifiability :: K.Context -> Type data Nullifiability context where-  NAggregate :: Nullifiability Aggregate   NExpr :: Nullifiability Expr   NName :: Nullifiability Name @@ -50,10 +47,6 @@   nullifiability :: Nullifiability context  -instance Nullifiable Aggregate where-  nullifiability = NAggregate-- instance Nullifiable Expr where   nullifiability = NExpr @@ -72,7 +65,6 @@   => 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 @@  absurd :: Nullifiability context -> NonNullifiability context -> a absurd = \case-  NAggregate -> \case   NExpr -> \case   NName -> \case @@ -94,7 +85,6 @@   -> 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 @@   -> 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 @@   -> 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 
+ src/Rel8/Schema/Escape.hs view
@@ -0,0 +1,20 @@+{-# language LambdaCase #-}++module Rel8.Schema.Escape+  ( escape+  )+where++-- base+import Prelude++-- pretty+import Text.PrettyPrint (Doc, doubleQuotes, text)+++escape :: String -> Doc+escape = doubleQuotes . text . concatMap go+  where+    go = \case+      '"' -> "\"\""+      c -> [c]
src/Rel8/Schema/HTable.hs view
@@ -15,15 +15,17 @@  module Rel8.Schema.HTable   ( HTable (HField, HConstrainTable)-  , hfield, htabulate, htraverse, hdicts, hspecs-  , hfoldMap, hmap, htabulateA, htraverseP, htraversePWithField+  , hfield, htabulate, hdicts, hspecs+  , hfoldMap, hmap, htabulateA, htabulateP+  , htraverse, htraverse_, htraverseP, htraversePWithField   ) where  -- base+import Data.Functor (void)+import Data.Functor.Compose ( Compose( Compose ), getCompose ) import Data.Functor.Const ( Const( Const ), getConst ) import Data.Kind ( Constraint, Type )-import Data.Functor.Compose ( Compose( Compose ), getCompose ) import Data.Proxy ( Proxy ) import GHC.Generics   ( (:*:)( (:*:) )@@ -46,7 +48,7 @@ import qualified Rel8.Schema.Kind as K  -- semigroupoids-import Data.Functor.Apply ( Apply, (<.>) )+import Data.Functor.Apply (Apply, (<.>), liftF2)  -- | A @HTable@ is a functor-indexed/higher-kinded data type that is -- representable ('htabulate'/'hfield'), constrainable ('hdicts'), and@@ -130,27 +132,53 @@ hmap f a = htabulate $ \field -> f (hfield a field)  +newtype Ap f a = Ap+  { getAp :: f a+  }+++instance (Apply f, Semigroup a) => Semigroup (Ap f a) where+  Ap a <> Ap b = Ap (liftF2 (<>) a b)+++htraverse_ :: (HTable t, Apply f)+  => (forall a. context a -> f b) -> t context -> f ()+htraverse_ f a = getAp $ hfoldMap (Ap . void . f) a++ htabulateA :: (HTable t, Apply m)   => (forall a. HField t a -> m (context a)) -> m (t context) htabulateA f = htraverse getCompose $ htabulate $ Compose . f {-# INLINABLE htabulateA #-} ++htabulateP :: (HTable t, ProductProfunctor p)+  => (forall a. HField t a -> p i (context a)) -> p i (t context)+htabulateP f = unApplyP $ htraverse (ApplyP . getCompose) $ htabulate $ Compose . f+{-# INLINABLE htabulateP #-}++ newtype ApplyP p a b = ApplyP { unApplyP :: p a b } + instance Profunctor p => Functor (ApplyP p a) where   fmap f = ApplyP . rmap f . unApplyP + instance ProductProfunctor p => Apply (ApplyP p a) where   ApplyP f <.> ApplyP x = ApplyP (rmap id f **** x) + htraverseP :: (HTable t, ProductProfunctor p)   => (forall a. p (f a) (g a)) -> p (t f) (t g) htraverseP f = htraversePWithField (const f) + htraversePWithField :: (HTable t, ProductProfunctor p)   => (forall a. HField t a -> p (f a) (g a)) -> p (t f) (t g)-htraversePWithField f = unApplyP $ htabulateA $ \field -> ApplyP $-  lmap (flip hfield field) (f field)+htraversePWithField f =+  htabulateP $ \field -> lmap (flip hfield field) (f field)+  type GHField :: K.HTable -> Type -> Type newtype GHField t a = GHField (HField (GHColumns (Rep (t Proxy))) a)
src/Rel8/Schema/HTable/Nullify.hs view
@@ -32,9 +32,17 @@ import GHC.Generics ( Generic ) import Prelude hiding ( null ) +-- profunctors+import Data.Profunctor (lmap, rmap)++-- product-profunctors+import Data.Profunctor.Product (ProductProfunctor)+ -- rel8 import Rel8.FCF ( Eval, Exp )-import Rel8.Schema.HTable ( HTable, hfield, htabulate, htabulateA, hspecs )+import Rel8.Schema.HTable+  ( HTable, hfield, hspecs, htabulate, htabulateA, htabulateP+  ) import Rel8.Schema.HTable.MapTable   ( HMapTable, HMapTableField( HMapTableField )   , MapSpec, mapInfo@@ -90,13 +98,11 @@ {-# INLINABLE hnulls #-}  -hnullify :: HTable t-  => (forall a. Spec a -> context a -> context (Type.Nullify a))-  -> t context-  -> HNullify t context-hnullify nullifier a = HNullify $ htabulate $ \(HMapTableField field) ->-  case hfield hspecs field of-    spec@Spec {} -> nullifier spec (hfield a field)+hnullify :: (HTable t, ProductProfunctor p)+  => (forall a. Spec a -> p (context a) (context (Type.Nullify a)))+  -> p (t context) (HNullify t context)+hnullify nullifier = rmap HNullify $ htabulateP $ \(HMapTableField field) ->+  lmap (`hfield` field) (nullifier (hfield hspecs field)) {-# INLINABLE hnullify #-}  
src/Rel8/Schema/HTable/Vectorize.hs view
@@ -2,13 +2,15 @@ {-# language ConstraintKinds #-} {-# language DataKinds #-} {-# language DeriveAnyClass #-}+{-# language DeriveFunctor #-} {-# language DeriveGeneric #-}-{-# language DerivingStrategies #-}+{-# language DerivingVia #-} {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language GADTs #-} {-# language LambdaCase #-} {-# language MultiParamTypeClasses #-}+{-# language NamedFieldPuns #-} {-# language RankNTypes #-} {-# language RecordWildCards #-} {-# language ScopedTypeVariables #-}@@ -19,24 +21,37 @@  module Rel8.Schema.HTable.Vectorize   ( HVectorize-  , hvectorize, hunvectorize+  , hvectorize, hvectorizeA, hunvectorize+  , hnullify   , happend, hempty   , hproject+  , htraverseVectorP   , hcolumn+  , First (..)   ) where  -- base import Data.Kind ( Constraint, Type ) import Data.List.NonEmpty ( NonEmpty )+import qualified Data.Semigroup as Base import GHC.Generics (Generic) import Prelude +-- product-profunctors+import Data.Profunctor.Product (ProductProfunctor)++-- profunctors+import Data.Profunctor (dimap)+ -- rel8 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+  , htraversePWithField+  ) import Rel8.Schema.HTable.Identity ( HIdentity( HIdentity ) ) import Rel8.Schema.HTable.MapTable   ( HMapTable( HMapTable ), HMapTableField( HMapTableField )@@ -44,15 +59,20 @@   , Precompose( Precompose )   ) import qualified Rel8.Schema.HTable.MapTable as HMapTable ( hproject )-import Rel8.Schema.Null ( Unnullify, NotNull, Nullity( NotNull ) )+import Rel8.Schema.HTable.Nullify (HNullify (HNullify))+import Rel8.Schema.Null (Nullify, Unnullify, NotNull, Nullity (NotNull)) import Rel8.Schema.Spec ( Spec(..) ) import Rel8.Type.Array ( listTypeInformation, nonEmptyTypeInformation ) import Rel8.Type.Information ( TypeInformation )  -- semialign-import Data.Zip ( Unzip, Zip, Zippy(..) )+import Data.Align (Semialign, alignWith)+import Data.Zip (Unzip, Zip, Zippy(..), zipWith) +-- semigroupoids+import Data.Functor.Apply (Apply) + type Vector :: (Type -> Type) -> Constraint class Vector list where   listNotNull :: proxy a -> Dict NotNull (list a)@@ -104,6 +124,16 @@ {-# 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@@ -139,5 +169,37 @@ hproject f (HVectorize a) = HVectorize (HMapTable.hproject f a)  +htraverseVectorP :: (HTable t, ProductProfunctor p)+  => (forall a. HField t a -> p (f (list a)) (g (list' a)))+  -> p (HVectorize list t f) (HVectorize list' t g)+htraverseVectorP f =+  dimap (\(HVectorize (HMapTable a)) -> a) (HVectorize . HMapTable) $+    htraversePWithField $ \field ->+      dimap (\(Precompose a) -> a) Precompose (f field)++ hcolumn :: HVectorize list (HIdentity a) context -> context (list a) hcolumn (HVectorize (HMapTable (HIdentity (Precompose a)))) = a+++hnullify :: forall t list context. (HTable t, Vector list)+  => (forall a. Spec a -> context (list a) -> context (Nullify a))+  -> HVectorize list t context+  -> HNullify t context+hnullify f (HVectorize table) = HNullify $+  htabulate $ \(HMapTableField field) -> case hfield hspecs field of+    spec -> case hfield table (HMapTableField field) of+      a -> f spec a+++newtype First a b = First {getFirst :: a}+  deriving stock Functor+  deriving (Semigroup) via (Base.First a)+++instance Semialign (First a) where+  alignWith _ (First a) _ = First a+++instance Zip (First a) where+  zipWith _ (First a) _ = First a
src/Rel8/Schema/Null.hs view
@@ -7,6 +7,7 @@ {-# language RankNTypes #-} {-# language StandaloneKindSignatures #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-} {-# language UndecidableSuperClasses #-} 
+ src/Rel8/Schema/QualifiedName.hs view
@@ -0,0 +1,54 @@+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language RecordWildCards #-}+{-# language StandaloneKindSignatures #-}+{-# language StrictData #-}++module Rel8.Schema.QualifiedName+  ( QualifiedName (..)+  , ppQualifiedName+  , showQualifiedName+  )+where++-- base+import Data.Kind (Type)+import Data.String (IsString, fromString)+import Prelude++-- pretty+import Text.PrettyPrint (Doc, text)++-- rel8+import Rel8.Schema.Escape (escape)+++-- | A name of an object (such as a table, view, function or sequence)+-- qualified by an optional schema. In the absence of an explicit schema,+-- the connection's @search_path@ will be used implicitly.+type QualifiedName :: Type+data QualifiedName = QualifiedName+  { name :: String+    -- ^ The name of the object.+  , schema :: Maybe String+    -- ^ The schema that this object belongs to. If 'Nothing', whatever is on+    -- the connection's @search_path@ will be used.+   }+  deriving stock (Eq, Ord, Show)+++-- | Constructs 'QualifiedName's with 'schema' set to 'Nothing'.+instance IsString QualifiedName where+  fromString name = QualifiedName {schema = Nothing, ..}+++ppQualifiedName :: QualifiedName -> Doc+ppQualifiedName QualifiedName {schema = mschema, ..} = case mschema of+  Nothing -> name'+  Just schema -> escape schema <> text "." <> name'+  where+    name' = escape name+++showQualifiedName :: QualifiedName -> String+showQualifiedName = show . ppQualifiedName
src/Rel8/Schema/Table.hs view
@@ -1,8 +1,9 @@ {-# language DeriveFunctor #-} {-# language DerivingStrategies #-}-{-# language DisambiguateRecordFields #-}+{-# language DuplicateRecordFields #-} {-# language NamedFieldPuns #-} {-# language StandaloneKindSignatures #-}+{-# language StrictData #-}  module Rel8.Schema.Table   ( TableSchema(..)@@ -14,14 +15,13 @@ import Data.Kind ( Type ) import Prelude --- opaleye-import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye-import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye- -- pretty import Text.PrettyPrint ( Doc ) +-- rel8+import Rel8.Schema.QualifiedName (QualifiedName, ppQualifiedName) + -- | The schema for a table. This is used to specify the name and schema that a -- table belongs to (the @FROM@ part of a SQL query), along with the schema of -- the columns within this table.@@ -30,20 +30,14 @@ -- @TableSchema@ in order to interact with the table via Rel8. type TableSchema :: Type -> Type data TableSchema names = TableSchema-  { name :: String+  { name :: QualifiedName     -- ^ The name of the table.-  , schema :: Maybe String-    -- ^ The schema that this table belongs to. If 'Nothing', whatever is on-    -- the connection's @search_path@ will be used.   , columns :: names-    -- ^ The columns of the table. Typically you would use a a higher-kinded-    -- data type here, parameterized by the 'Rel8.ColumnSchema.ColumnSchema' functor.+    -- ^ The columns of the table. Typically you would use a 'Rel8.Rel8able'+    -- data type here, parameterized by the 'Rel8.Name' context.   }   deriving stock Functor   ppTable :: TableSchema a -> Doc-ppTable TableSchema {name, schema} = Opaleye.ppTable Opaleye.SqlTable-  { sqlTableSchemaName = schema-  , sqlTableName = name-  }+ppTable TableSchema {name} = ppQualifiedName name
+ src/Rel8/Statement.hs view
@@ -0,0 +1,319 @@+{-# language DeriveFunctor #-}+{-# language DerivingVia #-}+{-# language FlexibleContexts #-}+{-# language GADTs #-}+{-# language LambdaCase #-}+{-# language NamedFieldPuns #-}+{-# language RankNTypes #-}+{-# language RecordWildCards #-}+{-# language ScopedTypeVariables #-}+{-# language StandaloneKindSignatures #-}+{-# language TypeApplications #-}++module Rel8.Statement+  ( Statement+  , statementReturning+  , statementNoReturning+  , ppDecodeStatement+  )+where++-- base+import Control.Applicative (liftA2)+import Control.Monad (ap, liftM2)+import Data.Foldable (fold, toList)+import Data.Int (Int64)+import Data.Kind (Type)+import Data.List.NonEmpty (NonEmpty, intersperse)+import Data.Monoid (Endo (Endo))+import Data.String (fromString)+import Prelude++-- hasql+import qualified Hasql.Decoders as Hasql++-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye++-- pretty+import Text.PrettyPrint+  ( Doc+  , (<+>)+  , ($$)+  , comma+  , hcat+  , parens+  , punctuate+  , text+  , vcat+  )++-- rel8+import Rel8.Expr (Expr)+import Rel8.Expr.Bool (false)+import Rel8.Query (Query)+import Rel8.Query.Aggregate (countRows)+import Rel8.Query.Each (each)+import Rel8.Schema.Escape (escape)+import Rel8.Schema.Table (TableSchema (..))+import Rel8.Statement.Rows (Rows (..))+import Rel8.Table (Table)+import Rel8.Table.Cols (fromCols)+import Rel8.Table.Name (namesFromLabelsWithA, showNames)+import Rel8.Table.Serialize (parse)++-- semigroupoids+import Data.Functor.Apply (Apply, WrappedApplicative (..))+import Data.Functor.Bind (Bind, (>>-))++-- transformers+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.State.Strict (State, evalState)+import Control.Monad.Trans.Writer.CPS (WriterT, runWriterT, tell)+++type Binding :: Type+data Binding = Binding+  { relation :: !String+  , columns :: !(Maybe (NonEmpty String))+  , doc :: !Doc+  , returning :: !Returning+  }+++type Result :: Type -> Type+data Result a = Unmodified !a | Modified !a+++instance Functor Result where+  fmap f = \case+    Unmodified a -> Modified (f a)+    Modified a -> Modified (f a)+++getResult :: Result a -> a+getResult = \case+  Unmodified a -> a+  Modified a -> a+++type Returning :: Type+data Returning where+  NoReturning :: Returning+  Returning :: Query (Expr Int64) -> Returning +++-- | 'Statement' represents a single PostgreSQL statement. Most commonly,+-- this is constructed using 'Rel8.select', 'Rel8.insert', 'Rel8.update'+-- or 'Rel8.delete'.+--+-- However, in addition to @SELECT@, @INSERT@, @UPDATE@ and @DELETE@,+-- PostgreSQL also supports compositions thereof via its statement-level+-- @WITH@ syntax (with some caveats). Each such \"sub-statement\" can+-- reference the results of previous sub-statements. 'Statement' provides a+-- 'Monad' instance that captures this \"binding\" pattern.+--+-- The caveat with this is that the [side-effects of these sub-statements+-- are not visible to other sub-statements](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING);+-- only the explicit results of previous sub-statements (from @SELECT@s or+-- @RETURNING@ clauses) are visible. So, for example, an @INSERT@ into a table+-- followed immediately by a @SELECT@ therefrom will not return the inserted+-- rows. However, it is possible to return the inserted rows using+-- @RETURNING@, 'Rel8.unionAll'ing this with the result of a @SELECT@+-- from the same table will produce the desired result.+--+-- An example of where this can be useful is if you want to delete rows from+-- a table and simultaneously log their deletion in a log table.+--+-- @+-- deleteFoo :: (Foo Expr -> Expr Bool) -> Statement ()+-- deleteFoo predicate = do+--   foos <-+--     delete Delete+--       { from = fooSchema+--       , using = pure ()+--       , deleteWhere = \_ -> predicate+--       , returning = Returning id+--       }+--   insert Insert+--     { into = deletedFooSchema+--     , rows = do+--         Foo {..} <- foos+--         let+--           deletedAt = 'Rel8.Expr.Time.now'+--         pure DeletedFoo {..}+--     , onConflict = Abort+--     , returning = NoReturning+--     }+-- @+newtype Statement a =+  Statement (WriterT (Endo [Binding]) (State Opaleye.Tag) (Result a))+  deriving stock (Functor)+  deriving (Apply) via WrappedApplicative Statement+++instance Applicative Statement where+  pure = Statement . pure . Modified+  (<*>) = ap+  liftA2 = liftM2+++instance Bind Statement where+  Statement m >>- f = Statement $ do+    result <- m+    case f (getResult result) of+      Statement m' -> m'+++instance Monad Statement where+  (>>=) = (>>-)+++statementNoReturning :: State Opaleye.Tag Doc -> Statement ()+statementNoReturning pp = Statement $ do+  binding <- lift $ do+    doc <- pp+    tag <- Opaleye.fresh+    let+      relation = Opaleye.tagWith tag "statement"+      columns = Nothing+      returning = NoReturning+      binding = Binding {..}+    pure binding+  tell (Endo (binding :))+  pure $ Unmodified ()+++statementReturning :: Table Expr a +  => State Opaleye.Tag Doc -> Statement (Query a)+statementReturning pp = Statement $ do+  (binding, query) <- lift $ do+    doc <- pp+    tag <- Opaleye.fresh+    let+      relation = Opaleye.tagWith tag "statement"+      symbol labels = do+        subtag <- Opaleye.fresh+        let+          suffix = Opaleye.tagWith tag (Opaleye.tagWith subtag "")+        pure $ take (63 - length suffix) label ++ suffix+        where+          label = fold (intersperse "/" labels)+      names = namesFromLabelsWithA symbol `evalState` Opaleye.start+      columns = Just $ showNames names+      query =+        fromCols <$> each+          TableSchema+            { name = fromString relation+            , columns = names+            }+      returning = Returning (countRows query)+      binding = Binding {..}+    pure (binding, query)+  tell (Endo (binding :))+  pure $ Unmodified query+++ppDecodeStatement :: ()+  => (forall x. Table Expr x => Query x -> State Opaleye.Tag Doc)+  -> Rows exprs a -> Statement exprs -> (Doc, Hasql.Result a)+ppDecodeStatement ppSelect rows (Statement m) = evalState go Opaleye.start+  where+    go = do+      (result, Endo dlist) <- runWriterT m+      let+        bindings' = dlist []+      case unsnoc bindings' of+        Nothing -> case rows of+          Void -> do+            doc <- ppSelect (pure false)+            pure (doc, Hasql.noResult)+          RowsAffected -> do+            doc <- ppSelect (pure false)+            pure (doc, 0 <$ Hasql.noResult)+          Single @exprs @a -> do+            doc <- ppSelect (getResult result)+            pure (doc, Hasql.singleRow (parse @exprs @a))+          Maybe @exprs @a -> do+            doc <- ppSelect (getResult result)+            pure (doc, Hasql.rowMaybe (parse @exprs @a))+          List @exprs @a -> do+            doc <- ppSelect (getResult result)+            pure (doc, Hasql.rowList (parse @exprs @a))+          Vector @exprs @a -> do+            doc <- ppSelect (getResult result)+            pure (doc, Hasql.rowVector (parse @exprs @a))+        Just (bindings, binding@Binding {doc = after}) -> case rows of+          Void -> pure (doc, Hasql.noResult)+            where+              doc = ppWith bindings after+          RowsAffected -> do+            case result of+              Unmodified _ -> pure (doc, Hasql.rowsAffected)+                where+                  doc = ppWith bindings after+              Modified _ -> case returning binding of+                NoReturning -> pure (doc, Hasql.rowsAffected)+                  where+                    doc = ppWith bindings after +                Returning query -> do+                  doc <- ppWith bindings' <$> ppSelect query+                  pure (doc, Hasql.singleRow parse)+          Single @exprs @a -> do+            case result of+              Unmodified _ -> pure (doc, Hasql.singleRow (parse @exprs @a))+                where+                  doc = ppWith bindings after+              Modified query -> do+                doc <- ppWith bindings' <$> ppSelect query+                pure (doc, Hasql.singleRow (parse @exprs @a))+          Maybe @exprs @a -> do+            case result of+              Unmodified _ -> pure (doc, Hasql.rowMaybe (parse @exprs @a))+                where+                  doc = ppWith bindings after+              Modified query -> do+                doc <- ppWith bindings' <$> ppSelect query+                pure (doc, Hasql.rowMaybe (parse @exprs @a))+          List @exprs @a -> do+            case result of+              Unmodified _ -> pure (doc, Hasql.rowList (parse @exprs @a))+                where+                  doc = ppWith bindings after+              Modified query -> do+                doc <- ppWith bindings' <$> ppSelect query+                pure (doc, Hasql.rowList (parse @exprs @a))+          Vector @exprs @a -> do+            case result of+              Unmodified _ -> pure (doc, Hasql.rowVector (parse @exprs @a))+                where+                  doc = ppWith bindings after+              Modified query -> do+                doc <- ppWith bindings' <$> ppSelect query+                pure (doc, Hasql.rowVector (parse @exprs @a))+++ppWith :: [Binding] -> Doc -> Doc+ppWith bindings after = pre $$ after+  where+    pre = case bindings of+      [] -> mempty+      _ ->+        text "WITH" <+>+        vcat (punctuate comma (map go bindings))+    go binding@Binding {doc = before} =+      ppAlias binding $$+      text "AS" <+>+      parens before+++ppAlias :: Binding -> Doc+ppAlias Binding {relation, columns = mcolumns} = case mcolumns of+  Nothing -> escape relation+  Just columns -> +    escape relation <+>+    parens (hcat (punctuate comma (escape <$> toList columns)))+++unsnoc :: [a] -> Maybe ([a], a)+unsnoc = foldr (\x -> Just . maybe ([], x) (\(~(a, b)) -> (x : a, b))) Nothing
src/Rel8/Statement/Delete.hs view
@@ -1,7 +1,7 @@ {-# language DuplicateRecordFields #-}+{-# language FlexibleContexts #-} {-# language GADTs #-} {-# language NamedFieldPuns #-}-{-# language RankNTypes #-} {-# language RecordWildCards #-} {-# language StandaloneKindSignatures #-} {-# language StrictData #-}@@ -17,9 +17,8 @@ import Data.Kind ( Type ) import Prelude --- hasql-import qualified Hasql.Encoders as Hasql-import qualified Hasql.Statement as Hasql+-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye  -- pretty import Text.PrettyPrint ( Doc, (<+>), ($$), text )@@ -29,13 +28,13 @@ import Rel8.Query ( Query ) import Rel8.Schema.Name ( Selects ) import Rel8.Schema.Table ( TableSchema, ppTable )-import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )+import Rel8.Statement (Statement)+import Rel8.Statement.Returning (Returning, ppReturning, runReturning) import Rel8.Statement.Using ( ppUsing ) import Rel8.Statement.Where ( ppWhere ) --- text-import qualified Data.Text as Text-import Data.Text.Encoding ( encodeUtf8 )+-- transformers+import Control.Monad.Trans.State.Strict (State)   -- | The constituent parts of a @DELETE@ statement.@@ -55,25 +54,21 @@     -> Delete a  -ppDelete :: Delete a -> Doc-ppDelete Delete {..} = case ppUsing using of-  Nothing ->-    text "DELETE FROM" <+> ppTable from $$-    text "WHERE false"-  Just (usingDoc, i) ->-    text "DELETE FROM" <+> ppTable from $$-    usingDoc $$-    ppWhere from (deleteWhere i) $$-    ppReturning from returning+-- | Build a @DELETE@ 'Statement'.+delete :: Delete a -> Statement a+delete statement@Delete {returning} =+  runReturning (ppDelete statement) returning  --- | Run a 'Delete' statement.-delete :: Delete a -> Hasql.Statement () a-delete d@Delete {returning} = Hasql.Statement bytes params decode prepare-  where-    bytes = encodeUtf8 $ Text.pack sql-    params = Hasql.noParams-    decode = decodeReturning returning-    prepare = False-    sql = show doc-    doc = ppDelete d+ppDelete :: Delete a -> State Opaleye.Tag Doc+ppDelete Delete {..} = do+  musing <- ppUsing using+  pure $ case musing of+    Nothing ->+      text "DELETE FROM" <+> ppTable from $$+      text "WHERE false"+    Just (usingDoc, i) ->+      text "DELETE FROM" <+> ppTable from $$+      usingDoc $$+      ppWhere from (deleteWhere i) $$+      ppReturning from returning
src/Rel8/Statement/Insert.hs view
@@ -19,12 +19,9 @@ import Data.Kind ( Type ) import Prelude --- hasql-import qualified Hasql.Encoders as Hasql-import qualified Hasql.Statement as Hasql- -- opaleye import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye+import qualified Opaleye.Internal.Tag as Opaleye  -- pretty import Text.PrettyPrint ( Doc, (<+>), ($$), parens, text )@@ -33,15 +30,15 @@ import Rel8.Query ( Query ) import Rel8.Schema.Name ( Name, Selects, ppColumn ) import Rel8.Schema.Table ( TableSchema(..), ppTable )+import Rel8.Statement (Statement) import Rel8.Statement.OnConflict ( OnConflict, ppOnConflict )-import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )+import Rel8.Statement.Returning (Returning, ppReturning, runReturning) import Rel8.Statement.Select ( ppRows ) import Rel8.Table ( Table ) import Rel8.Table.Name ( showNames ) --- text-import qualified Data.Text as Text ( pack )-import Data.Text.Encoding ( encodeUtf8 )+-- transformers+import Control.Monad.Trans.State.Strict (State)   -- | The constituent parts of a SQL @INSERT@ statement.@@ -62,28 +59,24 @@     -> Insert a  -ppInsert :: Insert a -> Doc-ppInsert Insert {..} =-  text "INSERT INTO" <+>-  ppInto into $$-  ppRows rows $$-  ppOnConflict into onConflict $$-  ppReturning into returning+-- | Build an @INSERT@ 'Statement'.+insert :: Insert a -> Statement a+insert statement@Insert {returning} =+  runReturning (ppInsert statement) returning  +ppInsert :: Insert a -> State Opaleye.Tag Doc+ppInsert Insert {..} = do+  rows' <- ppRows rows+  pure $+    text "INSERT INTO" <+>+    ppInto into $$+    rows' $$+    ppOnConflict into onConflict $$+    ppReturning into returning++ ppInto :: Table Name a => TableSchema a -> Doc ppInto table@TableSchema {columns} =   ppTable table <+>   parens (Opaleye.commaV ppColumn (toList (showNames columns)))----- | Run an 'Insert' statement.-insert :: Insert a -> Hasql.Statement () a-insert i@Insert {returning} = Hasql.Statement bytes params decode prepare-  where-    bytes = encodeUtf8 $ Text.pack sql-    params = Hasql.noParams-    decode = decodeReturning returning-    prepare = False-    sql = show doc-    doc = ppInsert i
src/Rel8/Statement/OnConflict.hs view
@@ -3,9 +3,11 @@ {-# language GADTs #-} {-# language LambdaCase #-} {-# language NamedFieldPuns #-}+{-# language OverloadedStrings #-} {-# language RecordWildCards #-} {-# language StandaloneKindSignatures #-} {-# language StrictData #-}+{-# language TypeOperators #-}  module Rel8.Statement.OnConflict   ( OnConflict(..)@@ -21,12 +23,14 @@  -- opaleye import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye+import qualified Opaleye.Internal.Sql as Opaleye  -- pretty import Text.PrettyPrint ( Doc, (<+>), ($$), parens, text )  -- rel8 import Rel8.Expr ( Expr )+import Rel8.Expr.Opaleye (toPrimExpr) import Rel8.Schema.Name ( Name, Selects, ppColumn ) import Rel8.Schema.Table ( TableSchema(..) ) import Rel8.Statement.Set ( ppSet )@@ -34,7 +38,7 @@ import Rel8.Table ( Table, toColumns ) import Rel8.Table.Cols ( Cols( Cols ) ) import Rel8.Table.Name ( showNames )-import Rel8.Table.Opaleye ( attributes )+import Rel8.Table.Opaleye (attributes, view) import Rel8.Table.Projection ( Projecting, Projection, apply )  @@ -60,15 +64,20 @@ -- can still be referenced from the @SET@ and @WHERE@ clauses of the @UPDATE@ -- statement. ----- Upsert in Postgres requires an explicit set of \"conflict targets\" — the--- set of columns comprising the @UNIQUE@ index from conflicts with which we--- would like to recover.+-- Upsert in Postgres a \"conflict target\" to be specified — this is the+-- @UNIQUE@ index from conflicts with which we would like to recover. Indexes+-- are specified by listing the columns that comprise them along with an+-- optional predicate in the case of partial indexes. type Upsert :: Type -> Type data Upsert names where   Upsert :: (Selects names exprs, Projecting names index, excluded ~ exprs) =>     { index :: Projection names index-      -- ^ The set of conflict targets, projected from the set of columns for-      -- the whole table+      -- ^ The set of columns comprising the @UNIQUE@ index that forms our+      -- conflict target, projected from the set of columns for the whole+      -- table+    , predicate :: Maybe (exprs -> Expr Bool)+      -- ^ An optional predicate used to specify a+      -- [partial index](https://www.postgresql.org/docs/current/indexes-partial.html).     , set :: excluded -> exprs -> exprs       -- ^ How to update each selected row.     , updateWhere :: excluded -> exprs -> Expr Bool@@ -87,20 +96,27 @@ ppUpsert :: TableSchema names -> Upsert names -> Doc ppUpsert schema@TableSchema {columns} Upsert {..} =   text "ON CONFLICT" <+>-  ppIndex schema index <+>+  ppIndex columns index <+> foldMap (ppPredicate columns) predicate <+>   text "DO UPDATE" $$   ppSet schema (set excluded) $$   ppWhere schema (updateWhere excluded)   where     excluded = attributes TableSchema-      { schema = Nothing-      , name = "excluded"+      { name = "excluded"       , columns       }   ppIndex :: (Table Name names, Projecting names index)-  => TableSchema names -> Projection names index -> Doc-ppIndex TableSchema {columns} index =+  => names -> Projection names index -> Doc+ppIndex columns index =   parens $ Opaleye.commaV ppColumn $ toList $     showNames $ Cols $ apply index $ toColumns columns+++ppPredicate :: Selects names exprs+  => names -> (exprs -> Expr Bool) -> Doc+ppPredicate schema where_ = text "WHERE" <+> ppExpr condition+  where+    ppExpr = Opaleye.ppSqlExpr . Opaleye.sqlExpr . toPrimExpr+    condition = where_ (view schema)
src/Rel8/Statement/Returning.hs view
@@ -1,3 +1,5 @@+{-# language DataKinds #-}+{-# language FlexibleContexts #-} {-# language GADTs #-} {-# language LambdaCase #-} {-# language NamedFieldPuns #-}@@ -8,120 +10,68 @@ {-# language TypeApplications #-}  module Rel8.Statement.Returning-  ( Returning( NumberOfRowsAffected, Projection )-  , decodeReturning+  ( Returning( NoReturning, Returning )+  , runReturning   , ppReturning   ) where  -- base-import Control.Applicative ( liftA2 ) import Data.Foldable ( toList )-import Data.Int ( Int64 ) import Data.Kind ( Type ) import Data.List.NonEmpty ( NonEmpty ) import Prelude --- hasql-import qualified Hasql.Decoders as Hasql- -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye import qualified Opaleye.Internal.Sql as Opaleye+import qualified Opaleye.Internal.Tag as Opaleye  -- pretty import Text.PrettyPrint ( Doc, (<+>), text )  -- rel8+import Rel8.Expr (Expr)+import Rel8.Query (Query) import Rel8.Schema.Name ( Selects ) import Rel8.Schema.Table ( TableSchema(..) )+import Rel8.Statement (Statement, statementNoReturning, statementReturning)+import Rel8.Table (Table) import Rel8.Table.Opaleye ( castTable, exprs, view )-import Rel8.Table.Serialize ( Serializable, parse ) --- semigropuoids-import Data.Functor.Apply ( Apply, (<.>) )+-- transformers+import Control.Monad.Trans.State.Strict (State)  --- | 'Rel8.Insert', 'Rel8.Update' and 'Rel8.Delete' all support returning either--- the number of rows affected, or the actual rows modified.+-- | 'Rel8.Insert', 'Rel8.Update' and 'Rel8.Delete' all support an optional+-- @RETURNING@ clause. type Returning :: Type -> Type -> Type data Returning names a where-  Pure :: a -> Returning names a-  Ap :: Returning names (a -> b) -> Returning names a -> Returning names b--  -- | Return the number of rows affected.-  NumberOfRowsAffected :: Returning names Int64+  -- | No @RETURNING@ clause+  NoReturning :: Returning names () -  -- | 'Projection' allows you to project out of the affected rows, which can+  -- | 'Returning' allows you to project out of the affected rows, which can   -- be useful if you want to log exactly which rows were deleted, or to view   -- a generated id (for example, if using a column with an autoincrementing   -- counter via 'Rel8.nextval').-  Projection :: (Selects names exprs, Serializable returning a)-    => (exprs -> returning)-    -> Returning names [a]---instance Functor (Returning names) where-  fmap f = \case-    Pure a -> Pure (f a)-    Ap g a -> Ap (fmap (f .) g) a-    m -> Ap (Pure f) m---instance Apply (Returning names) where-  (<.>) = Ap---instance Applicative (Returning names) where-  pure = Pure-  (<*>) = Ap+  Returning :: (Selects names exprs, Table Expr a) => (exprs -> a) -> Returning names (Query a)   projections :: ()   => TableSchema names -> Returning names a -> Maybe (NonEmpty Opaleye.PrimExpr)-projections schema@TableSchema {columns} = \case-  Pure _ -> Nothing-  Ap f a -> projections schema f <> projections schema a-  NumberOfRowsAffected -> Nothing-  Projection f -> Just (exprs (castTable (f (view columns))))---runReturning :: ()-  => ((Int64 -> a) -> r)-  -> (forall x. Hasql.Row x -> ([x] -> a) -> r)-  -> Returning names a-  -> r-runReturning rowCount rowList = \case-  Pure a -> rowCount (const a)-  Ap fs as ->-    runReturning-      (\withCount ->-         runReturning-           (\withCount' -> rowCount (withCount <*> withCount'))-           (\decoder -> rowList decoder . liftA2 withCount length64)-           as)-      (\decoder withRows ->-         runReturning-           (\withCount -> rowList decoder $ withRows <*> withCount . length64)-           (\decoder' withRows' ->-             rowList (liftA2 (,) decoder decoder') $-               withRows <$> fmap fst <*> withRows' . fmap snd)-           as)-      fs-  NumberOfRowsAffected -> rowCount id-  Projection (_ :: exprs -> returning) -> rowList decoder' id-    where-      decoder' = parse @returning-  where-    length64 :: Foldable f => f x -> Int64-    length64 = fromIntegral . length+projections TableSchema {columns} = \case+  NoReturning -> Nothing+  Returning f -> Just (exprs (castTable (f (view columns))))  -decodeReturning :: Returning names a -> Hasql.Result a-decodeReturning = runReturning-  (<$> Hasql.rowsAffected)-  (\decoder withRows -> withRows <$> Hasql.rowList decoder)+runReturning ::+  State Opaleye.Tag Doc ->+  Returning names a ->+  Statement a+runReturning pp = \case+  NoReturning -> statementNoReturning pp+  Returning _ -> statementReturning pp   ppReturning :: TableSchema names -> Returning names a -> Doc
+ src/Rel8/Statement/Rows.hs view
@@ -0,0 +1,30 @@+{-# language DataKinds #-}+{-# language GADTs #-}+{-# language StandaloneKindSignatures #-}++module Rel8.Statement.Rows+  ( Rows (..)    +  )+where++-- base+import Data.Int (Int64)+import Data.Kind (Type)+import Prelude++-- rel8+import Rel8.Query (Query)+import Rel8.Table.Serialize (Serializable)++-- vector+import Data.Vector (Vector)+++type Rows :: Type -> Type -> Type+data Rows returning result where+  Void :: Rows returning ()+  RowsAffected :: Rows () Int64+  Single :: Serializable exprs a => Rows (Query exprs) a+  Maybe :: Serializable exprs a => Rows (Query exprs) (Maybe a)+  List :: Serializable exprs a => Rows (Query exprs) [a]+  Vector :: Serializable exprs a => Rows (Query exprs) (Vector a)
+ src/Rel8/Statement/Run.hs view
@@ -0,0 +1,84 @@+module Rel8.Statement.Run+  ( run_+  , runN+  , run1+  , runMaybe+  , run+  , runVector+  )+where++-- base+import Data.Int (Int64)+import Prelude++-- hasql+import qualified Hasql.Encoders as Hasql+import qualified Hasql.Statement as Hasql++-- rel8+import Rel8.Query (Query)+import Rel8.Statement (Statement, ppDecodeStatement)+import Rel8.Statement.Rows (Rows (..))+import Rel8.Statement.Select (ppSelect)+import Rel8.Table.Serialize (Serializable)++-- text+import qualified Data.Text as Text+import Data.Text.Encoding (encodeUtf8)++-- vector+import Data.Vector (Vector)+++makeRun :: Rows exprs a -> Statement exprs -> Hasql.Statement () a+makeRun rows statement = Hasql.Statement bytes params decode prepare+  where+    bytes = encodeUtf8 $ Text.pack sql+    params = Hasql.noParams+    prepare = False+    sql = show doc+    (doc, decode) = ppDecodeStatement ppSelect rows statement+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', disregarding the+-- results of that statement (if any).+run_ :: Statement exprs -> Hasql.Statement () ()+run_ = makeRun Void+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', returning the+-- number of rows affected by that statement (for 'Rel8.insert's,+-- 'Rel8.update's or Rel8.delete's with 'Rel8.NoReturning').+runN :: Statement () -> Hasql.Statement () Int64+runN = makeRun RowsAffected+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', processing the+-- result of the statement as a single row. If the statement returns a number+-- of rows other than 1, a runtime exception is thrown.+run1 :: Serializable exprs +  a=> Statement (Query exprs) -> Hasql.Statement () a+run1 = makeRun Single+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', processing the+-- result of the statement as 'Maybe' a single row. If the statement returns+-- a number of rows other than 0 or 1, a runtime exception is thrown.+runMaybe :: Serializable exprs +  a=> Statement (Query exprs) -> Hasql.Statement () (Maybe a)+runMaybe = makeRun Maybe+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', processing the+-- result of the statement as a list of rows.+run :: Serializable exprs a+  => Statement (Query exprs) -> Hasql.Statement () [a]+run = makeRun List+++-- | Convert a 'Statement' to a runnable 'Hasql.Statement', processing the+-- result of the statement as a 'Vector' of rows.+runVector :: Serializable exprs a+  => Statement (Query exprs) -> Hasql.Statement () (Vector a)+runVector = makeRun Vector
src/Rel8/Statement/SQL.hs view
@@ -2,28 +2,43 @@   ( showDelete   , showInsert   , showUpdate+  , showStatement   ) where  -- base import Prelude +-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye+ -- rel8+import Rel8.Statement (Statement, ppDecodeStatement) import Rel8.Statement.Delete ( Delete, ppDelete ) import Rel8.Statement.Insert ( Insert, ppInsert )+import Rel8.Statement.Rows (Rows (Void))+import Rel8.Statement.Select (ppSelect) import Rel8.Statement.Update ( Update, ppUpdate ) +-- transformers+import Control.Monad.Trans.State.Strict (evalState) + -- | Convert a 'Delete' to a 'String' containing a @DELETE@ statement. showDelete :: Delete a -> String-showDelete = show . ppDelete+showDelete = show . (`evalState` Opaleye.start) . ppDelete   -- | Convert an 'Insert' to a 'String' containing an @INSERT@ statement. showInsert :: Insert a -> String-showInsert = show . ppInsert+showInsert = show . (`evalState` Opaleye.start) . ppInsert   -- | Convert an 'Update' to a 'String' containing an @UPDATE@ statement. showUpdate :: Update a -> String-showUpdate = show . ppUpdate+showUpdate = show . (`evalState` Opaleye.start) . ppUpdate+++-- | Convert a 'Statement' to a 'String' containing an SQL statement.+showStatement :: Statement a -> String+showStatement = show . fst . ppDecodeStatement ppSelect Void
src/Rel8/Statement/Select.hs view
@@ -1,3 +1,4 @@+{-# language DataKinds #-} {-# language DeriveTraversable #-} {-# language DerivingStrategies #-} {-# language FlexibleContexts #-}@@ -9,7 +10,6 @@ module Rel8.Statement.Select   ( select   , ppSelect-   , Optimized(..)   , ppPrimSelect   , ppRows@@ -22,11 +22,6 @@ import Data.Void ( Void ) import Prelude hiding ( undefined ) --- hasql-import qualified Hasql.Decoders as Hasql-import qualified Hasql.Encoders as Hasql-import qualified Hasql.Statement as Hasql- -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye@@ -48,52 +43,43 @@ import Rel8.Query ( Query ) import Rel8.Query.Opaleye ( toOpaleye ) import Rel8.Schema.Name ( Selects )+import Rel8.Statement (Statement, statementReturning) import Rel8.Table ( Table ) import Rel8.Table.Cols ( toCols ) import Rel8.Table.Name ( namesFromLabels ) import Rel8.Table.Opaleye ( castTable, exprsWithNames ) import qualified Rel8.Table.Opaleye as T-import Rel8.Table.Serialize ( Serializable, parse ) import Rel8.Table.Undefined ( undefined ) --- text-import qualified Data.Text as Text-import Data.Text.Encoding ( encodeUtf8 )+-- transformers+import Control.Monad.Trans.State.Strict (State)  --- | Run a @SELECT@ statement, returning all rows.-select :: forall exprs a. Serializable exprs a-  => Query exprs -> Hasql.Statement () [a]-select query = Hasql.Statement bytes params decode prepare-  where-    bytes = encodeUtf8 (Text.pack sql)-    params = Hasql.noParams-    decode = Hasql.rowList (parse @exprs @a)-    prepare = False-    sql = show doc-    doc = ppSelect query+-- | Build a @SELECT@ 'Statement'.+select :: Table Expr a => Query a -> Statement (Query a)+select query = statementReturning (ppSelect query)  -ppSelect :: Table Expr a => Query a -> Doc-ppSelect query =-  Opaleye.ppSql $ primSelectWith names (toCols exprs') primQuery'-  where-    names = namesFromLabels-    (exprs, primQuery, _) =-      Opaleye.runSimpleQueryArrStart (toOpaleye query) ()+ppSelect :: Table Expr a => Query a -> State Opaleye.Tag Doc+ppSelect query = do+  (exprs, primQuery) <- Opaleye.runSimpleSelect (toOpaleye query)+  let     (exprs', primQuery') = case optimize primQuery of       Empty -> (undefined, Opaleye.Product (pure (pure Opaleye.Unit)) never)       Unit -> (exprs, Opaleye.Unit)       Optimized pq -> (exprs, pq)+  pure $ Opaleye.ppSql $ primSelectWith names (toCols exprs') primQuery'+  where+    names = namesFromLabels     never = pure (toPrimExpr false)  -ppRows :: Table Expr a => Query a -> Doc+ppRows :: Table Expr a => Query a -> State Opaleye.Tag Doc ppRows query = case optimize primQuery of   -- Special case VALUES because we can't use DEFAULT inside a SELECT   Optimized (Opaleye.Values symbols rows)     | eqSymbols symbols (toList (T.exprs a)) ->-        Opaleye.ppValues_ (map Opaleye.sqlExpr <$> toList rows)+        pure $ Opaleye.ppValues_ (map Opaleye.sqlExpr <$> toList rows)   _ -> ppSelect query   where     (a, primQuery, _) = Opaleye.runSimpleQueryArrStart (toOpaleye query) ()@@ -110,11 +96,10 @@       = name == name' && tag == tag'  -ppPrimSelect :: Query a -> (Optimized Doc, a)-ppPrimSelect query =-  (Opaleye.ppSql . primSelect <$> optimize primQuery, a)-  where-    (a, primQuery, _) = Opaleye.runSimpleQueryArrStart (toOpaleye query) ()+ppPrimSelect :: Query a -> State Opaleye.Tag (Optimized Doc, a)+ppPrimSelect query = do+  (a, primQuery) <- Opaleye.runSimpleSelect (toOpaleye query)+  pure $ (Opaleye.ppSql . primSelect <$> optimize primQuery, a)   type Optimized :: Type -> Type
src/Rel8/Statement/Update.hs view
@@ -1,4 +1,5 @@ {-# language DuplicateRecordFields #-}+{-# language FlexibleContexts #-} {-# language GADTs #-} {-# language NamedFieldPuns #-} {-# language RecordWildCards #-}@@ -16,9 +17,8 @@ import Data.Kind ( Type ) import Prelude --- hasql-import qualified Hasql.Encoders as Hasql-import qualified Hasql.Statement as Hasql+-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye  -- pretty import Text.PrettyPrint ( Doc, (<+>), ($$), text )@@ -28,14 +28,14 @@ import Rel8.Query ( Query ) import Rel8.Schema.Name ( Selects ) import Rel8.Schema.Table ( TableSchema(..), ppTable )-import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )+import Rel8.Statement (Statement)+import Rel8.Statement.Returning (Returning, ppReturning, runReturning) import Rel8.Statement.Set ( ppSet ) import Rel8.Statement.Using ( ppFrom ) import Rel8.Statement.Where ( ppWhere ) --- text-import qualified Data.Text as Text-import Data.Text.Encoding ( encodeUtf8 )+-- transformers+import Control.Monad.Trans.State.Strict (State)   -- | The constituent parts of an @UPDATE@ statement.@@ -57,27 +57,23 @@     -> Update a  -ppUpdate :: Update a -> Doc-ppUpdate Update {..} = case ppFrom from of-  Nothing ->-    text "UPDATE" <+> ppTable target $$-    ppSet target id $$-    text "WHERE false"-  Just (fromDoc, i) ->-    text "UPDATE" <+> ppTable target $$-    ppSet target (set i) $$-    fromDoc $$-    ppWhere target (updateWhere i) $$-    ppReturning target returning+-- | Build an @UPDATE@ 'Statement'.+update :: Update a -> Statement a+update statement@Update {returning} =+  runReturning (ppUpdate statement) returning  --- | Run an @UPDATE@ statement.-update :: Update a -> Hasql.Statement () a-update u@Update {returning} = Hasql.Statement bytes params decode prepare-  where-    bytes = encodeUtf8 $ Text.pack sql-    params = Hasql.noParams-    decode = decodeReturning returning-    prepare = False-    sql = show doc-    doc = ppUpdate u+ppUpdate :: Update a -> State Opaleye.Tag Doc+ppUpdate Update {..} = do+  mfrom <- ppFrom from+  pure $ case mfrom of+    Nothing -> +      text "UPDATE" <+> ppTable target $$+      ppSet target id $$+      text "WHERE false"+    Just (fromDoc, i) ->+      text "UPDATE" <+> ppTable target $$+      ppSet target (set i) $$+      fromDoc $$+      ppWhere target (updateWhere i) $$+      ppReturning target returning
src/Rel8/Statement/Using.hs view
@@ -1,3 +1,5 @@+{-# language OverloadedStrings #-}+ module Rel8.Statement.Using   ( ppFrom   , ppUsing@@ -7,6 +9,9 @@ -- base import Prelude +-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye+ -- pretty import Text.PrettyPrint ( Doc, (<+>), parens, text ) @@ -15,22 +20,26 @@ import Rel8.Schema.Table ( TableSchema(..), ppTable ) import Rel8.Statement.Select ( Optimized(..), ppPrimSelect ) +-- transformers+import Control.Monad.Trans.State.Strict (State) -ppFrom :: Query a -> Maybe (Doc, a)++ppFrom :: Query a -> State Opaleye.Tag (Maybe (Doc, a)) ppFrom = ppJoin "FROM"  -ppUsing :: Query a -> Maybe (Doc, a)+ppUsing :: Query a -> State Opaleye.Tag (Maybe (Doc, a)) ppUsing = ppJoin "USING"  -ppJoin :: String -> Query a -> Maybe (Doc, a)+ppJoin :: String -> Query a -> State Opaleye.Tag (Maybe (Doc, a)) ppJoin clause join = do-  doc <- case ofrom of-    Empty -> Nothing-    Unit -> Just mempty-    Optimized doc -> Just $ text clause <+> parens doc <+> ppTable alias-  pure (doc, a)+  (ofrom, a) <- ppPrimSelect join+  pure $ do+    doc <- case ofrom of+      Empty -> Nothing+      Unit -> Just mempty+      Optimized doc -> Just $ text clause <+> parens doc <+> ppTable alias+    pure (doc, a)   where-    alias = TableSchema {name = "T1", schema = Nothing, columns = ()}-    (ofrom, a) = ppPrimSelect join+    alias = TableSchema {name = "T1", columns = ()}
src/Rel8/Statement/View.hs view
@@ -15,6 +15,12 @@ import qualified Hasql.Encoders as Hasql import qualified Hasql.Statement as Hasql +-- opaleye+import qualified Opaleye.Internal.Tag as Opaleye++-- pretty+import Text.PrettyPrint ( Doc, (<+>), ($$), text )+ -- rel8 import Rel8.Query ( Query ) import Rel8.Schema.Name ( Selects )@@ -22,14 +28,14 @@ import Rel8.Statement.Insert ( ppInto ) import Rel8.Statement.Select ( ppSelect ) --- pretty-import Text.PrettyPrint ( Doc, (<+>), ($$), text )- -- text import qualified Data.Text as Text import Data.Text.Encoding ( encodeUtf8 ) +-- transformers+import Control.Monad.Trans.State.Strict (evalState) + data CreateView = Create | CreateOrReplace  @@ -72,7 +78,7 @@   createOrReplace replace <+>   ppInto schema $$   text "AS" <+>-  ppSelect query+  evalState (ppSelect query) Opaleye.start   where     createOrReplace Create = text "CREATE VIEW"     createOrReplace CreateOrReplace = text "CREATE OR REPLACE VIEW"
src/Rel8/Table.hs view
@@ -8,6 +8,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table
src/Rel8/Table/ADT.hs view
@@ -18,9 +18,8 @@   , BuildADT, buildADT   , ConstructableADT   , ConstructADT, constructADT-  , DeconstructADT, deconstructADT+  , DeconstructADT, deconstructADT, deconstructAADT   , NameADT, nameADT-  , AggregateADT, aggregateADT   , ADTRep   ) where@@ -32,7 +31,6 @@ import Prelude  -- rel8-import Rel8.Aggregate ( Aggregate ) import Rel8.Expr ( Expr ) import Rel8.FCF ( Eval, Exp ) import Rel8.Generic.Construction@@ -40,9 +38,8 @@   , 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,7 +56,10 @@ 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,23 +146,18 @@   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)   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)
src/Rel8/Table/Aggregate.hs view
@@ -1,37 +1,57 @@+{-# 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+  , listCat, listCatOn, nonEmptyCat, nonEmptyCatOn+  , 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+  , toAggregator+  )+import Rel8.Aggregate.Fold (Fallback (Fallback)) import Rel8.Expr ( Expr ) import Rel8.Expr.Aggregate-  ( groupByExpr+  ( filterWhereExplicit+  , groupByExprOn   , slistAggExpr+  , slistCatExpr   , snonEmptyAggExpr+  , snonEmptyCatExpr   )+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, hspecs, htabulateA)+import Rel8.Schema.HTable.Vectorize (htraverseVectorP, 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,24 +63,50 @@ -- -- @ -- 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))  -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+-- | 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+++-- | 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 -- rows. This can be used to associate multiple rows with a single row, without -- changing the over cardinality of the query. This allows you to essentially@@ -74,19 +120,75 @@ -- 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+++-- | Concatenate lists into a single list.+listCat :: Table Expr a+  => Aggregator' fold (ListTable Expr a) (ListTable Expr a)+listCat = dimap toColumns fromColumns $+  htraverseVectorP (\field -> case hfield hspecs field of+    Spec {info} -> slistCatExpr info)+++-- | Applies 'listCat' to the list selected by the given function.+listCatOn :: Table Expr a+  => (i -> ListTable Expr a) -> Aggregator' fold i (ListTable Expr a)+listCatOn f = lmap f listCat+++-- | Concatenate non-empty lists into a single non-empty list.+nonEmptyCat :: Table Expr a+  => Aggregator1 (NonEmptyTable Expr a) (NonEmptyTable Expr a)+nonEmptyCat = dimap toColumns fromColumns $+  htraverseVectorP (\field -> case hfield hspecs field of+    Spec {info} -> snonEmptyCatExpr info)+++-- | Applies 'nonEmptyCat' to the non-empty list selected by the given+-- function.+nonEmptyCatOn :: Table Expr a+  => (i -> NonEmptyTable Expr a) -> Aggregator1 i (NonEmptyTable Expr a)+nonEmptyCatOn f = lmap f nonEmptyCat+++-- | 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 = toAggregator nothingTable . fmap justTable
src/Rel8/Table/Cols.hs view
@@ -3,6 +3,7 @@ {-# language MultiParamTypeClasses #-} {-# language StandaloneKindSignatures #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Cols
src/Rel8/Table/Either.hs view
@@ -10,6 +10,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  {-# options_ghc -fno-warn-orphans #-}@@ -32,10 +33,13 @@ -- comonad import Control.Comonad ( extract ) +-- profunctors+import Data.Profunctor (lmap)+ -- rel8-import Rel8.Aggregate ( Aggregate )+import Rel8.Aggregate (Aggregator', Aggregator1, toAggregator1) 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 )@@ -214,18 +218,17 @@ 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 (toAggregator1 (aggregateNullify a))+    <*> lmap right (toAggregator1 (aggregateNullify b))   -- | Construct a 'EitherTable' in the 'Name' context. This can be useful if you
src/Rel8/Table/HKD.hs view
@@ -8,6 +8,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-} {-# language UndecidableSuperClasses #-} @@ -18,9 +19,8 @@   , BuildHKD, buildHKD   , ConstructableHKD   , ConstructHKD, constructHKD-  , DeconstructHKD, deconstructHKD+  , DeconstructHKD, deconstructHKD, deconstructAHKD   , NameHKD, nameHKD-  , AggregateHKD, aggregateHKD   , HKDRep   ) where@@ -32,7 +32,6 @@ import Prelude  -- rel8-import Rel8.Aggregate ( Aggregate ) import Rel8.Column ( TColumn ) import Rel8.Expr ( Expr ) import Rel8.FCF ( Eval, Exp )@@ -42,9 +41,8 @@   , GGBuild, ggbuild   , GGConstructable   , GGConstruct, ggconstruct-  , GGDeconstruct, ggdeconstruct+  , GGDeconstruct, ggdeconstruct, ggdeconstructA   , GGName, ggname-  , GGAggregate, ggaggregate   ) import Rel8.Generic.Map ( GMap ) import Rel8.Generic.Record@@ -71,7 +69,10 @@   , TSerialize   ) +-- semigroupoids+import Data.Functor.Apply (Apply) + type GColumnsHKD :: Type -> K.HTable type GColumnsHKD a =   Eval (GGColumns (GAlgebra (Rep a)) TColumns (GRecord (GMap (TColumn Expr) (Rep a))))@@ -204,23 +205,17 @@ 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)   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)
src/Rel8/Table/List.hs view
@@ -7,6 +7,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.List@@ -14,23 +15,31 @@   , ($*)   , listTable   , nameListTable+  , head+  , index+  , last+  , length   ) where  -- base-import Data.Functor.Identity ( Identity( Identity ) )+import Data.Functor.Identity (Identity (Identity))+import Data.Int (Int32) import Data.Kind ( Type )-import Prelude+import Prelude hiding (head, last, length)  -- rel8 import Rel8.Expr ( Expr ) import Rel8.Expr.Array ( sappend, sempty, slistOf )+import Rel8.Expr.List (lengthExpr, sheadExpr, sindexExpr, slastExpr) import Rel8.Schema.Dict ( Dict( Dict ) ) import Rel8.Schema.HTable.List ( HListTable ) import Rel8.Schema.HTable.Vectorize   ( hvectorize, hunvectorize+  , hnullify   , happend, hempty   , hproject, hcolumn+  , First (..)   ) import qualified Rel8.Schema.Kind as K import Rel8.Schema.Name ( Name( Name ) )@@ -47,6 +56,7 @@   , AlternativeTable, emptyTable   ) import Rel8.Table.Eq ( EqTable, eqTable )+import Rel8.Table.Null (NullTable) import Rel8.Table.Ord ( OrdTable, ordTable ) import Rel8.Table.Projection   ( Projectable, Projecting, Projection, project, apply@@ -146,4 +156,38 @@   ListTable .   hvectorize (\_ (Identity (Name a)) -> Name a) .   pure .+  toColumns+++-- | Get the first element of a 'ListTable' (or 'Rel8.nullTable' if empty).+head :: Table Expr a => ListTable Expr a -> NullTable Expr a+head =+  fromColumns .+  hnullify (\Spec {info} -> sheadExpr info) .+  toColumns+++-- | @'index' i as@ extracts a single element from @as@, returning+-- 'Rel8.nullTable' if @i@ is out of range. Note that although PostgreSQL+-- array indexes are 1-based (by default), this function is always 0-based.+index :: Table Expr a => Expr Int32 -> ListTable Expr a -> NullTable Expr a+index i =+  fromColumns .+  hnullify (\Spec {info} -> sindexExpr info i) .+  toColumns+++-- | Get the last element of a 'ListTable' (or 'Rel8.nullTable' if empty).+last :: Table Expr a => ListTable Expr a -> NullTable Expr a+last =+  fromColumns .+  hnullify (\Spec {info} -> slastExpr info) .+  toColumns+++-- | Get the length of a 'ListTable'+length :: Table Expr a => ListTable Expr a -> Expr Int32+length =+  getFirst .+  hunvectorize (\_ -> First . lengthExpr) .   toColumns
src/Rel8/Table/Maybe.hs view
@@ -9,6 +9,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Maybe@@ -19,6 +20,7 @@   , ($?)   , aggregateMaybeTable   , nameMaybeTable+  , makeMaybeTable   ) where @@ -32,12 +34,20 @@ -- 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, toAggregator1) 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@@ -223,14 +233,15 @@ 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 (toAggregator1 (aggregateNullify aggregator))   -- | Construct a 'MaybeTable' in the 'Name' context. This can be useful if you@@ -244,3 +255,10 @@      -- ^ 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+  }
src/Rel8/Table/Name.hs view
@@ -12,6 +12,7 @@ module Rel8.Table.Name   ( namesFromLabels   , namesFromLabelsWith+  , namesFromLabelsWithA   , showLabels   , showNames   )@@ -20,17 +21,21 @@ -- base import Data.Foldable ( fold ) import Data.Functor.Const ( Const( Const ), getConst )+import Data.Functor.Identity (runIdentity) import Data.List.NonEmpty ( NonEmpty, intersperse, nonEmpty ) import Data.Maybe ( fromMaybe ) import Prelude  -- rel8-import Rel8.Schema.HTable ( htabulate, htabulateA, hfield, hspecs )+import Rel8.Schema.HTable (htabulateA, hfield, hspecs) import Rel8.Schema.Name ( Name( Name ) ) import Rel8.Schema.Spec ( Spec(..) ) import Rel8.Table ( Table(..) ) +-- semigroupoids+import Data.Functor.Apply (Apply) + -- | Construct a table in the 'Name' context containing the names of all -- columns. Nested column names will be combined with @/@. --@@ -58,9 +63,14 @@ -- to the name of the Haskell field. namesFromLabelsWith :: Table Name a   => (NonEmpty String -> String) -> a-namesFromLabelsWith f = fromColumns $ htabulate $ \field ->+namesFromLabelsWith = runIdentity . namesFromLabelsWithA . (pure .)+++namesFromLabelsWithA :: (Apply f, Table Name a)+  => (NonEmpty String -> f String) -> f a+namesFromLabelsWithA f = fmap fromColumns $ htabulateA $ \field ->   case hfield hspecs field of-    Spec {labels} -> Name (f (renderLabels labels))+    Spec {labels} -> Name <$> f (renderLabels labels)   showLabels :: forall a. Table (Context a) a => a -> [NonEmpty String]
src/Rel8/Table/NonEmpty.hs view
@@ -7,6 +7,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.NonEmpty@@ -14,11 +15,16 @@   , ($+)   , nonEmptyTable   , nameNonEmptyTable+  , head1+  , index1+  , last1+  , length1   ) where  -- base-import Data.Functor.Identity ( Identity( Identity ) )+import Data.Functor.Identity (Identity (Identity), runIdentity)+import Data.Int (Int32) import Data.Kind ( Type ) import Data.List.NonEmpty ( NonEmpty ) import Prelude hiding ( id )@@ -26,12 +32,15 @@ -- rel8 import Rel8.Expr ( Expr ) import Rel8.Expr.Array ( sappend1, snonEmptyOf )+import Rel8.Expr.NonEmpty (length1Expr, shead1Expr, sindex1Expr, slast1Expr) import Rel8.Schema.Dict ( Dict( Dict ) ) import Rel8.Schema.HTable.NonEmpty ( HNonEmptyTable ) import Rel8.Schema.HTable.Vectorize   ( hvectorize, hunvectorize+  , hnullify   , happend   , hproject, hcolumn+  , First (..)   ) import qualified Rel8.Schema.Kind as K import Rel8.Schema.Name ( Name( Name ) )@@ -45,6 +54,7 @@   ) import Rel8.Table.Alternative ( AltTable, (<|>:) ) import Rel8.Table.Eq ( EqTable, eqTable )+import Rel8.Table.Null (NullTable) import Rel8.Table.Ord ( OrdTable, ordTable ) import Rel8.Table.Projection   ( Projectable, Projecting, Projection, project, apply@@ -140,4 +150,40 @@   NonEmptyTable .   hvectorize (\_ (Identity (Name a)) -> Name a) .   pure .+  toColumns+++-- | Get the first element of a 'NonEmptyTable'.+head1 :: Table Expr a => NonEmptyTable Expr a -> a+head1 =+  fromColumns .+  runIdentity .+  hunvectorize (\Spec {info} -> Identity . shead1Expr info) .+  toColumns+++-- | @'index1' i as@ extracts a single element from @as@, returning+-- 'Rel8.nullTable' if @i@ is out of range. Note that although PostgreSQL+-- array indexes are 1-based (by default), this function is always 0-based.+index1 :: Table Expr a => Expr Int32 -> NonEmptyTable Expr a -> NullTable Expr a+index1 i =+  fromColumns .+  hnullify (\Spec {info} -> sindex1Expr info i) .+  toColumns+++-- | Get the last element of a 'NonEmptyTable'.+last1 :: Table Expr a => NonEmptyTable Expr a -> a+last1 =+  fromColumns .+  runIdentity .+  hunvectorize (\Spec {info} -> Identity . slast1Expr info) .+  toColumns+++-- | Get the length of a 'NonEmptyTable'+length1 :: Table Expr a => NonEmptyTable Expr a -> Expr Int32+length1 =+  getFirst .+  hunvectorize (\_ -> First . length1Expr) .   toColumns
src/Rel8/Table/Null.hs view
@@ -6,6 +6,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Null
src/Rel8/Table/Nullify.hs view
@@ -8,6 +8,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Nullify@@ -28,14 +29,17 @@ -- 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@@ -174,12 +178,14 @@   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)
src/Rel8/Table/Opaleye.hs view
@@ -10,22 +10,25 @@ {-# options_ghc -Wno-deprecations #-}  module Rel8.Table.Opaleye-  ( aggregator-  , attributes+  ( attributes   , binaryspec   , distinctspec   , exprs   , exprsWithNames+  , ifPP+  , relExprPP   , table   , tableFields   , unpackspec   , valuesspec   , view   , castTable+  , fromOrder   ) where  -- base+import Data.Foldable (traverse_) import Data.Functor.Const ( Const( Const ), getConst ) import Data.List.NonEmpty ( NonEmpty ) import Prelude@@ -33,8 +36,11 @@ -- 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.Operators as Opaleye+import qualified Opaleye.Internal.Order as Opaleye+import qualified Opaleye.Internal.PackMap as Opaleye+import qualified Opaleye.Internal.Unpackspec as Opaleye import qualified Opaleye.Internal.Values as Opaleye import qualified Opaleye.Table as Opaleye @@ -42,31 +48,28 @@ import Data.Profunctor ( dimap, lmap )  -- rel8-import Rel8.Aggregate ( Aggregate( Aggregate ), Aggregates ) import Rel8.Expr ( Expr ) import Rel8.Expr.Opaleye   ( fromPrimExpr, toPrimExpr   , scastExpr, traverseFieldP   )-import Rel8.Schema.HTable ( htabulateA, hfield, hspecs, htabulate,-                            htraverseP, htraversePWithField )+import Rel8.Schema.HTable+  ( htabulateA, hfield, hspecs, htabulate+  , htraverseP, htraversePWithField+  ) import Rel8.Schema.Name ( Name( Name ), Selects, ppColumn )+import Rel8.Schema.QualifiedName (QualifiedName (QualifiedName)) import Rel8.Schema.Spec ( Spec(..) ) import Rel8.Schema.Table ( TableSchema(..), ppTable ) import Rel8.Table ( Table, fromColumns, toColumns ) import Rel8.Type.Information ( typeName )+import Rel8.Type.Name (showTypeName)  -- semigroupoids 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,8 +106,16 @@     (Name name, expr) -> Const (pure (name, toPrimExpr expr))  +ifPP :: Table Expr a => Opaleye.IfPP a a+ifPP = fromOpaleyespec Opaleye.ifPPField+++relExprPP :: Table Expr a => Opaleye.RelExprPP a a+relExprPP = fromOpaleyespec Opaleye.relExprColumn++ table :: Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs-table (TableSchema name schema columns) =+table (TableSchema (QualifiedName name schema) columns) =   case schema of     Nothing -> Opaleye.table name (tableFields columns)     Just schemaName -> Opaleye.tableWithSchema schemaName name (tableFields columns)@@ -131,7 +142,8 @@ valuesspec :: Table Expr a => Opaleye.Valuesspec a a valuesspec = dimap toColumns fromColumns $   htraversePWithField (traverseFieldP . Opaleye.valuesspecFieldType . typeName)-  where typeName = Rel8.Type.Information.typeName . info . hfield hspecs+  where+    typeName = showTypeName . Rel8.Type.Information.typeName . info . hfield hspecs   view :: Selects names exprs => names -> exprs@@ -148,3 +160,9 @@   case hfield hspecs field of     Spec {info} -> case hfield as field of         expr -> scastExpr info expr+++fromOrder :: Opaleye.Order a -> Opaleye.Unpackspec a a+fromOrder (Opaleye.Order o) =+  Opaleye.Unpackspec $ Opaleye.PackMap $ \f a ->+    a <$ traverse_ (f . snd) (o a)
src/Rel8/Table/Rel8able.hs view
@@ -5,6 +5,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  {-# options_ghc -fno-warn-orphans #-}@@ -16,6 +17,9 @@  -- base import Prelude ()++-- base-compat+import Data.Type.Equality.Compat  -- rel8 import Rel8.Expr ( Expr )
src/Rel8/Table/Serialize.hs view
@@ -7,6 +7,7 @@ {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Serialize
src/Rel8/Table/These.hs view
@@ -10,6 +10,7 @@ {-# language TupleSections #-} {-# language TypeApplications #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  {-# options_ghc -fno-warn-orphans #-}@@ -32,8 +33,11 @@ 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 )@@ -324,17 +328,16 @@     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
src/Rel8/Table/Transpose.hs view
@@ -3,6 +3,7 @@ {-# language FunctionalDependencies #-} {-# language StandaloneKindSignatures #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  module Rel8.Table.Transpose@@ -13,6 +14,9 @@ -- base import Data.Kind ( Constraint, Type ) import Prelude ()++-- base-compat+import Data.Type.Equality.Compat  -- rel8 import qualified Rel8.Schema.Kind as K
src/Rel8/Table/Window.hs view
@@ -1,43 +1,120 @@+{-# language ApplicativeDo #-}+{-# language FlexibleContexts #-} {-# language MonoLocalBinds #-}+{-# language NamedFieldPuns #-}  module Rel8.Table.Window-  ( cumulative-  , cumulative_-  , currentRow+  ( currentRow+  , lag, lagOn+  , lead, leadOn+  , firstValue, firstValueOn+  , lastValue, lastValueOn+  , nthValue, nthValueOn   ) where  -- base-import Prelude+import Data.Int (Int32)+import Prelude hiding (null)  -- opaleye import qualified Opaleye.Window as Opaleye +-- profunctor+import Data.Profunctor (dimap, lmap)+ -- 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 ) )+import Rel8.Expr (Expr)+import Rel8.Expr.Null (null, nullify, snull)+import Rel8.Expr.Serialize (litExpr)+import Rel8.Expr.Window+  ( lagExpr, lagExprOn+  , leadExpr, leadExprOn+  , firstValueExpr+  , lastValueExpr+  , nthValueExpr, nthValueExprOn+  )+import Rel8.Schema.HTable (htraverseP)+import Rel8.Schema.HTable.Identity (HIdentity (HIdentity))+import Rel8.Schema.HTable.Label (hlabel)+import Rel8.Schema.HTable.Maybe (HMaybeTable (HMaybeTable))+import Rel8.Schema.HTable.Nullify (hnullify)+import Rel8.Schema.Null (Nullity (NotNull, Null))+import Rel8.Schema.Spec (Spec (..))+import Rel8.Table (Table, fromColumns, toColumns)+import Rel8.Table.Maybe (MaybeTable)+import Rel8.Type.Tag (MaybeTag (IsJust))+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)+-- | Return every column of the current row of a window query.+currentRow :: Window a a+currentRow = Window $ Opaleye.over (Opaleye.noWindowFunction id) mempty mempty  --- | 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+-- | @'lag' n@ returns the row @n@ rows before the current row in a given+-- window. Returns 'Rel8.nothingTable' if @n@ is out of bounds.+lag :: Table Expr a => Expr Int32 -> Window a (MaybeTable Expr a)+lag n = do+  htag <- lagExprOn n null (\_ -> nullify (litExpr IsJust))+  hjust <- lmap toColumns $ hnullify $ \Spec {info, nullity} ->+    case nullity of+      NotNull -> lagExprOn n (snull info) nullify+      Null -> lagExpr n (snull info)+  pure $ fromColumns $ HMaybeTable (hlabel (HIdentity htag)) (hlabel hjust)  --- | Return every column of the current row of a window query.-currentRow :: Window a a-currentRow = Window $ Opaleye.over (Opaleye.noWindowFunction id) mempty mempty+-- | Applies 'lag' to the columns selected by the given function.+lagOn :: Table Expr a => Expr Int32 -> (i -> a) -> Window i (MaybeTable Expr a)+lagOn n f = lmap f (lag n)+++-- | @'lead' n@ returns the row @n@ rows after the current row in a given+-- window. Returns 'Rel8.nothingTable' if @n@ is out of bounds.+lead :: Table Expr a => Expr Int32 -> Window a (MaybeTable Expr a)+lead n = do+  htag <- leadExprOn n null (\_ -> nullify (litExpr IsJust))+  hjust <- lmap toColumns $ hnullify $ \Spec {info, nullity} ->+    case nullity of+      NotNull -> leadExprOn n (snull info) nullify+      Null -> leadExpr n (snull info)+  pure $ fromColumns $ HMaybeTable (hlabel (HIdentity htag)) (hlabel hjust)+++-- | Applies 'lead' to the columns selected by the given function.+leadOn :: Table Expr a => Expr Int32 -> (i -> a) -> Window i (MaybeTable Expr a)+leadOn n f = lmap f (lead n)+++-- | 'firstValue' returns the first row of the window of the current row.+firstValue :: Table Expr a => Window a a+firstValue = dimap toColumns fromColumns $ htraverseP firstValueExpr+++-- | Applies 'firstValue' to the columns selected by the given function.+firstValueOn :: Table Expr a => (i -> a) -> Window i a+firstValueOn f = lmap f firstValue+++-- | 'lastValue' returns the first row of the window of the current row.+lastValue :: Table Expr a => Window a a+lastValue = dimap toColumns fromColumns $ htraverseP lastValueExpr+++-- | Applies 'lastValue' to the columns selected by the given function.+lastValueOn :: Table Expr a => (i -> a) -> Window i a+lastValueOn f = lmap f lastValue+++-- | @'nthValue' n@ returns the @n@th row of the window of the current row.+-- Returns 'Rel8.nothingTable' if @n@ is out of bounds.+nthValue :: Table Expr a => Expr Int32 -> Window a (MaybeTable Expr a)+nthValue n = do+  htag <- nthValueExprOn n (\_ -> litExpr IsJust)+  hjust <- lmap toColumns $ hnullify $ \_ -> nthValueExpr n+  pure $ fromColumns $ HMaybeTable (hlabel (HIdentity htag)) (hlabel hjust)+++-- | Applies 'nthValue' to the columns selected by the given function.+nthValueOn :: Table Expr a => Expr Int32 -> (i -> a) -> Window i (MaybeTable Expr a)+nthValueOn n f = lmap f (nthValue n)
src/Rel8/Tabulate.hs view
@@ -2,7 +2,6 @@ {-# language MonoLocalBinds #-} {-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-}-{-# language TypeApplications #-} {-# language TupleSections #-} {-# language UndecidableInstances #-} @@ -22,9 +21,13 @@      -- * Aggregation and Ordering   , aggregate+  , aggregate1   , distinct   , order +    -- * Materialize+  , materialize+     -- ** Magic 'Tabulation's     -- $magic   , count@@ -68,7 +71,6 @@ import Control.Comonad ( extract )  -- opaleye-import qualified Opaleye.Aggregate as Opaleye import qualified Opaleye.Order as Opaleye ( orderBy, distinctOnExplicit )  -- profunctors@@ -82,31 +84,33 @@ import qualified Data.Profunctor.Product as PP  -- rel8-import Rel8.Aggregate ( Aggregates )+import Rel8.Aggregate (Aggregator' (Aggregator), Aggregator, toAggregator1)+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 )+import qualified Rel8.Query.Materialize as Q import qualified Rel8.Query.Maybe as Q ( optional ) 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@@ -329,19 +333,23 @@     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 i, 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, Table Expr i)+  => Aggregator' fold i a -> Tabulation k i -> Tabulation k a+aggregate1 aggregator (Tabulation f) =+  Tabulation $ Q.aggregateU (keyed unpackspec unpackspec) (keyed groupBy (toAggregator1 aggregator)) . f   -- | 'distinct' ensures a 'Tabulation' has at most one value for@@ -408,11 +416,7 @@ -- 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 . (true <$)   -- | 'optional' produces a \"magic\" 'Tabulation' whereby each@@ -439,11 +443,7 @@ -- '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@@ -452,10 +452,7 @@ -- '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@@ -628,6 +625,17 @@ -- @do@-notation. difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a difference a b = a <* absent b+++-- | 'Q.materialize' for 'Tabulation's.+materialize :: (Table Expr k, Table Expr a, Table Expr b)+  => Tabulation k a -> (Tabulation k a -> Query b) -> Query b+materialize tabulation f = case peek tabulation of+  Tabulation query -> do+    (_, equery) <- query mempty+    case equery of+      Left as -> Q.materialize as (f . liftQuery)+      Right kas -> Q.materialize kas (f . fromQuery)   -- | 'Tabulation's can be produced with either 'fromQuery' or 'liftQuery', and
src/Rel8/Type.hs view
@@ -1,8 +1,12 @@+{-# language LambdaCase #-} {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language MonoLocalBinds #-} {-# language MultiWayIf #-}+{-# language OverloadedStrings #-}+{-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-}+{-# language TypeApplications #-} {-# language UndecidableInstances #-}  module Rel8.Type@@ -14,14 +18,23 @@ import Data.Aeson ( Value ) import qualified Data.Aeson as Aeson +-- attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as A++-- attoparsec-aeson+import qualified Data.Aeson.Parser as Aeson+ -- base+import Control.Applicative ((<|>))+import Data.Fixed (Fixed) import Data.Int ( Int16, Int32, Int64 ) import Data.List.NonEmpty ( NonEmpty ) import Data.Kind ( Constraint, Type ) import Prelude  -- bytestring-import Data.ByteString ( ByteString )+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as Lazy ( ByteString ) import qualified Data.ByteString.Lazy as ByteString ( fromStrict, toStrict ) @@ -29,9 +42,15 @@ import Data.CaseInsensitive ( CI ) import qualified Data.CaseInsensitive as CI +-- data-textual+import Data.Textual (textual)+ -- hasql import qualified Hasql.Decoders as Hasql +-- network-ip+import qualified Network.IP.Addr as IP+ -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye import qualified Opaleye.Internal.HaskellDB.Sql.Default as Opaleye ( quote )@@ -39,7 +58,13 @@ -- rel8 import Rel8.Schema.Null ( NotNull, Sql, nullable ) import Rel8.Type.Array ( listTypeInformation, nonEmptyTypeInformation )+import Rel8.Type.Decimal (PowerOf10, resolution)+import Rel8.Type.Decoder ( Decoder(..) ) import Rel8.Type.Information ( TypeInformation(..), mapTypeInformation )+import Rel8.Type.Name (TypeName (..))+import Rel8.Type.Parser (parse)+import Rel8.Type.Parser.ByteString (bytestring)+import qualified Rel8.Type.Parser.Time as Time  -- scientific import Data.Scientific ( Scientific )@@ -47,20 +72,24 @@ -- text import Data.Text ( Text ) import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text (decodeUtf8) import qualified Data.Text.Lazy as Lazy ( Text, unpack ) import qualified Data.Text.Lazy as Text ( fromStrict, toStrict ) import qualified Data.Text.Lazy.Encoding as Lazy ( decodeUtf8 )  -- time-import Data.Time.Calendar ( Day )-import Data.Time.Clock ( UTCTime )+import Data.Time.Calendar (Day)+import Data.Time.Clock (UTCTime) import Data.Time.LocalTime-  ( CalendarDiffTime( CalendarDiffTime )+  ( CalendarDiffTime (CalendarDiffTime)   , LocalTime   , TimeOfDay   )-import Data.Time.Format ( formatTime, defaultTimeLocale )+import Data.Time.Format (formatTime, defaultTimeLocale) +-- utf8+import qualified Data.ByteString.UTF8 as UTF8+ -- uuid import Data.UUID ( UUID ) import qualified Data.UUID as UUID@@ -83,7 +112,15 @@ instance DBType Bool where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.BoolLit-    , decode = Hasql.bool+    , decode =+        Decoder+          { binary = Hasql.bool+          , parser = \case+              "t" -> pure True+              "f" -> pure False+              input -> Left $ "bool: bad bool " <> show input+          , delimiter = ','+          }     , typeName = "bool"     } @@ -92,8 +129,20 @@ instance DBType Char where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.StringLit . pure-    , decode = Hasql.char-    , typeName = "char"+    , typeName =+        TypeName+          { name = "bpchar"+          , modifiers = ["1"]+          , arrayDepth = 0+          }+    , decode = +        Decoder+          { binary = Hasql.char+          , parser = \input -> case UTF8.uncons input of+              Just (char, rest) | BS.null rest -> pure char+              _ -> Left $ "char: bad char " <> show input+          , delimiter = ','+          }     }  @@ -101,7 +150,12 @@ instance DBType Int16 where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.IntegerLit . toInteger-    , decode = Hasql.int2+    , decode =+        Decoder+          { binary = Hasql.int2+          , parser = parse (A.signed A.decimal)+          , delimiter = ','+          }     , typeName = "int2"     } @@ -110,7 +164,12 @@ instance DBType Int32 where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.IntegerLit . toInteger-    , decode = Hasql.int4+    , decode =+        Decoder+          { binary = Hasql.int4+          , parser = parse (A.signed A.decimal)+          , delimiter = ','+          }     , typeName = "int4"     } @@ -119,7 +178,12 @@ instance DBType Int64 where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.IntegerLit . toInteger-    , decode = Hasql.int8+    , decode =+        Decoder+          { binary = Hasql.int8+          , parser = parse (A.signed A.decimal)+          , delimiter = ','+          }     , typeName = "int8"     } @@ -131,8 +195,13 @@         if | x == (1 / 0)  -> Opaleye.OtherLit "'Infinity'"            | isNaN x       -> Opaleye.OtherLit "'NaN'"            | x == (-1 / 0) -> Opaleye.OtherLit "'-Infinity'"-           | otherwise     -> Opaleye.NumericLit $ realToFrac x-    , decode = Hasql.float4+           | otherwise     -> Opaleye.DoubleLit $ realToFrac x+    , decode =+        Decoder+          { binary = Hasql.float4+          , parser = parse (floating (realToFrac <$> A.double))+          , delimiter = ','+          }     , typeName = "float4"     } @@ -144,8 +213,13 @@         if | x == (1 / 0)  -> Opaleye.OtherLit "'Infinity'"            | isNaN x       -> Opaleye.OtherLit "'NaN'"            | x == (-1 / 0) -> Opaleye.OtherLit "'-Infinity'"-           | otherwise     -> Opaleye.NumericLit $ realToFrac x-    , decode = Hasql.float8+           | otherwise     -> Opaleye.DoubleLit x+    , decode =+        Decoder+          { binary = Hasql.float8+          , parser = parse (floating A.double)+          , delimiter = ','+          }     , typeName = "float8"     } @@ -154,18 +228,48 @@ instance DBType Scientific where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.NumericLit-    , decode = Hasql.numeric+    , decode =+        Decoder+          { binary = Hasql.numeric+          , parser = parse A.scientific+          , delimiter = ','+          }     , typeName = "numeric"     }  +-- | Corresponds to @numeric(1000, log₁₀ n)@+instance PowerOf10 n => DBType (Fixed n) where+  typeInformation = TypeInformation+    { encode = Opaleye.ConstExpr . Opaleye.NumericLit . realToFrac+    , decode =+        realToFrac <$>+        Decoder+          { binary = Hasql.numeric+          , parser = parse A.scientific+          , delimiter = ','+          }+    , typeName =+        TypeName+          { name = "numeric"+          , modifiers = ["1000", show (resolution @n)]+          , arrayDepth = 0+          }+    }++ -- | Corresponds to @timestamptz@ instance DBType UTCTime where   typeInformation = TypeInformation     { encode =         Opaleye.ConstExpr . Opaleye.OtherLit .         formatTime defaultTimeLocale "'%FT%T%QZ'"-    , decode = Hasql.timestamptz+    , decode =+        Decoder+          { binary = Hasql.timestamptz+          , parser = parse Time.utcTime+          , delimiter = ','+          }     , typeName = "timestamptz"     } @@ -176,7 +280,12 @@     { encode =         Opaleye.ConstExpr . Opaleye.OtherLit .         formatTime defaultTimeLocale "'%F'"-    , decode = Hasql.date+    , decode =+        Decoder+          { binary = Hasql.date+          , parser = parse Time.day+          , delimiter = ','+          }     , typeName = "date"     } @@ -187,7 +296,12 @@     { encode =         Opaleye.ConstExpr . Opaleye.OtherLit .         formatTime defaultTimeLocale "'%FT%T%Q'"-    , decode = Hasql.timestamp+    , decode =+        Decoder+          { binary = Hasql.timestamp+          , parser = parse Time.localTime+          , delimiter = ','+          }     , typeName = "timestamp"     } @@ -198,7 +312,12 @@     { encode =         Opaleye.ConstExpr . Opaleye.OtherLit .         formatTime defaultTimeLocale "'%T%Q'"-    , decode = Hasql.time+    , decode =+        Decoder+          { binary = Hasql.time+          , parser = parse Time.timeOfDay+          , delimiter = ','+          }     , typeName = "time"     } @@ -209,7 +328,12 @@     { encode =         Opaleye.ConstExpr . Opaleye.OtherLit .         formatTime defaultTimeLocale "'%bmon %0Es'"-    , decode = CalendarDiffTime 0 . realToFrac <$> Hasql.interval+    , decode =+        Decoder+          { binary = CalendarDiffTime 0 . realToFrac <$> Hasql.interval+          , parser = parse Time.calendarDiffTime+          , delimiter = ','+          }     , typeName = "interval"     } @@ -218,7 +342,12 @@ instance DBType Text where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.StringLit . Text.unpack-    , decode = Hasql.text+    , decode =+        Decoder+          { binary = Hasql.text+          , parser = pure . Text.decodeUtf8+          , delimiter = ','+          }     , typeName = "text"     } @@ -247,7 +376,12 @@ instance DBType ByteString where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.ByteStringLit-    , decode = Hasql.bytea+    , decode =+        Decoder+          { binary = Hasql.bytea+          , parser = parse bytestring+          , delimiter = ','+          }     , typeName = "bytea"     } @@ -263,7 +397,14 @@ instance DBType UUID where   typeInformation = TypeInformation     { encode = Opaleye.ConstExpr . Opaleye.StringLit . UUID.toString-    , decode = Hasql.uuid+    , decode =+        Decoder+          { binary = Hasql.uuid+          , parser = \input -> case UUID.fromASCIIBytes input of+              Just a -> pure a+              Nothing -> Left $ "uuid: bad UUID " <> show input+          , delimiter = ','+          }     , typeName = "uuid"     } @@ -275,14 +416,41 @@         Opaleye.ConstExpr . Opaleye.OtherLit .         Opaleye.quote .         Lazy.unpack . Lazy.decodeUtf8 . Aeson.encode-    , decode = Hasql.jsonb+    , decode =+        Decoder+          { binary = Hasql.jsonb+          , parser = parse Aeson.value+          , delimiter = ','+          }     , typeName = "jsonb"     }  +-- | Corresponds to @inet@+instance DBType (IP.NetAddr IP.IP) where+  typeInformation = TypeInformation+    { encode =+        Opaleye.ConstExpr . Opaleye.StringLit . IP.printNetAddr+    , decode =+        Decoder+          { binary = Hasql.inet+          , parser = parse $+              textual+                <|> (`IP.netAddr` 32) . IP.IPv4 <$> textual+                <|> (`IP.netAddr` 128) . IP.IPv6 <$> textual+          , delimiter = ','+          }+    , typeName = "inet"+    }++ instance Sql DBType a => DBType [a] where   typeInformation = listTypeInformation nullable typeInformation   instance Sql DBType a => DBType (NonEmpty a) where   typeInformation = nonEmptyTypeInformation nullable typeInformation+++floating :: Floating a => A.Parser a -> A.Parser a+floating p = p <|> A.signed (1.0 / 0 <$ "Infinity") <|> 0.0 / 0 <$ "NaN"
src/Rel8/Type/Array.hs view
@@ -2,20 +2,32 @@ {-# language LambdaCase #-} {-# language NamedFieldPuns #-} {-# language OverloadedStrings #-}+{-# language TypeApplications #-} {-# language ViewPatterns #-}  module Rel8.Type.Array   ( array, encodeArrayElement, extractArrayElement+  , arrayTypeName   , listTypeInformation   , nonEmptyTypeInformation+  , head, index, last, length   ) where +-- attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as A+ -- base-import Data.Foldable ( toList )+import Control.Applicative ((<|>), many)+import Data.Bifunctor (first)+import Data.Foldable (fold, toList) import Data.List.NonEmpty ( NonEmpty, nonEmpty )-import Prelude hiding ( null, repeat, zipWith )+import Prelude hiding ( head, last, length, null, repeat, zipWith ) +-- bytestring+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS+ -- hasql import qualified Hasql.Decoders as Hasql @@ -24,13 +36,19 @@  -- rel8 import Rel8.Schema.Null ( Unnullify, Nullity( Null, NotNull ) )+import Rel8.Type.Decoder (Decoder (..), NullableOrNot (..), Parser) import Rel8.Type.Information ( TypeInformation(..), parseTypeInformation )+import Rel8.Type.Name (TypeName (..), showTypeName)+import Rel8.Type.Parser (parse) +-- text+import qualified Data.Text as Text + array :: Foldable f   => TypeInformation a -> f Opaleye.PrimExpr -> Opaleye.PrimExpr array info =-  Opaleye.CastExpr (arrayType info <> "[]") .+  Opaleye.CastExpr (showTypeName (arrayType info) <> "[]") .   Opaleye.ArrayExpr . map (encodeArrayElement info) . toList {-# INLINABLE array #-} @@ -41,11 +59,16 @@   -> TypeInformation [a] listTypeInformation nullity info@TypeInformation {encode, decode} =   TypeInformation-    { decode = case nullity of-        Null ->-          Hasql.listArray (decodeArrayElement info (Hasql.nullable decode))-        NotNull ->-          Hasql.listArray (decodeArrayElement info (Hasql.nonNullable decode))+    { decode =+        Decoder+          { binary = Hasql.listArray $ case nullity of+              Null -> Hasql.nullable (decodeArrayElement info decode)+              NotNull -> Hasql.nonNullable (decodeArrayElement info decode)+          , parser = case nullity of+              Null -> arrayParser (Nullable decode)+              NotNull -> arrayParser (NonNullable decode)+          , delimiter = ','+          }     , encode = case nullity of         Null ->           Opaleye.ArrayExpr .@@ -53,7 +76,7 @@         NotNull ->           Opaleye.ArrayExpr .           fmap (encodeArrayElement info . encode)-    , typeName = arrayType info <> "[]"+    , typeName = arrayTypeName info     }   where     null = Opaleye.ConstExpr Opaleye.NullLit@@ -64,37 +87,110 @@   -> TypeInformation (Unnullify a)   -> TypeInformation (NonEmpty a) nonEmptyTypeInformation nullity =-  parseTypeInformation parse toList . listTypeInformation nullity+  parseTypeInformation fromList toList . listTypeInformation nullity   where-    parse = maybe (Left message) Right . nonEmpty+    fromList = maybe (Left message) Right . nonEmpty     message = "failed to decode NonEmptyList: got empty list"  +arrayTypeName :: TypeInformation a -> TypeName+arrayTypeName info = (arrayType info) {arrayDepth = 1}++ isArray :: TypeInformation a -> Bool-isArray = \case-  (reverse . typeName -> ']' : '[' : _) -> True-  _ -> False+isArray = (> 0) . arrayDepth . typeName  -arrayType :: TypeInformation a -> String+arrayType :: TypeInformation a -> TypeName arrayType info-  | isArray info = "record"+  | isArray info = "text"   | otherwise = typeName info  -decodeArrayElement :: TypeInformation a -> Hasql.NullableOrNot Hasql.Value x -> Hasql.NullableOrNot Hasql.Value x+decodeArrayElement :: TypeInformation a -> Decoder x -> Hasql.Value x decodeArrayElement info-  | isArray info = Hasql.nonNullable . Hasql.composite . Hasql.field-  | otherwise = id+  | isArray info = \decoder ->+      Hasql.refine (first Text.pack . parser decoder) Hasql.bytea+  | otherwise = binary   encodeArrayElement :: TypeInformation a -> Opaleye.PrimExpr -> Opaleye.PrimExpr encodeArrayElement info-  | isArray info = Opaleye.UnExpr (Opaleye.UnOpOther "ROW")+  | isArray info = Opaleye.CastExpr "text" . Opaleye.CastExpr (showTypeName (typeName info))   | otherwise = id   extractArrayElement :: TypeInformation a -> Opaleye.PrimExpr -> Opaleye.PrimExpr extractArrayElement info-  | isArray info = flip Opaleye.CompositeExpr "f1"+  | isArray info = Opaleye.CastExpr (showTypeName (typeName info))   | otherwise = id+++parseArray :: Char -> ByteString -> Either String [Maybe ByteString]+parseArray delimiter = parse $ do+  A.char '{' *> A.sepBy element (A.char delimiter) <* A.char '}'+  where+    element = null <|> nonNull+      where+        null = Nothing <$ A.string "NULL"+        nonNull = Just <$> (quoted <|> unquoted)+          where+            unquoted = A.takeWhile1 (A.notInClass (delimiter : "\"{}"))+            quoted = A.char '"' *> contents <* A.char '"'+              where+                contents = fold <$> many (unquote <|> unescape)+                  where+                    unquote = A.takeWhile1 (A.notInClass "\"\\")+                    unescape = A.char '\\' *> do+                      BS.singleton <$> do+                        A.char '\\' <|> A.char '"'+++arrayParser :: NullableOrNot Decoder a -> Parser [a]+arrayParser = \case+  Nullable Decoder {parser, delimiter} -> \input -> do+    elements <- parseArray delimiter input+    traverse (traverse parser) elements+  NonNullable Decoder {parser, delimiter} -> \input -> do+    elements <- parseArray delimiter input+    traverse (maybe (Left "array: unexpected null") parser) elements+++head :: TypeInformation a -> Opaleye.PrimExpr -> Opaleye.PrimExpr+head info a = extractArrayElement info $ subscript (lower a) a+++last :: TypeInformation a -> Opaleye.PrimExpr -> Opaleye.PrimExpr+last info a = extractArrayElement info $ subscript (upper a) a+++subscript :: Opaleye.PrimExpr -> Opaleye.PrimExpr -> Opaleye.PrimExpr+subscript i a = Opaleye.ArrayIndex a i+++index :: TypeInformation a -> Opaleye.PrimExpr -> Opaleye.PrimExpr -> Opaleye.PrimExpr+index info i a = extractArrayElement info $ subscript (plus (lower a) i) a+++lower :: Opaleye.PrimExpr -> Opaleye.PrimExpr+lower a = Opaleye.FunExpr "array_lower" [a, one]+++upper :: Opaleye.PrimExpr -> Opaleye.PrimExpr+upper a = Opaleye.FunExpr "array_lower" [a, one]+++length :: Opaleye.PrimExpr -> Opaleye.PrimExpr+length a = Opaleye.FunExpr "coalesce" [Opaleye.FunExpr "array_length" [a, one], zero]+++one :: Opaleye.PrimExpr+one = Opaleye.ConstExpr (Opaleye.IntegerLit 1)+++zero :: Opaleye.PrimExpr+zero = Opaleye.ConstExpr (Opaleye.IntegerLit 0)+++plus :: Opaleye.PrimExpr -> Opaleye.PrimExpr -> Opaleye.PrimExpr+plus = Opaleye.BinExpr (Opaleye.:+)
src/Rel8/Type/Composite.hs view
@@ -1,9 +1,11 @@ {-# language AllowAmbiguousTypes #-} {-# language BlockArguments #-} {-# language DataKinds #-}+{-# language DisambiguateRecordFields #-} {-# language FlexibleContexts #-} {-# language GADTs #-} {-# language NamedFieldPuns #-}+{-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-} {-# language StandaloneKindSignatures #-} {-# language TypeApplications #-}@@ -18,12 +20,22 @@   ) where +-- attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as A+ -- base-import Data.Functor.Const ( Const( Const ), getConst )-import Data.Functor.Identity ( Identity( Identity ) )+import Control.Applicative ((<|>), many, optional)+import Data.Foldable (fold)+import Data.Functor.Const (Const (Const), getConst)+import Data.Functor.Identity (Identity (Identity)) import Data.Kind ( Constraint, Type )+import Data.List (uncons) import Prelude +-- bytestring+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS+ -- hasql import qualified Hasql.Decoders as Hasql @@ -36,6 +48,7 @@ import Rel8.Schema.HTable ( HTable, hfield, hspecs, htabulate, htabulateA ) import Rel8.Schema.Name ( Name( Name ) ) import Rel8.Schema.Null ( Nullity( Null, NotNull ) )+import Rel8.Schema.QualifiedName (QualifiedName) import Rel8.Schema.Result ( Result ) import Rel8.Schema.Spec ( Spec( Spec, nullity, info ) ) import Rel8.Table ( fromColumns, toColumns, fromResult, toResult )@@ -45,14 +58,22 @@ import Rel8.Table.Rel8able () import Rel8.Table.Serialize ( litHTable ) import Rel8.Type ( DBType, typeInformation )+import Rel8.Type.Decoder (Decoder (Decoder), Parser)+import qualified Rel8.Type.Decoder as Decoder import Rel8.Type.Eq ( DBEq ) import Rel8.Type.Information ( TypeInformation(..) )+import Rel8.Type.Name (TypeName (..)) import Rel8.Type.Ord ( DBOrd, DBMax, DBMin )+import Rel8.Type.Parser (parse)  -- semigroupoids import Data.Functor.Apply ( WrappedApplicative(..) ) +-- transformers+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.State.Strict (StateT (StateT), runStateT) + -- | A deriving-via helper type for column types that store a Haskell product -- type in a single Postgres column using a Postgres composite type. --@@ -68,9 +89,19 @@  instance DBComposite a => DBType (Composite a) where   typeInformation = TypeInformation-    { decode = Hasql.composite (Composite . fromResult @_ @(HKD a Expr) <$> decoder)+    { decode =+        Decoder+          { binary = Hasql.composite (Composite . fromResult @_ @(HKD a Expr) <$> decoder)+          , parser = fmap (Composite . fromResult @_ @(HKD a Expr)) . parser+          , delimiter = ','+          }     , encode = encoder . litHTable . toResult @_ @(HKD a Expr) . unComposite-    , typeName = compositeTypeName @a+    , typeName =+        TypeName+          { name = compositeTypeName @a+          , modifiers = []+          , arrayDepth = 0+          }     }  @@ -94,7 +125,7 @@   compositeFields :: HKD a Name    -- | The name of the composite type that @a@ maps to.-  compositeTypeName :: String+  compositeTypeName :: QualifiedName   -- | Collapse a 'HKD' into a PostgreSQL composite type.@@ -124,8 +155,8 @@   case hfield hspecs field of     Spec {nullity, info} -> WrapApplicative $ Identity <$>       case nullity of-        Null -> Hasql.field $ Hasql.nullable $ decode info-        NotNull -> Hasql.field $ Hasql.nonNullable $ decode info+        Null -> Hasql.field $ Hasql.nullable $ Decoder.binary $ decode info+        NotNull -> Hasql.field $ Hasql.nonNullable $ Decoder.binary $ decode info   encoder :: HTable t => t Expr -> Opaleye.PrimExpr@@ -133,3 +164,40 @@   where     exprs = getConst $ htabulateA \field -> case hfield a field of       expr -> Const [toPrimExpr expr]+++parser :: HTable t => Parser (t Result)+parser input = do+  fields <- parseRow input+  (a, rest) <- runStateT go fields+  case rest of+    [] -> pure a+    _ -> Left "composite: too many fields"+  where+    go = htabulateA \field -> do+      mbytes <- StateT $ maybe missing pure . uncons+      lift $ Identity <$> case hfield hspecs field of+        Spec {nullity, info} -> case nullity of+          Null -> traverse (Decoder.parser (decode info)) mbytes+          NotNull -> case mbytes of+            Nothing -> Left "composite: unexpected null"+            Just bytes -> Decoder.parser (decode info) bytes+    missing = Left "composite: missing fields"+++parseRow :: ByteString -> Either String [Maybe ByteString]+parseRow = parse $ do+  A.char '(' *> A.sepBy element (A.char ',') <* A.char ')'+  where+    element = optional (quoted <|> unquoted)+      where+        unquoted = A.takeWhile1 (A.notInClass ",\"()")+        quoted = A.char '"' *> contents <* A.char '"'+          where+            contents = fold <$> many (unquote <|> unescape <|> quote)+              where+                unquote = A.takeWhile1 (A.notInClass "\"\\")+                unescape = A.char '\\' *> do+                  BS.singleton <$> do+                    A.char '\\' <|> A.char '"'+                quote = "\"" <$ A.string "\"\""
+ src/Rel8/Type/Decimal.hs view
@@ -0,0 +1,103 @@+{-# language AllowAmbiguousTypes #-}+{-# language ConstraintKinds #-}+{-# language DataKinds #-}+{-# language FlexibleContexts #-}+{-# language FlexibleInstances #-}+{-# language NoStarIsType #-}+{-# language PolyKinds #-}+{-# language RankNTypes #-}+{-# language ScopedTypeVariables #-}+{-# language StandaloneKindSignatures #-}+{-# language TypeApplications #-}+{-# language TypeFamilies #-}+{-# language TypeOperators #-}+{-# language UndecidableInstances #-}++module Rel8.Type.Decimal+  ( PowerOf10+  , resolution+  )+where++-- base+import Data.Fixed (E0, E1, E2, E3, E6, E9, E12, HasResolution)+import Data.Proxy (Proxy (Proxy))+import Data.Type.Equality (type (==))+import Data.Type.Ord (type (<?))+import Data.Kind (Constraint)+import GHC.TypeLits (ErrorMessage ((:<>:), ShowType, Text), TypeError)+import GHC.TypeNats (KnownNat, Nat, type (+), type (-), type (*), Div, natVal)+import Numeric.Natural (Natural)+import Prelude+++type PowerOf10 :: a -> Constraint+class (HasResolution n, KnownNat (Log n)) => PowerOf10 n where+  type Log n :: Nat+++instance (KnownNat n, KnownNat (Log n), IsPowerOf10 n) => PowerOf10 n where+  type Log n = Log10 n+++instance PowerOf10 E0 where+  type Log E0 = 0+++instance PowerOf10 E1 where+  type Log E1 = 1+++instance PowerOf10 E2 where+  type Log E2 = 2+++instance PowerOf10 E3 where+  type Log E3 = 3+++instance PowerOf10 E6 where+  type Log E6 = 6+++instance PowerOf10 E9 where+  type Log E9 = 9+++instance PowerOf10 E12 where+  type Log E12 = 12+++resolution :: forall n. PowerOf10 n => Natural+resolution = natVal (Proxy @(Log n))+++type Exp10 :: Nat -> Nat+type Exp10 n = Exp10' 1 n+++type Exp10' :: Nat -> Nat -> Nat+type family Exp10' x n where+  Exp10' x 0 = x+  Exp10' x n = Exp10' (x * 10) (n - 1)+++type Log10 :: Nat -> Nat+type Log10 n = Log10' (n <? 10) n+++type Log10' :: Bool -> Nat -> Nat+type family Log10' bool n where+  Log10' 'True _n = 0+  Log10' 'False n = 1 + Log10 (Div n 10)+++type IsPowerOf10 :: Nat -> Constraint+type IsPowerOf10 n = IsPowerOf10' (Exp10 (Log10 n) == n) n+++type IsPowerOf10' :: Bool -> Nat -> Constraint+type family IsPowerOf10' bool n where+  IsPowerOf10' 'True _n = ()+  IsPowerOf10' 'False n =+    TypeError ('ShowType n ':<>: 'Text " is not a power of 10")
+ src/Rel8/Type/Decoder.hs view
@@ -0,0 +1,64 @@+{-# language DerivingStrategies #-}+{-# language DeriveFunctor #-}+{-# language GADTs #-}+{-# language NamedFieldPuns #-}+{-# language StandaloneKindSignatures #-}++module Rel8.Type.Decoder (+  Decoder (..),+  NullableOrNot (..),+  Parser,+  parseDecoder,+) where++-- base+import Control.Monad ((>=>))+import Data.Bifunctor (first)+import Data.Kind (Type)+import Prelude++-- bytestring+import Data.ByteString (ByteString)++-- hasql+import qualified Hasql.Decoders as Hasql++-- text+import qualified Data.Text as Text+++type Parser :: Type -> Type+type Parser a = ByteString -> Either String a+++type Decoder :: Type -> Type+data Decoder a = Decoder+  { binary :: Hasql.Value a+    -- ^ How to deserialize from PostgreSQL's binary format.+  , parser :: Parser a+    -- ^ How to deserialize from PostgreSQL's text format.+  , delimiter :: Char+    -- ^ The delimiter that is used in PostgreSQL's text format in arrays of+    -- this type (this is almost always ',').+  }+  deriving stock (Functor)+++-- | Apply a parser to 'Decoder'.+--+-- This can be used if the data stored in the database should only be subset of+-- a given 'Decoder'. The parser is applied when deserializing rows+-- returned.+parseDecoder :: (a -> Either String b) -> Decoder a -> Decoder b+parseDecoder f Decoder {binary, parser, delimiter} =+  Decoder+    { binary = Hasql.refine (first Text.pack . f) binary+    , parser = parser >=> f+    , delimiter+    }+++type NullableOrNot :: (Type -> Type) -> Type -> Type+data NullableOrNot decoder a where+  NonNullable :: decoder a -> NullableOrNot decoder a+  Nullable :: decoder a -> NullableOrNot decoder (Maybe a)
src/Rel8/Type/Enum.hs view
@@ -38,13 +38,17 @@ import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye  -- rel8+import Rel8.Schema.QualifiedName (QualifiedName) import Rel8.Type ( DBType, typeInformation )+import Rel8.Type.Decoder (Decoder (..)) import Rel8.Type.Eq ( DBEq ) import Rel8.Type.Information ( TypeInformation(..) )+import Rel8.Type.Name (TypeName (..)) import Rel8.Type.Ord ( DBOrd, DBMax, DBMin )  -- text-import Data.Text ( pack )+import Data.Text (pack)+import Data.Text.Encoding (decodeUtf8)   -- | A deriving-via helper type for column types that store an \"enum\" type@@ -66,16 +70,26 @@ instance DBEnum a => DBType (Enum a) where   typeInformation = TypeInformation     { decode =-        Hasql.enum $-        flip lookup $-        map ((pack . enumValue &&& Enum) . to) $-        genumerate @(Rep a)+        let+          mapping = (pack . enumValue &&& Enum) . to <$> genumerate @(Rep a)+          unrecognised = Left "enum: unrecognised value"+        in+          Decoder+            { binary = Hasql.enum (`lookup` mapping)+            , parser = maybe unrecognised pure . (`lookup` mapping) . decodeUtf8+            , delimiter = ','+            }     , encode =         Opaleye.ConstExpr .         Opaleye.StringLit .         enumValue @a .         unEnum-    , typeName = enumTypeName @a+    , typeName =+        TypeName+          { name = enumTypeName @a+          , modifiers = []+          , arrayDepth = 0+          }     }  @@ -101,7 +115,7 @@   enumValue = gshow @(Rep a) . from    -- | The name of the PostgreSQL @enum@ type that @a@ maps to.-  enumTypeName :: String+  enumTypeName :: QualifiedName   -- | Types that are sum types, where each constructor is unary (that is, has no
src/Rel8/Type/Eq.hs view
@@ -14,9 +14,10 @@ import Data.Aeson ( Value )  -- base-import Data.List.NonEmpty ( NonEmpty )+import Data.Fixed (Fixed) import Data.Int ( Int16, Int32, Int64 ) import Data.Kind ( Constraint, Type )+import Data.List.NonEmpty ( NonEmpty ) import Prelude  -- bytestring@@ -29,6 +30,7 @@ -- rel8 import Rel8.Schema.Null ( Sql ) import Rel8.Type ( DBType )+import Rel8.Type.Decimal (PowerOf10)  -- scientific import Data.Scientific ( Scientific )@@ -58,6 +60,7 @@ instance DBEq Int16 instance DBEq Int32 instance DBEq Int64+instance PowerOf10 n => DBEq (Fixed n) instance DBEq Float instance DBEq Double instance DBEq Scientific
src/Rel8/Type/Information.hs view
@@ -1,6 +1,7 @@ {-# language GADTs #-} {-# language NamedFieldPuns #-} {-# language StandaloneKindSignatures #-}+{-# language StrictData #-}  module Rel8.Type.Information   ( TypeInformation(..)@@ -10,18 +11,17 @@ where  -- base-import Data.Bifunctor ( first ) import Data.Kind ( Type ) import Prelude --- hasql-import qualified Hasql.Decoders as Hasql- -- opaleye import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye +-- rel8+import Rel8.Type.Name (TypeName)+ -- text-import qualified Data.Text as Text+import Rel8.Type.Decoder (Decoder, parseDecoder)   -- | @TypeInformation@ describes how to encode and decode a Haskell type to and@@ -31,9 +31,9 @@ data TypeInformation a = TypeInformation   { encode :: a -> Opaleye.PrimExpr     -- ^ How to encode a single Haskell value as a SQL expression.-  , decode :: Hasql.Value a+  , decode :: Decoder a     -- ^ How to deserialize a single result back to Haskell.-  , typeName :: String+  , typeName :: TypeName     -- ^ The name of the SQL type.   } @@ -62,6 +62,6 @@ parseTypeInformation to from TypeInformation {encode, decode, typeName} =   TypeInformation     { encode = encode . from-    , decode = Hasql.refine (first Text.pack . to) decode+    , decode = parseDecoder to decode     , typeName     }
src/Rel8/Type/JSONBEncoded.hs view
@@ -1,10 +1,14 @@+{-# language OverloadedStrings #-} {-# language StandaloneKindSignatures #-} -module Rel8.Type.JSONBEncoded ( JSONBEncoded(..) ) where+module Rel8.Type.JSONBEncoded+  ( JSONBEncoded(..)+  )+where  -- aeson-import Data.Aeson ( FromJSON, ToJSON, parseJSON, toJSON )-import Data.Aeson.Types ( parseEither )+import Data.Aeson (FromJSON, ToJSON, eitherDecodeStrict, parseJSON, toJSON)+import Data.Aeson.Types (parseEither)  -- base import Data.Bifunctor ( first )@@ -16,6 +20,7 @@  -- rel8 import Rel8.Type ( DBType(..) )+import Rel8.Type.Decoder (Decoder (..)) import Rel8.Type.Information ( TypeInformation(..) )  -- text@@ -30,6 +35,11 @@ instance (FromJSON a, ToJSON a) => DBType (JSONBEncoded a) where   typeInformation = TypeInformation     { encode = encode typeInformation . toJSON . fromJSONBEncoded-    , decode = Hasql.refine (first pack . fmap JSONBEncoded . parseEither parseJSON) Hasql.jsonb+    , decode =+        Decoder+          { binary = Hasql.refine (first pack . fmap JSONBEncoded . parseEither parseJSON) Hasql.jsonb+          , parser = fmap JSONBEncoded . eitherDecodeStrict+          , delimiter = ','+          }     , typeName = "jsonb"     }
+ src/Rel8/Type/Name.hs view
@@ -0,0 +1,58 @@+{-# language RecordWildCards #-}+{-# language StrictData #-}++module Rel8.Type.Name+  ( TypeName (..)+  , showTypeName+  )+where++-- base+import Data.Semigroup (mtimesDefault)+import Data.String (IsString, fromString)+import Prelude++-- pretty+import Text.PrettyPrint (Doc, comma, hcat, parens, punctuate, text)++-- rel8+import Rel8.Schema.QualifiedName (QualifiedName, ppQualifiedName)+++-- | A PostgreSQL type consists of a 'QualifiedName' (name, schema), and+-- optional 'modifiers' and 'arrayDepth'. 'modifiers' will usually be @[]@,+-- but a type like @numeric(6, 2)@ will have @["6", "2"]@. 'arrayDepth' is+-- always @0@ for non-array types.+data TypeName = TypeName+  { name :: QualifiedName+    -- ^ The name (and schema) of the type.+  , modifiers :: [String]+    -- ^ Any modifiers applied to the underlying type.+  , arrayDepth :: Word+    -- ^ If this is an array type, the depth of that array (@1@ for @[]@, @2@+    -- for @[][]@, etc).+  }+++-- | Constructs 'TypeName's with 'schema' set to 'Nothing', 'modifiers' set+-- to @[]@ and 'arrayDepth' set to @0@.+instance IsString TypeName where+  fromString string =+    TypeName+      { name = fromString string+      , modifiers = []+      , arrayDepth = 0+      }+++ppTypeName :: TypeName -> Doc+ppTypeName TypeName {..} =+  ppQualifiedName name <> modifier <> mtimesDefault arrayDepth (text "[]")+  where+    modifier+      | null modifiers = mempty+      | otherwise = parens (hcat $ punctuate comma $ text <$> modifiers)+++showTypeName :: TypeName -> String+showTypeName = show . ppTypeName
src/Rel8/Type/Num.hs view
@@ -12,12 +12,14 @@ where  -- base+import Data.Fixed (Fixed) import Data.Int ( Int16, Int32, Int64 ) import Data.Kind ( Constraint, Type ) import Prelude  -- rel8 import Rel8.Type ( DBType )+import Rel8.Type.Decimal (PowerOf10) import Rel8.Type.Ord ( DBOrd )  -- scientific@@ -31,6 +33,7 @@ instance DBNum Int16 instance DBNum Int32 instance DBNum Int64+instance PowerOf10 n => DBNum (Fixed n) instance DBNum Float instance DBNum Double instance DBNum Scientific@@ -49,6 +52,7 @@ -- | The class of database types that support the @/@ operator. type DBFractional :: Type -> Constraint class DBNum a => DBFractional a+instance PowerOf10 n => DBFractional (Fixed n) instance DBFractional Float instance DBFractional Double instance DBFractional Scientific
src/Rel8/Type/Ord.hs view
@@ -12,6 +12,7 @@ where  -- base+import Data.Fixed (Fixed) import Data.Int ( Int16, Int32, Int64 ) import Data.Kind ( Constraint, Type ) import Data.List.NonEmpty ( NonEmpty )@@ -26,6 +27,7 @@  -- rel8 import Rel8.Schema.Null ( Sql )+import Rel8.Type.Decimal (PowerOf10) import Rel8.Type.Eq ( DBEq )  -- scientific@@ -53,6 +55,7 @@ instance DBOrd Int16 instance DBOrd Int32 instance DBOrd Int64+instance PowerOf10 n => DBOrd (Fixed n) instance DBOrd Float instance DBOrd Double instance DBOrd Scientific@@ -79,6 +82,7 @@ instance DBMax Int16 instance DBMax Int32 instance DBMax Int64+instance PowerOf10 n => DBMax (Fixed n) instance DBMax Float instance DBMax Double instance DBMax Scientific@@ -104,6 +108,7 @@ instance DBMin Int16 instance DBMin Int32 instance DBMin Int64+instance PowerOf10 n => DBMin (Fixed n) instance DBMin Float instance DBMin Double instance DBMin Scientific
+ src/Rel8/Type/Parser.hs view
@@ -0,0 +1,17 @@+module Rel8.Type.Parser+  ( parse+  )+where++-- attoparsec+import qualified Data.Attoparsec.ByteString as A++-- base+import Prelude++-- bytestring+import Data.ByteString (ByteString)+++parse :: A.Parser a -> ByteString -> Either String a+parse parser = A.parseOnly (parser <* A.endOfInput)
+ src/Rel8/Type/Parser/ByteString.hs view
@@ -0,0 +1,54 @@+{-# language OverloadedStrings #-}+{-# language TypeApplications #-}++module Rel8.Type.Parser.ByteString+  ( bytestring+  )+where++-- attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as A++-- base+import Control.Applicative ((<|>), many)+import Control.Monad (guard)+import Data.Bits ((.|.), shiftL)+import Data.Char (isOctDigit)+import Data.Foldable (fold)+import Prelude++-- base16+import Data.ByteString.Base16 (decodeBase16Untyped)++-- bytestring+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS++-- text+import qualified Data.Text as Text+++bytestring :: A.Parser ByteString+bytestring = hex <|> escape+  where+    hex = do+      digits <- "\\x" *> A.takeByteString+      either (fail . Text.unpack) pure $ decodeBase16Untyped digits+    escape = fold <$> many (escaped <|> unescaped)+      where+        unescaped = A.takeWhile1 (/= '\\')+        escaped = BS.singleton <$> (backslash <|> octal)+          where+            backslash = '\\' <$ "\\\\"+            octal = do+              a <- A.char '\\' *> digit+              b <- digit+              c <- digit+              let+                result = a `shiftL` 6 .|. b `shiftL` 3 .|. c+              guard $ result < 0o400+              pure $ toEnum result+              where+                digit = do+                  c <- A.satisfy isOctDigit+                  pure $ fromEnum c - fromEnum '0'
+ src/Rel8/Type/Parser/Time.hs view
@@ -0,0 +1,156 @@+{-# language OverloadedStrings #-}+{-# language TypeApplications #-}++module Rel8.Type.Parser.Time+  ( calendarDiffTime+  , day+  , localTime+  , timeOfDay+  , utcTime+  )+where++-- attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as A++-- base+import Control.Applicative ((<|>), optional)+import Data.Bits ((.&.))+import Data.Bool (bool)+import Data.Fixed (Fixed (MkFixed), Pico, divMod')+import Data.Functor (void)+import Data.Int (Int64)+import Prelude++-- bytestring+import qualified Data.ByteString as BS++-- time+import Data.Time.Calendar (Day, addDays, fromGregorianValid)+import Data.Time.Clock (DiffTime, UTCTime (UTCTime))+import Data.Time.Format.ISO8601 (iso8601ParseM)+import Data.Time.LocalTime+  ( CalendarDiffTime (CalendarDiffTime)+  , LocalTime (LocalTime)+  , TimeOfDay (TimeOfDay)+  , sinceMidnight+  )++-- utf8+import qualified Data.ByteString.UTF8 as UTF8+++day :: A.Parser Day+day = do+  y <- A.decimal <* A.char '-'+  m <- twoDigits <* A.char '-'+  d <- twoDigits+  maybe (fail "Day: invalid date") pure $ fromGregorianValid y m d+++timeOfDay :: A.Parser TimeOfDay+timeOfDay = do+  h <- twoDigits+  m <- A.char ':' *> twoDigits+  s <- A.char ':' *> secondsParser+  if h < 24 && m < 60 && s <= 60+    then pure $ TimeOfDay h m s+    else fail "TimeOfDay: invalid time"+++localTime :: A.Parser LocalTime+localTime = LocalTime <$> day <* separator <*> timeOfDay+  where+    separator = A.char ' ' <|> A.char 'T'+++utcTime :: A.Parser UTCTime+utcTime = do+  LocalTime date time <- localTime+  tz <- timeZone+  let+    (days, time') = (sinceMidnight time + tz) `divMod'` oneDay+      where+        oneDay = 24 * 60 * 60+    date' = addDays days date+  pure $ UTCTime date' time'+++calendarDiffTime :: A.Parser CalendarDiffTime+calendarDiffTime = iso8601 <|> postgres+  where+    iso8601 = A.takeByteString >>= iso8601ParseM . UTF8.toString+    at = optional (A.char '@') *> A.skipSpace+    plural unit = A.skipSpace <* (unit <* optional "s") <* A.skipSpace+    parseMonths = sql <|> postgresql+      where+        sql = A.signed $ do+          years <- A.decimal <* A.char '-'+          months <- A.decimal <* A.skipSpace+          pure $ years * 12 + months+        postgresql = do+          at+          years <- A.signed A.decimal <* plural "year" <|> pure 0+          months <- A.signed A.decimal <* plural "mon" <|> pure 0+          pure $ years * 12 + months+    parseTime = (+) <$> parseDays <*> time+      where+        time = realToFrac <$> (sql <|> postgresql)+          where+            sql = A.signed $ do+              h <- A.signed A.decimal <* A.char ':'+              m <- twoDigits <* A.char ':'+              s <- secondsParser+              pure $ fromIntegral (((h * 60) + m) * 60) + s+            postgresql = do+              h <- A.signed A.decimal <* plural "hour" <|> pure 0+              m <- A.signed A.decimal <* plural "min" <|> pure 0+              s <- secondsParser <* plural "sec" <|> pure 0+              pure $ fromIntegral @Int (((h * 60) + m) * 60) + s+        parseDays = do+          days <- A.signed A.decimal <* (plural "days" <|> skipSpace1) <|> pure 0+          pure $ fromIntegral @Int days * 24 * 60 * 60+    postgres = do+      months <- parseMonths+      time <- parseTime+      ago <- (True <$ (A.skipSpace *> "ago")) <|> pure False+      pure $ CalendarDiffTime (bool id negate ago months) (bool id negate ago time)+++secondsParser :: A.Parser Pico+secondsParser = do+  integral <- twoDigits+  mfractional <- optional (A.char '.' *> A.takeWhile1 A.isDigit)+  pure $ case mfractional of+    Nothing -> fromIntegral integral+    Just fractional -> parseFraction (fromIntegral integral) fractional+ where+  parseFraction integral digits = MkFixed (fromIntegral (n * 10 ^ e))+    where+      e = max 0 (12 - BS.length digits)+      n = BS.foldl' go (integral :: Int64) (BS.take 12 digits)+        where+          go acc digit = 10 * acc + fromIntegral (fromEnum digit .&. 0xf)+++twoDigits :: A.Parser Int+twoDigits = do+  u <- A.digit+  l <- A.digit+  pure $ fromEnum u .&. 0xf * 10 + fromEnum l .&. 0xf+++timeZone :: A.Parser DiffTime+timeZone = 0 <$ A.char 'Z' <|> diffTime+++diffTime :: A.Parser DiffTime+diffTime = A.signed $ do+  h <- twoDigits+  m <- A.char ':' *> twoDigits <|> pure 0+  s <- A.char ':' *> secondsParser <|> pure 0+  pure $ sinceMidnight $ TimeOfDay h m s+++skipSpace1 :: A.Parser ()+skipSpace1 = void $ A.takeWhile1 A.isSpace
src/Rel8/Type/Sum.hs view
@@ -12,12 +12,14 @@ where  -- base+import Data.Fixed (Fixed) import Data.Int ( Int16, Int32, Int64 ) import Data.Kind ( Constraint, Type ) import Prelude  -- rel8 import Rel8.Type ( DBType )+import Rel8.Type.Decimal (PowerOf10)  -- scientific import Data.Scientific ( Scientific )@@ -32,6 +34,7 @@ instance DBSum Int16 instance DBSum Int32 instance DBSum Int64+instance PowerOf10 n => DBSum (Fixed n) instance DBSum Float instance DBSum Double instance DBSum Scientific
tests/Main.hs view
@@ -2,7 +2,7 @@ {-# language BlockArguments #-} {-# language DeriveAnyClass #-} {-# language DeriveGeneric #-}-{-# language DerivingStrategies #-}+{-# language DerivingVia #-} {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language MonoLocalBinds #-}@@ -21,15 +21,20 @@ -- base import Control.Applicative ( empty, liftA2, liftA3 ) import Control.Exception ( bracket, throwIO )-import Control.Monad ( (>=>), void )+import Control.Monad ((>=>)) import Data.Bifunctor ( bimap )+import Data.Fixed (Fixed (MkFixed)) import Data.Foldable ( for_ )+import Data.Fixed (Centi)+import Data.Functor (void) import Data.Int ( Int32, Int64 ) import Data.List ( nub, sort ) import Data.Maybe ( catMaybes )+import Data.Ratio ((%)) import Data.String ( fromString )-import Data.Word (Word32)+import Data.Word (Word32, Word8) import GHC.Generics ( Generic )+import Prelude hiding (truncate)  -- bytestring import qualified Data.ByteString.Lazy@@ -57,6 +62,10 @@ -- mmorph import Control.Monad.Morph ( hoist ) +-- network-ip+import Network.IP.Addr (NetAddr, IP, IP4(..), IP6(..), IP46(..), net4Addr, net6Addr, fromNetAddr46, Net4Addr, Net6Addr)+import Data.DoubleWord (Word128(..))+ -- rel8 import Rel8 ( Result ) import qualified Rel8@@ -97,6 +106,7 @@   withResource startTestDatabase stopTestDatabase \getTestDatabase ->   testGroup "rel8"     [ testSelectTestTable getTestDatabase+    , testWithStatement getTestDatabase     , testWhere_ getTestDatabase     , testFilter getTestDatabase     , testLimit getTestDatabase@@ -112,6 +122,7 @@     , testDBType getTestDatabase     , testDBEq getTestDatabase     , testTableEquality getTestDatabase+    , testFromRational getTestDatabase     , testFromString getTestDatabase     , testCatMaybeTable getTestDatabase     , testCatMaybe getTestDatabase@@ -140,6 +151,7 @@           sql "CREATE TABLE test_table ( column1 text not null, column2 bool not null )"           sql "CREATE TABLE unique_table ( \"key\" text not null unique, \"value\" text not null )"           sql "CREATE SEQUENCE test_seq"+          sql "CREATE TYPE composite AS (\"bool\" bool, \"char\" char, \"array\" int4[])"        return db @@ -180,7 +192,6 @@ testTableSchema =   Rel8.TableSchema     { name = "test_table"-    , schema = Nothing     , columns = TestTable         { testTableColumn1 = "column1"         , testTableColumn2 = "column2"@@ -194,14 +205,14 @@    transaction do     selected <- lift do-      statement () $ Rel8.insert Rel8.Insert+      statement () $ Rel8.run_ $ Rel8.insert Rel8.Insert         { into = testTableSchema         , rows = Rel8.values $ map Rel8.lit rows         , onConflict = Rel8.DoNothing-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.each testTableSchema      sort selected === sort rows@@ -221,7 +232,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         t <- Rel8.values $ Rel8.lit <$> rows         Rel8.where_ $ testTableColumn2 t Rel8.==. Rel8.lit magicBool         return t@@ -241,7 +252,7 @@     let expected = filter testTableColumn2 rows      selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.filter testTableColumn2 =<< Rel8.values (Rel8.lit <$> rows)      sort selected === sort expected@@ -259,7 +270,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.limit n $ Rel8.values (Rel8.lit <$> rows)      diff (length selected) (<=) (fromIntegral n)@@ -280,7 +291,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.values (Rel8.lit <$> nub left) `Rel8.union` Rel8.values (Rel8.lit <$> nub right)      sort selected === sort (nub (left ++ right))@@ -292,7 +303,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.distinct do           Rel8.values (Rel8.lit <$> rows) @@ -309,12 +320,12 @@    transaction do     exists <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run1 $ Rel8.select do         Rel8.exists $ Rel8.values $ Rel8.lit <$> rows      case rows of-      [] -> exists === [False]-      _ -> exists === [True]+      [] -> exists === False+      _ -> exists === True   testOptional :: IO TmpPostgres.DB -> TestTree@@ -323,7 +334,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.optional $ Rel8.values (Rel8.lit <$> rows)      case rows of@@ -336,8 +347,8 @@   (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool    transaction do-    [result] <- lift do-      statement () $ Rel8.select do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do         pure $ Rel8.lit x Rel8.&&. Rel8.lit y      result === (x && y)@@ -348,8 +359,8 @@   (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool    transaction do-    [result] <- lift do-      statement () $ Rel8.select $ pure $+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select $ pure $         Rel8.lit x Rel8.||. Rel8.lit y      result === (x || y)@@ -360,8 +371,8 @@   (u, v, w, x) <- forAll $ (,,,) <$> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool    transaction do-    [result] <- lift do-      statement () $ Rel8.select do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do         pure $ Rel8.lit u Rel8.||. Rel8.lit v Rel8.&&. Rel8.lit w Rel8.==. Rel8.lit x      result === (u || v && w == x)@@ -372,8 +383,8 @@   x <- forAll Gen.bool    transaction do-    [result] <- lift do-      statement () $ Rel8.select do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do         pure $ Rel8.not_ $ Rel8.lit x      result === not x@@ -384,8 +395,8 @@   (x, y, z) <- forAll $ liftA3 (,,) Gen.bool Gen.bool Gen.bool    transaction do-    [result] <- lift do-      statement () $ Rel8.select do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do         pure $ Rel8.bool (Rel8.lit z) (Rel8.lit y) (Rel8.lit x)      result === if x then y else z@@ -400,53 +411,113 @@    transaction do     result <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         liftA2 (,) (Rel8.values (Rel8.lit <$> rows1)) (Rel8.values (Rel8.lit <$> rows2))      sort result === sort (liftA2 (,) rows1 rows2)  +data Composite = Composite+  { bool :: !Bool+  , char :: !Char+  , array :: ![Int32]+  }+  deriving stock (Eq, Show, Generic)+  deriving (Rel8.DBType) via Rel8.Composite Composite+++instance Rel8.DBComposite Composite where+  compositeTypeName = "composite"+  compositeFields = Rel8.namesFromLabels++ testDBType :: IO TmpPostgres.DB -> TestTree testDBType getTestDatabase = testGroup "DBType instances"   [ dbTypeTest "Bool" Gen.bool   , dbTypeTest "ByteString" $ Gen.bytes (Range.linear 0 128)+  , dbTypeTest "CalendarDiffTime" genCalendarDiffTime   , dbTypeTest "CI Lazy Text" $ mk . Data.Text.Lazy.fromStrict <$> Gen.text (Range.linear 0 10) Gen.unicode   , dbTypeTest "CI Text" $ mk <$> Gen.text (Range.linear 0 10) Gen.unicode+  , dbTypeTest "Composite" genComposite   , dbTypeTest "Day" genDay-  , dbTypeTest "Double" $ (/10) . fromIntegral @Int @Double <$> Gen.integral (Range.linear (-100) 100)-  , dbTypeTest "Float" $ (/10) . fromIntegral @Int @Float <$> Gen.integral (Range.linear (-100) 100)+  , dbTypeTest "Double" $ (/ 10) . fromIntegral @Int @Double <$> Gen.integral (Range.linear (-100) 100)+  , dbTypeTest "Fixed" $ toEnum @Centi <$> Gen.integral (Range.linear (-10000) 10000)+  , dbTypeTest "Float" $ (/ 10) . fromIntegral @Int @Float <$> Gen.integral (Range.linear (-100) 100)   , dbTypeTest "Int32" $ Gen.integral @_ @Int32 Range.linearBounded   , dbTypeTest "Int64" $ Gen.integral @_ @Int64 Range.linearBounded   , dbTypeTest "Lazy ByteString" $ Data.ByteString.Lazy.fromStrict <$> Gen.bytes (Range.linear 0 128)   , dbTypeTest "Lazy Text" $ Data.Text.Lazy.fromStrict <$> Gen.text (Range.linear 0 10) Gen.unicode   , dbTypeTest "LocalTime" genLocalTime-  , dbTypeTest "Scientific" $ (/10) . fromIntegral @Int @Scientific <$> Gen.integral (Range.linear (-100) 100)+  , dbTypeTest "Scientific" $ (/ 10) . fromIntegral @Int @Scientific <$> Gen.integral (Range.linear (-100) 100)   , dbTypeTest "Text" $ Gen.text (Range.linear 0 10) Gen.unicode   , dbTypeTest "TimeOfDay" genTimeOfDay   , dbTypeTest "UTCTime" $ UTCTime <$> genDay <*> genDiffTime   , dbTypeTest "UUID" $ Data.UUID.fromWords <$> genWord32 <*> genWord32 <*> genWord32 <*> genWord32+  , dbTypeTest "INet" genNetAddrIP   ]    where-    dbTypeTest :: (Eq a, Show a, Rel8.DBType a) => TestName -> Gen a -> TestTree+    dbTypeTest :: (Eq a, Show a, Rel8.DBType a, Rel8.ToExprs (Rel8.Expr a) a) => TestName -> Gen a -> TestTree     dbTypeTest name generator = testGroup name       [ databasePropertyTest name (t generator) getTestDatabase       , databasePropertyTest ("Maybe " <> name) (t (Gen.maybe generator)) getTestDatabase       ] -    t :: forall a b. (Eq a, Show a, Rel8.Sql Rel8.DBType a)+    t :: forall a b. (Eq a, Show a, Rel8.Sql Rel8.DBType a, Rel8.ToExprs (Rel8.Expr a) a)       => Gen a       -> (TestT Transaction () -> PropertyT IO b)       -> PropertyT IO b     t generator transaction = do       x <- forAll generator+      y <- forAll generator+      xss <- forAll $ Gen.list (Range.linear 0 10) (Gen.list (Range.linear 0 10) generator)+      xsss <- forAll $ Gen.list (Range.linear 0 10) (Gen.list (Range.linear 0 10) (Gen.list (Range.linear 0 10) generator))        transaction do-        [res] <- lift do-          statement () $ Rel8.select do+        res <- lift do+          statement () $ Rel8.run1 $ Rel8.select do             pure (Rel8.litExpr x)         diff res (==) x+        res' <- lift do+          statement () $ Rel8.run1 $ Rel8.select $ Rel8.many $ Rel8.many do+            Rel8.values [Rel8.litExpr x, Rel8.litExpr y]+        diff res' (==) [[x, y]]+        res3 <- lift do+          statement () $ Rel8.run1 $ Rel8.select $ Rel8.many $ Rel8.many $ Rel8.many do+            Rel8.values [Rel8.litExpr x, Rel8.litExpr y]+        diff res3 (==) [[[x, y]]]+        res'' <- lift do+          statement () $ Rel8.run $ Rel8.select do+            xs <- Rel8.catListTable (Rel8.listTable [Rel8.listTable [Rel8.litExpr x, Rel8.litExpr y]])+            Rel8.catListTable xs+        diff res'' (==) [x, y]+        res''' <- lift do+          statement () $ Rel8.run $ Rel8.select do+            xss' <- Rel8.catListTable (Rel8.listTable [Rel8.listTable [Rel8.listTable [Rel8.litExpr x, Rel8.litExpr y]]])+            xs <- Rel8.catListTable xss'+            Rel8.catListTable xs+        diff res''' (==) [x, y]+        res'''' <- lift do+          statement () $ Rel8.run1 $ Rel8.select $+            Rel8.aggregate Rel8.listCatExpr $+              Rel8.values $ map Rel8.litExpr xss+        diff res'''' (==) (concat xss)+        res''''' <- lift do+          statement () $ Rel8.run1 $ Rel8.select $+            Rel8.aggregate Rel8.listCatExpr $+              Rel8.values $ map Rel8.litExpr xsss+        diff res''''' (==) (concat xsss)+       +      ++    genComposite :: Gen Composite+    genComposite = do+      bool <- Gen.bool+      char <- Gen.unicode+      array <- Gen.list (Range.linear 0 10) (Gen.int32 (Range.linear (-10000) 10000))+      pure Composite {..}+     genDay :: Gen Day     genDay = do       year <- Gen.integral (Range.linear 1970 3000)@@ -454,6 +525,14 @@       day <- Gen.integral (Range.linear 1 31)       Gen.just $ pure $ fromGregorianValid year month day +    genCalendarDiffTime :: Gen CalendarDiffTime+    genCalendarDiffTime = do+      -- hardcoded to 0 because Hasql's 'interval' decoder needs to return a+      -- CalendarDiffTime for this to be properly round-trippable+      months <- pure 0 -- Gen.integral (Range.linear 0 120)+      diffTime <- secondsToNominalDiffTime . MkFixed . (* 1000000) <$> Gen.integral (Range.linear 0 2147483647999999)+      pure $ CalendarDiffTime months diffTime+     genDiffTime :: Gen DiffTime     genDiffTime = secondsToDiffTime <$> Gen.integral (Range.linear 0 86401) @@ -469,7 +548,27 @@     genWord32 :: Gen Word32     genWord32 = Gen.integral Range.linearBounded +    genWord128 :: Gen Word128+    genWord128 = Gen.integral Range.linearBounded +    genNetAddrIP  :: Gen (NetAddr IP)+    genNetAddrIP =+      let+        genIP4Mask :: Gen Word8+        genIP4Mask = Gen.integral (Range.linearFrom 0 0 32)++        genIPv4 :: Gen (IP46 Net4Addr Net6Addr)+        genIPv4 = IPv4 <$> (liftA2 net4Addr (IP4 <$> genWord32) genIP4Mask)++        genIP6Mask :: Gen Word8+        genIP6Mask = Gen.integral (Range.linearFrom 0 0 128)++        genIPv6 :: Gen (IP46 Net4Addr Net6Addr)+        genIPv6 = IPv6 <$> (liftA2 net6Addr (IP6 <$> genWord128) genIP6Mask)++       in fromNetAddr46 <$> Gen.choice [ genIPv4, genIPv6 ]++ testDBEq :: IO TmpPostgres.DB -> TestTree testDBEq getTestDatabase = testGroup "DBEq instances"   [ dbEqTest "Bool" Gen.bool@@ -493,8 +592,8 @@       (x, y) <- forAll (liftA2 (,) generator generator)        transaction do-        [res] <- lift do-          statement () $ Rel8.select do+        res <- lift do+          statement () $ Rel8.run1 $ Rel8.select do             pure $ Rel8.litExpr x Rel8.==. Rel8.litExpr y         res === (x == y) @@ -504,20 +603,39 @@    (x, y) <- forAll $ liftA2 (,) genTestTable genTestTable     transaction do-     [eq] <- lift do-       statement () $ Rel8.select do+     eq <- lift do+       statement () $ Rel8.run1 $ Rel8.select do          pure $ Rel8.lit x Rel8.==: Rel8.lit y       eq === (x == y)  +testFromRational :: IO TmpPostgres.DB -> TestTree+testFromRational = databasePropertyTest "fromRational" \transaction -> do+  numerator <- forAll $ Gen.int64 Range.linearBounded+  denominator <- forAll $ Gen.int64 $ Range.linear 1 maxBound++  let+    rational = toInteger numerator % toInteger denominator+    double = fromRational @Double rational++  transaction do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do+        pure $ fromRational rational+    diff result (~=) double+  where+    a ~= b = abs (a - b) < 1e-15+    infix 4 ~=++ testFromString :: IO TmpPostgres.DB -> TestTree-testFromString = databasePropertyTest "FromString" \transaction -> do+testFromString = databasePropertyTest "fromString" \transaction -> do   str <- forAll $ Gen.list (Range.linear 0 10) Gen.unicode    transaction do-    [result] <- lift do-      statement () $ Rel8.select do+    result <- lift do+      statement () $ Rel8.run1 $ Rel8.select do         pure $ fromString str     result === pack str @@ -528,7 +646,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         testTable <- Rel8.values $ Rel8.lit <$> rows         Rel8.catMaybeTable $ Rel8.bool Rel8.nothingTable (pure testTable) (testTableColumn2 testTable) @@ -541,7 +659,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.catNull =<< Rel8.values (map Rel8.lit rows)      sort selected === sort (catMaybes rows)@@ -553,7 +671,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.maybeTable (Rel8.lit def) id <$> Rel8.optional (Rel8.values (Rel8.lit <$> rows))      case rows of@@ -577,8 +695,8 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do-        Rel8.aggregate $ Rel8.aggregateMaybeTable Rel8.sum <$> Rel8.values (Rel8.lit <$> rows)+      statement () $ Rel8.run $ Rel8.select do+        Rel8.aggregate1 (Rel8.aggregateMaybeTable Rel8.sum) $ Rel8.values (Rel8.lit <$> rows)      sort selected === aggregate rows @@ -605,7 +723,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.values (Rel8.lit <$> rows)      sort selected === sort rows@@ -618,7 +736,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         as <- Rel8.optional (Rel8.values (Rel8.lit <$> rows1))         bs <- Rel8.optional (Rel8.values (Rel8.lit <$> rows2))         pure $ liftA2 (,) as bs@@ -647,14 +765,14 @@    transaction do     selected <- lift do-      statement () $ Rel8.insert Rel8.Insert+      statement () $ Rel8.run_ $ Rel8.insert Rel8.Insert         { into = testTableSchema         , rows = Rel8.values $ map Rel8.lit $ Map.keys rows         , onConflict = Rel8.DoNothing-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      statement () $ Rel8.update Rel8.Update+      statement () $ Rel8.run_ $ Rel8.update Rel8.Update         { target = testTableSchema         , from = pure ()         , set = \_ r ->@@ -672,10 +790,10 @@               r               updates         , updateWhere = \_ _ -> Rel8.lit True-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.each testTableSchema      sort selected === sort (Map.elems rows)@@ -691,21 +809,21 @@    transaction do     (deleted, selected) <- lift do-      statement () $ Rel8.insert Rel8.Insert+      statement () $ Rel8.run_ $ Rel8.insert Rel8.Insert         { into = testTableSchema         , rows = Rel8.values $ map Rel8.lit rows         , onConflict = Rel8.DoNothing-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      deleted <- statement () $ Rel8.delete Rel8.Delete+      deleted <- statement () $ Rel8.run $ Rel8.delete Rel8.Delete           { from = testTableSchema           , using = pure ()           , deleteWhere = const testTableColumn2-          , returning = Rel8.Projection id+          , returning = Rel8.Returning id           } -      selected <- statement () $ Rel8.select do+      selected <- statement () $ Rel8.run $ Rel8.select do         Rel8.each testTableSchema        pure (deleted, selected)@@ -713,6 +831,80 @@     sort (deleted <> selected) === sort rows  +testWithStatement :: IO TmpPostgres.DB -> TestTree+testWithStatement genTestDatabase =+  testGroup "WITH"+    [ selectUnionInsert genTestDatabase+    , rowsAffectedNoReturning genTestDatabase+    , rowsAffectedReturing genTestDatabase+    , pureQuery genTestDatabase+    ]+  where+    selectUnionInsert = +      databasePropertyTest "Can UNION results of SELECT with results of INSERT" \transaction -> do+        rows <- forAll $ Gen.list (Range.linear 0 50) genTestTable++        transaction do+          rows' <- lift do+            statement () $ Rel8.run $ do+              values <- Rel8.select $ Rel8.values $ map Rel8.lit rows++              inserted <- Rel8.insert $ Rel8.Insert+                { into = testTableSchema+                , rows = values+                , onConflict = Rel8.DoNothing+                , returning = Rel8.Returning id+                }++              pure $ values <> inserted++          sort rows' === sort (rows <> rows)++    rowsAffectedNoReturning = +      databasePropertyTest "Can read rows affected from INSERT without RETURNING" \transaction -> do+        rows <- forAll $ Gen.list (Range.linear 0 50) genTestTable++        transaction do+          affected <- lift do+            statement () $ Rel8.runN $ do+              Rel8.insert $ Rel8.Insert+                { into = testTableSchema+                , rows = Rel8.values $ map Rel8.lit rows+                , onConflict = Rel8.DoNothing+                , returning = Rel8.NoReturning+                }++          length rows === fromIntegral affected++    rowsAffectedReturing = +      databasePropertyTest "Can read rows affected from INSERT with RETURNING" \transaction -> do+        rows <- forAll $ Gen.list (Range.linear 0 50) genTestTable++        transaction do+          affected <- lift do+            statement () $ Rel8.runN $ void $ do+              Rel8.insert $ Rel8.Insert+                { into = testTableSchema+                , rows = Rel8.values $ map Rel8.lit rows+                , onConflict = Rel8.DoNothing+                , returning = Rel8.Returning id+                }++          length rows === fromIntegral affected++    pureQuery = +      databasePropertyTest "Can read pure Query" \transaction -> do+        rows <- forAll $ Gen.list (Range.linear 0 50) genTestTable++        transaction do+          rows' <- lift do+            statement () $ Rel8.run $ pure do+              Rel8.values $ map Rel8.lit rows++          sort rows === sort rows'+++ data UniqueTable f = UniqueTable   { uniqueTableKey :: Rel8.Column f Text   , uniqueTableValue :: Rel8.Column f Text@@ -730,7 +922,6 @@ uniqueTableSchema =   Rel8.TableSchema     { name = "unique_table"-    , schema = Nothing     , columns = UniqueTable         { uniqueTableKey = "key"         , uniqueTableValue = "value"@@ -752,25 +943,26 @@    transaction do     selected <- lift do-      statement () $ Rel8.insert Rel8.Insert+      statement () $ Rel8.run_ $ Rel8.insert Rel8.Insert         { into = uniqueTableSchema         , rows = Rel8.values $ Rel8.lit <$> as         , onConflict = Rel8.DoNothing-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      statement () $ Rel8.insert Rel8.Insert+      statement () $ Rel8.run_ $ Rel8.insert Rel8.Insert         { into = uniqueTableSchema         , rows = Rel8.values $ Rel8.lit <$> bs         , onConflict = Rel8.DoUpdate Rel8.Upsert             { index = uniqueTableKey+            , predicate = Nothing             , set = \UniqueTable {uniqueTableValue} old -> old {uniqueTableValue}             , updateWhere = \_ _ -> Rel8.true             }-        , returning = pure ()+        , returning = Rel8.NoReturning         } -      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.each uniqueTableSchema      fromUniqueTables selected === fromUniqueTables bs <> fromUniqueTables as@@ -794,7 +986,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         Rel8.values $ map Rel8.lit rows      sort selected === sort rows@@ -806,13 +998,13 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run1 $ Rel8.select do         Rel8.many $ Rel8.values (map Rel8.lit rows) -    selected === [foldMap pure rows]+    selected === rows      selected' <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         a <- Rel8.catListTable =<< do           Rel8.many $ Rel8.values (map Rel8.lit rows)         b <- Rel8.catListTable =<< do@@ -841,11 +1033,11 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run1 $ Rel8.select do         x <- Rel8.values [Rel8.lit example]         pure $ Rel8.maybeTable (Rel8.lit False) (\_ -> Rel8.lit True) (nmt2 x) -    selected === [True]+    selected === True   testEvaluate :: IO TmpPostgres.DB -> TestTree@@ -853,7 +1045,7 @@    transaction do     selected <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])         y <- Rel8.evaluate (Rel8.nextval "test_seq")         pure (x, (y, y))@@ -865,7 +1057,7 @@       ]      selected' <- lift do-      statement () $ Rel8.select do+      statement () $ Rel8.run $ Rel8.select do         x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])         y <- Rel8.values (Rel8.lit <$> ['d', 'e', 'f'])         z <- Rel8.evaluate (Rel8.nextval "test_seq")
tests/Rel8/Generic/Rel8able/Test.hs view
@@ -7,6 +7,7 @@ {-# language MultiParamTypeClasses #-} {-# language StandaloneKindSignatures #-} {-# language TypeFamilies #-}+{-# language TypeOperators #-} {-# language UndecidableInstances #-}  {-# options_ghc -O0 #-}