Commit Graph

474 Commits

Author SHA1 Message Date
Shane
b5789a692e
Add ability to use custom aggregation functions with aggregateFunction (#283) 2024-01-09 01:28:04 +00:00
Shane
91e7a1bfab
Make Composite's text parser handle text containing double quotes (#304) 2024-01-09 01:19:51 +00:00
Shane
2d9b6abc74
Add ordered set aggregation functions (#282) 2024-01-08 21:28:04 +00:00
Shane
dee4f8a3aa
Upgrade flake (#303) 2024-01-08 17:50:35 +00:00
Chris Manning
6df1bd2a9d
Export Decoder to allow user-defined DBType instances (#297) 2023-12-05 12:35:06 +00:00
Shane
4a772725f4
Wrap rebind in dummy offset 0 (#289)
Somewhere between PostgreSQL 11 and PostgreSQL 15, PostgreSQL's optimiser gained the ability to see "through" subqueries, and it seems to choose to do this even when we don't really want it to.

E.g., it started transforming the following:

```haskell
SELECT
  x * y + x * y
FROM (
  SELECT
    a + b + c AS x
    d + e + f AS y
  FROM
    foo
) _
```

into:

```haskell
SELECT
  (a + b + c) * (d + e + f) + (a + b + c) * (d + e + f)
FROM
  foo
```

before evaluating.

You can see how more complicated expressions nested several levels deep could get expanded into crazy big expressions. This seems to be what PostgreSQL actually does on Rel8 code that uses `rebind`. Compared to older versions of PostgreSQL, this increases the planning time and execution time dramatically.

Given that Rel8's `rebind` is intended to function as a "let binding", and the user needs to go out of their way to choose to use it (they could just use `pure` if they wanted the fully expanded expression), we want a way to force PostgreSQL to evaluate the `a + b + c` and the `d + e + f` first before worrying about trying to simplify `x * y + x * y`. Adding `OFFSET 0` to the inner query seems to achieve that.

```haskell
SELECT
  x * y + x * y
FROM (
  SELECT
    a + b + c AS x
    d + e + f AS y
  FROM
    foo
  OFFSET
    0
) _
```
2023-10-29 19:40:31 +00:00
Shane
a744f058cf
Relax type of distinctAggregate (#287)
The `Sql DBEq a` constraint on the return type of the aggregator was wrong. It also isn't quite right to have a `EqTable i` constraint on the input type of the `Aggregator`, because technically what we want is a `Sql DBEq` constraint on whichever column(s) within `i` are used by aggregation functions, but we don't know which columns were used at this point. We could give `distinctAggregate` a type like `Sql DBEq i => Aggregator (Expr i) a` and make people run it through `lmap` manually, but that makes it impractical to use with `ListTable` without exposing more machinery. So we just drop the equality constraint for now.
2023-10-20 14:22:23 +01:00
Shane
bfb437fea1
Move some ListTable operations to Rel8.Array to avoid name clashes (#286) 2023-10-20 12:17:36 +01:00
Shane
9fb416607d
Add index and index1 for indexing arrays (ListTable and NonEmptyTable) (#285) 2023-10-19 18:40:07 +01:00
Shane
d0ba116149
Change window functions to operate on tables rather than columns (#281) 2023-10-07 12:11:29 +00:00
Ashwin Mathi
fbce52a287
Fix typo in dbtype.rst (#273) 2023-10-07 07:49:43 +00:00
Shane
7636a86fb3
Add DBType instance for Fixed (#280)
This resolves issue #218.
2023-10-06 16:15:35 +01:00
Shane
7eec1f85ee
Add loopDistinct (#277) 2023-10-02 18:33:31 +00:00
Shane
d058b6467b
Upgrade flake inputs (#278) 2023-09-29 11:23:22 +00:00
Shane
0ebb70f95f
Add array concenation aggregators (#270) 2023-09-27 10:41:27 +00:00
Shane
10eab21d3a
Support nested catListTable (by represented nested arrays as text) (#242)
This is one possible "fix" to #168. With this we can `catListTable` arbitrarily deep trees of `ListTable`s.

It comes at a relatively high cost, however.

Currently we represent nested arrays with anonymous records. This works reasonably well, except that we can't extract the field from the anonymous record when we need it (PostgreSQL [theoretically](https://www.postgresql.org/docs/13/release-13.html#id-1.11.6.16.5.6) suports `.f1` syntax since PG13 but it only works in very limited situations). But it does mean we can decode the results using Hasql's binary decoders, and ordering works how we expect ('array[row(array[9])] < array[row(array[10])]'.

What this PR does is instead represent nested arrays as text. To be able to decode this, we need each 'DBType' to supply a text parser in addition to a binary decoder. It also means that ordering is no longer intuitive, because `array[array[9]::text] > array[array[10]::text]`. However, it does mean we can nest `catListTable`s to our heart's content and it will always just work.
2023-09-27 11:39:04 +01:00
Shane
72a76aaf9a
Postgres' pi() function is lowercase (#275) 2023-09-21 14:47:32 +00:00
Shane
8a468f51c9
Length of empty array should return 0, not NULL (#269) 2023-08-16 02:07:24 +00:00
Shane
9e7a44757c
Add length{,1} functions for getting the length of {List,NonEmpty}Tables (#268) 2023-08-15 18:09:13 +00:00
Shane
0e24745497
Allow partial indexes as upsert conflict targets (#264) 2023-08-01 15:00:05 +01:00
Shane
c06bd5f2f1
Expand use of QualifiedName to types, composites, enums (#263)
Types in PostgreSQL can also be qualified with a schema. However, it's not sufficient to just change the type of `TypeInformation`'s `typeName` to `QualifiedName`, because a type isn't *just* a name. Postgres types can also be parameterised by modifiers (e.g., `numeric(7, 2)`) and array types of arbitrary depth (e.g., `int4[][]`).

To accomodate this, a new type is introduced, `TypeName`. Like `QualifiedName`, it has an `IsString` instance, so the common case (`schema` set to `Nothing`, no modifiers, scalar type) will continue working as before.
2023-07-23 17:12:02 +01:00
Shane
7ec674d2a8
Make binaryOperator take a QualifiedName instead of a String (#262) 2023-07-15 16:24:36 +01:00
Shane
cdf0c761d3
Export materialize (#260)
The main reason I wasn't happy with this before is that there was nothing stopping you from writing `materialize query pure`. This returned query would produce invalid SQL if you actually tried to use it, because it would attempt to reference a common table expression outside the scope of the `WITH` statement.

The "solution" here is just to throw a `rebind` around the result such that `materialize` incurs an extra `Table Expr b` constraint, which means that you can't return `Query (Query a)` because `Query a` can't satisfy a `Table Expr` constraint.
2023-07-15 11:25:04 +00:00
Shane
9f372dc649
Support selecting from table-returning functions with queryFunction (#241)
This fixes #71.
2023-07-11 14:32:24 +01:00
Shane
bf63d70ff3
Unify function and nullaryFunction (#258)
This does away with the weird variadic arguments thing we had going on with `function`.

Functions with no arguments are now written as:

```haskell
now :: Expr UTCTime
now = function "now" ()
```

Functions with multiple arguments are now written as:

```haskell
quot :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
quot n d = function "div" (n, d)
```

Single-argument functions are written exactly as before.
2023-07-11 14:05:02 +01:00
Shane
c778ac1763
Introduce QualifiedName (fixes #228) (#257)
This adds a new type `QualifiedName` for named PostgreSQL objects (tables, views, functions and sequences) that can optionally be qualified by a schema. Previously only `TableSchema` could be qualified in this way.

`QualifiedName` has an `IsString` instance so the common case (where the schema is `Nothing`) doesn't have to care about schemas (if `OverloadedStrings` is enabled).

This also refactors `TableSchema` to use `QualifiedName` for its `name` field and drops its `schema` field.

Thanks to @elldritch for the bug report and the inspiration.
2023-07-11 12:06:36 +00:00
Ollie Charles
8cec776fa6
Set up scriv and add pending changelog entries (#255) 2023-07-11 11:49:53 +00:00
Ollie Charles
6554cfc841
Switch to Opaleye 0.9.7.0 (#259)
We actually require this now, so this updates the lower bound, and builds from Hackage rather than a `source-repository-package`.
2023-07-08 22:58:24 +01:00
Ollie Charles
5d07aa549d
Hide Rel8.materialise (#256) 2023-07-07 18:22:36 +00:00
Ollie Charles
8b50ce5f1b
Use base-compat instead of CPP (#254) 2023-07-07 17:48:21 +00:00
Ollie Charles
044b9189ec
Switch to Nix flakes (#253) 2023-07-07 18:25:55 +01:00
Shane
3c0b67f99e
Statements overhaul (support for statement-level WITH) (#250)
The motivation behind this PR is to add support for PostreSQL's `WITH` syntax at the statement level, which gives the ability to, e.g., delete some rows from a table and then re-insert those deleted rows into another table, without any round-trips between the application and the database.

To support this, this PR introduces a new type called `Statement`, which represents a single PostgreSQL statement. It has a `Monad` instance which allows sub-statements (such as `DELETE` and `INSERT` statements) to be composed together and their results bound to values that can be referenced in subsequent sub-statements. These "compound" statements are then rendered as a `WITH` statement.

`select`, `insert`, `update` and `delete` have all been altered to produce the `Statement` type described above instead of the `Hasql.Statement` type.

Some changes were necessary to the `Returning` type. `Returning` previously bundled two different concepts together: whether or not to generate a `RETURNING` clause in the SQL for a manipulation statement, and how to decode the returned rows (if any). It was necessary to break these concepts apart because with `WITH` we need the ability to generate manipulation statements with `RETURNING` clauses that are never actually decoded at all (the results just get passed to the next statement without touching the application).

Now, the `Returning` type is only concerned with whether or not to generate a `RETURNING` clause, and the question of how to decode the returned the result of the statement is handled by the `run` functions. `run` converts a `Statement` into a runnable `Hasql.Statement`, decoding the result of the statement as a list of rows. The other variations, `run_`, `runN`, `run1`, `runMaybe` and `runVector` can be used when you want to decode as something other than a list of rows.

This also gains us support for decoding the result of a query directly to a `Vector` for the first time, which brings a performance improvement over lists for those who need it.
2023-07-07 11:29:15 +01:00
Shane
0357176c7b
Add head and last functions for extracting elements of ListTable (#245) 2023-06-24 11:27:56 +01:00
Shane
3e282eebf5
Bypass unneccessary toColumns/fromColumns in extract (#244) 2023-06-18 22:19:58 +01:00
Shane
2230452b19
Support "rank 2" catListTable (by "parsing" anonymous record) (#243)
This is another possible "fix" to #168 (as opposed to #242). It doesn't really fix the problem, but it allows us to use two levels of `catListTable` instead of only one. Instead of trying to use Postgres's broken `.f1` syntax, we cast the anonymous record to text, remove the parentheses and quotes and unescape any escaped quotes or backslashes, and then cast the resulting text back to the appropriate type. The reason this only works one level deep is that if the type we cast the text back to is itself an anonymous record, then PostgreSQL doesn't know how to parse the text.

It's kind of ugly and hacky but it does work and otherwise maintains the status quo. Comparison operators on nested lists continue to work as before and we don't need to burden `DBType` with parsing nonsense.
2023-06-18 21:57:00 +01:00
Shane
2eca87772f
Aggregation overhaul — return to Profunctor and semi-aggregations (#235)
This PR makes a number of changes to how aggregation works in Rel8.

The biggest change is that we drop the `Aggregate` context and we return to the `Profunctor`-based `Aggregator` that Opaleye uses (as in #37). While working with `Profunctor`s is more awkward for many common use-cases, it's ultimately more powerful. The big thing it gives you that we don't currently have is the ability to "post-map" on the result of an aggregation function. Pretend for a moment that Postgres does not have the `avg` function built-in. With the previous Rel8, there is no way to directly write `sum(x) / count(x)`. The best you could do would something like:

```haskell
fmap (\(total, size) -> total / fromIntegral size) $ aggregate $ do
  foo <- each fooSchema
  pure (sum foo.x, count foo.x)
```

The key thing is that the mapping can only happen after `aggregate` is called. Whereas with the `Profunctor`-based `Aggregator` this is just `(/) <$> sum <*> fmap fromIntegral count`. This isn't too bad if the only thing you want to do is computing the average, but if you're doing a complicated aggregation with several things happening at once then you might need to do several unrelated post-processings after the `aggregate`. We really want a way to bundle up the postmapping with the aggregation itself and have that as a singular composable unit. Another example is the `listAggExpr` function. The only reason Rel8 exports this is because it can't be directly expressed in terms of `listAgg`. With the `Profunctor`-based `Aggregator` it can be, it's just `(id $*) <$> listAgg`, it no longer needs to be a special case.

The original attempt in #37 recognised that it can be awkward to have to write `lmap (.x) sum`, so instead of sum having the type signature `Aggregator (Expr a) (Expr a)`, it had the type signature `(i -> Expr a) -> Aggregator i (Expr a)`, so that you wouldn't have to use `lmap`, you could just type `sum (.x)`. However, there are many ways to compose `Aggregator`s — for example, if you wanted to use combinators from `product-profunctor` to combine aggregators, then you'd rather type `sum ***! count` than `sum id ***! count id`. So in this PR we keep the type of `sum` as `Aggregator (Expr a) (Expr a)`, but we also export `sumOn`, which has the bundled `lmap`.

The other major change is that this PR introduces two forms of aggregation — "semi"-aggregation and "full"-aggregation. Up until now, all aggregation in Rel8 was "semi"-aggregation, but "full"-aggregation feels a bit more natural and Haskelly.

Up until now, the `aggrgegate` combinator in Rel8 would return zero rows if given a query that itself returned zero rows, even if the aggregation functions that comprised it had identity values. So it was very common to see code like `fmap (fromMaybeTable 0) $ optional $ aggregate $ sum <$> _`. Again, we "know" that `0` is the identity value for `sum` and we really want some way to bundle those together and to say "return the identity value if there are zero rows". Rel8 now has this ability — it has both `Aggregator` and `Aggregator1`, with the former having identity values and the latter not. The `aggregate` function now takes an `Aggregator` and returns the identity value when encountering zero rows, whereas the `aggregate1` function takes an `Aggregator1` and behaves as before. `count`, `sum`, `and`, `or`, `listAgg` are `Aggregator`s (with the identity values `0`, `0`, `true`, `false` and `listTable []` respectively) and `groupBy`, `max` and `min` are `Aggregator1`s.

This also means that `many` is now just `aggregate listAgg` instead of `fmap (fromMaybeTable (listTable [])) . optional . aggregate . fmap listAgg`.

It should also be noted that these functions are actually polymorphic — `sum` will actually give you an `Aggregator'` that can be used as either `Aggregator` or `Aggregator1` without needing to explicitly convert between them. Similarly `aggregate1` can take either an `Aggegator` or an `Aggregator1` (though it won't use the identity value of the former).

Aggregation in Rel8 now supports more of the features of PostgresSQL supports. Three new combinators are introduced — `distinctAggregate`, `filterWhere` and `orderAggregateBy`.

Opaleye itself already supported `distinctAggregate` and indeed we used this to implement `countDistinct` as a special case, but we now support using `DISTINCT` on arbitrary aggregation functions.

`filterWhere` is new to both Rel8 and Opaleye. It corresponds to PostgreSQL's `FILTER (WHERE ...)` syntax in aggregations. It also uses the identity value of an `Aggregator` in the case where the given predicate returns zero rows. There is also `filterWhereOptional` which can be used with `Aggregator1`s.

`orderAggregateBy` allows the values within an aggregation to be ordered using a given ordering, mainly non-commutative aggregation functions like `listAgg`.
2023-06-18 20:05:00 +00:00
Shane
baaabe7ced
Fix #219 (#240)
We need to `rebind` immediately after calling `UNNEST`, before we call `fromColumns`.
2023-06-17 23:04:34 +00:00
Shane
e2a77b7310
Passing pure to withExplicit is invalid; materialize needs to take a function (#231) 2023-04-24 12:03:01 +01:00
Jonathan Lorimer
ecf34ae506
Add inet DBType (#227) 2023-04-14 09:56:03 +00:00
Shane
bbc97eaa45
Fix build on GHC 9.2 (#230)
PR #229 broke on GHC versions before 9.4 because `~` was not exported from `Data.Type.Equality` then.
2023-04-13 12:51:05 +01:00
Shane
e7cf7b2a34
Add support for two forms of PostgreSQL WITH syntax; loop and materialize (#180) 2023-03-28 17:50:29 +00:00
Shane
fef9064f91
Fix warnings on latest GHC (#229)
These are due to `~` no longer being built-in syntax.
2023-03-23 17:07:27 +00:00
Ollie Charles
20ff61fe37
Release 1.4.1.0 (#226) 2023-01-20 11:05:37 +00:00
Ollie Charles
408e353de0
Support GHC 9.4 (#199) 2023-01-20 10:59:39 +00:00
tomjaguarpaw
6c038ecb1d
Use less Opaleye internals in aggregation (#204) 2022-11-30 17:47:24 +00:00
Shane
42c1e1c853
Call quote_ident before nextval (#217)
It turns out you can't do `nextval('FooBar_id_seq')` in PostgreSQL, you need to write `nextval('"FooBar_id_seq"')`.
2022-11-22 17:05:36 +00:00
Shane
ed4b5bb0ac
Add supported for polymorphic nested Rel8ables (#215)
```haskell
data Nest t u f = Nest
  { foo :: t f
  , bar :: u f
  }
  deriving (Generic, Rel8able)
``
2022-11-08 13:47:12 +00:00
Gary Pamparà
42e38c70cc
Expose the createOrReplaceView function in the Rel8 module (#212) 2022-11-04 18:35:55 +00:00
Shane
36c2b1ef0c
Restore zero-based indexed (#214)
In #182 we introduced window functions, and also refactored `indexed` to use the new window function support via `rowNumber`.

However, previously `indexed` was zero-based, whereas PostgreSQL's `row_number()` function counts from one, so this change silently changed the behaviour of `indexed`.

This commit restores the previous behaviour.
2022-11-02 18:35:48 +00:00
Shane
fd0b449c8c
Add a rebind step to alignWith (#213)
`Rel8.Tabulate.alignWith` uses `theseTable` to recover the key, which translates to a mess of nested `CASE` expressions. This becomes exponential with repeated uses of `alignWith`. Adding a `rebind` step to `alignWith` to rewrite this `key` expression each time makes this linear.
2022-11-01 14:34:20 +00:00