on master help sign in
.github/workflows/build.yaml+3−3
since splitsnapshots
unifiedsplit
@@ -9,14 +9,14 @@ jobs:
9 persist-credentials: false
9 persist-credentials: false
10 submodules: true
10 submodules: true
11
11
12− - uses: cachix/install-nix-action@v12
12+ - uses: cachix/install-nix-action@v14.1
13 with:
13 with:
14 nix_path: nixpkgs=channel:nixos-unstable
14 nix_path: nixpkgs=channel:nixos-unstable
15 extra_nix_config: |
15 extra_nix_config: |
16 trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=
16 trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=
17− substituters = https://cache.nixos.org/ https://hydra.iohk.io
17+ substituters = https://cache.nixos.org/ https://cache.iog.io
18
18
19− - uses: cachix/cachix-action@v8
19+ - uses: cachix/cachix-action@v10
20 with:
20 with:
21 name: rel8
21 name: rel8
22 authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
22 authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
Changelog.md+112−10
@@ -1,20 +1,90 @@
1−# 1.1.0.0 (??)
1+# 1.4.0.0 (2002-08-17)
2
2
3## Breaking changes
3## Breaking changes
4
4
5−* Rename `whereExists` and `whereNotExists` to `present` and `absent` respectively. ([#57](https://github.com/circuithub/rel8/pull/57))
5+* The behavior of `greatest`/`least` has been corrected, and was previously flipped. ([#183](https://github.com/circuithub/rel8/pull/183))
6
6
7−* Simplify `evaluate` to run directly inside the `Query` monad, rendering the `Evaluate` monad unnecessary. ([#56](https://github.com/circuithub/rel8/pull/56))
7+## New features
8
8
9−* The `Labelable` type class has been removed as it's no longer necessary. This class was mostly internal, though was visible in some public API functions. ([#76](https://github.com/circuithub/rel8/pull/76))
9+* `NullTable`/`HNull` have been added. This is an alternative to `MaybeTable` that doesn't use a tag columns. It's less flexible (no `Functor` or `Applicative` instance) and is meaningless when used with a table that has no non-nullable columns (so nesting `NullTable` is redundant). But in situations where the underlying `Table` does have non-nullable columns, it can losslessly converted to and from `MaybeTable`. It is useful for embedding into a base table when you don't want to store the extra tag column in your schema. ([#173](https://github.com/circuithub/rel8/pull/173))
10+* Add `fromMaybeTable`. ([#179](https://github.com/circuithub/rel8/pull/179))
11+* Add `alignMaybeTable`. ([#196](https://github.com/circuithub/rel8/pull/196))
10
12
11−* `EitherTable`, `ListTable`, `MaybeTable`, `NonEmptyTable` and `TheseTable` now all have an extra parameter which tracks the context of the table. If you manually specified any types before, you need to provide this parameter - usually `Expr` will be the correct choice. ([#101](https://github.com/circuithub/rel8/pull/101))
13+## Improvements
12
14
13−* We've done a lot of work to simplify the internals of Rel8, but some of these are breaking changes. However, most of these API changes shouldn't affect users as they are mostly types used in type inference, or affect classes where users don't need to define their own instances.
15+* Optimize implementation of `AltTable` for `Tabulation` ([#178](https://github.com/circuithub/rel8/pull/178))
14
16
15− * The kind of a `Rel8able` is now `(Type -> Type) -> Type`. Previously it was `(X -> Type) -> Type`.
17+## Other
16− * `Table` has a new associated type - `FromExprs`. This was previously an open type family.
18+
17− * `Table` has a new associated type - `Transpose` - and `Recontextualise` has been renamed to `Transposes`. This `Transposes` class now operates in terms of `Transpose`.
19+* Documentation improvements for `HADT`. ([#177](https://github.com/circuithub/rel8/pull/177))
20+* Document example usage of `groupBy`. ([#184](https://github.com/circuithub/rel8/pull/184))
21+* Build with and require Opaleye >= 0.9.3.3. ([#190](https://github.com/circuithub/rel8/pull/190))
22+* Build with `hasql` 1.6. ([#195](https://github.com/circuithub/rel8/pull/195))
23+
24+# 1.3.1.0 (2022-01-20)
25+
26+## Other
27+
28+* Rel8 now requires Opaleye >= 0.9.1. ([#165](https://github.com/circuithub/rel8/pull/165))
29+
30+# 1.3.0.0 (2022-01-31)
31+
32+## Breaking changes
33+
34+* `div` and `mod` have been changed to match Haskell semantics. If you need the PostgreSQL `div()` and `mod()` functions, use `quot` and `rem`. While this is not an API change, we feel this is a breaking change in semantics and have bumped the major version number. ([#155](https://github.com/circuithub/rel8/pull/155))
35+
36+## New features
37+
38+* `divMod` and `quotRem` functions have been added, matching Haskell's `Prelude` functions. ([#155](https://github.com/circuithub/rel8/pull/155))
39+* `avg` and `mode` aggregation functions to find the mean value of an expression, or the most common row in a query, respectively. ([#152](https://github.com/circuithub/rel8/pull/152))
40+* The full `EqTable` and `OrdTable` classes have been exported, allowing for instances to be manually created. ([#157](https://github.com/circuithub/rel8/pull/157))
41+* Added `like` and `ilike` (for the `LIKE` and `ILIKE` operators). ([#146](https://github.com/circuithub/rel8/pull/146))
42+
43+## Other
44+
45+* Rel8 now requires Opaleye 0.9. ([#158](https://github.com/circuithub/rel8/pull/158))
46+* Rel8's test suite supports Hedgehog 1.1. ([#160](https://github.com/circuithub/rel8/pull/160))
47+* The documentation for binary operations has been corrected. ([#162](https://github.com/circuithub/rel8/pull/162))
48+
49+# 1.2.2.0 (2021-11-21)
50+
51+## Other
52+
53+* Support GHC 9.2 ([#145](https://github.com/circuithub/rel8/pull/145))
54+* Correct the documentation for `except` ([#147](https://github.com/circuithub/rel8/pull/147))
55+* Support `hasql` 1.5 ([#149](https://github.com/circuithub/rel8/pull/149))
56+
57+# 1.2.1.0 (2021-11-21)
58+
59+## New features
60+
61+* `castTable` has been added, which casts all columns selected from in `Query` according to the types of columns. This can occasionally be useful when using `many`/`some` on older PostgreSQL versions. ([#137](https://github.com/circuithub/rel8/pull/137))
62+
63+* Added `<:`, `<=:`, `>:`, `>=:`, `greatest` and `least`, which sort all columns in a table lexicographically. These operators are like the `<.` operators which operate on `Expr`s, but the `<:` operate on entire tables. ([#139](https://github.com/circuithub/rel8/pull/139))
64+
65+## Other
66+
67+* Support opaleye-0.8 ([#142](https://github.com/circuithub/rel8/pull/142))
68+
69+# 1.2.0.0 (2021-10-22)
70+
71+## New features
72+
73+* New `$*` and `$+` operators for projecting out of `ListTable` and `NonEmptyTable` respectively (analogous to the existing `$?` for `MaybeTable`). ([#125](https://github.com/circuithub/rel8/pull/125))
74+
75+## Bug fixes
76+
77+* Fix `UPSERT` support (and add tests), which was broken due to a typo inside Rel8 that made it impossible to construct `Projection`s which are needed by `UPSERT`. ([#134](https://github.com/circuithub/rel8/pull/134))
78+
79+* Remove `DBMin` and `DBMax` instances for `Bool`, which are invalid as far as Postgres is concerned. `and` and `or` can be used to achieve the same thing. ([#126](https://github.com/circuithub/rel8/pull/126))
80+
81+* Fix `aggregateMaybeTable`, `aggregateEitherTable` and `aggregateTheseTable`, which generated invalid SQL previously. ([#127](https://github.com/circuithub/rel8/pull/127))
82+
83+## Breaking changes
84+
85+* `rebind` now takes an additional argument for the "name" of the binding. ([#128](https://github.com/circuithub/rel8/pull/128))
86+
87+# 1.1.0.0 (2021-07-16)
18
88
19## New features
89## New features
20
90
@@ -26,13 +96,45 @@
26 , myTableY :: f Text
96 , myTableY :: f Text
27 } deriving (Generic, Rel8able)
97 } deriving (Generic, Rel8able)
28 ```
98 ```
29−
99+
30* The `Projectable` and `Biprojectable` type classes have been introduced. These classes model a type of functors and bifunctors (respectively) on tables, where the mapping function is only able to rearrange columns. For example, the following is now possible:
100* The `Projectable` and `Biprojectable` type classes have been introduced. These classes model a type of functors and bifunctors (respectively) on tables, where the mapping function is only able to rearrange columns. For example, the following is now possible:
31
101
32 ```haskell
102 ```haskell
33 x <- project myTableX <$> many ...
103 x <- project myTableX <$> many ...
34 ```
104 ```
35
105
106+* `Insert`'s `onConflict` field now has a new option - `DoUpdate`. This maps to PostgreSQL's `ON CONFLICT DO UPDATE` syntax, which provides upsert support. ([#85](https://github.com/circuithub/rel8/pull/85))
107+
108+* `Insert`, `Update` and `Delete` has all been expanded to work with `Query`s. In particular:
109+
110+ * `Delete` now supports a `USING` sub-query
111+ * `Update` now supports a `FROM` sub-query
112+ * `Insert` can delete the result of any arbitrary `Query` (providing the types match)
113+
114+* The new `Rel8.Tabulate` module has been added, which gives a `Map`-esque interface to writing and composing queries. ([#70](https://github.com/circuithub/rel8/pull/70))
115+
116+* The new `indexed` `Query -> Query` function was added. This function augments each row in a query with it's 0-based index. ([#117](https://github.com/circuithub/rel8/pull/117))
117+
118+## Breaking changes
119+
120+* `Insert`, `Delete` and `Update` have all changed. In particular, for `Insert` users should now replace `rows = xs` with `rows = values xs`. ([#85](https://github.com/circuithub/rel8/pull/85))
121+
122+* Rename `whereExists` and `whereNotExists` to `present` and `absent` respectively. ([#57](https://github.com/circuithub/rel8/pull/57))
123+
124+* Simplify `evaluate` to run directly inside the `Query` monad, rendering the `Evaluate` monad unnecessary. ([#56](https://github.com/circuithub/rel8/pull/56))
125+
126+* The `Labelable` type class has been removed as it's no longer necessary. This class was mostly internal, though was visible in some public API functions. ([#76](https://github.com/circuithub/rel8/pull/76))
127+
128+* `EitherTable`, `ListTable`, `MaybeTable`, `NonEmptyTable` and `TheseTable` now all have an extra parameter which tracks the context of the table. If you manually specified any types before, you need to provide this parameter - usually `Expr` will be the correct choice. ([#101](https://github.com/circuithub/rel8/pull/101))
129+
130+* We've done a lot of work to simplify the internals of Rel8, but some of these are breaking changes. However, most of these API changes shouldn't affect users as they are mostly types used in type inference, or affect classes where users don't need to define their own instances.
131+
132+ * The kind of a `Rel8able` is now `(Type -> Type) -> Type`. Previously it was `(X -> Type) -> Type`.
133+ * `Table` has a new associated type - `FromExprs`. This was previously an open type family.
134+ * `Table` has a new associated type - `Transpose` - and `Recontextualise` has been renamed to `Transposes`. This `Transposes` class now operates in terms of `Transpose`.
135+
136+* `select`, `insert`, `update` and `delete` now produce Hasql `Statement`s, rather than actually running the statement as IO. This allows Rel8 to be used with transaction/connection-managing monads like [`hasql-transaction`](https://hackage.haskell.org/package/hasql-transaction). ([#94](https://github.com/circuithub/rel8/pull/94))
137+
36## Bug fixes
138## Bug fixes
37
139
38* Fixes a bug where cartesian products of queries using `catListTable`, `catNonEmptyTable`, `catList` and `catNonEmpty` would incorrectly be zipped instead. ([#61](https://github.com/circuithub/rel8/pull/61))
140* Fixes a bug where cartesian products of queries using `catListTable`, `catNonEmptyTable`, `catList` and `catNonEmpty` would incorrectly be zipped instead. ([#61](https://github.com/circuithub/rel8/pull/61))
cabal.project−5
@@ -1,6 +1 @@
1packages: .
1packages: .
2−
3−allow-newer: generic-monoid:base
4−allow-newer: cryptohash-sha1:base
5−allow-newer: cryptohash-md5:base
6−allow-newer: entropy:Cabal
cabal.project.haskell-nix−6
@@ -2,9 +2,3 @@
2-- will interpret them as local packages, and try to build them when we cabal
2-- will interpret them as local packages, and try to build them when we cabal
3-- build. The only reason we have to specify these is for Haskell.nix to know to
3-- build. The only reason we have to specify these is for Haskell.nix to know to
4-- override these packages by fetching them rather than using Hackage.
4-- override these packages by fetching them rather than using Hackage.
5−
6−source-repository-package
7− type: git
8− location: git://github.com/jcpetruzza/barbies
9− tag: f99b05454874192e3511bd133555dfb6cc6a6ecb
10− --sha256: 0yy2i2jbllwavv5d2176rf8lmm4l1ws90lxkmdlfgvfzqxidx0gi
default.nix+2−2
@@ -1,11 +1,11 @@
1let
1let
2 haskellNix = import (import ./nix/sources.nix)."haskell.nix" {};
2 haskellNix = import (import ./nix/sources.nix)."haskell.nix" {};
3
3
4− nixpkgsSrc = haskellNix.sources.nixpkgs-2009;
4+ nixpkgsSrc = haskellNix.sources.nixpkgs-unstable;
5
5
6 nixpkgsArgs = haskellNix.nixpkgsArgs;
6 nixpkgsArgs = haskellNix.nixpkgsArgs;
7
7
8− compiler-nix-name = "ghc901";
8+ compiler-nix-name = "ghc924";
9
9
10 pkgs = import nixpkgsSrc nixpkgsArgs;
10 pkgs = import nixpkgsSrc nixpkgsArgs;
11
11
docs/cookbook.rst+9−7
@@ -142,23 +142,25 @@ rows, but can also return *tree*\s.
142To understand what this means, we'll consider a small example query for blog
142To understand what this means, we'll consider a small example query for blog
143posts. We want our query to return:
143posts. We want our query to return:
144
144
145−1. The latest 5 blog posts.
145+1. The latest 5 blog posts that have at least one tag each.
1462. For each blog post, all tags.
1462. For each blog post, all tags.
147−3. For each blog post, the latest 3 comments.
147+3. For each blog post, the latest 3 comments if they exist.
148
148
149In Rel8, we can write this query as::
149In Rel8, we can write this query as::
150
150
151 latestBlogPosts = do
151 latestBlogPosts = do
152 post <- each postSchema
152 post <- each postSchema
153
153
154+ -- Returns a `NonEmptyTable a` which ends up as a `Data.List.NonEmpty a` after the query is run
154− tags <- aggregate do
155+ tags <- some $ do
155 tag <- each tagSchema
156 tag <- each tagSchema
156 where_ (tagPostId tag ==. postId post)
157 where_ (tagPostId tag ==. postId post)
157− return (listAgg (tagName tag))
158+ return (tagName tag)
158
159
160+ -- Returns a `ListTable a` which ends up as a `[a]` after the query is run
159− latestComments <-
161+ latestComments <-
160− many $
162+ many $
161− limit 3 $
163+ limit 3 $
162 orderBy (commentCreatedAt >$< desc) do
164 orderBy (commentCreatedAt >$< desc) do
163 comment <- each commentSchema
165 comment <- each commentSchema
164 where_ (commentPostId comment ==. postId post)
166 where_ (commentPostId comment ==. postId post)
nix/sources.json+9−9
@@ -5,10 +5,10 @@
5 "homepage": "https://input-output-hk.github.io/haskell.nix",
5 "homepage": "https://input-output-hk.github.io/haskell.nix",
6 "owner": "input-output-hk",
6 "owner": "input-output-hk",
7 "repo": "haskell.nix",
7 "repo": "haskell.nix",
8− "rev": "3856d2d24dca0ecc71fcfc314253a2a2d07a3c4f",
8+ "rev": "0837156c56cb5350dd9bd84eb424187e00aa1e8a",
9− "sha256": "0s69nasqhkv8n0qmdv075k83p3z584ja9vkzdnp2291mymghvx87",
9+ "sha256": "1djmv5a0vfj78csw2cad6nicrs8iqrp928b9b3v4h7lwwhhzliwl",
10 "type": "tarball",
10 "type": "tarball",
11− "url": "https://github.com/input-output-hk/haskell.nix/archive/3856d2d24dca0ecc71fcfc314253a2a2d07a3c4f.tar.gz",
11+ "url": "https://github.com/input-output-hk/haskell.nix/archive/0837156c56cb5350dd9bd84eb424187e00aa1e8a.tar.gz",
12 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
12 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
13 },
13 },
14 "niv": {
14 "niv": {
@@ -17,10 +17,10 @@
17 "homepage": "https://github.com/nmattia/niv",
17 "homepage": "https://github.com/nmattia/niv",
18 "owner": "nmattia",
18 "owner": "nmattia",
19 "repo": "niv",
19 "repo": "niv",
20− "rev": "e0ca65c81a2d7a4d82a189f1e23a48d59ad42070",
20+ "rev": "acdb1726783637852c01e0ad639ac9cbec28d8c8",
21− "sha256": "1pq9nh1d8nn3xvbdny8fafzw87mj7gsmp6pxkdl65w2g18rmcmzx",
21+ "sha256": "1wszl8q70z6wfgkh2dvgmhhry6qp9nar421qq2ifd17hq2jj9ck3",
22 "type": "tarball",
22 "type": "tarball",
23− "url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz",
23+ "url": "https://github.com/nmattia/niv/archive/acdb1726783637852c01e0ad639ac9cbec28d8c8.tar.gz",
24 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
24 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
25 },
25 },
26 "nixpkgs": {
26 "nixpkgs": {
@@ -29,10 +29,10 @@
29 "homepage": null,
29 "homepage": null,
30 "owner": "nixos",
30 "owner": "nixos",
31 "repo": "nixpkgs",
31 "repo": "nixpkgs",
32− "rev": "59b8d9cf24e9fcf10341a0923c9bdca088dca8c8",
32+ "rev": "1c1f5649bb9c1b0d98637c8c365228f57126f361",
33− "sha256": "08f38v4b2kcxnbapdwrb54bglka92cxj9qlnqlk5px206jyq9v4c",
33+ "sha256": "0f2nvdijyxfgl5kwyb4465pppd5vkhqxddx6v40k2s0z9jfhj0xl",
34 "type": "tarball",
34 "type": "tarball",
35− "url": "https://github.com/nixos/nixpkgs/archive/59b8d9cf24e9fcf10341a0923c9bdca088dca8c8.tar.gz",
35+ "url": "https://github.com/nixos/nixpkgs/archive/1c1f5649bb9c1b0d98637c8c365228f57126f361.tar.gz",
36 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
36 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
37 }
37 }
38}
38}
nix/sources.nix+21−1
@@ -31,8 +31,28 @@ let
31 if spec ? branch then "refs/heads/${spec.branch}" else
31 if spec ? branch then "refs/heads/${spec.branch}" else
32 if spec ? tag then "refs/tags/${spec.tag}" else
32 if spec ? tag then "refs/tags/${spec.tag}" else
33 abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
33 abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
34+ submodules = if spec ? submodules then spec.submodules else false;
35+ submoduleArg =
36+ let
37+ nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
38+ emptyArgWithWarning =
39+ if submodules == true
40+ then
41+ builtins.trace
42+ (
43+ "The niv input \"${name}\" uses submodules "
44+ + "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
45+ + "does not support them"
46+ )
47+ {}
48+ else {};
49+ in
50+ if nixSupportsSubmodules
51+ then { inherit submodules; }
52+ else emptyArgWithWarning;
34 in
53 in
54+ builtins.fetchGit
35− builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
55+ ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
36
56
37 fetch_local = spec: spec.path;
57 fetch_local = spec: spec.path;
38
58
rel8.cabal+22−8
@@ -1,6 +1,6 @@
1cabal-version: 2.0
1cabal-version: 2.0
2name: rel8
2name: rel8
3−version: 1.0.0.1
3+version: 1.4.0.0
4synopsis: Hey! Hey! Can u rel8?
4synopsis: Hey! Hey! Can u rel8?
5license: BSD3
5license: BSD3
6license-file: LICENSE
6license-file: LICENSE
@@ -20,22 +20,23 @@ source-repository head
20library
20library
21 build-depends:
21 build-depends:
22 aeson
22 aeson
23− , base ^>= 4.14 || ^>=4.15
23+ , base ^>= 4.14 || ^>=4.15 || ^>=4.16
24+ , bifunctors
24 , bytestring
25 , bytestring
25 , case-insensitive
26 , case-insensitive
26 , comonad
27 , comonad
27 , contravariant
28 , contravariant
28− , hasql ^>= 1.4.5.1
29+ , hasql ^>= 1.4.5.1 || ^>= 1.5.0.0 || ^>= 1.6.0.0
29− , opaleye ^>= 0.7.3.0
30+ , opaleye ^>= 0.9.6.1
30 , pretty
31 , pretty
31 , profunctors
32 , profunctors
33+ , product-profunctors
32 , scientific
34 , scientific
33 , semialign
35 , semialign
34 , semigroupoids
36 , semigroupoids
35 , text
37 , text
36 , these
38 , these
37 , time
39 , time
38− , transformers
39 , uuid
40 , uuid
40 default-language:
41 default-language:
41 Haskell2010
42 Haskell2010
@@ -45,6 +46,7 @@ library
45 -Wno-missing-import-lists -Wno-prepositive-qualified-module
46 -Wno-missing-import-lists -Wno-prepositive-qualified-module
46 -Wno-monomorphism-restriction
47 -Wno-monomorphism-restriction
47 -Wno-missing-local-signatures
48 -Wno-missing-local-signatures
49+ -Wno-missing-kind-signatures
48 hs-source-dirs:
50 hs-source-dirs:
49 src
51 src
50 exposed-modules:
52 exposed-modules:
@@ -52,6 +54,7 @@ library
52 Rel8.Expr.Num
54 Rel8.Expr.Num
53 Rel8.Expr.Text
55 Rel8.Expr.Text
54 Rel8.Expr.Time
56 Rel8.Expr.Time
57+ Rel8.Tabulate
55
58
56 other-modules:
59 other-modules:
57 Rel8.Aggregate
60 Rel8.Aggregate
@@ -63,6 +66,7 @@ library
63 Rel8.Column.List
66 Rel8.Column.List
64 Rel8.Column.Maybe
67 Rel8.Column.Maybe
65 Rel8.Column.NonEmpty
68 Rel8.Column.NonEmpty
69+ Rel8.Column.Null
66 Rel8.Column.These
70 Rel8.Column.These
67
71
68 Rel8.Expr
72 Rel8.Expr
@@ -78,6 +82,7 @@ library
78 Rel8.Expr.Order
82 Rel8.Expr.Order
79 Rel8.Expr.Sequence
83 Rel8.Expr.Sequence
80 Rel8.Expr.Serialize
84 Rel8.Expr.Serialize
85+ Rel8.Expr.Window
81
86
82 Rel8.FCF
87 Rel8.FCF
83
88
@@ -104,16 +109,19 @@ library
104 Rel8.Query.Evaluate
109 Rel8.Query.Evaluate
105 Rel8.Query.Exists
110 Rel8.Query.Exists
106 Rel8.Query.Filter
111 Rel8.Query.Filter
112+ Rel8.Query.Indexed
107 Rel8.Query.Limit
113 Rel8.Query.Limit
108 Rel8.Query.List
114 Rel8.Query.List
109 Rel8.Query.Maybe
115 Rel8.Query.Maybe
110 Rel8.Query.Null
116 Rel8.Query.Null
111 Rel8.Query.Opaleye
117 Rel8.Query.Opaleye
112 Rel8.Query.Order
118 Rel8.Query.Order
119+ Rel8.Query.Rebind
113 Rel8.Query.Set
120 Rel8.Query.Set
114 Rel8.Query.SQL
121 Rel8.Query.SQL
115 Rel8.Query.These
122 Rel8.Query.These
116 Rel8.Query.Values
123 Rel8.Query.Values
124+ Rel8.Query.Window
117
125
118 Rel8.Schema.Context.Nullify
126 Rel8.Schema.Context.Nullify
119 Rel8.Schema.Dict
127 Rel8.Schema.Dict
@@ -162,6 +170,7 @@ library
162 Rel8.Table.Maybe
170 Rel8.Table.Maybe
163 Rel8.Table.Name
171 Rel8.Table.Name
164 Rel8.Table.NonEmpty
172 Rel8.Table.NonEmpty
173+ Rel8.Table.Null
165 Rel8.Table.Nullify
174 Rel8.Table.Nullify
166 Rel8.Table.Opaleye
175 Rel8.Table.Opaleye
167 Rel8.Table.Ord
176 Rel8.Table.Ord
@@ -172,6 +181,7 @@ library
172 Rel8.Table.These
181 Rel8.Table.These
173 Rel8.Table.Transpose
182 Rel8.Table.Transpose
174 Rel8.Table.Undefined
183 Rel8.Table.Undefined
184+ Rel8.Table.Window
175
185
176 Rel8.Type
186 Rel8.Type
177 Rel8.Type.Array
187 Rel8.Type.Array
@@ -190,6 +200,9 @@ library
190 Rel8.Type.Sum
200 Rel8.Type.Sum
191 Rel8.Type.Tag
201 Rel8.Type.Tag
192
202
203+ Rel8.Window
204+
205+
193test-suite tests
206test-suite tests
194 type: exitcode-stdio-1.0
207 type: exitcode-stdio-1.0
195 build-depends:
208 build-depends:
@@ -198,9 +211,9 @@ test-suite tests
198 , case-insensitive
211 , case-insensitive
199 , containers
212 , containers
200 , hasql
213 , hasql
214+ , hasql-transaction
201− , hedgehog ^>=1.0.2
215+ , hedgehog ^>= 1.0 || ^>= 1.1
202− , lifted-base ^>=0.2.3.12
216+ , mmorph
203− , monad-control ^>=1.0.2.3
204 , rel8
217 , rel8
205 , scientific
218 , scientific
206 , tasty
219 , tasty
@@ -208,6 +221,7 @@ test-suite tests
208 , text
221 , text
209 , time
222 , time
210 , tmp-postgres ^>=1.34.1.0
223 , tmp-postgres ^>=1.34.1.0
224+ , transformers
211 , uuid
225 , uuid
212
226
213 other-modules:
227 other-modules:
shell.nix+1−1
@@ -1,6 +1,6 @@
1let
1let
2 sources = import ./nix/sources.nix;
2 sources = import ./nix/sources.nix;
3− pkgs = import sources.nixpkgs {};
3+ pkgs = import sources.nixpkgs {overlays = [];};
4 hsPkgs = import ./default.nix;
4 hsPkgs = import ./default.nix;
5in
5in
6 hsPkgs.shellFor {
6 hsPkgs.shellFor {
src/Rel8.hs+277−8
@@ -38,6 +38,7 @@ module Rel8
38 , HMaybe
38 , HMaybe
39 , HList
39 , HList
40 , HNonEmpty
40 , HNonEmpty
41+ , HNull
41 , HThese
42 , HThese
42 , Lift
43 , Lift
43
44
@@ -46,16 +47,18 @@ module Rel8
46 , Transposes
47 , Transposes
47 , AltTable((<|>:))
48 , AltTable((<|>:))
48 , AlternativeTable( emptyTable )
49 , AlternativeTable( emptyTable )
49− , EqTable, (==:), (/=:)
50+ , EqTable(..), (==:), (/=:)
50− , OrdTable, ascTable, descTable
51+ , OrdTable(..), (<:), (<=:), (>:), (>=:), ascTable, descTable, greatest, least
51 , lit
52 , lit
52 , bool
53 , bool
53 , case_
54 , case_
55+ , castTable
54
56
55 -- ** @MaybeTable@
57 -- ** @MaybeTable@
56 , MaybeTable
58 , MaybeTable
57 , maybeTable, ($?), nothingTable, justTable
59 , maybeTable, ($?), nothingTable, justTable
58 , isNothingTable, isJustTable
60 , isNothingTable, isJustTable
61+ , fromMaybeTable
59 , optional
62 , optional
60 , catMaybeTable
63 , catMaybeTable
61 , traverseMaybeTable
64 , traverseMaybeTable
@@ -78,6 +81,7 @@ module Rel8
78 , isThisTable, isThatTable, isThoseTable
81 , isThisTable, isThatTable, isThoseTable
79 , hasHereTable, hasThereTable
82 , hasHereTable, hasThereTable
80 , justHereTable, justThereTable
83 , justHereTable, justThereTable
84+ , alignMaybeTable
81 , alignBy
85 , alignBy
82 , keepHereTable, loseHereTable
86 , keepHereTable, loseHereTable
83 , keepThereTable, loseThereTable
87 , keepThereTable, loseThereTable
@@ -90,7 +94,7 @@ module Rel8
90
94
91 -- ** @ListTable@
95 -- ** @ListTable@
92 , ListTable
96 , ListTable
93− , listTable
97+ , listTable, ($*)
94 , nameListTable
98 , nameListTable
95 , many
99 , many
96 , manyExpr
100 , manyExpr
@@ -99,21 +103,44 @@ module Rel8
99
103
100 -- ** @NonEmptyTable@
104 -- ** @NonEmptyTable@
101 , NonEmptyTable
105 , NonEmptyTable
102− , nonEmptyTable
106+ , nonEmptyTable, ($+)
103 , nameNonEmptyTable
107 , nameNonEmptyTable
104 , some
108 , some
105 , someExpr
109 , someExpr
106 , catNonEmptyTable
110 , catNonEmptyTable
107 , catNonEmpty
111 , catNonEmpty
108
112
109− -- ** @ADT@
113+ -- ** @NullTable@
114+ , NullTable
115+ , nullableTable, nullTable, nullifyTable
116+ , isNullTable, isNonNullTable
117+ , catNullTable
118+ , nameNullTable
119+ , toNullTable, toMaybeTable
120+
121+ -- ** Algebraic data types / sum types
122+ -- $adts
123+
124+ -- *** Naming of ADTs
125+ -- $naming
126+ , NameADT, nameADT
110 , ADT, ADTable
127 , ADT, ADTable
128+
129+ -- *** Deconstruction of ADTs
130+ -- $deconstruction
131+ , DeconstructADT, deconstructADT
132+
133+ -- *** Construction of ADTs
134+ -- $construction
111 , BuildADT, buildADT
135 , BuildADT, buildADT
112 , ConstructADT, constructADT
136 , ConstructADT, constructADT
113− , DeconstructADT, deconstructADT
137+
114− , NameADT, nameADT
138+ -- *** Other ADT operations
115 , AggregateADT, aggregateADT
139 , AggregateADT, aggregateADT
116
140
141+ -- *** Miscellaneous notes
142+ -- $misc-notes
143+
117 -- ** @HKD@
144 -- ** @HKD@
118 , HKD, HKDable
145 , HKD, HKDable
119 , BuildHKD, buildHKD
146 , BuildHKD, buildHKD
@@ -138,6 +165,7 @@ module Rel8
138 -- ** @null@
165 -- ** @null@
139 , NotNull
166 , NotNull
140 , Nullable
167 , Nullable
168+ , Homonullable
141 , null
169 , null
142 , nullify
170 , nullify
143 , nullable
171 , nullable
@@ -156,6 +184,7 @@ module Rel8
156 , (==.), (/=.), (==?), (/=?)
184 , (==.), (/=.), (==?), (/=?)
157 , in_
185 , in_
158 , boolExpr, caseExpr
186 , boolExpr, caseExpr
187+ , like, ilike
159
188
160 -- ** Ordering
189 -- ** Ordering
161 , DBOrd
190 , DBOrd
@@ -224,10 +253,11 @@ module Rel8
224 , countRows
253 , countRows
225 , groupBy
254 , groupBy
226 , listAgg, listAggExpr
255 , listAgg, listAggExpr
256+ , mode
227 , nonEmptyAgg, nonEmptyAggExpr
257 , nonEmptyAgg, nonEmptyAggExpr
228 , DBMax, max
258 , DBMax, max
229 , DBMin, min
259 , DBMin, min
230− , DBSum, sum, sumWhere
260+ , DBSum, sum, sumWhere, avg
231 , DBString, stringAgg
261 , DBString, stringAgg
232 , count
262 , count
233 , countStar
263 , countStar
@@ -244,6 +274,29 @@ module Rel8
244 , nullsFirst
274 , nullsFirst
245 , nullsLast
275 , nullsLast
246
276
277+ -- ** Window functions
278+ , Window
279+ , window
280+ , Partition
281+ , over
282+ , partitionBy
283+ , orderPartitionBy
284+ , cumulative
285+ , cumulative_
286+ , currentRow
287+ , rowNumber
288+ , rank
289+ , denseRank
290+ , percentRank
291+ , cumeDist
292+ , ntile
293+ , lag
294+ , lead
295+ , firstValue
296+ , lastValue
297+ , nthValue
298+ , indexed
299+
247 -- ** Bindings
300 -- ** Bindings
248 , rebind
301 , rebind
249
302
@@ -281,6 +334,7 @@ module Rel8
281
334
282 -- ** @CREATE VIEW@
335 -- ** @CREATE VIEW@
283 , createView
336 , createView
337+ , createOrReplaceView
284
338
285 -- ** Sequences
339 -- ** Sequences
286 , nextval
340 , nextval
@@ -299,6 +353,7 @@ import Rel8.Column.Lift
299import Rel8.Column.List
353import Rel8.Column.List
300import Rel8.Column.Maybe
354import Rel8.Column.Maybe
301import Rel8.Column.NonEmpty
355import Rel8.Column.NonEmpty
356+import Rel8.Column.Null
302import Rel8.Column.These
357import Rel8.Column.These
303import Rel8.Expr
358import Rel8.Expr
304import Rel8.Expr.Aggregate
359import Rel8.Expr.Aggregate
@@ -312,6 +367,8 @@ import Rel8.Expr.Ord
312import Rel8.Expr.Order
367import Rel8.Expr.Order
313import Rel8.Expr.Serialize
368import Rel8.Expr.Serialize
314import Rel8.Expr.Sequence
369import Rel8.Expr.Sequence
370+import Rel8.Expr.Text ( like, ilike )
371+import Rel8.Expr.Window hiding ( cumulative )
315import Rel8.Generic.Rel8able ( KRel8able, Rel8able )
372import Rel8.Generic.Rel8able ( KRel8able, Rel8able )
316import Rel8.Order
373import Rel8.Order
317import Rel8.Query
374import Rel8.Query
@@ -322,15 +379,18 @@ import Rel8.Query.Either
322import Rel8.Query.Evaluate
379import Rel8.Query.Evaluate
323import Rel8.Query.Exists
380import Rel8.Query.Exists
324import Rel8.Query.Filter
381import Rel8.Query.Filter
382+import Rel8.Query.Indexed
325import Rel8.Query.Limit
383import Rel8.Query.Limit
326import Rel8.Query.List
384import Rel8.Query.List
327import Rel8.Query.Maybe
385import Rel8.Query.Maybe
328import Rel8.Query.Null
386import Rel8.Query.Null
329import Rel8.Query.Order
387import Rel8.Query.Order
388+import Rel8.Query.Rebind
330import Rel8.Query.SQL (showQuery)
389import Rel8.Query.SQL (showQuery)
331import Rel8.Query.Set
390import Rel8.Query.Set
332import Rel8.Query.These
391import Rel8.Query.These
333import Rel8.Query.Values
392import Rel8.Query.Values
393+import Rel8.Query.Window
334import Rel8.Schema.Field
394import Rel8.Schema.Field
335import Rel8.Schema.HTable
395import Rel8.Schema.HTable
336import Rel8.Schema.Name
396import Rel8.Schema.Name
@@ -357,6 +417,8 @@ import Rel8.Table.List
357import Rel8.Table.Maybe
417import Rel8.Table.Maybe
358import Rel8.Table.Name
418import Rel8.Table.Name
359import Rel8.Table.NonEmpty
419import Rel8.Table.NonEmpty
420+import Rel8.Table.Null
421+import Rel8.Table.Opaleye ( castTable )
360import Rel8.Table.Ord
422import Rel8.Table.Ord
361import Rel8.Table.Order
423import Rel8.Table.Order
362import Rel8.Table.Projection
424import Rel8.Table.Projection
@@ -364,6 +426,7 @@ import Rel8.Table.Rel8able ()
364import Rel8.Table.Serialize
426import Rel8.Table.Serialize
365import Rel8.Table.These
427import Rel8.Table.These
366import Rel8.Table.Transpose
428import Rel8.Table.Transpose
429+import Rel8.Table.Window
367import Rel8.Type
430import Rel8.Type
368import Rel8.Type.Composite
431import Rel8.Type.Composite
369import Rel8.Type.Eq
432import Rel8.Type.Eq
@@ -378,6 +441,7 @@ import Rel8.Type.ReadShow
378import Rel8.Type.Semigroup
441import Rel8.Type.Semigroup
379import Rel8.Type.String
442import Rel8.Type.String
380import Rel8.Type.Sum
443import Rel8.Type.Sum
444+import Rel8.Window
381
445
382
446
383-- $running
447-- $running
@@ -385,3 +449,208 @@ import Rel8.Type.Sum
385-- provides 'select', 'insert', 'update' and 'delete' functions. Note that
449-- provides 'select', 'insert', 'update' and 'delete' functions. Note that
386-- 'insert', 'update' and 'delete' will generally need the
450-- 'insert', 'update' and 'delete' will generally need the
387-- `DuplicateRecordFields` language extension enabled.
451-- `DuplicateRecordFields` language extension enabled.
452+
453+-- $adts
454+-- Algebraic data types can be modelled between Haskell and SQL.
455+--
456+-- * Your SQL table needs a certain text field that tags which Haskell constructor is in use.
457+-- * You have to use a few combinators to specify the sum type's individual constructors.
458+-- * If you want to do case analysis at the @Expr@ (SQL) level, you can use 'maybe'/'either'-like eliminators.
459+--
460+-- The documentation in this section will assume a set of database types like this:
461+--
462+-- @
463+-- data Thing f = ThingEmployer (Employer f) | ThingPotato (Potato f) | Nullary
464+-- deriving stock Generic
465+--
466+-- data Employer f = Employer { employerId :: f Int32, employerName :: f Text}
467+-- deriving stock Generic
468+-- deriving anyclass Rel8able
469+--
470+-- data Potato f = Potato { size :: f Int32, grower :: f Text }
471+-- deriving stock Generic
472+-- deriving anyclass Rel8able
473+-- @
474+
475+-- $naming
476+--
477+-- First, in your 'TableSchema', name your type like this:
478+--
479+-- @
480+-- thingSchema :: TableSchema (ADT Thing Name)
481+-- thingSchema =
482+-- TableSchema
483+-- { schema = Nothing,
484+-- name = \"thing\",
485+-- columns =
486+-- nameADT @Thing
487+-- \"tag\"
488+-- Employer
489+-- { employerName = \"name\",
490+-- employerId = \"id\"
491+-- }
492+-- Potato {size = \"size\", grower = \"Mary\"}
493+-- }
494+-- @
495+--
496+-- Note that @nameADT \@Thing "tag"@ is variadic: it accepts one
497+-- argument per constructor, except the nullary ones (Nullary) because
498+-- there's nothing to do for them.
499+
500+-- $deconstruction
501+--
502+-- To deconstruct sum types at the SQL level, use 'deconstructADT',
503+-- which is also variadic, and has one argument for each
504+-- constructor. Similar to 'maybe'.
505+--
506+-- @
507+-- query :: Query (ADT Thing Expr)
508+-- query = do
509+-- thingExpr <- each thingSchema
510+-- where_ $
511+-- deconstructADT @Thing
512+-- (\employer -> employerName employer ==. lit \"Mary\")
513+-- (\potato -> grower potato ==. lit \"Mary\")
514+-- (lit False) -- Nullary case
515+-- thingExpr
516+-- pure thingExpr
517+-- @
518+--
519+-- SQL output:
520+--
521+-- @
522+-- SELECT
523+-- CAST("tag0_1" AS text) as "tag",
524+-- CAST("id1_1" AS int4) as "ThingEmployer/_1/employerId",
525+-- CAST("name2_1" AS text) as "ThingEmployer/_1/employerName",
526+-- CAST("size3_1" AS int4) as "ThingPotato/_1/size",
527+-- CAST("Mary4_1" AS text) as "ThingPotato/_1/grower"
528+-- FROM (SELECT
529+-- *
530+-- FROM (SELECT
531+-- "tag" as "tag0_1",
532+-- "id" as "id1_1",
533+-- "name" as "name2_1",
534+-- "size" as "size3_1",
535+-- "Mary" as "Mary4_1"
536+-- FROM "thing" as "T1") as "T1"
537+-- WHERE (CASE WHEN ("tag0_1") = (CAST(E'ThingPotato' AS text)) THEN ("Mary4_1") = (CAST(E'Mary' AS text))
538+-- WHEN ("tag0_1") = (CAST(E'Nullary' AS text)) THEN CAST(FALSE AS bool) ELSE ("name2_1") = (CAST(E'Mary' AS text)) END)) as "T1"
539+-- @
540+
541+-- $construction
542+--
543+-- To construct an ADT, you can use 'buildADT' or 'constructADT'. Consider the following type:
544+--
545+-- @
546+-- data Task f = Pending | Complete (CompletedTask f)
547+-- @
548+--
549+-- 'buildADT' is for constructing values of 'Task' in the 'Expr'
550+-- context. 'buildADT' needs two type-level arguments before its type
551+-- makes any sense. The first argument is the type of the "ADT", which
552+-- in our case is 'Task'. The second is the name of the constructor we
553+-- want to use. So that means we have the following possible
554+-- instantiations of 'buildADT' for 'Task':
555+--
556+-- @
557+-- > :t buildADT @Task @\"Pending\"
558+-- buildADT @Task @\"Pending\" :: ADT Task Expr
559+-- > :t buildADT @Task @\"Complete\"
560+-- buildADT @Task @\"Complete\" :: CompletedTask Expr -> ADT Task Expr
561+-- @
562+--
563+-- Note that as the "Pending" constructor has no fields, @buildADT
564+-- \@Task \@"Pending"@ is equivalent to @lit Pending@. But @buildADT
565+-- \@Task \@"Complete"@ is not the same as @lit . Complete@:
566+--
567+-- @
568+-- > :t lit . Complete
569+-- lit . Complete :: CompletedTask Result -> ADT Task Expr
570+-- @
571+--
572+--
573+-- Note that the former takes a @CompletedTask Expr@ while the latter
574+-- takes a @CompletedTask Result@. The former is more powerful because
575+-- you can construct @Task@s using dynamic values coming a database
576+-- query.
577+--
578+-- To show what this can look like in SQL, consider:
579+--
580+-- @
581+-- > :{
582+-- showQuery $ values
583+-- [ buildADT @Task @\"Pending\"
584+-- , buildADT @Task @\"Complete\" CompletedTask {date = Rel8.Expr.Time.now}
585+-- ]
586+-- :}
587+-- @
588+--
589+-- This produces the following SQL:
590+--
591+-- @
592+-- SELECT
593+-- CAST(\"values0_1\" AS text) as \"tag\",
594+-- CAST(\"values1_1\" AS timestamptz) as \"Complete/_1/date\"
595+-- FROM (SELECT
596+-- *
597+-- FROM (SELECT \"column1\" as \"values0_1\",
598+-- \"column2\" as \"values1_1\"
599+-- FROM
600+-- (VALUES
601+-- (CAST(E'Pending' AS text),CAST(NULL AS timestamptz)),
602+-- (CAST(E'Complete' AS text),CAST(now() AS timestamptz))) as \"V\") as \"T1\") as \"T1\"
603+-- @
604+--
605+-- This is what you get if you run it in @psql@:
606+--
607+--
608+-- @
609+-- tag | Complete/_1/date
610+-- ----------+-------------------------------
611+-- Pending |
612+-- Complete | 2022-05-19 21:28:23.969065+00
613+-- (2 rows)
614+-- @
615+--
616+-- "constructADT" is less convenient but more general alternative to
617+-- "buildADT". It requires only one type-level argument for its type
618+-- to make sense:
619+--
620+-- @
621+-- > :t constructADT @Task
622+-- constructADT @Task
623+-- :: (forall r. r -> (CompletedTask Expr -> r) -> r) -> ADT Task Expr
624+-- @
625+--
626+-- This might still seem a bit opaque, but basically it gives you a
627+-- Church-encoded constructor for arbitrary algebraic data types. You
628+-- might use it as follows:
629+--
630+-- @
631+-- let
632+-- pending :: ADT Task Expr
633+-- pending = constructADT @Task $ \pending _complete -> pending
634+--
635+-- complete :: ADT Task Expr
636+-- complete = constructADT @Task $ \_pending complete -> complete CompletedTask {date = Rel8.Expr.Time.now}
637+-- @
638+--
639+-- These values are otherwise identical to the ones we saw above with
640+-- @buildADT@, it's just a different style of constructing them.
641+--
642+
643+-- $misc-notes
644+--
645+-- 1. Note that the order of the arguments for all of these functions
646+-- is determined by the order of the constructors in the data
647+-- definition. If it were @data Task = Complete (CompletedTask f) |
648+-- Pending@ then the order of all the invocations of @constructADT@
649+-- and @deconstructADT@ would need to change.
650+--
651+-- 2. Maybe this is obvious, but just to spell it out: once you're in
652+-- the @Result@ context, you can of course construct @Task@ values
653+-- normally and use standard Haskell pattern-matching. @constructADT@
654+-- and @deconstructADT@ are specifically only needed in the @Expr@
655+-- context, and they allow you to do the equivalent of pattern
656+-- matching in PostgreSQL.
src/Rel8/Aggregate.hs+19−43
@@ -2,29 +2,31 @@
2{-# language FlexibleContexts #-}
2{-# language FlexibleContexts #-}
3{-# language FlexibleInstances #-}
3{-# language FlexibleInstances #-}
4{-# language MultiParamTypeClasses #-}
4{-# language MultiParamTypeClasses #-}
5−{-# language NamedFieldPuns #-}
6{-# language RankNTypes #-}
5{-# language RankNTypes #-}
7{-# language StandaloneKindSignatures #-}
6{-# language StandaloneKindSignatures #-}
8{-# language TypeFamilies #-}
7{-# language TypeFamilies #-}
9{-# language UndecidableInstances #-}
8{-# language UndecidableInstances #-}
10
9
11module Rel8.Aggregate
10module Rel8.Aggregate
12− ( Aggregate(..), foldInputs, mapInputs
11+ ( Aggregate(..), zipOutputs
13− , Aggregator(..), unsafeMakeAggregate
12+ , unsafeMakeAggregate
14 , Aggregates
13 , Aggregates
15 )
14 )
16where
15where
17
16
18-- base
17-- base
19−import Data.Functor.Const ( Const( Const ), getConst )
18+import Control.Applicative ( liftA2 )
20import Data.Functor.Identity ( Identity( Identity ) )
19import Data.Functor.Identity ( Identity( Identity ) )
21import Data.Kind ( Constraint, Type )
20import Data.Kind ( Constraint, Type )
22import Prelude
21import Prelude
23
22
23+-- profunctors
24+import Data.Profunctor ( dimap )
25+
24-- opaleye
26-- opaleye
25−import qualified Opaleye.Internal.Aggregate as Opaleye
27+import qualified Opaleye.Aggregate as Opaleye
26import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
28import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
27−import qualified Opaleye.Internal.PackMap as Opaleye
29+import qualified Opaleye.Internal.Column as Opaleye
28
30
29-- rel8
31-- rel8
30import Rel8.Expr ( Expr )
32import Rel8.Expr ( Expr )
@@ -40,11 +42,7 @@ import Rel8.Table.Transpose ( Transposes )
40import Rel8.Type ( DBType )
42import Rel8.Type ( DBType )
41
43
42
44
43−-- | An @Aggregate a@ describes how to aggregate @Table@s of type @a@. You can
45+-- | 'Aggregate' is a special context used by 'Rel8.aggregate'.
44−-- unpack an @Aggregate@ back to @a@ by running it with 'Rel8.aggregate'. As
45−-- @Aggregate@ is almost an 'Applicative' functor - but there is no 'pure'
46−-- operation. This means 'Aggregate' is an instance of 'Apply', and you can
47−-- combine @Aggregate@s using the @<.>@ combinator.
48type Aggregate :: K.Context
46type Aggregate :: K.Context
49newtype Aggregate a = Aggregate (Opaleye.Aggregator () (Expr a))
47newtype Aggregate a = Aggregate (Opaleye.Aggregator () (Expr a))
50
48
@@ -61,47 +59,25 @@ instance Sql DBType a => Table Aggregate (Aggregate a) where
61 fromResult (HIdentity (Identity a)) = a
59 fromResult (HIdentity (Identity a)) = a
62
60
63
61
64−-- | @Aggregates a b@ means that the columns in @a@ are all 'Aggregate' 'Expr's
62+-- | @Aggregates a b@ means that the columns in @a@ are all 'Aggregate's
65−-- for the columns in @b@.
63+-- for the 'Expr' columns in @b@.
66type Aggregates :: Type -> Type -> Constraint
64type Aggregates :: Type -> Type -> Constraint
67class Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs
65class Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs
68instance Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs
66instance Transposes Aggregate Expr aggregates exprs => Aggregates aggregates exprs
69
67
70
68
71−foldInputs :: forall (a :: Type) (b :: Type). Monoid b
69+zipOutputs :: ()
72− => (Maybe Aggregator -> Opaleye.PrimExpr -> b) -> Aggregate a -> b
70+ => (Expr a -> Expr b -> Expr c) -> Aggregate a -> Aggregate b -> Aggregate c
73−foldInputs f (Aggregate (Opaleye.Aggregator (Opaleye.PackMap agg))) =
71+zipOutputs f (Aggregate a) (Aggregate b) = Aggregate (liftA2 f a b)
74− getConst $ flip agg () $ \(aggregator, a) ->
75− Const $ f (detuplize <$> aggregator) a
76− where
77− detuplize (operation, ordering, distinction) =
78− Aggregator {operation, ordering, distinction}
79−
80−
81−mapInputs :: forall (a :: Type). ()
82− => (Opaleye.PrimExpr -> Opaleye.PrimExpr) -> Aggregate a -> Aggregate a
83−mapInputs transform (Aggregate (Opaleye.Aggregator (Opaleye.PackMap agg))) =
84− Aggregate $ Opaleye.Aggregator $ Opaleye.PackMap $ agg . \f input ->
85− f (fmap transform input)
86−
87−
88−type Aggregator :: Type
89−data Aggregator = Aggregator
90− { operation :: Opaleye.AggrOp
91− , ordering :: [Opaleye.OrderExpr]
92− , distinction :: Opaleye.AggrDistinct
93− }
94
72
95
73
96−unsafeMakeAggregate :: forall (input :: Type) (output :: Type). ()
74+unsafeMakeAggregate :: forall (input :: Type) (output :: Type) n n' a a'. ()
97 => (Expr input -> Opaleye.PrimExpr)
75 => (Expr input -> Opaleye.PrimExpr)
98 -> (Opaleye.PrimExpr -> Expr output)
76 -> (Opaleye.PrimExpr -> Expr output)
99− -> Maybe Aggregator
77+ -> Opaleye.Aggregator (Opaleye.Field_ n a) (Opaleye.Field_ n' a')
100 -> Expr input
78 -> Expr input
101 -> Aggregate output
79 -> Aggregate output
102unsafeMakeAggregate input output aggregator expr =
80unsafeMakeAggregate input output aggregator expr =
103− Aggregate $ Opaleye.Aggregator $ Opaleye.PackMap $ \f _ ->
81+ Aggregate $ dimap in_ out aggregator
104− output <$> f (tuplize <$> aggregator, input expr)
82+ where out = output . Opaleye.unColumn
105− where
83+ in_ = Opaleye.Column . input . const expr
106− tuplize Aggregator {operation, ordering, distinction} =
107− (operation, ordering, distinction)
src/Rel8/Column/Null.hsnew file+26
@@ -0,0 +1,26 @@
1+{-# language DataKinds #-}
2+{-# language StandaloneKindSignatures #-}
3+{-# language TypeFamilyDependencies #-}
4+
5+module Rel8.Column.Null
6+ ( HNull
7+ )
8+where
9+
10+-- base
11+import Data.Kind ( Type )
12+import Prelude
13+
14+-- rel8
15+import qualified Rel8.Schema.Kind as K
16+import Rel8.Schema.Result ( Result )
17+import Rel8.Table.Null ( NullTable )
18+
19+
20+-- | Nest a 'Null' value within a 'Rel8able'. @HNull f a@ will produce a
21+-- 'NullTable' @a@ in the 'Expr' context, and a @'Maybe' a@ in the 'Result'
22+-- context.
23+type HNull :: K.Context -> Type -> Type
24+type family HNull context = maybe | maybe -> context where
25+ HNull Result = Maybe
26+ HNull context = NullTable context
src/Rel8/Expr.hs+1
@@ -1,5 +1,6 @@
1{-# language DataKinds #-}
1{-# language DataKinds #-}
2{-# language DerivingStrategies #-}
2{-# language DerivingStrategies #-}
3+{-# language FlexibleContexts #-}
3{-# language FlexibleInstances #-}
4{-# language FlexibleInstances #-}
4{-# language MultiParamTypeClasses #-}
5{-# language MultiParamTypeClasses #-}
5{-# language ScopedTypeVariables #-}
6{-# language ScopedTypeVariables #-}
src/Rel8/Expr/Aggregate.hs+25−64
@@ -10,6 +10,7 @@ module Rel8.Expr.Aggregate
10 , and, or
10 , and, or
11 , min, max
11 , min, max
12 , sum, sumWhere
12 , sum, sumWhere
13+ , avg
13 , stringAgg
14 , stringAgg
14 , groupByExpr
15 , groupByExpr
15 , listAggExpr, nonEmptyAggExpr
16 , listAggExpr, nonEmptyAggExpr
@@ -23,10 +24,12 @@ import Data.List.NonEmpty ( NonEmpty )
23import Prelude hiding ( and, max, min, null, or, sum )
24import Prelude hiding ( and, max, min, null, or, sum )
24
25
25-- opaleye
26-- opaleye
26−import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
27+import qualified Opaleye.Internal.Aggregate as Opaleye
28+import Opaleye.Internal.Column ( Field_( Column ) )
29+import qualified Opaleye.Aggregate as Opaleye
27
30
28-- rel8
31-- rel8
29−import Rel8.Aggregate ( Aggregate, Aggregator(..), unsafeMakeAggregate )
32+import Rel8.Aggregate ( Aggregate, unsafeMakeAggregate )
30import Rel8.Expr ( Expr )
33import Rel8.Expr ( Expr )
31import Rel8.Expr.Bool ( caseExpr )
34import Rel8.Expr.Bool ( caseExpr )
32import Rel8.Expr.Opaleye
35import Rel8.Expr.Opaleye
@@ -50,23 +53,13 @@ import Rel8.Type.Sum ( DBSum )
50
53
51-- | Count the occurances of a single column. Corresponds to @COUNT(a)@
54-- | Count the occurances of a single column. Corresponds to @COUNT(a)@
52count :: Expr a -> Aggregate Int64
55count :: Expr a -> Aggregate Int64
53−count = unsafeMakeAggregate toPrimExpr fromPrimExpr $
56+count = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.count
54− Just Aggregator
55− { operation = Opaleye.AggrCount
56− , ordering = []
57− , distinction = Opaleye.AggrAll
58− }
59−
60
57
61-- | Count the number of distinct occurances of a single column. Corresponds to
58-- | Count the number of distinct occurances of a single column. Corresponds to
62-- @COUNT(DISTINCT a)@
59-- @COUNT(DISTINCT a)@
63countDistinct :: Sql DBEq a => Expr a -> Aggregate Int64
60countDistinct :: Sql DBEq a => Expr a -> Aggregate Int64
64countDistinct = unsafeMakeAggregate toPrimExpr fromPrimExpr $
61countDistinct = unsafeMakeAggregate toPrimExpr fromPrimExpr $
65− Just Aggregator
62+ Opaleye.distinctAggregator Opaleye.count
66− { operation = Opaleye.AggrCount
67− , ordering = []
68− , distinction = Opaleye.AggrDistinct
69− }
70
63
71
64
72-- | Corresponds to @COUNT(*)@.
65-- | Corresponds to @COUNT(*)@.
@@ -81,56 +74,39 @@ countWhere condition = count (caseExpr [(condition, litExpr (Just True))] null)
81
74
82-- | Corresponds to @bool_and@.
75-- | Corresponds to @bool_and@.
83and :: Expr Bool -> Aggregate Bool
76and :: Expr Bool -> Aggregate Bool
84−and = unsafeMakeAggregate toPrimExpr fromPrimExpr $
77+and = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.boolAnd
85− Just Aggregator
86− { operation = Opaleye.AggrBoolAnd
87− , ordering = []
88− , distinction = Opaleye.AggrAll
89− }
90
78
91
79
92-- | Corresponds to @bool_or@.
80-- | Corresponds to @bool_or@.
93or :: Expr Bool -> Aggregate Bool
81or :: Expr Bool -> Aggregate Bool
94−or = unsafeMakeAggregate toPrimExpr fromPrimExpr $
82+or = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.boolOr
95− Just Aggregator
96− { operation = Opaleye.AggrBoolOr
97− , ordering = []
98− , distinction = Opaleye.AggrAll
99− }
100
83
101
84
102-- | Produce an aggregation for @Expr a@ using the @max@ function.
85-- | Produce an aggregation for @Expr a@ using the @max@ function.
103max :: Sql DBMax a => Expr a -> Aggregate a
86max :: Sql DBMax a => Expr a -> Aggregate a
104−max = unsafeMakeAggregate toPrimExpr fromPrimExpr $
87+max = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMax
105− Just Aggregator
106− { operation = Opaleye.AggrMax
107− , ordering = []
108− , distinction = Opaleye.AggrAll
109− }
110
88
111
89
112-- | Produce an aggregation for @Expr a@ using the @max@ function.
90-- | Produce an aggregation for @Expr a@ using the @max@ function.
113min :: Sql DBMin a => Expr a -> Aggregate a
91min :: Sql DBMin a => Expr a -> Aggregate a
114−min = unsafeMakeAggregate toPrimExpr fromPrimExpr $
92+min = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.unsafeMin
115− Just Aggregator
116− { operation = Opaleye.AggrMin
117− , ordering = []
118− , distinction = Opaleye.AggrAll
119− }
120
93
121-- | Corresponds to @sum@. Note that in SQL, @sum@ is type changing - for
94-- | Corresponds to @sum@. Note that in SQL, @sum@ is type changing - for
122-- example the @sum@ of @integer@ returns a @bigint@. Rel8 doesn't support
95-- example the @sum@ of @integer@ returns a @bigint@. Rel8 doesn't support
123−-- this, and will add explicit cast back to the original input type. This can
96+-- this, and will add explicit casts back to the original input type. This can
124-- lead to overflows, and if you anticipate very large sums, you should upcast
97-- lead to overflows, and if you anticipate very large sums, you should upcast
125-- your input.
98-- your input.
126sum :: Sql DBSum a => Expr a -> Aggregate a
99sum :: Sql DBSum a => Expr a -> Aggregate a
127−sum = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) $
100+sum = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) Opaleye.unsafeSum
128− Just Aggregator
101+
129− { operation = Opaleye.AggrSum
130− , ordering = []
131− , distinction = Opaleye.AggrAll
132− }
133
102
103+-- | Corresponds to @avg@. Note that in SQL, @avg@ is type changing - for
104+-- example, the @avg@ of @integer@ returns a @numeric@. Rel8 doesn't support
105+-- this, and will add explicit casts back to the original input type. If you
106+-- need a fractional result on an integral column, you should cast your input
107+-- to 'Double' or 'Data.Scientific.Scientific' before calling 'avg'.
108+avg :: Sql DBSum a => Expr a -> Aggregate a
109+avg = unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) Opaleye.unsafeAvg
134
110
135-- | Take the sum of all expressions that satisfy a predicate.
111-- | Take the sum of all expressions that satisfy a predicate.
136sumWhere :: (Sql DBNum a, Sql DBSum a)
112sumWhere :: (Sql DBNum a, Sql DBSum a)
@@ -142,17 +118,12 @@ sumWhere condition a = sum (caseExpr [(condition, a)] 0)
142stringAgg :: Sql DBString a
118stringAgg :: Sql DBString a
143 => Expr db -> Expr a -> Aggregate a
119 => Expr db -> Expr a -> Aggregate a
144stringAgg delimiter =
120stringAgg delimiter =
145− unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) $
121+ unsafeMakeAggregate toPrimExpr (castExpr . fromPrimExpr) (Opaleye.stringAgg (Column (toPrimExpr delimiter)))
146− Just Aggregator
147− { operation = Opaleye.AggrStringAggr (toPrimExpr delimiter)
148− , ordering = []
149− , distinction = Opaleye.AggrAll
150− }
151
122
152
123
153-- | Aggregate a value by grouping by it.
124-- | Aggregate a value by grouping by it.
154groupByExpr :: Sql DBEq a => Expr a -> Aggregate a
125groupByExpr :: Sql DBEq a => Expr a -> Aggregate a
155−groupByExpr = unsafeMakeAggregate toPrimExpr fromPrimExpr Nothing
126+groupByExpr = unsafeMakeAggregate toPrimExpr fromPrimExpr Opaleye.groupBy
156
127
157
128
158-- | Collect expressions values as a list.
129-- | Collect expressions values as a list.
@@ -167,23 +138,13 @@ nonEmptyAggExpr = snonEmptyAggExpr typeInformation
167
138
168slistAggExpr :: ()
139slistAggExpr :: ()
169 => TypeInformation (Unnullify a) -> Expr a -> Aggregate [a]
140 => TypeInformation (Unnullify a) -> Expr a -> Aggregate [a]
170−slistAggExpr info = unsafeMakeAggregate to fromPrimExpr $ Just
141+slistAggExpr info = unsafeMakeAggregate to fromPrimExpr Opaleye.arrayAgg
171− Aggregator
172− { operation = Opaleye.AggrArr
173− , ordering = []
174− , distinction = Opaleye.AggrAll
175− }
176 where
142 where
177 to = encodeArrayElement info . toPrimExpr
143 to = encodeArrayElement info . toPrimExpr
178
144
179
145
180snonEmptyAggExpr :: ()
146snonEmptyAggExpr :: ()
181 => TypeInformation (Unnullify a) -> Expr a -> Aggregate (NonEmpty a)
147 => TypeInformation (Unnullify a) -> Expr a -> Aggregate (NonEmpty a)
182−snonEmptyAggExpr info = unsafeMakeAggregate to fromPrimExpr $ Just
148+snonEmptyAggExpr info = unsafeMakeAggregate to fromPrimExpr Opaleye.arrayAgg
183− Aggregator
184− { operation = Opaleye.AggrArr
185− , ordering = []
186− , distinction = Opaleye.AggrAll
187− }
188 where
149 where
189 to = encodeArrayElement info . toPrimExpr
150 to = encodeArrayElement info . toPrimExpr
src/Rel8/Expr/Num.hs+39−6
@@ -4,18 +4,24 @@
4{-# options_ghc -fno-warn-redundant-constraints #-}
4{-# options_ghc -fno-warn-redundant-constraints #-}
5
5
6module Rel8.Expr.Num
6module Rel8.Expr.Num
7+ ( fromIntegral
8+ , realToFrac
9+ , div, mod, divMod
10+ , quot, rem, quotRem
7− ( fromIntegral, realToFrac, div, mod, ceiling, floor, round, truncate
11+ , ceiling, floor, round, truncate
8 )
12 )
9where
13where
10
14
11-- base
15-- base
12−import Prelude ()
16+import Prelude ( (+), (-), fst, negate, signum, snd )
13
17
14-- rel
18-- rel
15import Rel8.Expr ( Expr( Expr ) )
19import Rel8.Expr ( Expr( Expr ) )
20+import Rel8.Expr.Eq ( (==.) )
16import Rel8.Expr.Function ( function )
21import Rel8.Expr.Function ( function )
17import Rel8.Expr.Opaleye ( castExpr )
22import Rel8.Expr.Opaleye ( castExpr )
18import Rel8.Schema.Null ( Homonullable, Sql )
23import Rel8.Schema.Null ( Homonullable, Sql )
24+import Rel8.Table.Bool ( bool )
19import Rel8.Type.Num ( DBFractional, DBIntegral, DBNum )
25import Rel8.Type.Num ( DBFractional, DBIntegral, DBNum )
20
26
21
27
@@ -42,14 +48,41 @@ ceiling :: (Sql DBFractional a, Sql DBIntegral b, Homonullable a b)
42ceiling = function "ceiling"
48ceiling = function "ceiling"
43
49
44
50
45−-- | Perform integral division. Corresponds to the @div()@ function.
51+-- | Emulates the behaviour of the Haskell function 'Prelude.div' in
52+-- PostgreSQL.
46div :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
53div :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
47−div = function "div"
54+div n d = fst (divMod n d)
48
55
49
56
50−-- | Corresponds to the @mod()@ function.
57+-- | Emulates the behaviour of the Haskell function 'Prelude.mod' in
58+-- PostgreSQL.
51mod :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
59mod :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
52−mod = function "mod"
60+mod n d = snd (divMod n d)
61+
62+
63+-- | Simultaneous 'div' and 'mod'.
64+divMod :: Sql DBIntegral a => Expr a -> Expr a -> (Expr a, Expr a)
65+divMod n d = bool qr (q - 1, r + d) (signum r ==. negate (signum d))
66+ where
67+ qr@(q, r) = quotRem n d
68+
69+
70+-- | Perform integral division. Corresponds to the @div()@ function in
71+-- PostgreSQL, which behaves like Haskell's 'Prelude.quot' rather than
72+-- 'Prelude.div'.
73+quot :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
74+quot = function "div"
75+
76+
77+-- | Corresponds to the @mod()@ function in PostgreSQL, which behaves like
78+-- Haskell's 'Prelude.rem' rather than 'Prelude.mod'.
79+rem :: Sql DBIntegral a => Expr a -> Expr a -> Expr a
80+rem = function "mod"
81+
82+
83+-- | Simultaneous 'quot' and 'rem'.
84+quotRem :: Sql DBIntegral a => Expr a -> Expr a -> (Expr a, Expr a)
85+quotRem n d = (quot n d, rem n d)
53
86
54
87
55-- | Round a 'DFractional' to a 'DBIntegral' by rounding to the nearest smaller
88-- | Round a 'DFractional' to a 'DBIntegral' by rounding to the nearest smaller
src/Rel8/Expr/Opaleye.hs+12−3
@@ -10,7 +10,7 @@ module Rel8.Expr.Opaleye
10 , scastExpr, sunsafeCastExpr
10 , scastExpr, sunsafeCastExpr
11 , unsafeLiteral
11 , unsafeLiteral
12 , fromPrimExpr, toPrimExpr, mapPrimExpr, zipPrimExprsWith, traversePrimExpr
12 , fromPrimExpr, toPrimExpr, mapPrimExpr, zipPrimExprsWith, traversePrimExpr
13− , toColumn, fromColumn
13+ , toColumn, fromColumn, traverseFieldP
14 )
14 )
15where
15where
16
16
@@ -27,6 +27,9 @@ import Rel8.Schema.Null ( Unnullify, Sql )
27import Rel8.Type ( DBType, typeInformation )
27import Rel8.Type ( DBType, typeInformation )
28import Rel8.Type.Information ( TypeInformation(..) )
28import Rel8.Type.Information ( TypeInformation(..) )
29
29
30+-- profunctors
31+import Data.Profunctor ( Profunctor, dimap )
32+
30
33
31castExpr :: Sql DBType a => Expr a -> Expr a
34castExpr :: Sql DBType a => Expr a -> Expr a
32castExpr = scastExpr typeInformation
35castExpr = scastExpr typeInformation
@@ -80,9 +83,15 @@ traversePrimExpr :: Functor f
80traversePrimExpr f = fmap fromPrimExpr . f . toPrimExpr
83traversePrimExpr f = fmap fromPrimExpr . f . toPrimExpr
81
84
82
85
86+traverseFieldP :: Profunctor p
87+ => p (Opaleye.Field_ n x) (Opaleye.Field_ m y)
88+ -> p (Expr a) (Expr b)
89+traverseFieldP = dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn)
90+
91+
83−toColumn :: Opaleye.PrimExpr -> Opaleye.Column b
92+toColumn :: Opaleye.PrimExpr -> Opaleye.Field_ n b
84toColumn = Opaleye.Column
93toColumn = Opaleye.Column
85
94
86
95
87−fromColumn :: Opaleye.Column b -> Opaleye.PrimExpr
96+fromColumn :: Opaleye.Field_ n b -> Opaleye.PrimExpr
88fromColumn (Opaleye.Column a) = a
97fromColumn (Opaleye.Column a) = a
src/Rel8/Expr/Sequence.hs+2−1
@@ -11,6 +11,7 @@ import Prelude
11import Rel8.Expr ( Expr )
11import Rel8.Expr ( Expr )
12import Rel8.Expr.Function ( function )
12import Rel8.Expr.Function ( function )
13import Rel8.Expr.Serialize ( litExpr )
13import Rel8.Expr.Serialize ( litExpr )
14+import Rel8.Expr.Text ( quoteIdent )
14
15
15-- text
16-- text
16import Data.Text ( pack )
17import Data.Text ( pack )
@@ -18,4 +19,4 @@ import Data.Text ( pack )
18
19
19-- | See https://www.postgresql.org/docs/current/functions-sequence.html
20-- | See https://www.postgresql.org/docs/current/functions-sequence.html
20nextval :: String -> Expr Int64
21nextval :: String -> Expr Int64
21−nextval = function "nextval" . litExpr . pack
22+nextval = function "nextval" . quoteIdent . litExpr . pack
src/Rel8/Expr/Text.hs+23−2
@@ -17,6 +17,9 @@ module Rel8.Expr.Text
17 , pgClientEncoding, quoteIdent, quoteLiteral, quoteNullable, regexpReplace
17 , pgClientEncoding, quoteIdent, quoteLiteral, quoteNullable, regexpReplace
18 , regexpSplitToArray, repeat, replace, reverse, right, rpad, rtrim
18 , regexpSplitToArray, repeat, replace, reverse, right, rpad, rtrim
19 , splitPart, strpos, substr, translate
19 , splitPart, strpos, substr, translate
20+
21+ -- * @LIKE@ and @ILIKE@
22+ , like, ilike
20 )
23 )
21where
24where
22
25
@@ -24,7 +27,7 @@ where
24import Data.Bool ( Bool )
27import Data.Bool ( Bool )
25import Data.Int ( Int32 )
28import Data.Int ( Int32 )
26import Data.Maybe ( Maybe( Nothing, Just ) )
29import Data.Maybe ( Maybe( Nothing, Just ) )
27−import Prelude ()
30+import Prelude ( flip )
28
31
29-- bytestring
32-- bytestring
30import Data.ByteString ( ByteString )
33import Data.ByteString ( ByteString )
@@ -49,7 +52,7 @@ infixr 6 ++.
49
52
50
53
51-- | Matches regular expression, case sensitive
54-- | Matches regular expression, case sensitive
52−--
55+--
53-- Corresponds to the @~.@ operator.
56-- Corresponds to the @~.@ operator.
54(~.) :: Expr Text -> Expr Text -> Expr Bool
57(~.) :: Expr Text -> Expr Text -> Expr Bool
55(~.) = binaryOperator "~."
58(~.) = binaryOperator "~."
@@ -273,3 +276,21 @@ substr a b Nothing = function "substr" a b
273-- | Corresponds to the @translate@ function.
276-- | Corresponds to the @translate@ function.
274translate :: Expr Text -> Expr Text -> Expr Text -> Expr Text
277translate :: Expr Text -> Expr Text -> Expr Text -> Expr Text
275translate = function "translate"
278translate = function "translate"
279+
280+
281+-- | @like x y@ corresponds to the expression @y LIKE x@.
282+--
283+-- Note that the arguments to @like@ are swapped. This is to aid currying, so
284+-- you can write expressions like
285+-- @filter (like "Rel%" . packageName) =<< each haskellPackages@
286+like :: Expr Text -> Expr Text -> Expr Bool
287+like = flip (binaryOperator "LIKE")
288+
289+
290+-- | @ilike x y@ corresponds to the expression @y ILIKE x@.
291+--
292+-- Note that the arguments to @ilike@ are swapped. This is to aid currying, so
293+-- you can write expressions like
294+-- @filter (ilike "Rel%" . packageName) =<< each haskellPackages@
295+ilike :: Expr Text -> Expr Text -> Expr Bool
296+ilike = flip (binaryOperator "ILIKE")
src/Rel8/Expr/Window.hsnew file+121
@@ -0,0 +1,121 @@
1+module Rel8.Expr.Window
2+ ( cumulative
3+ , rowNumber
4+ , rank
5+ , denseRank
6+ , percentRank
7+ , cumeDist
8+ , ntile
9+ , lag
10+ , lead
11+ , firstValue
12+ , lastValue
13+ , nthValue
14+ )
15+where
16+
17+-- base
18+import Data.Int ( Int32, Int64 )
19+import Prelude
20+
21+-- opaleye
22+import qualified Opaleye.Internal.Aggregate as Opaleye
23+import qualified Opaleye.Internal.PackMap as Opaleye
24+import qualified Opaleye.Internal.Window as Opaleye
25+import qualified Opaleye.Window as Opaleye
26+
27+-- profunctors
28+import Data.Profunctor (dimap)
29+
30+-- rel8
31+import Rel8.Aggregate ( Aggregate( Aggregate ) )
32+import Rel8.Expr ( Expr )
33+import Rel8.Expr.Opaleye ( fromColumn, fromPrimExpr, toColumn, toPrimExpr )
34+import Rel8.Schema.Null ( Nullify )
35+import Rel8.Window ( Window( Window ) )
36+
37+
38+cumulative :: (a -> Aggregate b) -> Window a (Expr b)
39+cumulative f =
40+ fromWindowFunction $ Opaleye.aggregatorWindowFunction (fromAggregate f) id
41+
42+
43+-- | [@row_number()@](https://www.postgresql.org/docs/current/functions-window.html)
44+rowNumber :: Window a (Expr Int64)
45+rowNumber = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.rowNumber
46+
47+
48+-- | [@rank()@](https://www.postgresql.org/docs/current/functions-window.html)
49+rank :: Window a (Expr Int64)
50+rank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.rank
51+
52+
53+-- | [@dense_rank()@](https://www.postgresql.org/docs/current/functions-window.html)
54+denseRank :: Window a (Expr Int64)
55+denseRank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.denseRank
56+
57+
58+-- | [@percent_rank()@](https://www.postgresql.org/docs/current/functions-window.html)
59+percentRank :: Window a (Expr Double)
60+percentRank = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.percentRank
61+
62+
63+-- | [@cume_dist()@](https://www.postgresql.org/docs/current/functions-window.html)
64+cumeDist :: Window a (Expr Double)
65+cumeDist = fromWindowFunction $ fromPrimExpr . fromColumn <$> Opaleye.cumeDist
66+
67+
68+-- | [@ntile(num_buckets)@](https://www.postgresql.org/docs/current/functions-window.html)
69+ntile :: Expr Int32 -> Window a (Expr Int32)
70+ntile buckets = fromWindowFunction $ fromPrimExpr . fromColumn <$>
71+ Opaleye.ntile (toColumn (toPrimExpr buckets))
72+
73+
74+-- | [@lag(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html)
75+lag :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a)
76+lag offset def =
77+ fromWindowFunction $
78+ dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $
79+ Opaleye.lag (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def))
80+
81+
82+-- | [@lead(value, offset, default)@](https://www.postgresql.org/docs/current/functions-window.html)
83+lead :: Expr Int32 -> Expr a -> Window (Expr a) (Expr a)
84+lead offset def =
85+ fromWindowFunction $
86+ dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $
87+ Opaleye.lead (toColumn (toPrimExpr offset)) (toColumn (toPrimExpr def))
88+
89+
90+-- | [@first_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)
91+firstValue :: Window (Expr a) (Expr a)
92+firstValue =
93+ fromWindowFunction $
94+ dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn)
95+ Opaleye.firstValue
96+
97+
98+-- | [@last_value(value)@](https://www.postgresql.org/docs/current/functions-window.html)
99+lastValue :: Window (Expr a) (Expr a)
100+lastValue =
101+ fromWindowFunction $
102+ dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn)
103+ Opaleye.lastValue
104+
105+
106+-- | [@nth_value(value, n)@](https://www.postgresql.org/docs/current/functions-window.html)
107+nthValue :: Expr Int32 -> Window (Expr a) (Expr (Nullify a))
108+nthValue n =
109+ fromWindowFunction $
110+ dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $
111+ Opaleye.nthValue (toColumn (toPrimExpr n))
112+
113+
114+fromAggregate :: (a -> Aggregate b) -> Opaleye.Aggregator a (Expr b)
115+fromAggregate f = Opaleye.Aggregator $ Opaleye.PackMap $ \w a -> case f a of
116+ Aggregate (Opaleye.Aggregator (Opaleye.PackMap x)) -> x w ()
117+
118+
119+fromWindowFunction :: Opaleye.WindowFunction a b -> Window a b
120+fromWindowFunction (Opaleye.WindowFunction (Opaleye.PackMap w)) =
121+ Window $ Opaleye.Windows $ Opaleye.PackMap $ \f -> w $ \o -> f (o, mempty)
src/Rel8/Generic/Construction/ADT.hs+1
@@ -1,6 +1,7 @@
1{-# language AllowAmbiguousTypes #-}
1{-# language AllowAmbiguousTypes #-}
2{-# language BlockArguments #-}
2{-# language BlockArguments #-}
3{-# language DataKinds #-}
3{-# language DataKinds #-}
4+{-# language FlexibleContexts #-}
4{-# language FlexibleInstances #-}
5{-# language FlexibleInstances #-}
5{-# language MultiParamTypeClasses #-}
6{-# language MultiParamTypeClasses #-}
6{-# language RankNTypes #-}
7{-# language RankNTypes #-}
src/Rel8/Generic/Record.hs+2
@@ -1,5 +1,6 @@
1{-# language AllowAmbiguousTypes #-}
1{-# language AllowAmbiguousTypes #-}
2{-# language DataKinds #-}
2{-# language DataKinds #-}
3+{-# language FlexibleContexts #-}
3{-# language FlexibleInstances #-}
4{-# language FlexibleInstances #-}
4{-# language MultiParamTypeClasses #-}
5{-# language MultiParamTypeClasses #-}
5{-# language PolyKinds #-}
6{-# language PolyKinds #-}
@@ -141,6 +142,7 @@ instance {-# OVERLAPPABLE #-} Snd (Count n rep) ~ rep => Countable n rep where
141 uncount = id
142 uncount = id
142
143
143
144
145+type Record :: Type -> Type
144newtype Record a = Record
146newtype Record a = Record
145 { unrecord :: a
147 { unrecord :: a
146 }
148 }
src/Rel8/Generic/Rel8able.hs+6−1
@@ -63,6 +63,11 @@ type KRel8able = K.Rel8able
63-- This is almost 'Data.Type.Equality.==', but we add an extra case.
63-- This is almost 'Data.Type.Equality.==', but we add an extra case.
64type (==) :: k -> k -> Bool
64type (==) :: k -> k -> Bool
65type family a == b where
65type family a == b where
66+ -- This extra case is needed to solve the equation "a == a" (where a is a
67+ -- KRel8able), which occurs when we have polymorphic Rel8ables
68+ -- (e.g., newtype T t f = T { x :: t a })
69+ (a :: KRel8able) == (a :: KRel8able) = 'True
70+
66 -- This extra case is needed to solve the equation "a == Identity a",
71 -- This extra case is needed to solve the equation "a == Identity a",
67 -- which occurs when we have polymorphic Rel8ables
72 -- which occurs when we have polymorphic Rel8ables
68 -- (e.g., newtype T a f = T { x :: Column f a })
73 -- (e.g., newtype T a f = T { x :: Column f a })
@@ -141,7 +146,7 @@ type instance Eval (TSerialize expr a) =
141-- [Supplying @Rel8able@ instances]
146-- [Supplying @Rel8able@ instances]
142--
147--
143-- This type class should be derived generically for all table types in your
148-- This type class should be derived generically for all table types in your
144−-- project. To do this, enable the @DeriveAnyType@ and @DeriveGeneric@ language
149+-- project. To do this, enable the @DeriveAnyClass@ and @DeriveGeneric@ language
145-- extensions:
150-- extensions:
146--
151--
147-- @
152-- @
src/Rel8/Generic/Table/ADT.hs+1
@@ -1,5 +1,6 @@
1{-# language AllowAmbiguousTypes #-}
1{-# language AllowAmbiguousTypes #-}
2{-# language DataKinds #-}
2{-# language DataKinds #-}
3+{-# language FlexibleContexts #-}
3{-# language FlexibleInstances #-}
4{-# language FlexibleInstances #-}
4{-# language LambdaCase #-}
5{-# language LambdaCase #-}
5{-# language MultiParamTypeClasses #-}
6{-# language MultiParamTypeClasses #-}
src/Rel8/Order.hs+1−8
@@ -4,7 +4,6 @@
4
4
5module Rel8.Order
5module Rel8.Order
6 ( Order(..)
6 ( Order(..)
7− , toOrderExprs
8 )
7 )
9where
8where
10
9
@@ -17,8 +16,7 @@ import Prelude
17import Data.Functor.Contravariant.Divisible ( Decidable, Divisible )
16import Data.Functor.Contravariant.Divisible ( Decidable, Divisible )
18
17
19-- opaleye
18-- opaleye
20−import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
19+import qualified Opaleye.Order as Opaleye
21−import qualified Opaleye.Internal.Order as Opaleye
22
20
23
21
24-- | An ordering expression for @a@. Primitive orderings are defined with
22-- | An ordering expression for @a@. Primitive orderings are defined with
@@ -30,8 +28,3 @@ import qualified Opaleye.Internal.Order as Opaleye
30type Order :: Type -> Type
28type Order :: Type -> Type
31newtype Order a = Order (Opaleye.Order a)
29newtype Order a = Order (Opaleye.Order a)
32 deriving newtype (Contravariant, Divisible, Decidable, Semigroup, Monoid)
30 deriving newtype (Contravariant, Divisible, Decidable, Semigroup, Monoid)
33−
34−
35−toOrderExprs :: Order a -> a -> [Opaleye.OrderExpr]
36−toOrderExprs (Order (Opaleye.Order order)) a =
37− uncurry Opaleye.OrderExpr <$> order a
src/Rel8/Query.hs+28−9
@@ -1,4 +1,6 @@
1+{-# language FlexibleContexts #-}
1{-# language StandaloneKindSignatures #-}
2{-# language StandaloneKindSignatures #-}
3+{-# language UndecidableInstances #-}
2
4
3module Rel8.Query
5module Rel8.Query
4 ( Query( Query )
6 ( Query( Query )
@@ -15,18 +17,21 @@ import Prelude
15-- opaleye
17-- opaleye
16import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
18import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
17import qualified Opaleye.Internal.PackMap as Opaleye
19import qualified Opaleye.Internal.PackMap as Opaleye
18−import qualified Opaleye.Internal.PrimQuery as Opaleye
20+import qualified Opaleye.Internal.PrimQuery as Opaleye hiding (lateral)
19import qualified Opaleye.Internal.QueryArr as Opaleye
21import qualified Opaleye.Internal.QueryArr as Opaleye
20import qualified Opaleye.Internal.Tag as Opaleye
22import qualified Opaleye.Internal.Tag as Opaleye
21
23
22-- rel8
24-- rel8
25+import Rel8.Expr ( Expr )
23import Rel8.Query.Set ( unionAll )
26import Rel8.Query.Set ( unionAll )
24import Rel8.Query.Opaleye ( fromOpaleye )
27import Rel8.Query.Opaleye ( fromOpaleye )
25import Rel8.Query.Values ( values )
28import Rel8.Query.Values ( values )
29+import Rel8.Table ( Table, fromColumns, toColumns )
26import Rel8.Table.Alternative
30import Rel8.Table.Alternative
27 ( AltTable, (<|>:)
31 ( AltTable, (<|>:)
28 , AlternativeTable, emptyTable
32 , AlternativeTable, emptyTable
29 )
33 )
34+import Rel8.Table.Projection ( Projectable, apply, project )
30
35
31-- semigroupoids
36-- semigroupoids
32import Data.Functor.Apply ( Apply, (<.>) )
37import Data.Functor.Apply ( Apply, (<.>) )
@@ -151,6 +156,10 @@ newtype Query a =
151 )
156 )
152
157
153
158
159+instance Projectable Query where
160+ project f = fmap (fromColumns . apply f . toColumns)
161+
162+
154instance Functor Query where
163instance Functor Query where
155 fmap f (Query a) = Query (fmap (fmap (fmap f)) a)
164 fmap f (Query a) = Query (fmap (fmap (fmap f)) a)
156
165
@@ -169,14 +178,14 @@ instance Bind Query where
169
178
170
179
171instance Monad Query where
180instance Monad Query where
172− Query q >>= f = Query $ \dummies -> Opaleye.QueryArr $ \(_, tag) ->
181+ Query q >>= f = Query $ \dummies -> Opaleye.stateQueryArr $ \_ tag ->
173 let
182 let
174− Opaleye.QueryArr qa = q dummies
183+ qa = q dummies
175− ((m, a), query, tag') = qa ((), tag)
184+ ((m, a), query, tag') = Opaleye.runStateQueryArr qa () tag
176 Query q' = f a
185 Query q' = f a
177 (dummies', query', tag'') =
186 (dummies', query', tag'') =
178 ( dummy : dummies
187 ( dummy : dummies
179− , \lateral -> Opaleye.Rebind True bindings . query lateral
188+ , query <> Opaleye.aRebind bindings
180 , Opaleye.next tag'
189 , Opaleye.next tag'
181 )
190 )
182 where
191 where
@@ -184,11 +193,11 @@ instance Monad Query where
184 where
193 where
185 random = Opaleye.FunExpr "random" []
194 random = Opaleye.FunExpr "random" []
186 name = Opaleye.extractAttr "dummy" tag'
195 name = Opaleye.extractAttr "dummy" tag'
187− Opaleye.QueryArr qa' = Opaleye.lateral $ \_ -> q' dummies'
196+ qa' = Opaleye.lateral $ \_ -> q' dummies'
188− ((m'@(Any needsDummies), b), query'', tag''') = qa' ((), tag'')
197+ ((m'@(Any needsDummies), b), query'', tag''') = Opaleye.runStateQueryArr qa' () tag''
189 query'''
198 query'''
190− | needsDummies = \lateral -> query'' lateral . query' lateral
199+ | needsDummies = query' <> query''
191− | otherwise = \lateral -> query'' lateral . query lateral
200+ | otherwise = query <> query''
192 m'' = m <> m'
201 m'' = m <> m'
193 in
202 in
194 ((m'', b), query''', tag''')
203 ((m'', b), query''', tag''')
@@ -202,3 +211,13 @@ instance AltTable Query where
202-- | 'emptyTable' = 'values' @[]@.
211-- | 'emptyTable' = 'values' @[]@.
203instance AlternativeTable Query where
212instance AlternativeTable Query where
204 emptyTable = values []
213 emptyTable = values []
214+
215+
216+-- | '<>' = 'unionAll'.
217+instance Table Expr a => Semigroup (Query a) where
218+ (<>) = (<|>:)
219+
220+
221+-- | 'mempty' = @'values' []@.
222+instance Table Expr a => Monoid (Query a) where
223+ mempty = emptyTable
src/Rel8/Query/Aggregate.hs+19
@@ -1,13 +1,17 @@
1{-# language FlexibleContexts #-}
1{-# language FlexibleContexts #-}
2{-# language MonoLocalBinds #-}
2{-# language MonoLocalBinds #-}
3+{-# language ScopedTypeVariables #-}
4+{-# language TypeApplications #-}
3
5
4module Rel8.Query.Aggregate
6module Rel8.Query.Aggregate
5 ( aggregate
7 ( aggregate
6 , countRows
8 , countRows
9+ , mode
7 )
10 )
8where
11where
9
12
10-- base
13-- base
14+import Data.Functor.Contravariant ( (>$<) )
11import Data.Int ( Int64 )
15import Data.Int ( Int64 )
12import Prelude
16import Prelude
13
17
@@ -18,9 +22,16 @@ import qualified Opaleye.Aggregate as Opaleye
18import Rel8.Aggregate ( Aggregates )
22import Rel8.Aggregate ( Aggregates )
19import Rel8.Expr ( Expr )
23import Rel8.Expr ( Expr )
20import Rel8.Expr.Aggregate ( countStar )
24import Rel8.Expr.Aggregate ( countStar )
25+import Rel8.Expr.Order ( desc )
21import Rel8.Query ( Query )
26import Rel8.Query ( Query )
27+import Rel8.Query.Limit ( limit )
22import Rel8.Query.Maybe ( optional )
28import Rel8.Query.Maybe ( optional )
23import Rel8.Query.Opaleye ( mapOpaleye )
29import Rel8.Query.Opaleye ( mapOpaleye )
30+import Rel8.Query.Order ( orderBy )
31+import Rel8.Table ( toColumns )
32+import Rel8.Table.Aggregate ( hgroupBy )
33+import Rel8.Table.Cols ( Cols( Cols ), fromCols )
34+import Rel8.Table.Eq ( EqTable, eqTable )
24import Rel8.Table.Opaleye ( aggregator )
35import Rel8.Table.Opaleye ( aggregator )
25import Rel8.Table.Maybe ( maybeTable )
36import Rel8.Table.Maybe ( maybeTable )
26
37
@@ -35,3 +46,11 @@ aggregate = mapOpaleye (Opaleye.aggregate aggregator)
35-- will return @0@.
46-- will return @0@.
36countRows :: Query a -> Query (Expr Int64)
47countRows :: Query a -> Query (Expr Int64)
37countRows = fmap (maybeTable 0 id) . optional . aggregate . fmap (const countStar)
48countRows = fmap (maybeTable 0 id) . optional . aggregate . fmap (const countStar)
49+
50+
51+-- | Return the most common row in a query.
52+mode :: forall a. EqTable a => Query a -> Query a
53+mode rows = limit 1 $ fmap (fromCols . snd) $ orderBy (fst >$< desc) $ do
54+ aggregate $ do
55+ row <- toColumns <$> rows
56+ pure (countStar, Cols $ hgroupBy (eqTable @a) row)
src/Rel8/Query/Distinct.hs+4−9
@@ -1,5 +1,3 @@
1−{-# options_ghc -fno-warn-redundant-constraints #-}
2−
3module Rel8.Query.Distinct
1module Rel8.Query.Distinct
4 ( distinct
2 ( distinct
5 , distinctOn
3 , distinctOn
@@ -8,12 +6,11 @@ module Rel8.Query.Distinct
8where
6where
9
7
10-- base
8-- base
11−import Prelude
9+import Prelude ()
12
10
13-- opaleye
11-- opaleye
14import qualified Opaleye.Distinct as Opaleye
12import qualified Opaleye.Distinct as Opaleye
15−import qualified Opaleye.Internal.Order as Opaleye
13+import qualified Opaleye.Order as Opaleye
16−import qualified Opaleye.Internal.QueryArr as Opaleye
17
14
18-- rel8
15-- rel8
19import Rel8.Order ( Order( Order ) )
16import Rel8.Order ( Order( Order ) )
@@ -33,13 +30,11 @@ distinct = mapOpaleye (Opaleye.distinctExplicit distinctspec)
33-- to a projection. If multiple rows have the same projection, it is
30-- to a projection. If multiple rows have the same projection, it is
34-- unspecified which row will be returned. If this matters, use 'distinctOnBy'.
31-- unspecified which row will be returned. If this matters, use 'distinctOnBy'.
35distinctOn :: EqTable b => (a -> b) -> Query a -> Query a
32distinctOn :: EqTable b => (a -> b) -> Query a -> Query a
36−distinctOn proj =
37− mapOpaleye (\q -> Opaleye.productQueryArr (Opaleye.distinctOn unpackspec proj . Opaleye.runSimpleQueryArr q))
33+distinctOn proj = mapOpaleye (Opaleye.distinctOnExplicit unpackspec proj)
38
34
39
35
40-- | Select all distinct rows from a query, where rows are equivalent according
36-- | Select all distinct rows from a query, where rows are equivalent according
41-- to a projection. If there are multiple rows with the same projection, the
37-- to a projection. If there are multiple rows with the same projection, the
42-- first row according to the specified 'Order' will be returned.
38-- first row according to the specified 'Order' will be returned.
43distinctOnBy :: EqTable b => (a -> b) -> Order a -> Query a -> Query a
39distinctOnBy :: EqTable b => (a -> b) -> Order a -> Query a -> Query a
44−distinctOnBy proj (Order order) =
40+distinctOnBy proj (Order order) = mapOpaleye (Opaleye.distinctOnByExplicit unpackspec proj order)
45− mapOpaleye (\q -> Opaleye.productQueryArr (Opaleye.distinctOnBy unpackspec proj order . Opaleye.runSimpleQueryArr q))
src/Rel8/Query/Evaluate.hs+3−22
@@ -3,7 +3,6 @@
3
3
4module Rel8.Query.Evaluate
4module Rel8.Query.Evaluate
5 ( evaluate
5 ( evaluate
6− , rebind
7 )
6 )
8where
7where
9
8
@@ -16,28 +15,23 @@ import Prelude hiding ( undefined )
16
15
17-- opaleye
16-- opaleye
18import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
17import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
19−import qualified Opaleye.Internal.PackMap as Opaleye
20−import qualified Opaleye.Internal.PrimQuery as Opaleye
21−import qualified Opaleye.Internal.QueryArr as Opaleye
22−import qualified Opaleye.Internal.Tag as Opaleye
23−import qualified Opaleye.Internal.Unpackspec as Opaleye
24
18
25-- rel8
19-- rel8
26import Rel8.Expr ( Expr )
20import Rel8.Expr ( Expr )
27import Rel8.Expr.Bool ( (&&.) )
21import Rel8.Expr.Bool ( (&&.) )
28import Rel8.Expr.Opaleye ( fromPrimExpr )
22import Rel8.Expr.Opaleye ( fromPrimExpr )
29import Rel8.Query ( Query( Query ) )
23import Rel8.Query ( Query( Query ) )
24+import Rel8.Query.Rebind ( rebind )
30import Rel8.Table ( Table )
25import Rel8.Table ( Table )
31import Rel8.Table.Bool ( case_ )
26import Rel8.Table.Bool ( case_ )
32−import Rel8.Table.Opaleye ( unpackspec )
27+import Rel8.Table.Undefined ( undefined )
33−import Rel8.Table.Undefined
34
28
35
29
36-- | 'evaluate' takes expressions that could potentially have side effects and
30-- | 'evaluate' takes expressions that could potentially have side effects and
37-- \"runs\" them in the 'Query' monad. The returned expressions have no side
31-- \"runs\" them in the 'Query' monad. The returned expressions have no side
38-- effects and can safely be reused.
32-- effects and can safely be reused.
39evaluate :: Table Expr a => a -> Query a
33evaluate :: Table Expr a => a -> Query a
40−evaluate = laterally >=> rebind
34+evaluate = laterally >=> rebind "eval"
41
35
42
36
43laterally :: Table Expr a => a -> Query a
37laterally :: Table Expr a => a -> Query a
@@ -51,18 +45,5 @@ laterally a = Query $ \bindings -> pure $ (Any True,) $
51 go = fromPrimExpr . Opaleye.UnExpr Opaleye.OpIsNotNull
45 go = fromPrimExpr . Opaleye.UnExpr Opaleye.OpIsNotNull
52
46
53
47
54−-- | 'rebind' takes some expressions, and binds each of them to a new
55−-- variable in the SQL. The @a@ returned consists only of these
56−-- variables. It's essentially a @let@ binding for Postgres expressions.
57−rebind :: Table Expr a => a -> Query a
58−rebind a = Query $ \_ -> Opaleye.QueryArr $ \(_, tag) ->
59− let
60− tag' = Opaleye.next tag
61− (a', bindings) = Opaleye.run $
62− Opaleye.runUnpackspec unpackspec (Opaleye.extractAttr "eval" tag') a
63− in
64− ((mempty, a'), \_ -> Opaleye.Rebind True bindings, tag')
65−
66−
67foldl1' :: (a -> a -> a) -> NonEmpty a -> a
48foldl1' :: (a -> a -> a) -> NonEmpty a -> a
68foldl1' f (a :| as) = foldl' f a as
49foldl1' f (a :| as) = foldl' f a as
src/Rel8/Query/Exists.hs+1−1
@@ -12,7 +12,7 @@ import Prelude hiding ( filter )
12
12
13-- opaleye
13-- opaleye
14import qualified Opaleye.Exists as Opaleye
14import qualified Opaleye.Exists as Opaleye
15−import qualified Opaleye.Operators as Opaleye hiding ( exists )
15+import qualified Opaleye.Operators as Opaleye
16
16
17-- rel8
17-- rel8
18import Rel8.Expr ( Expr )
18import Rel8.Expr ( Expr )
src/Rel8/Query/Indexed.hsnew file+21
@@ -0,0 +1,21 @@
1+module Rel8.Query.Indexed
2+ ( indexed
3+ )
4+where
5+
6+-- base
7+import Control.Applicative ( liftA2 )
8+import Data.Int ( Int64 )
9+import Prelude
10+
11+-- rel8
12+import Rel8.Expr ( Expr )
13+import Rel8.Expr.Window ( rowNumber )
14+import Rel8.Query ( Query )
15+import Rel8.Query.Window ( window )
16+import Rel8.Table.Window ( currentRow )
17+
18+
19+-- | Pair each row of a query with its index within the query.
20+indexed :: Query a -> Query (Expr Int64, a)
21+indexed = window (liftA2 (,) (subtract 1 <$> rowNumber) currentRow)
src/Rel8/Query/List.hs+9−7
@@ -24,8 +24,8 @@ import Rel8.Expr.Aggregate ( listAggExpr, nonEmptyAggExpr )
24import Rel8.Expr.Opaleye ( mapPrimExpr )
24import Rel8.Expr.Opaleye ( mapPrimExpr )
25import Rel8.Query ( Query )
25import Rel8.Query ( Query )
26import Rel8.Query.Aggregate ( aggregate )
26import Rel8.Query.Aggregate ( aggregate )
27−import Rel8.Query.Evaluate ( rebind )
28import Rel8.Query.Maybe ( optional )
27import Rel8.Query.Maybe ( optional )
28+import Rel8.Query.Rebind ( rebind )
29import Rel8.Schema.HTable.Vectorize ( hunvectorize )
29import Rel8.Schema.HTable.Vectorize ( hunvectorize )
30import Rel8.Schema.Null ( Sql, Unnullify )
30import Rel8.Schema.Null ( Sql, Unnullify )
31import Rel8.Schema.Spec ( Spec( Spec, info ) )
31import Rel8.Schema.Spec ( Spec( Spec, info ) )
@@ -85,8 +85,9 @@ someExpr = aggregate . fmap nonEmptyAggExpr
85--
85--
86-- @catListTable@ is an inverse to 'many'.
86-- @catListTable@ is an inverse to 'many'.
87catListTable :: Table Expr a => ListTable Expr a -> Query a
87catListTable :: Table Expr a => ListTable Expr a -> Query a
88+catListTable (ListTable as) =
88−catListTable (ListTable as) = rebind $ fromColumns $ runIdentity $
89+ rebind "unnest" $ fromColumns $ runIdentity $
89− hunvectorize (\Spec {info} -> pure . sunnest info) as
90+ hunvectorize (\Spec {info} -> pure . sunnest info) as
90
91
91
92
92-- | Expand a 'NonEmptyTable' into a 'Query', where each row in the query is an
93-- | Expand a 'NonEmptyTable' into a 'Query', where each row in the query is an
@@ -94,8 +95,9 @@ catListTable (ListTable as) = rebind $ fromColumns $ runIdentity $
94--
95--
95-- @catNonEmptyTable@ is an inverse to 'some'.
96-- @catNonEmptyTable@ is an inverse to 'some'.
96catNonEmptyTable :: Table Expr a => NonEmptyTable Expr a -> Query a
97catNonEmptyTable :: Table Expr a => NonEmptyTable Expr a -> Query a
97−catNonEmptyTable (NonEmptyTable as) = rebind $ fromColumns $ runIdentity $
98+catNonEmptyTable (NonEmptyTable as) =
99+ rebind "unnest" $ fromColumns $ runIdentity $
98− hunvectorize (\Spec {info} -> pure . sunnest info) as
100+ hunvectorize (\Spec {info} -> pure . sunnest info) as
99
101
100
102
101-- | Expand an expression that contains a list into a 'Query', where each row
103-- | Expand an expression that contains a list into a 'Query', where each row
@@ -103,7 +105,7 @@ catNonEmptyTable (NonEmptyTable as) = rebind $ fromColumns $ runIdentity $
103--
105--
104-- @catList@ is an inverse to 'manyExpr'.
106-- @catList@ is an inverse to 'manyExpr'.
105catList :: Sql DBType a => Expr [a] -> Query (Expr a)
107catList :: Sql DBType a => Expr [a] -> Query (Expr a)
106−catList = rebind . sunnest typeInformation
108+catList = rebind "unnest" . sunnest typeInformation
107
109
108
110
109-- | Expand an expression that contains a non-empty list into a 'Query', where
111-- | Expand an expression that contains a non-empty list into a 'Query', where
@@ -111,7 +113,7 @@ catList = rebind . sunnest typeInformation
111--
113--
112-- @catNonEmpty@ is an inverse to 'someExpr'.
114-- @catNonEmpty@ is an inverse to 'someExpr'.
113catNonEmpty :: Sql DBType a => Expr (NonEmpty a) -> Query (Expr a)
115catNonEmpty :: Sql DBType a => Expr (NonEmpty a) -> Query (Expr a)
114−catNonEmpty = rebind . sunnest typeInformation
116+catNonEmpty = rebind "unnest" . sunnest typeInformation
115
117
116
118
117sunnest :: TypeInformation (Unnullify a) -> Expr (list a) -> Expr a
119sunnest :: TypeInformation (Unnullify a) -> Expr (list a) -> Expr a
src/Rel8/Query/Null.hs+15
@@ -1,5 +1,8 @@
1+{-# language FlexibleContexts #-}
2+
1module Rel8.Query.Null
3module Rel8.Query.Null
2 ( catNull
4 ( catNull
5+ , catNullTable
3 )
6 )
4where
7where
5
8
@@ -9,6 +12,8 @@ import Prelude
9-- rel8
12-- rel8
10import Rel8.Expr ( Expr )
13import Rel8.Expr ( Expr )
11import Rel8.Expr.Null ( isNonNull, unsafeUnnullify )
14import Rel8.Expr.Null ( isNonNull, unsafeUnnullify )
15+import Rel8.Table ( Table )
16+import Rel8.Table.Null ( NullTable, isNonNullTable, unsafeUnnullifyTable )
12import Rel8.Query ( Query )
17import Rel8.Query ( Query )
13import Rel8.Query.Filter ( where_ )
18import Rel8.Query.Filter ( where_ )
14
19
@@ -21,3 +26,13 @@ catNull :: Expr (Maybe a) -> Query (Expr a)
21catNull a = do
26catNull a = do
22 where_ $ isNonNull a
27 where_ $ isNonNull a
23 pure $ unsafeUnnullify a
28 pure $ unsafeUnnullify a
29+
30+
31+-- | Filter a 'Query' that might return @nullTable@ to a 'Query' without any
32+-- @nullTable@s.
33+--
34+-- Corresponds to 'Data.Maybe.catMaybes'.
35+catNullTable :: Table Expr a => NullTable Expr a -> Query a
36+catNullTable a = do
37+ where_ $ isNonNullTable a
38+ pure $ unsafeUnnullifyTable a
src/Rel8/Query/Opaleye.hs+18−10
@@ -5,6 +5,7 @@ module Rel8.Query.Opaleye
5 , toOpaleye
5 , toOpaleye
6 , mapOpaleye
6 , mapOpaleye
7 , zipOpaleyeWith
7 , zipOpaleyeWith
8+ , unsafePeekQuery
8 )
9 )
9where
10where
10
11
@@ -14,6 +15,7 @@ import Prelude
14
15
15-- opaleye
16-- opaleye
16import qualified Opaleye.Internal.QueryArr as Opaleye
17import qualified Opaleye.Internal.QueryArr as Opaleye
18+import qualified Opaleye.Internal.Tag as Opaleye
17
19
18-- rel8
20-- rel8
19import {-# SOURCE #-} Rel8.Query ( Query( Query ) )
21import {-# SOURCE #-} Rel8.Query ( Query( Query ) )
@@ -37,26 +39,32 @@ zipOpaleyeWith :: ()
37zipOpaleyeWith f (Query a) (Query b) = Query $ liftA2 (zipping f) a b
39zipOpaleyeWith f (Query a) (Query b) = Query $ liftA2 (zipping f) a b
38
40
39
41
42+unsafePeekQuery :: Query a -> a
43+unsafePeekQuery (Query q) = case q mempty of
44+ f -> case Opaleye.runStateQueryArr f () Opaleye.start of
45+ ((_, a), _, _) -> a
46+
47+
40mapping :: ()
48mapping :: ()
41 => (Opaleye.Select a -> Opaleye.Select b)
49 => (Opaleye.Select a -> Opaleye.Select b)
42 -> Opaleye.Select (m, a) -> Opaleye.Select (m, b)
50 -> Opaleye.Select (m, a) -> Opaleye.Select (m, b)
43−mapping f q@(Opaleye.QueryArr qa) = Opaleye.QueryArr $ \(_, tag) ->
51+mapping f q = Opaleye.stateQueryArr $ \_ tag ->
44 let
52 let
45− ((m, _), _, _) = qa ((), tag)
53+ ((m, _), _, _) = Opaleye.runStateQueryArr q () tag
46− Opaleye.QueryArr qa' = (m,) <$> f (snd <$> q)
54+ q' = (m,) <$> f (snd <$> q)
47 in
55 in
48− qa' ((), tag)
56+ Opaleye.runStateQueryArr q' () tag
49
57
50
58
51zipping :: Semigroup m
59zipping :: Semigroup m
52 => (Opaleye.Select a -> Opaleye.Select b -> Opaleye.Select c)
60 => (Opaleye.Select a -> Opaleye.Select b -> Opaleye.Select c)
53 -> Opaleye.Select (m, a) -> Opaleye.Select (m, b) -> Opaleye.Select (m, c)
61 -> Opaleye.Select (m, a) -> Opaleye.Select (m, b) -> Opaleye.Select (m, c)
54−zipping f q@(Opaleye.QueryArr qa) q'@(Opaleye.QueryArr qa') =
62+zipping f q q' =
55− Opaleye.QueryArr $ \(_, tag) ->
63+ Opaleye.stateQueryArr $ \_ tag ->
56 let
64 let
57− ((m, _), _, _) = qa ((), tag)
65+ ((m, _), _, _) = Opaleye.runStateQueryArr q () tag
58− ((m', _), _, _) = qa' ((), tag)
66+ ((m', _), _, _) = Opaleye.runStateQueryArr q' () tag
59 m'' = m <> m'
67 m'' = m <> m'
60− Opaleye.QueryArr qa'' = (m'',) <$> f (snd <$> q) (snd <$> q')
68+ q'' = (m'',) <$> f (snd <$> q) (snd <$> q')
61 in
69 in
62− qa'' ((), tag)
70+ Opaleye.runStateQueryArr q'' () tag
src/Rel8/Query/Rebind.hsnew file+27
@@ -0,0 +1,27 @@
1+{-# language FlexibleContexts #-}
2+
3+module Rel8.Query.Rebind
4+ ( rebind
5+ )
6+where
7+
8+-- base
9+import Prelude
10+import Control.Arrow ((<<<))
11+
12+-- opaleye
13+import qualified Opaleye.Internal.Rebind as Opaleye
14+
15+-- rel8
16+import Rel8.Expr ( Expr )
17+import Rel8.Query ( Query )
18+import Rel8.Table ( Table )
19+import Rel8.Table.Opaleye ( unpackspec )
20+import Rel8.Query.Opaleye (fromOpaleye)
21+
22+
23+-- | 'rebind' takes a variable name, some expressions, and binds each of them
24+-- to a new variable in the SQL. The @a@ returned consists only of these
25+-- variables. It's essentially a @let@ binding for Postgres expressions.
26+rebind :: Table Expr a => String -> a -> Query a
27+rebind prefix a = fromOpaleye (Opaleye.rebindExplicitPrefix prefix unpackspec <<< pure a)
src/Rel8/Query/SQL.hs+1−1
@@ -18,4 +18,4 @@ import Rel8.Table ( Table )
18
18
19-- | Convert a 'Query' to a 'String' containing a @SELECT@ statement.
19-- | Convert a 'Query' to a 'String' containing a @SELECT@ statement.
20showQuery :: Table Expr a => Query a -> String
20showQuery :: Table Expr a => Query a -> String
21−showQuery = foldMap show . ppSelect
21+showQuery = show . ppSelect
src/Rel8/Query/Set.hs+6−6
@@ -23,36 +23,36 @@ import Rel8.Table.Opaleye ( binaryspec )
23
23
24
24
25-- | Combine the results of two queries of the same type, collapsing
25-- | Combine the results of two queries of the same type, collapsing
26−-- duplicates. @union a b@ is the same as the SQL statement @x UNION b@.
26+-- duplicates. @union a b@ is the same as the SQL statement @a UNION b@.
27union :: EqTable a => Query a -> Query a -> Query a
27union :: EqTable a => Query a -> Query a -> Query a
28union = zipOpaleyeWith (Opaleye.unionExplicit binaryspec)
28union = zipOpaleyeWith (Opaleye.unionExplicit binaryspec)
29
29
30
30
31-- | Combine the results of two queries of the same type, retaining duplicates.
31-- | Combine the results of two queries of the same type, retaining duplicates.
32−-- @unionAll a b@ is the same as the SQL statement @x UNION ALL b@.
32+-- @unionAll a b@ is the same as the SQL statement @a UNION ALL b@.
33unionAll :: Table Expr a => Query a -> Query a -> Query a
33unionAll :: Table Expr a => Query a -> Query a -> Query a
34unionAll = zipOpaleyeWith (Opaleye.unionAllExplicit binaryspec)
34unionAll = zipOpaleyeWith (Opaleye.unionAllExplicit binaryspec)
35
35
36
36
37-- | Find the intersection of two queries, collapsing duplicates. @intersect a
37-- | Find the intersection of two queries, collapsing duplicates. @intersect a
38−-- b@ is the same as the SQL statement @x INTERSECT b@.
38+-- b@ is the same as the SQL statement @a INTERSECT b@.
39intersect :: EqTable a => Query a -> Query a -> Query a
39intersect :: EqTable a => Query a -> Query a -> Query a
40intersect = zipOpaleyeWith (Opaleye.intersectExplicit binaryspec)
40intersect = zipOpaleyeWith (Opaleye.intersectExplicit binaryspec)
41
41
42
42
43-- | Find the intersection of two queries, retaining duplicates. @intersectAll
43-- | Find the intersection of two queries, retaining duplicates. @intersectAll
44−-- a b@ is the same as the SQL statement @x INTERSECT ALL b@.
44+-- a b@ is the same as the SQL statement @a INTERSECT ALL b@.
45intersectAll :: EqTable a => Query a -> Query a -> Query a
45intersectAll :: EqTable a => Query a -> Query a -> Query a
46intersectAll = zipOpaleyeWith (Opaleye.intersectAllExplicit binaryspec)
46intersectAll = zipOpaleyeWith (Opaleye.intersectAllExplicit binaryspec)
47
47
48
48
49-- | Find the difference of two queries, collapsing duplicates @except a b@ is
49-- | Find the difference of two queries, collapsing duplicates @except a b@ is
50−-- the same as the SQL statement @x INTERSECT b@.
50+-- the same as the SQL statement @a EXCEPT b@.
51except :: EqTable a => Query a -> Query a -> Query a
51except :: EqTable a => Query a -> Query a -> Query a
52except = zipOpaleyeWith (Opaleye.exceptExplicit binaryspec)
52except = zipOpaleyeWith (Opaleye.exceptExplicit binaryspec)
53
53
54
54
55-- | Find the difference of two queries, retaining duplicates. @exceptAll a b@
55-- | Find the difference of two queries, retaining duplicates. @exceptAll a b@
56−-- is the same as the SQL statement @x EXCEPT ALL b@.
56+-- is the same as the SQL statement @a EXCEPT ALL b@.
57exceptAll :: EqTable a => Query a -> Query a -> Query a
57exceptAll :: EqTable a => Query a -> Query a -> Query a
58exceptAll = zipOpaleyeWith (Opaleye.exceptAllExplicit binaryspec)
58exceptAll = zipOpaleyeWith (Opaleye.exceptAllExplicit binaryspec)
src/Rel8/Query/These.hs+8−8
@@ -48,11 +48,11 @@ import Rel8.Type.Tag ( EitherTag( IsLeft, IsRight ) )
48alignBy :: ()
48alignBy :: ()
49 => (a -> b -> Expr Bool)
49 => (a -> b -> Expr Bool)
50 -> Query a -> Query b -> Query (TheseTable Expr a b)
50 -> Query a -> Query b -> Query (TheseTable Expr a b)
51−alignBy condition = zipOpaleyeWith $ \left right -> Opaleye.QueryArr $ \i -> case i of
51+alignBy condition = zipOpaleyeWith $ \left right -> Opaleye.stateQueryArr $ \_ t -> case t of
52− (_, tag) -> (tab, join', tag''')
52+ tag -> (tab, join', tag''')
53 where
53 where
54− (ma, left', tag') = Opaleye.runSimpleQueryArr (pure <$> left) ((), tag)
54+ (ma, left', tag') = Opaleye.runStateQueryArr (pure <$> left) () tag
55− (mb, right', tag'') = Opaleye.runSimpleQueryArr (pure <$> right) ((), tag')
55+ (mb, right', tag'') = Opaleye.runStateQueryArr (pure <$> right) () tag'
56 MaybeTable hasHere a = ma
56 MaybeTable hasHere a = ma
57 MaybeTable hasThere b = mb
57 MaybeTable hasThere b = mb
58 (hasHere', lbindings) = Opaleye.run $ do
58 (hasHere', lbindings) = Opaleye.run $ do
@@ -60,15 +60,15 @@ alignBy condition = zipOpaleyeWith $ \left right -> Opaleye.QueryArr $ \i -> cas
60 (hasThere', rbindings) = Opaleye.run $ do
60 (hasThere', rbindings) = Opaleye.run $ do
61 traversePrimExpr (Opaleye.extractAttr "hasThere" tag'') hasThere
61 traversePrimExpr (Opaleye.extractAttr "hasThere" tag'') hasThere
62 tag''' = Opaleye.next tag''
62 tag''' = Opaleye.next tag''
63− join lateral = Opaleye.Join Opaleye.FullJoin on left'' right''
63+ join = Opaleye.Join Opaleye.FullJoin on left'' right''
64 where
64 where
65 on = toPrimExpr $ condition (extract a) (extract b)
65 on = toPrimExpr $ condition (extract a) (extract b)
66− left'' = (lateral, Opaleye.Rebind True lbindings left')
66+ left'' = (Opaleye.NonLateral, Opaleye.toPrimQuery (left' <> Opaleye.aRebind lbindings))
67− right'' = (lateral, Opaleye.Rebind True rbindings right')
67+ right'' = (Opaleye.NonLateral, Opaleye.toPrimQuery (right' <> Opaleye.aRebind rbindings))
68 ma' = MaybeTable hasHere' a
68 ma' = MaybeTable hasHere' a
69 mb' = MaybeTable hasThere' b
69 mb' = MaybeTable hasThere' b
70 tab = TheseTable {here = ma', there = mb'}
70 tab = TheseTable {here = ma', there = mb'}
71− join' lateral input = Opaleye.times lateral input (join lateral)
71+ join' = Opaleye.aProduct join
72
72
73
73
74keepHereTable :: TheseTable Expr a b -> Query (a, MaybeTable Expr b)
74keepHereTable :: TheseTable Expr a b -> Query (a, MaybeTable Expr b)
src/Rel8/Query/Window.hsnew file+26
@@ -0,0 +1,26 @@
1+module Rel8.Query.Window
2+ ( window
3+ )
4+where
5+
6+-- base
7+import Prelude ()
8+
9+-- opaleye
10+import qualified Opaleye.Window as Opaleye
11+
12+-- rel8
13+import Rel8.Query ( Query )
14+import Rel8.Query.Opaleye ( mapOpaleye )
15+import Rel8.Window ( Window( Window ) )
16+
17+
18+-- | 'window' runs a query composed of expressions containing
19+-- [window functions](https://www.postgresql.org/docs/current/tutorial-window.html).
20+-- 'window' is similar to 'Rel8.aggregate', with the main difference being
21+-- that in a window query, each input row corresponds to one output row,
22+-- whereas aggregation queries fold the entire input query down into a single
23+-- row. To put this into a Haskell context, 'Rel8.aggregate' is to 'foldl' as
24+-- 'window' is to 'scanl'.
25+window :: Window a b -> Query a -> Query b
26+window (Window a) = mapOpaleye (Opaleye.runWindows a)
src/Rel8/Schema/Context/Nullify.hs+6−14
@@ -18,18 +18,17 @@ where
18import Data.Bool ( bool )
18import Data.Bool ( bool )
19import Data.Functor.Identity ( Identity( Identity ) )
19import Data.Functor.Identity ( Identity( Identity ) )
20import Data.Kind ( Constraint, Type )
20import Data.Kind ( Constraint, Type )
21−import Data.Monoid ( getFirst )
22import Prelude hiding ( null )
21import Prelude hiding ( null )
23
22
24-- opaleye
23-- opaleye
25import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
24import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
26
25
27-- rel8
26-- rel8
28−import Rel8.Aggregate ( Aggregate( Aggregate ), foldInputs, mapInputs )
27+import Rel8.Aggregate ( Aggregate(..), zipOutputs )
29import Rel8.Expr ( Expr )
28import Rel8.Expr ( Expr )
30import Rel8.Expr.Bool ( boolExpr )
29import Rel8.Expr.Bool ( boolExpr )
31import Rel8.Expr.Null ( nullify, unsafeUnnullify )
30import Rel8.Expr.Null ( nullify, unsafeUnnullify )
32−import Rel8.Expr.Opaleye ( fromPrimExpr, toPrimExpr )
31+import Rel8.Expr.Opaleye ( fromPrimExpr )
33import Rel8.Kind.Context ( SContext(..) )
32import Rel8.Kind.Context ( SContext(..) )
34import Rel8.Schema.Field ( Field )
33import Rel8.Schema.Field ( Field )
35import qualified Rel8.Schema.Kind as K
34import qualified Rel8.Schema.Kind as K
@@ -95,17 +94,10 @@ guarder :: ()
95 -> context (Maybe a)
94 -> context (Maybe a)
96 -> context (Maybe a)
95 -> context (Maybe a)
97guarder = \case
96guarder = \case
98− SAggregate -> \tag _ isNonNull (Aggregate a) ->
97+ SAggregate -> \tag _ isNonNull -> zipOutputs (sguard . isNonNull) tag
99− let
100− mtag = foldInputs (\_ -> pure . fromPrimExpr) tag
101− run = maybe id (sguard . isNonNull) (getFirst mtag)
102− in
103− mapInputs (toPrimExpr . run . fromPrimExpr) $
104− Aggregate $
105− run <$> a
106− SExpr -> \tag _ isNonNull a -> sguard (isNonNull tag) a
98+ SExpr -> \tag _ isNonNull -> sguard (isNonNull tag)
107− SField -> \_ _ _ field -> field
99+ SField -> \_ _ _ -> id
108− SName -> \_ _ _ name -> name
100+ SName -> \_ _ _ -> id
109 SResult -> \(Identity tag) isNonNull _ (Identity a) ->
101 SResult -> \(Identity tag) isNonNull _ (Identity a) ->
110 Identity (bool Nothing a (isNonNull tag))
102 Identity (bool Nothing a (isNonNull tag))
111
103
src/Rel8/Schema/HTable.hs+29−2
@@ -16,11 +16,12 @@
16module Rel8.Schema.HTable
16module Rel8.Schema.HTable
17 ( HTable (HField, HConstrainTable)
17 ( HTable (HField, HConstrainTable)
18 , hfield, htabulate, htraverse, hdicts, hspecs
18 , hfield, htabulate, htraverse, hdicts, hspecs
19− , hmap, htabulateA
19+ , hfoldMap, hmap, htabulateA, htraverseP, htraversePWithField
20 )
20 )
21where
21where
22
22
23-- base
23-- base
24+import Data.Functor.Const ( Const( Const ), getConst )
24import Data.Kind ( Constraint, Type )
25import Data.Kind ( Constraint, Type )
25import Data.Functor.Compose ( Compose( Compose ), getCompose )
26import Data.Functor.Compose ( Compose( Compose ), getCompose )
26import Data.Proxy ( Proxy )
27import Data.Proxy ( Proxy )
@@ -32,6 +33,12 @@ import GHC.Generics
32 )
33 )
33import Prelude
34import Prelude
34
35
36+-- profunctors
37+import Data.Profunctor ( rmap, Profunctor (lmap) )
38+
39+-- product-profunctors
40+import Data.Profunctor.Product ( ProductProfunctor ((****)) )
41+
35-- rel8
42-- rel8
36import Rel8.Schema.Dict ( Dict )
43import Rel8.Schema.Dict ( Dict )
37import Rel8.Schema.Spec ( Spec )
44import Rel8.Schema.Spec ( Spec )
@@ -41,7 +48,6 @@ import qualified Rel8.Schema.Kind as K
41-- semigroupoids
48-- semigroupoids
42import Data.Functor.Apply ( Apply, (<.>) )
49import Data.Functor.Apply ( Apply, (<.>) )
43
50
44−
45-- | A @HTable@ is a functor-indexed/higher-kinded data type that is
51-- | A @HTable@ is a functor-indexed/higher-kinded data type that is
46-- representable ('htabulate'/'hfield'), constrainable ('hdicts'), and
52-- representable ('htabulate'/'hfield'), constrainable ('hdicts'), and
47-- specified ('hspecs').
53-- specified ('hspecs').
@@ -114,6 +120,11 @@ class HTable t where
114 {-# INLINABLE hspecs #-}
120 {-# INLINABLE hspecs #-}
115
121
116
122
123+hfoldMap :: (HTable t, Semigroup s)
124+ => (forall a. context a -> s) -> t context -> s
125+hfoldMap f a = getConst $ htraverse (Const . f) a
126+
127+
117hmap :: HTable t
128hmap :: HTable t
118 => (forall a. context a -> context' a) -> t context -> t context'
129 => (forall a. context a -> context' a) -> t context -> t context'
119hmap f a = htabulate $ \field -> f (hfield a field)
130hmap f a = htabulate $ \field -> f (hfield a field)
@@ -124,6 +135,22 @@ htabulateA :: (HTable t, Apply m)
124htabulateA f = htraverse getCompose $ htabulate $ Compose . f
135htabulateA f = htraverse getCompose $ htabulate $ Compose . f
125{-# INLINABLE htabulateA #-}
136{-# INLINABLE htabulateA #-}
126
137
138+newtype ApplyP p a b = ApplyP { unApplyP :: p a b }
139+
140+instance Profunctor p => Functor (ApplyP p a) where
141+ fmap f = ApplyP . rmap f . unApplyP
142+
143+instance ProductProfunctor p => Apply (ApplyP p a) where
144+ ApplyP f <.> ApplyP x = ApplyP (rmap id f **** x)
145+
146+htraverseP :: (HTable t, ProductProfunctor p)
147+ => (forall a. p (f a) (g a)) -> p (t f) (t g)
148+htraverseP f = htraversePWithField (const f)
149+
150+htraversePWithField :: (HTable t, ProductProfunctor p)
151+ => (forall a. HField t a -> p (f a) (g a)) -> p (t f) (t g)
152+htraversePWithField f = unApplyP $ htabulateA $ \field -> ApplyP $
153+ lmap (flip hfield field) (f field)
127
154
128type GHField :: K.HTable -> Type -> Type
155type GHField :: K.HTable -> Type -> Type
129newtype GHField t a = GHField (HField (GHColumns (Rep (t Proxy))) a)
156newtype GHField t a = GHField (HField (GHColumns (Rep (t Proxy))) a)
src/Rel8/Schema/HTable/Vectorize.hs+10−2
@@ -22,11 +22,12 @@ module Rel8.Schema.HTable.Vectorize
22 , hvectorize, hunvectorize
22 , hvectorize, hunvectorize
23 , happend, hempty
23 , happend, hempty
24 , hproject
24 , hproject
25+ , hcolumn
25 )
26 )
26where
27where
27
28
28-- base
29-- base
29−import Data.Kind ( Type )
30+import Data.Kind ( Constraint, Type )
30import Data.List.NonEmpty ( NonEmpty )
31import Data.List.NonEmpty ( NonEmpty )
31import GHC.Generics (Generic)
32import GHC.Generics (Generic)
32import Prelude
33import Prelude
@@ -36,9 +37,11 @@ import Rel8.FCF ( Eval, Exp )
36import Rel8.Schema.Dict ( Dict( Dict ) )
37import Rel8.Schema.Dict ( Dict( Dict ) )
37import qualified Rel8.Schema.Kind as K
38import qualified Rel8.Schema.Kind as K
38import Rel8.Schema.HTable ( HTable, hfield, htabulate, htabulateA, hspecs )
39import Rel8.Schema.HTable ( HTable, hfield, htabulate, htabulateA, hspecs )
40+import Rel8.Schema.HTable.Identity ( HIdentity( HIdentity ) )
39import Rel8.Schema.HTable.MapTable
41import Rel8.Schema.HTable.MapTable
40− ( HMapTable, HMapTableField( HMapTableField )
42+ ( HMapTable( HMapTable ), HMapTableField( HMapTableField )
41 , MapSpec, mapInfo
43 , MapSpec, mapInfo
44+ , Precompose( Precompose )
42 )
45 )
43import qualified Rel8.Schema.HTable.MapTable as HMapTable ( hproject )
46import qualified Rel8.Schema.HTable.MapTable as HMapTable ( hproject )
44import Rel8.Schema.Null ( Unnullify, NotNull, Nullity( NotNull ) )
47import Rel8.Schema.Null ( Unnullify, NotNull, Nullity( NotNull ) )
@@ -50,6 +53,7 @@ import Rel8.Type.Information ( TypeInformation )
50import Data.Zip ( Unzip, Zip, Zippy(..) )
53import Data.Zip ( Unzip, Zip, Zippy(..) )
51
54
52
55
56+type Vector :: (Type -> Type) -> Constraint
53class Vector list where
57class Vector list where
54 listNotNull :: proxy a -> Dict NotNull (list a)
58 listNotNull :: proxy a -> Dict NotNull (list a)
55 vectorTypeInformation :: ()
59 vectorTypeInformation :: ()
@@ -133,3 +137,7 @@ hproject :: ()
133 => (forall ctx. t ctx -> t' ctx)
137 => (forall ctx. t ctx -> t' ctx)
134 -> HVectorize list t context -> HVectorize list t' context
138 -> HVectorize list t context -> HVectorize list t' context
135hproject f (HVectorize a) = HVectorize (HMapTable.hproject f a)
139hproject f (HVectorize a) = HVectorize (HMapTable.hproject f a)
140+
141+
142+hcolumn :: HVectorize list (HIdentity a) context -> context (list a)
143+hcolumn (HVectorize (HMapTable (HIdentity (Precompose a)))) = a
src/Rel8/Schema/Table.hs+3
@@ -2,6 +2,7 @@
2{-# language DerivingStrategies #-}
2{-# language DerivingStrategies #-}
3{-# language DisambiguateRecordFields #-}
3{-# language DisambiguateRecordFields #-}
4{-# language NamedFieldPuns #-}
4{-# language NamedFieldPuns #-}
5+{-# language StandaloneKindSignatures #-}
5
6
6module Rel8.Schema.Table
7module Rel8.Schema.Table
7 ( TableSchema(..)
8 ( TableSchema(..)
@@ -10,6 +11,7 @@ module Rel8.Schema.Table
10where
11where
11
12
12-- base
13-- base
14+import Data.Kind ( Type )
13import Prelude
15import Prelude
14
16
15-- opaleye
17-- opaleye
@@ -26,6 +28,7 @@ import Text.PrettyPrint ( Doc )
26--
28--
27-- For each selectable table in your database, you should provide a
29-- For each selectable table in your database, you should provide a
28-- @TableSchema@ in order to interact with the table via Rel8.
30-- @TableSchema@ in order to interact with the table via Rel8.
31+type TableSchema :: Type -> Type
29data TableSchema names = TableSchema
32data TableSchema names = TableSchema
30 { name :: String
33 { name :: String
31 -- ^ The name of the table.
34 -- ^ The name of the table.
src/Rel8/Statement/Delete.hs+20−27
@@ -14,14 +14,11 @@ module Rel8.Statement.Delete
14where
14where
15
15
16-- base
16-- base
17−import Control.Exception ( throwIO )
18import Data.Kind ( Type )
17import Data.Kind ( Type )
19import Prelude
18import Prelude
20
19
21-- hasql
20-- hasql
22−import Hasql.Connection ( Connection )
23import qualified Hasql.Encoders as Hasql
21import qualified Hasql.Encoders as Hasql
24−import qualified Hasql.Session as Hasql
25import qualified Hasql.Statement as Hasql
22import qualified Hasql.Statement as Hasql
26
23
27-- pretty
24-- pretty
@@ -32,10 +29,7 @@ import Rel8.Expr ( Expr )
32import Rel8.Query ( Query )
29import Rel8.Query ( Query )
33import Rel8.Schema.Name ( Selects )
30import Rel8.Schema.Name ( Selects )
34import Rel8.Schema.Table ( TableSchema, ppTable )
31import Rel8.Schema.Table ( TableSchema, ppTable )
35−import Rel8.Statement.Returning
32+import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )
36− ( Returning
37− , decodeReturning, emptyReturning, ppReturning
38− )
39import Rel8.Statement.Using ( ppUsing )
33import Rel8.Statement.Using ( ppUsing )
40import Rel8.Statement.Where ( ppWhere )
34import Rel8.Statement.Where ( ppWhere )
41
35
@@ -61,26 +55,25 @@ data Delete a where
61 -> Delete a
55 -> Delete a
62
56
63
57
64−ppDelete :: Delete a -> Maybe Doc
58+ppDelete :: Delete a -> Doc
65−ppDelete Delete {..} = do
59+ppDelete Delete {..} = case ppUsing using of
66− (usingDoc, i) <- ppUsing using
60+ Nothing ->
67− pure $ text "DELETE FROM" <+> ppTable from
61+ text "DELETE FROM" <+> ppTable from $$
62+ text "WHERE false"
68− $$ usingDoc
63+ Just (usingDoc, i) ->
64+ text "DELETE FROM" <+> ppTable from $$
65+ usingDoc $$
69− $$ ppWhere from (deleteWhere i)
66+ ppWhere from (deleteWhere i) $$
70− $$ ppReturning from returning
67+ ppReturning from returning
71
68
72
69
73-- | Run a 'Delete' statement.
70-- | Run a 'Delete' statement.
74−delete :: Connection -> Delete a -> IO a
71+delete :: Delete a -> Hasql.Statement () a
75−delete connection d@Delete {returning} =
72+delete d@Delete {returning} = Hasql.Statement bytes params decode prepare
76− case show <$> ppDelete d of
77− Nothing -> pure (emptyReturning returning)
78− Just sql ->
79− Hasql.run session connection >>= either throwIO pure
80− where
73+ where
81− session = Hasql.statement () statement
82− statement = Hasql.Statement bytes params decode prepare
83− bytes = encodeUtf8 $ Text.pack sql
74+ bytes = encodeUtf8 $ Text.pack sql
84− params = Hasql.noParams
75+ params = Hasql.noParams
85− decode = decodeReturning returning
76+ decode = decodeReturning returning
86− prepare = False
77+ prepare = False
78+ sql = show doc
79+ doc = ppDelete d
src/Rel8/Statement/Insert.hs+18−28
@@ -15,15 +15,12 @@ module Rel8.Statement.Insert
15where
15where
16
16
17-- base
17-- base
18−import Control.Exception ( throwIO )
19import Data.Foldable ( toList )
18import Data.Foldable ( toList )
20import Data.Kind ( Type )
19import Data.Kind ( Type )
21import Prelude
20import Prelude
22
21
23-- hasql
22-- hasql
24−import Hasql.Connection ( Connection )
25import qualified Hasql.Encoders as Hasql
23import qualified Hasql.Encoders as Hasql
26−import qualified Hasql.Session as Hasql
27import qualified Hasql.Statement as Hasql
24import qualified Hasql.Statement as Hasql
28
25
29-- opaleye
26-- opaleye
@@ -37,11 +34,8 @@ import Rel8.Query ( Query )
37import Rel8.Schema.Name ( Name, Selects, ppColumn )
34import Rel8.Schema.Name ( Name, Selects, ppColumn )
38import Rel8.Schema.Table ( TableSchema(..), ppTable )
35import Rel8.Schema.Table ( TableSchema(..), ppTable )
39import Rel8.Statement.OnConflict ( OnConflict, ppOnConflict )
36import Rel8.Statement.OnConflict ( OnConflict, ppOnConflict )
40−import Rel8.Statement.Returning
37+import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )
41− ( Returning
42− , decodeReturning, emptyReturning, ppReturning
43− )
44−import Rel8.Statement.Select ( ppSelect )
38+import Rel8.Statement.Select ( ppRows )
45import Rel8.Table ( Table )
39import Rel8.Table ( Table )
46import Rel8.Table.Name ( showNames )
40import Rel8.Table.Name ( showNames )
47
41
@@ -68,13 +62,13 @@ data Insert a where
68 -> Insert a
62 -> Insert a
69
63
70
64
71−ppInsert :: Insert a -> Maybe Doc
65+ppInsert :: Insert a -> Doc
72−ppInsert Insert {..} = do
66+ppInsert Insert {..} =
73− rows' <- ppSelect rows
74− pure $ text "INSERT INTO" <+> ppInto into
67+ text "INSERT INTO" <+>
68+ ppInto into $$
75− $$ rows'
69+ ppRows rows $$
76− $$ ppOnConflict into onConflict
70+ ppOnConflict into onConflict $$
77− $$ ppReturning into returning
71+ ppReturning into returning
78
72
79
73
80ppInto :: Table Name a => TableSchema a -> Doc
74ppInto :: Table Name a => TableSchema a -> Doc
@@ -84,16 +78,12 @@ ppInto table@TableSchema {columns} =
84
78
85
79
86-- | Run an 'Insert' statement.
80-- | Run an 'Insert' statement.
87−insert :: Connection -> Insert a -> IO a
81+insert :: Insert a -> Hasql.Statement () a
88−insert connection i@Insert {returning} =
82+insert i@Insert {returning} = Hasql.Statement bytes params decode prepare
89− case show <$> ppInsert i of
90− Nothing -> pure (emptyReturning returning)
91− Just sql ->
92− Hasql.run session connection >>= either throwIO pure
93− where
83+ where
94− session = Hasql.statement () statement
95− statement = Hasql.Statement bytes params decode prepare
96− bytes = encodeUtf8 $ Text.pack sql
84+ bytes = encodeUtf8 $ Text.pack sql
97− params = Hasql.noParams
85+ params = Hasql.noParams
98− decode = decodeReturning returning
86+ decode = decodeReturning returning
99− prepare = False
87+ prepare = False
88+ sql = show doc
89+ doc = ppInsert i
src/Rel8/Statement/Returning.hs−7
@@ -9,9 +9,7 @@
9
9
10module Rel8.Statement.Returning
10module Rel8.Statement.Returning
11 ( Returning( NumberOfRowsAffected, Projection )
11 ( Returning( NumberOfRowsAffected, Projection )
12−
13 , decodeReturning
12 , decodeReturning
14− , emptyReturning
15 , ppReturning
13 , ppReturning
16 )
14 )
17where
15where
@@ -126,11 +124,6 @@ decodeReturning = runReturning
126 (\decoder withRows -> withRows <$> Hasql.rowList decoder)
124 (\decoder withRows -> withRows <$> Hasql.rowList decoder)
127
125
128
126
129−emptyReturning :: Returning names a -> a
130−emptyReturning =
131− runReturning (\withCount -> withCount 0) (\_ withRows -> withRows [])
132−
133−
134ppReturning :: TableSchema names -> Returning names a -> Doc
127ppReturning :: TableSchema names -> Returning names a -> Doc
135ppReturning schema returning = case projections schema returning of
128ppReturning schema returning = case projections schema returning of
136 Nothing -> mempty
129 Nothing -> mempty
src/Rel8/Statement/SQL.hs+3−3
@@ -16,14 +16,14 @@ import Rel8.Statement.Update ( Update, ppUpdate )
16
16
17-- | Convert a 'Delete' to a 'String' containing a @DELETE@ statement.
17-- | Convert a 'Delete' to a 'String' containing a @DELETE@ statement.
18showDelete :: Delete a -> String
18showDelete :: Delete a -> String
19−showDelete = foldMap show . ppDelete
19+showDelete = show . ppDelete
20
20
21
21
22-- | Convert an 'Insert' to a 'String' containing an @INSERT@ statement.
22-- | Convert an 'Insert' to a 'String' containing an @INSERT@ statement.
23showInsert :: Insert a -> String
23showInsert :: Insert a -> String
24−showInsert = foldMap show . ppInsert
24+showInsert = show . ppInsert
25
25
26
26
27-- | Convert an 'Update' to a 'String' containing an @UPDATE@ statement.
27-- | Convert an 'Update' to a 'String' containing an @UPDATE@ statement.
28showUpdate :: Update a -> String
28showUpdate :: Update a -> String
29−showUpdate = foldMap show . ppUpdate
29+showUpdate = show . ppUpdate
src/Rel8/Statement/Select.hs+57−29
@@ -3,6 +3,7 @@
3{-# language FlexibleContexts #-}
3{-# language FlexibleContexts #-}
4{-# language MonoLocalBinds #-}
4{-# language MonoLocalBinds #-}
5{-# language ScopedTypeVariables #-}
5{-# language ScopedTypeVariables #-}
6+{-# language StandaloneKindSignatures #-}
6{-# language TypeApplications #-}
7{-# language TypeApplications #-}
7
8
8module Rel8.Statement.Select
9module Rel8.Statement.Select
@@ -11,34 +12,39 @@ module Rel8.Statement.Select
11
12
12 , Optimized(..)
13 , Optimized(..)
13 , ppPrimSelect
14 , ppPrimSelect
15+ , ppRows
14 )
16 )
15where
17where
16
18
17-- base
19-- base
18−import Control.Exception ( throwIO )
20+import Data.Foldable ( toList )
21+import Data.Kind ( Type )
19import Data.Void ( Void )
22import Data.Void ( Void )
20−import Prelude
23+import Prelude hiding ( undefined )
21
24
22-- hasql
25-- hasql
23−import Hasql.Connection ( Connection )
24import qualified Hasql.Decoders as Hasql
26import qualified Hasql.Decoders as Hasql
25import qualified Hasql.Encoders as Hasql
27import qualified Hasql.Encoders as Hasql
26−import qualified Hasql.Session as Hasql
27import qualified Hasql.Statement as Hasql
28import qualified Hasql.Statement as Hasql
28
29
29-- opaleye
30-- opaleye
31+import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
30import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
32import qualified Opaleye.Internal.HaskellDB.Sql as Opaleye
33+import qualified Opaleye.Internal.HaskellDB.Sql.Print as Opaleye
31import qualified Opaleye.Internal.PrimQuery as Opaleye
34import qualified Opaleye.Internal.PrimQuery as Opaleye
32import qualified Opaleye.Internal.Print as Opaleye
35import qualified Opaleye.Internal.Print as Opaleye
33import qualified Opaleye.Internal.Optimize as Opaleye
36import qualified Opaleye.Internal.Optimize as Opaleye
34import qualified Opaleye.Internal.QueryArr as Opaleye hiding ( Select )
37import qualified Opaleye.Internal.QueryArr as Opaleye hiding ( Select )
35−import qualified Opaleye.Internal.Sql as Opaleye
38+import qualified Opaleye.Internal.Sql as Opaleye hiding ( Values )
39+import qualified Opaleye.Internal.Tag as Opaleye
36
40
37-- pretty
41-- pretty
38import Text.PrettyPrint ( Doc )
42import Text.PrettyPrint ( Doc )
39
43
40-- rel8
44-- rel8
41import Rel8.Expr ( Expr )
45import Rel8.Expr ( Expr )
46+import Rel8.Expr.Bool ( false )
47+import Rel8.Expr.Opaleye ( toPrimExpr )
42import Rel8.Query ( Query )
48import Rel8.Query ( Query )
43import Rel8.Query.Opaleye ( toOpaleye )
49import Rel8.Query.Opaleye ( toOpaleye )
44import Rel8.Schema.Name ( Selects )
50import Rel8.Schema.Name ( Selects )
@@ -46,7 +52,9 @@ import Rel8.Table ( Table )
46import Rel8.Table.Cols ( toCols )
52import Rel8.Table.Cols ( toCols )
47import Rel8.Table.Name ( namesFromLabels )
53import Rel8.Table.Name ( namesFromLabels )
48import Rel8.Table.Opaleye ( castTable, exprsWithNames )
54import Rel8.Table.Opaleye ( castTable, exprsWithNames )
55+import qualified Rel8.Table.Opaleye as T
49import Rel8.Table.Serialize ( Serializable, parse )
56import Rel8.Table.Serialize ( Serializable, parse )
57+import Rel8.Table.Undefined ( undefined )
50
58
51-- text
59-- text
52import qualified Data.Text as Text
60import qualified Data.Text as Text
@@ -55,43 +63,63 @@ import Data.Text.Encoding ( encodeUtf8 )
55
63
56-- | Run a @SELECT@ statement, returning all rows.
64-- | Run a @SELECT@ statement, returning all rows.
57select :: forall exprs a. Serializable exprs a
65select :: forall exprs a. Serializable exprs a
58− => Connection -> Query exprs -> IO [a]
66+ => Query exprs -> Hasql.Statement () [a]
59−select c query = case ppSelect query of
67+select query = Hasql.Statement bytes params decode prepare
60− Nothing -> pure []
61− Just doc -> Hasql.run session c >>= either throwIO pure
62− where
68+ where
63− session = Hasql.statement () statement
64− statement = Hasql.Statement bytes params decode prepare
65− bytes = encodeUtf8 (Text.pack sql)
69+ bytes = encodeUtf8 (Text.pack sql)
66− params = Hasql.noParams
70+ params = Hasql.noParams
67− decode = Hasql.rowList (parse @exprs @a)
71+ decode = Hasql.rowList (parse @exprs @a)
68− prepare = False
72+ prepare = False
69− sql = show doc
73+ sql = show doc
70−
74+ doc = ppSelect query
71−
75+
76+
72−ppSelect :: Table Expr a => Query a -> Maybe Doc
77+ppSelect :: Table Expr a => Query a -> Doc
73−ppSelect query = do
78+ppSelect query =
74− primQuery' <- case optimize primQuery of
75− Empty -> Nothing
76− Unit -> Just Opaleye.Unit
77− Optimized primQuery' -> Just primQuery'
78− pure $ Opaleye.ppSql $ primSelectWith names (toCols exprs) primQuery'
79+ Opaleye.ppSql $ primSelectWith names (toCols exprs') primQuery'
79 where
80 where
80 names = namesFromLabels
81 names = namesFromLabels
81 (exprs, primQuery, _) =
82 (exprs, primQuery, _) =
82 Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
83 Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
84+ (exprs', primQuery') = case optimize primQuery of
85+ Empty -> (undefined, Opaleye.Product (pure (pure Opaleye.Unit)) never)
86+ Unit -> (exprs, Opaleye.Unit)
87+ Optimized pq -> (exprs, pq)
88+ never = pure (toPrimExpr false)
89+
90+
91+ppRows :: Table Expr a => Query a -> Doc
92+ppRows query = case optimize primQuery of
93+ -- Special case VALUES because we can't use DEFAULT inside a SELECT
94+ Optimized (Opaleye.Values symbols rows)
95+ | eqSymbols symbols (toList (T.exprs a)) ->
96+ Opaleye.ppValues_ (map Opaleye.sqlExpr <$> toList rows)
97+ _ -> ppSelect query
98+ where
99+ (a, primQuery, _) = Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
100+
101+ eqSymbols (symbol : symbols) (Opaleye.AttrExpr symbol' : exprs)
102+ | eqSymbol symbol symbol' = eqSymbols symbols exprs
103+ | otherwise = False
104+ eqSymbols [] [] = True
105+ eqSymbols _ _ = False
106+
107+ eqSymbol
108+ (Opaleye.Symbol name (Opaleye.UnsafeTag tag))
109+ (Opaleye.Symbol name' (Opaleye.UnsafeTag tag'))
110+ = name == name' && tag == tag'
83
111
84
112
85ppPrimSelect :: Query a -> (Optimized Doc, a)
113ppPrimSelect :: Query a -> (Optimized Doc, a)
86ppPrimSelect query =
114ppPrimSelect query =
87 (Opaleye.ppSql . primSelect <$> optimize primQuery, a)
115 (Opaleye.ppSql . primSelect <$> optimize primQuery, a)
88 where
116 where
89− (a, primQuery, _) =
90− Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
117+ (a, primQuery, _) = Opaleye.runSimpleQueryArrStart (toOpaleye query) ()
91
118
92
119
120+type Optimized :: Type -> Type
93data Optimized a = Empty | Unit | Optimized a
121data Optimized a = Empty | Unit | Optimized a
94− deriving stock (Functor, Foldable, Traversable)
122+ deriving stock (Functor, Foldable, Traversable, Show)
95
123
96
124
97optimize :: Opaleye.PrimQuery' a -> Optimized (Opaleye.PrimQuery' Void)
125optimize :: Opaleye.PrimQuery' a -> Optimized (Opaleye.PrimQuery' Void)
src/Rel8/Statement/Update.hs+18−26
@@ -13,14 +13,11 @@ module Rel8.Statement.Update
13where
13where
14
14
15-- base
15-- base
16−import Control.Exception ( throwIO )
17import Data.Kind ( Type )
16import Data.Kind ( Type )
18import Prelude
17import Prelude
19
18
20-- hasql
19-- hasql
21−import Hasql.Connection ( Connection )
22import qualified Hasql.Encoders as Hasql
20import qualified Hasql.Encoders as Hasql
23−import qualified Hasql.Session as Hasql
24import qualified Hasql.Statement as Hasql
21import qualified Hasql.Statement as Hasql
25
22
26-- pretty
23-- pretty
@@ -31,10 +28,7 @@ import Rel8.Expr ( Expr )
31import Rel8.Query ( Query )
28import Rel8.Query ( Query )
32import Rel8.Schema.Name ( Selects )
29import Rel8.Schema.Name ( Selects )
33import Rel8.Schema.Table ( TableSchema(..), ppTable )
30import Rel8.Schema.Table ( TableSchema(..), ppTable )
34−import Rel8.Statement.Returning
31+import Rel8.Statement.Returning ( Returning, decodeReturning, ppReturning )
35− ( Returning
36− , decodeReturning, emptyReturning, ppReturning
37− )
38import Rel8.Statement.Set ( ppSet )
32import Rel8.Statement.Set ( ppSet )
39import Rel8.Statement.Using ( ppFrom )
33import Rel8.Statement.Using ( ppFrom )
40import Rel8.Statement.Where ( ppWhere )
34import Rel8.Statement.Where ( ppWhere )
@@ -63,12 +57,14 @@ data Update a where
63 -> Update a
57 -> Update a
64
58
65
59
66−ppUpdate :: Update a -> Maybe Doc
60+ppUpdate :: Update a -> Doc
67−ppUpdate Update {..} = do
61+ppUpdate Update {..} = case ppFrom from of
62+ Nothing ->
63+ text "UPDATE" <+> ppTable target $$
64+ ppSet target id $$
65+ text "WHERE false"
68− (fromDoc, i) <- ppFrom from
66+ Just (fromDoc, i) ->
69− pure $
70− text "UPDATE" <+>
71− ppTable target $$
67+ text "UPDATE" <+> ppTable target $$
72 ppSet target (set i) $$
68 ppSet target (set i) $$
73 fromDoc $$
69 fromDoc $$
74 ppWhere target (updateWhere i) $$
70 ppWhere target (updateWhere i) $$
@@ -76,16 +72,12 @@ ppUpdate Update {..} = do
76
72
77
73
78-- | Run an @UPDATE@ statement.
74-- | Run an @UPDATE@ statement.
79−update :: Connection -> Update a -> IO a
75+update :: Update a -> Hasql.Statement () a
80−update connection u@Update {returning} =
76+update u@Update {returning} = Hasql.Statement bytes params decode prepare
81− case show <$> ppUpdate u of
82− Nothing -> pure (emptyReturning returning)
83− Just sql ->
84− Hasql.run session connection >>= either throwIO pure
85− where
77+ where
86− session = Hasql.statement () statement
87− statement = Hasql.Statement bytes params decode prepare
88− bytes = encodeUtf8 $ Text.pack sql
78+ bytes = encodeUtf8 $ Text.pack sql
89− params = Hasql.noParams
79+ params = Hasql.noParams
90− decode = decodeReturning returning
80+ decode = decodeReturning returning
91− prepare = False
81+ prepare = False
82+ sql = show doc
83+ doc = ppUpdate u
src/Rel8/Statement/View.hs+32−17
@@ -3,20 +3,16 @@
3
3
4module Rel8.Statement.View
4module Rel8.Statement.View
5 ( createView
5 ( createView
6+ , createOrReplaceView
6 )
7 )
7where
8where
8
9
9-- base
10-- base
10−import Control.Exception ( throwIO )
11−import Data.Foldable ( fold )
12−import Data.Maybe ( fromMaybe )
13import Prelude
11import Prelude
14
12
15-- hasql
13-- hasql
16−import Hasql.Connection ( Connection )
17import qualified Hasql.Decoders as Hasql
14import qualified Hasql.Decoders as Hasql
18import qualified Hasql.Encoders as Hasql
15import qualified Hasql.Encoders as Hasql
19−import qualified Hasql.Session as Hasql
20import qualified Hasql.Statement as Hasql
16import qualified Hasql.Statement as Hasql
21
17
22-- rel8
18-- rel8
@@ -25,7 +21,6 @@ import Rel8.Schema.Name ( Selects )
25import Rel8.Schema.Table ( TableSchema )
21import Rel8.Schema.Table ( TableSchema )
26import Rel8.Statement.Insert ( ppInto )
22import Rel8.Statement.Insert ( ppInto )
27import Rel8.Statement.Select ( ppSelect )
23import Rel8.Statement.Select ( ppSelect )
28−import Rel8.Table.Alternative ( emptyTable )
29
24
30-- pretty
25-- pretty
31import Text.PrettyPrint ( Doc, (<+>), ($$), text )
26import Text.PrettyPrint ( Doc, (<+>), ($$), text )
@@ -35,29 +30,49 @@ import qualified Data.Text as Text
35import Data.Text.Encoding ( encodeUtf8 )
30import Data.Text.Encoding ( encodeUtf8 )
36
31
37
32
33+data CreateView = Create | CreateOrReplace
34+
35+
38-- | Given a 'TableSchema' and 'Query', @createView@ runs a @CREATE VIEW@
36-- | Given a 'TableSchema' and 'Query', @createView@ runs a @CREATE VIEW@
39-- statement that will save the given query as a view. This can be useful if
37-- statement that will save the given query as a view. This can be useful if
40-- you want to share Rel8 queries with other applications.
38-- you want to share Rel8 queries with other applications.
41createView :: Selects names exprs
39createView :: Selects names exprs
42− => Connection -> TableSchema names -> Query exprs -> IO ()
40+ => TableSchema names -> Query exprs -> Hasql.Statement () ()
43−createView connection schema query =
41+createView =
44− Hasql.run session connection >>= either throwIO pure
42+ createViewGeneric Create
43+
44+
45+-- | Given a 'TableSchema' and 'Query', @createOrReplaceView@ runs a
46+-- @CREATE OR REPLACE VIEW@ statement that will save the given query
47+-- as a view, replacing the current view definition if it exists and
48+-- adheres to the restrictions in place for replacing a view in
49+-- PostgreSQL.
50+createOrReplaceView :: Selects names exprs
51+ => TableSchema names -> Query exprs -> Hasql.Statement () ()
52+createOrReplaceView =
53+ createViewGeneric CreateOrReplace
54+
55+
56+createViewGeneric :: Selects names exprs
57+ => CreateView -> TableSchema names -> Query exprs -> Hasql.Statement () ()
58+createViewGeneric replace schema query =
59+ Hasql.Statement bytes params decode prepare
45 where
60 where
46− session = Hasql.statement () statement
47− statement = Hasql.Statement bytes params decode prepare
48 bytes = encodeUtf8 (Text.pack sql)
61 bytes = encodeUtf8 (Text.pack sql)
49 params = Hasql.noParams
62 params = Hasql.noParams
50 decode = Hasql.noResult
63 decode = Hasql.noResult
51 prepare = False
64 prepare = False
65+ sql = show doc
52− sql = show (ppCreateView schema query)
66+ doc = ppCreateView schema query replace
53
67
54
68
55ppCreateView :: Selects names exprs
69ppCreateView :: Selects names exprs
56− => TableSchema names -> Query exprs -> Doc
70+ => TableSchema names -> Query exprs -> CreateView -> Doc
57−ppCreateView schema query =
71+ppCreateView schema query replace =
58− text "CREATE VIEW" <+>
72+ createOrReplace replace <+>
59 ppInto schema $$
73 ppInto schema $$
60 text "AS" <+>
74 text "AS" <+>
61− fromMaybe fallback (ppSelect query)
75+ ppSelect query
62 where
76 where
63− fallback = fold (ppSelect (emptyTable `asTypeOf` query))
77+ createOrReplace Create = text "CREATE VIEW"
78+ createOrReplace CreateOrReplace = text "CREATE OR REPLACE VIEW"
src/Rel8/Table/Aggregate.hs+12
@@ -37,6 +37,18 @@ import Rel8.Type.Eq ( DBEq )
37
37
38-- | Group equal tables together. This works by aggregating each column in the
38-- | Group equal tables together. This works by aggregating each column in the
39-- given table with 'groupByExpr'.
39-- given table with 'groupByExpr'.
40+--
41+-- For example, if we have a table of items, we could group the items by the
42+-- order they belong to:
43+--
44+-- @
45+-- itemsByOrder :: Query (OrderId Expr, ListTable Expr (Item Expr))
46+-- itemsByOrder = aggregate $ do
47+-- item <- each itemSchema
48+-- let orderId = groupBy (itemOrderId item)
49+-- let orderItems = listAgg item
50+-- pure (orderId, orderItems)
51+-- @
40groupBy :: forall exprs aggregates. (EqTable exprs, Aggregates aggregates exprs)
52groupBy :: forall exprs aggregates. (EqTable exprs, Aggregates aggregates exprs)
41 => exprs -> aggregates
53 => exprs -> aggregates
42groupBy = fromColumns . hgroupBy (eqTable @exprs) . toColumns
54groupBy = fromColumns . hgroupBy (eqTable @exprs) . toColumns
src/Rel8/Table/HKD.hs+1
@@ -157,6 +157,7 @@ instance
157 => HKDable a
157 => HKDable a
158
158
159
159
160+type Top_ :: Constraint
160class Top_
161class Top_
161instance Top_
162instance Top_
162
163
src/Rel8/Table/List.hs+14−3
@@ -11,7 +11,9 @@
11
11
12module Rel8.Table.List
12module Rel8.Table.List
13 ( ListTable(..)
13 ( ListTable(..)
14+ , ($*)
15+ , listTable
14− , listTable, nameListTable
16+ , nameListTable
15 )
17 )
16where
18where
17
19
@@ -28,7 +30,7 @@ import Rel8.Schema.HTable.List ( HListTable )
28import Rel8.Schema.HTable.Vectorize
30import Rel8.Schema.HTable.Vectorize
29 ( hvectorize, hunvectorize
31 ( hvectorize, hunvectorize
30 , happend, hempty
32 , happend, hempty
31− , hproject
33+ , hproject, hcolumn
32 )
34 )
33import qualified Rel8.Schema.Kind as K
35import qualified Rel8.Schema.Kind as K
34import Rel8.Schema.Name ( Name( Name ) )
36import Rel8.Schema.Name ( Name( Name ) )
@@ -46,7 +48,9 @@ import Rel8.Table.Alternative
46 )
48 )
47import Rel8.Table.Eq ( EqTable, eqTable )
49import Rel8.Table.Eq ( EqTable, eqTable )
48import Rel8.Table.Ord ( OrdTable, ordTable )
50import Rel8.Table.Ord ( OrdTable, ordTable )
49−import Rel8.Table.Projection ( Projectable, project, apply )
51+import Rel8.Table.Projection
52+ ( Projectable, Projecting, Projection, project, apply
53+ )
50import Rel8.Table.Serialize ( ToExprs )
54import Rel8.Table.Serialize ( ToExprs )
51
55
52
56
@@ -116,6 +120,13 @@ instance (context ~ Expr, Table Expr a) =>
116 mempty = ListTable $ hempty $ \Spec {info} -> sempty info
120 mempty = ListTable $ hempty $ \Spec {info} -> sempty info
117
121
118
122
123+-- | Project a single expression out of a 'ListTable'.
124+($*) :: Projecting a (Expr b)
125+ => Projection a (Expr b) -> ListTable Expr a -> Expr [b]
126+f $* ListTable a = hcolumn $ hproject (apply f) a
127+infixl 4 $*
128+
129+
119-- | Construct a @ListTable@ from a list of expressions.
130-- | Construct a @ListTable@ from a list of expressions.
120listTable :: Table Expr a => [a] -> ListTable Expr a
131listTable :: Table Expr a => [a] -> ListTable Expr a
121listTable =
132listTable =
src/Rel8/Table/Maybe.hs+6
@@ -15,6 +15,7 @@ module Rel8.Table.Maybe
15 ( MaybeTable(..)
15 ( MaybeTable(..)
16 , maybeTable, nothingTable, justTable
16 , maybeTable, nothingTable, justTable
17 , isNothingTable, isJustTable
17 , isNothingTable, isJustTable
18+ , fromMaybeTable
18 , ($?)
19 , ($?)
19 , aggregateMaybeTable
20 , aggregateMaybeTable
20 , nameMaybeTable
21 , nameMaybeTable
@@ -206,6 +207,11 @@ justTable :: a -> MaybeTable Expr a
206justTable = MaybeTable mempty . pure
207justTable = MaybeTable mempty . pure
207
208
208
209
210+-- | 'Data.Maybe.fromMaybe' for 'MaybeTable's.
211+fromMaybeTable :: Table Expr a => a -> MaybeTable Expr a -> a
212+fromMaybeTable fallback = maybeTable fallback id
213+
214+
209-- | Project a single expression out of a 'MaybeTable'. You can think of this
215-- | Project a single expression out of a 'MaybeTable'. You can think of this
210-- operator like the '$' operator, but it also has the ability to return
216-- operator like the '$' operator, but it also has the ability to return
211-- @null@.
217-- @null@.
src/Rel8/Table/NonEmpty.hs+14−3
@@ -11,7 +11,9 @@
11
11
12module Rel8.Table.NonEmpty
12module Rel8.Table.NonEmpty
13 ( NonEmptyTable(..)
13 ( NonEmptyTable(..)
14+ , ($+)
15+ , nonEmptyTable
14− , nonEmptyTable, nameNonEmptyTable
16+ , nameNonEmptyTable
15 )
17 )
16where
18where
17
19
@@ -29,7 +31,7 @@ import Rel8.Schema.HTable.NonEmpty ( HNonEmptyTable )
29import Rel8.Schema.HTable.Vectorize
31import Rel8.Schema.HTable.Vectorize
30 ( hvectorize, hunvectorize
32 ( hvectorize, hunvectorize
31 , happend
33 , happend
32− , hproject
34+ , hproject, hcolumn
33 )
35 )
34import qualified Rel8.Schema.Kind as K
36import qualified Rel8.Schema.Kind as K
35import Rel8.Schema.Name ( Name( Name ) )
37import Rel8.Schema.Name ( Name( Name ) )
@@ -44,7 +46,9 @@ import Rel8.Table
44import Rel8.Table.Alternative ( AltTable, (<|>:) )
46import Rel8.Table.Alternative ( AltTable, (<|>:) )
45import Rel8.Table.Eq ( EqTable, eqTable )
47import Rel8.Table.Eq ( EqTable, eqTable )
46import Rel8.Table.Ord ( OrdTable, ordTable )
48import Rel8.Table.Ord ( OrdTable, ordTable )
47−import Rel8.Table.Projection ( Projectable, project, apply )
49+import Rel8.Table.Projection
50+ ( Projectable, Projecting, Projection, project, apply
51+ )
48import Rel8.Table.Serialize ( ToExprs )
52import Rel8.Table.Serialize ( ToExprs )
49
53
50
54
@@ -110,6 +114,13 @@ instance (Table Expr a, context ~ Expr) => Semigroup (NonEmptyTable context a)
110 happend (const sappend1) as bs
114 happend (const sappend1) as bs
111
115
112
116
117+-- | Project a single expression out of a 'NonEmptyTable'.
118+($+) :: Projecting a (Expr b)
119+ => Projection a (Expr b) -> NonEmptyTable Expr a -> Expr (NonEmpty b)
120+f $+ NonEmptyTable a = hcolumn $ hproject (apply f) a
121+infixl 4 $+
122+
123+
113-- | Construct a @NonEmptyTable@ from a non-empty list of expressions.
124-- | Construct a @NonEmptyTable@ from a non-empty list of expressions.
114nonEmptyTable :: Table Expr a => NonEmpty a -> NonEmptyTable Expr a
125nonEmptyTable :: Table Expr a => NonEmpty a -> NonEmptyTable Expr a
115nonEmptyTable =
126nonEmptyTable =
src/Rel8/Table/Null.hsnew file+147
@@ -0,0 +1,147 @@
1+{-# language DataKinds #-}
2+{-# language FlexibleContexts #-}
3+{-# language FlexibleInstances #-}
4+{-# language MultiParamTypeClasses #-}
5+{-# language ScopedTypeVariables #-}
6+{-# language StandaloneKindSignatures #-}
7+{-# language TypeApplications #-}
8+{-# language TypeFamilies #-}
9+{-# language UndecidableInstances #-}
10+
11+module Rel8.Table.Null
12+ ( NullTable(..)
13+ , nullableTable, nullTable, nullifyTable, unsafeUnnullifyTable
14+ , isNullTable, isNonNullTable
15+ , nameNullTable
16+ , toMaybeTable, toNullTable
17+ )
18+where
19+
20+-- base
21+import Data.Kind ( Type )
22+import Prelude hiding ( null, undefined )
23+
24+-- comonad
25+import Control.Comonad ( extract )
26+
27+-- rel8
28+import Rel8.Expr ( Expr )
29+import Rel8.Expr.Bool ( not_ )
30+import Rel8.Kind.Context ( Reifiable )
31+import qualified Rel8.Schema.Kind as K
32+import Rel8.Schema.Name ( Name )
33+import Rel8.Table
34+ ( Table, Columns, Context, fromColumns, toColumns
35+ , FromExprs, fromResult, toResult
36+ , Transpose
37+ )
38+import Rel8.Table.Alternative
39+ ( AltTable, (<|>:)
40+ , AlternativeTable, emptyTable
41+ )
42+import Rel8.Table.Bool ( bool )
43+import Rel8.Table.Eq ( EqTable, eqTable )
44+import Rel8.Table.Maybe ( MaybeTable, justTable, maybeTable, nothingTable )
45+import Rel8.Table.Nullify ( Nullify, isNull )
46+import Rel8.Table.Ord ( OrdTable, ordTable )
47+import Rel8.Table.Projection ( Projectable, project )
48+import Rel8.Table.Serialize ( ToExprs )
49+import Rel8.Table.Undefined ( undefined )
50+
51+
52+-- | @NullTable t@ is the table @t@, but where all the columns in @t@ have the
53+-- possibility of being 'Rel8.null'. This is very similar to
54+-- 'Rel8.MaybeTable', except that it does not use an extra tag field, so it
55+-- cannot distinguish between @Nothing@ and @Just Nothing@ if nested. In other
56+-- words, if all of the columns of the @t@ passed to @NullTable@ are already
57+-- nullable, then @NullTable@ has no effect.
58+type NullTable :: K.Context -> Type -> Type
59+newtype NullTable context a = NullTable (Nullify context a)
60+
61+
62+instance Projectable (NullTable context) where
63+ project f (NullTable a) = NullTable (project f a)
64+
65+
66+instance context ~ Expr => AltTable (NullTable context) where
67+ ma <|>: mb = bool ma mb (isNullTable ma)
68+
69+
70+instance context ~ Expr => AlternativeTable (NullTable context) where
71+ emptyTable = nullTable
72+
73+
74+instance (Table context a, Reifiable context, context ~ context') =>
75+ Table context' (NullTable context a)
76+ where
77+ type Columns (NullTable context a) = Columns (Nullify context a)
78+ type Context (NullTable context a) = Context (Nullify context a)
79+ type FromExprs (NullTable context a) = FromExprs (Nullify context a)
80+ type Transpose to (NullTable context a) = NullTable to (Transpose to a)
81+
82+ toColumns (NullTable a) = toColumns a
83+ fromColumns = NullTable . fromColumns
84+
85+ toResult = toResult @_ @(Nullify context a)
86+ fromResult = fromResult @_ @(Nullify context a)
87+
88+
89+instance (EqTable a, context ~ Expr) => EqTable (NullTable context a) where
90+ eqTable = eqTable @(Nullify context a)
91+
92+
93+instance (OrdTable a, context ~ Expr) => OrdTable (NullTable context a) where
94+ ordTable = ordTable @(Nullify context a)
95+
96+
97+instance (ToExprs exprs a, context ~ Expr) =>
98+ ToExprs (NullTable context exprs) (Maybe a)
99+
100+
101+-- | Check if any of the non-nullable fields of @a@ are 'Rel8.null' under the
102+-- 'NullTable'. Returns 'Rel8.false' if @a@ has no non-nullable fields.
103+isNullTable :: Table Expr a => NullTable Expr a -> Expr Bool
104+isNullTable (NullTable a) = isNull a
105+
106+
107+-- | The inverse of 'isNullTable'.
108+isNonNullTable :: Table Expr a => NullTable Expr a -> Expr Bool
109+isNonNullTable = not_ . isNullTable
110+
111+
112+-- | Like 'Rel8.nullable'.
113+nullableTable :: (Table Expr a, Table Expr b)
114+ => b -> (a -> b) -> NullTable Expr a -> b
115+nullableTable b f ma@(NullTable a) = bool (f (extract a)) b (isNullTable ma)
116+
117+
118+-- | The null table. Like 'Rel8.null'.
119+nullTable :: Table Expr a => NullTable Expr a
120+nullTable = NullTable (pure undefined)
121+
122+
123+-- | Lift any table into 'NullTable'. Like 'Rel8.nullify'.
124+nullifyTable :: a -> NullTable Expr a
125+nullifyTable = NullTable . pure
126+
127+
128+unsafeUnnullifyTable :: NullTable Expr a -> a
129+unsafeUnnullifyTable (NullTable a) = extract a
130+
131+
132+-- | Construct a 'NullTable' in the 'Name' context. This can be useful if you
133+-- have a 'NullTable' that you are storing in a table and need to construct a
134+-- 'TableSchema'.
135+nameNullTable :: a -> NullTable Name a
136+nameNullTable = NullTable . pure
137+
138+
139+-- | Convert a 'NullTable' to a 'MaybeTable'.
140+toMaybeTable :: Table Expr a => NullTable Expr a -> MaybeTable Expr a
141+toMaybeTable = nullableTable nothingTable justTable
142+
143+
144+-- | Convert a 'MaybeTable' to a 'NullTable'. Note that if the underlying @a@
145+-- has no non-nullable fields, this is a lossy conversion.
146+toNullTable :: Table Expr a => MaybeTable Expr a -> NullTable Expr a
147+toNullTable = maybeTable nullTable nullifyTable
src/Rel8/Table/Nullify.hs+27
@@ -1,7 +1,9 @@
1{-# language DataKinds #-}
1{-# language DataKinds #-}
2+{-# language FlexibleContexts #-}
2{-# language FlexibleInstances #-}
3{-# language FlexibleInstances #-}
3{-# language LambdaCase #-}
4{-# language LambdaCase #-}
4{-# language MultiParamTypeClasses #-}
5{-# language MultiParamTypeClasses #-}
6+{-# language NamedFieldPuns #-}
5{-# language ScopedTypeVariables #-}
7{-# language ScopedTypeVariables #-}
6{-# language StandaloneKindSignatures #-}
8{-# language StandaloneKindSignatures #-}
7{-# language TypeApplications #-}
9{-# language TypeApplications #-}
@@ -12,11 +14,13 @@ module Rel8.Table.Nullify
12 ( Nullify
14 ( Nullify
13 , aggregateNullify
15 , aggregateNullify
14 , guard
16 , guard
17+ , isNull
15 )
18 )
16where
19where
17
20
18-- base
21-- base
19import Control.Applicative ( liftA2 )
22import Control.Applicative ( liftA2 )
23+import Data.Functor.Const ( Const( Const ), getConst )
20import Data.Functor.Identity ( runIdentity )
24import Data.Functor.Identity ( runIdentity )
21import Data.Kind ( Type )
25import Data.Kind ( Type )
22import Prelude
26import Prelude
@@ -27,6 +31,8 @@ import Control.Comonad ( Comonad, duplicate, extract, ComonadApply, (<@>) )
27-- rel8
31-- rel8
28import Rel8.Aggregate ( Aggregate )
32import Rel8.Aggregate ( Aggregate )
29import Rel8.Expr ( Expr )
33import Rel8.Expr ( Expr )
34+import Rel8.Expr.Bool ( (||.), false )
35+import qualified Rel8.Expr.Null as Expr
30import Rel8.Kind.Context ( Reifiable, contextSing )
36import Rel8.Kind.Context ( Reifiable, contextSing )
31import Rel8.Schema.Context.Nullify
37import Rel8.Schema.Context.Nullify
32 ( Nullifiability( NAggregate, NExpr )
38 ( Nullifiability( NAggregate, NExpr )
@@ -45,12 +51,14 @@ import Rel8.Schema.HTable.Nullify
45 , hproject
51 , hproject
46 )
52 )
47import qualified Rel8.Schema.Kind as K
53import qualified Rel8.Schema.Kind as K
54+import Rel8.Schema.Null ( Nullity( NotNull, Null ) )
48import qualified Rel8.Schema.Result as R
55import qualified Rel8.Schema.Result as R
49import Rel8.Table
56import Rel8.Table
50 ( Table, Columns, Context, toColumns, fromColumns
57 ( Table, Columns, Context, toColumns, fromColumns
51 , FromExprs, fromResult, toResult
58 , FromExprs, fromResult, toResult
52 , Transpose
59 , Transpose
53 )
60 )
61+import Rel8.Schema.Spec ( Spec( Spec, nullity ) )
54import Rel8.Table.Eq ( EqTable, eqTable )
62import Rel8.Table.Eq ( EqTable, eqTable )
55import Rel8.Table.Ord ( OrdTable, ordTable )
63import Rel8.Table.Ord ( OrdTable, ordTable )
56import Rel8.Table.Projection ( Projectable, apply, project )
64import Rel8.Table.Projection ( Projectable, apply, project )
@@ -182,3 +190,22 @@ guard :: (Reifiable context, HTable t)
182 -> HNullify t context
190 -> HNullify t context
183guard tag isNonNull isNonNullExpr =
191guard tag isNonNull isNonNullExpr =
184 hguard (guarder contextSing tag isNonNull isNonNullExpr)
192 hguard (guarder contextSing tag isNonNull isNonNullExpr)
193+
194+
195+isNull :: forall a. Table Expr a => Nullify Expr a -> Expr Bool
196+isNull =
197+ maybe false getAny .
198+ getConst .
199+ hunnullify (\Spec {nullity} a -> Const $ case nullity of
200+ NotNull -> Just $ Any $ Expr.isNull a
201+ Null -> Nothing) .
202+ toColumns
203+
204+
205+newtype Any = Any
206+ { getAny :: Expr Bool
207+ }
208+
209+
210+instance Semigroup Any where
211+ Any a <> Any b = Any (a ||. b)
src/Rel8/Table/Opaleye.hs+31−47
@@ -7,6 +7,8 @@
7{-# language TypeFamilies #-}
7{-# language TypeFamilies #-}
8{-# language ViewPatterns #-}
8{-# language ViewPatterns #-}
9
9
10+{-# options_ghc -Wno-deprecations #-}
11+
10module Rel8.Table.Opaleye
12module Rel8.Table.Opaleye
11 ( aggregator
13 ( aggregator
12 , attributes
14 , attributes
@@ -26,17 +28,15 @@ where
26-- base
28-- base
27import Data.Functor.Const ( Const( Const ), getConst )
29import Data.Functor.Const ( Const( Const ), getConst )
28import Data.List.NonEmpty ( NonEmpty )
30import Data.List.NonEmpty ( NonEmpty )
29−import Prelude hiding ( undefined )
31+import Prelude
30
32
31-- opaleye
33-- opaleye
34+import qualified Opaleye.Adaptors as Opaleye
35+import qualified Opaleye.Field as Opaleye ( Field_ )
32import qualified Opaleye.Internal.Aggregate as Opaleye
36import qualified Opaleye.Internal.Aggregate as Opaleye
33−import qualified Opaleye.Internal.Binary as Opaleye
34−import qualified Opaleye.Internal.Distinct as Opaleye
35import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
37import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye
36−import qualified Opaleye.Internal.PackMap as Opaleye
37−import qualified Opaleye.Internal.Unpackspec as Opaleye
38import qualified Opaleye.Internal.Values as Opaleye
38import qualified Opaleye.Internal.Values as Opaleye
39−import qualified Opaleye.Internal.Table as Opaleye
39+import qualified Opaleye.Table as Opaleye
40
40
41-- profunctors
41-- profunctors
42import Data.Profunctor ( dimap, lmap )
42import Data.Profunctor ( dimap, lmap )
@@ -46,27 +46,25 @@ import Rel8.Aggregate ( Aggregate( Aggregate ), Aggregates )
46import Rel8.Expr ( Expr )
46import Rel8.Expr ( Expr )
47import Rel8.Expr.Opaleye
47import Rel8.Expr.Opaleye
48 ( fromPrimExpr, toPrimExpr
48 ( fromPrimExpr, toPrimExpr
49− , traversePrimExpr
50− , fromColumn, toColumn
51− , scastExpr
49+ , scastExpr, traverseFieldP
52 )
50 )
53−import Rel8.Schema.HTable ( htabulateA, hfield, htraverse, hspecs, htabulate )
51+import Rel8.Schema.HTable ( htabulateA, hfield, hspecs, htabulate,
52+ htraverseP, htraversePWithField )
54import Rel8.Schema.Name ( Name( Name ), Selects, ppColumn )
53import Rel8.Schema.Name ( Name( Name ), Selects, ppColumn )
55import Rel8.Schema.Spec ( Spec(..) )
54import Rel8.Schema.Spec ( Spec(..) )
56import Rel8.Schema.Table ( TableSchema(..), ppTable )
55import Rel8.Schema.Table ( TableSchema(..), ppTable )
57import Rel8.Table ( Table, fromColumns, toColumns )
56import Rel8.Table ( Table, fromColumns, toColumns )
58−import Rel8.Table.Undefined ( undefined )
57+import Rel8.Type.Information ( typeName )
59
58
60-- semigroupoids
59-- semigroupoids
61import Data.Functor.Apply ( WrappedApplicative(..) )
60import Data.Functor.Apply ( WrappedApplicative(..) )
61+import Data.Profunctor.Product ( ProductProfunctor )
62
62
63
63
64aggregator :: Aggregates aggregates exprs => Opaleye.Aggregator aggregates exprs
64aggregator :: Aggregates aggregates exprs => Opaleye.Aggregator aggregates exprs
65−aggregator = Opaleye.Aggregator $ Opaleye.PackMap $ \f aggregates ->
65+aggregator = dimap toColumns fromColumns $
66− fmap fromColumns $ unwrapApplicative $ htabulateA $ \field ->
66+ htraverseP $
67− WrapApplicative $ case hfield (toColumns aggregates) field of
67+ lmap (\(Aggregate a) -> (a, ())) Opaleye.aggregatorApply
68− Aggregate (Opaleye.Aggregator (Opaleye.PackMap inner)) ->
69− inner f ()
70
68
71
69
72attributes :: Selects names exprs => TableSchema names -> exprs
70attributes :: Selects names exprs => TableSchema names -> exprs
@@ -77,22 +75,19 @@ attributes schema@TableSchema {columns} = fromColumns $ htabulate $ \field ->
77 show (ppTable schema) <> "." <> show (ppColumn column)
75 show (ppTable schema) <> "." <> show (ppColumn column)
78
76
79
77
78+fromOpaleyespec :: (ProductProfunctor p, Table Expr a)
79+ => p (Opaleye.Field_ n x) (Opaleye.Field_ n x)
80+ -> p a a
81+fromOpaleyespec x =
82+ dimap toColumns fromColumns (htraverseP (traverseFieldP x))
83+
84+
80binaryspec :: Table Expr a => Opaleye.Binaryspec a a
85binaryspec :: Table Expr a => Opaleye.Binaryspec a a
81−binaryspec = Opaleye.Binaryspec $ Opaleye.PackMap $ \f (as, bs) ->
86+binaryspec = fromOpaleyespec Opaleye.binaryspecField
82− fmap fromColumns $ unwrapApplicative $ htabulateA $ \field ->
83− WrapApplicative $
84− case (hfield (toColumns as) field, hfield (toColumns bs) field) of
85− (a, b) -> fromPrimExpr <$> f (toPrimExpr a, toPrimExpr b)
86
87
87
88
88distinctspec :: Table Expr a => Opaleye.Distinctspec a a
89distinctspec :: Table Expr a => Opaleye.Distinctspec a a
89−distinctspec =
90+distinctspec = fromOpaleyespec Opaleye.distinctspecField
90− Opaleye.Distinctspec $ Opaleye.Aggregator $ Opaleye.PackMap $ \f ->
91− fmap fromColumns .
92− unwrapApplicative .
93− htraverse
94− (\a -> WrapApplicative $ fromPrimExpr <$> f (Nothing, toPrimExpr a)) .
95− toColumns
96
91
97
92
98exprs :: Table Expr a => a -> NonEmpty Opaleye.PrimExpr
93exprs :: Table Expr a => a -> NonEmpty Opaleye.PrimExpr
@@ -111,8 +106,8 @@ exprsWithNames names as = getConst $ htabulateA $ \field ->
111table :: Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs
106table :: Selects names exprs => TableSchema names -> Opaleye.Table exprs exprs
112table (TableSchema name schema columns) =
107table (TableSchema name schema columns) =
113 case schema of
108 case schema of
114− Nothing -> Opaleye.Table name (tableFields columns)
109+ Nothing -> Opaleye.table name (tableFields columns)
115− Just schemaName -> Opaleye.TableWithSchema schemaName name (tableFields columns)
110+ Just schemaName -> Opaleye.tableWithSchema schemaName name (tableFields columns)
116
111
117
112
118tableFields :: Selects names exprs
113tableFields :: Selects names exprs
@@ -124,21 +119,19 @@ tableFields (toColumns -> names) = dimap toColumns fromColumns $
124 where
119 where
125 go :: Name a -> Opaleye.TableFields (Expr a) (Expr a)
120 go :: Name a -> Opaleye.TableFields (Expr a) (Expr a)
126 go (Name name) =
121 go (Name name) =
127− dimap (toColumn . toPrimExpr) (fromPrimExpr . fromColumn) $
122+ traverseFieldP $
128 Opaleye.requiredTableField name
123 Opaleye.requiredTableField name
129
124
130
125
131unpackspec :: Table Expr a => Opaleye.Unpackspec a a
126unpackspec :: Table Expr a => Opaleye.Unpackspec a a
132−unpackspec = Opaleye.Unpackspec $ Opaleye.PackMap $ \f ->
127+unpackspec = fromOpaleyespec Opaleye.unpackspecField
133− fmap fromColumns .
134− unwrapApplicative .
135− htraverse (WrapApplicative . traversePrimExpr f) .
136− toColumns
137{-# INLINABLE unpackspec #-}
128{-# INLINABLE unpackspec #-}
138
129
139
130
140−valuesspec :: Table Expr a => Opaleye.ValuesspecSafe a a
131+valuesspec :: Table Expr a => Opaleye.Valuesspec a a
141−valuesspec = Opaleye.ValuesspecSafe (toPackMap undefined) unpackspec
132+valuesspec = dimap toColumns fromColumns $
133+ htraversePWithField (traverseFieldP . Opaleye.valuesspecFieldType . typeName)
134+ where typeName = Rel8.Type.Information.typeName . info . hfield hspecs
142
135
143
136
144view :: Selects names exprs => names -> exprs
137view :: Selects names exprs => names -> exprs
@@ -147,15 +140,6 @@ view columns = fromColumns $ htabulate $ \field ->
147 Name column -> fromPrimExpr $ Opaleye.BaseTableAttrExpr column
140 Name column -> fromPrimExpr $ Opaleye.BaseTableAttrExpr column
148
141
149
142
150−toPackMap :: Table Expr a
151− => a -> Opaleye.PackMap Opaleye.PrimExpr Opaleye.PrimExpr () a
152−toPackMap as = Opaleye.PackMap $ \f () ->
153− fmap fromColumns $
154− unwrapApplicative .
155− htraverse (WrapApplicative . traversePrimExpr f) $
156− toColumns as
157−
158−
159-- | Transform a table by adding 'CAST' to all columns. This is most useful for
143-- | Transform a table by adding 'CAST' to all columns. This is most useful for
160-- finalising a SELECT or RETURNING statement, guaranteed that the output
144-- finalising a SELECT or RETURNING statement, guaranteed that the output
161-- matches what is encoded in each columns TypeInformation.
145-- matches what is encoded in each columns TypeInformation.
src/Rel8/Table/Ord.hs+2−2
@@ -146,9 +146,9 @@ infix 4 >=:
146
146
147-- | Given two 'Table's, return the table that sorts before the other.
147-- | Given two 'Table's, return the table that sorts before the other.
148least :: OrdTable a => a -> a -> a
148least :: OrdTable a => a -> a -> a
149−least a b = bool a b (a <: b)
149+least a b = bool b a (a <: b)
150
150
151
151
152-- | Given two 'Table's, return the table that sorts after the other.
152-- | Given two 'Table's, return the table that sorts after the other.
153greatest :: OrdTable a => a -> a -> a
153greatest :: OrdTable a => a -> a -> a
154−greatest a b = bool a b (a >: b)
154+greatest a b = bool b a (a >: b)
src/Rel8/Table/Projection.hs+1−1
@@ -35,7 +35,7 @@ class
35 => Projecting a b
35 => Projecting a b
36instance
36instance
37 ( Transposes (Context a) (Field a) a (Transpose (Field a) a)
37 ( Transposes (Context a) (Field a) a (Transpose (Field a) a)
38− , Transposes (Context a) (Field a) b (Transpose (Field b) b)
38+ , Transposes (Context a) (Field a) b (Transpose (Field a) b)
39 )
39 )
40 => Projecting a b
40 => Projecting a b
41
41
src/Rel8/Table/These.hs+14−3
@@ -20,6 +20,7 @@ module Rel8.Table.These
20 , isThisTable, isThatTable, isThoseTable
20 , isThisTable, isThatTable, isThoseTable
21 , hasHereTable, hasThereTable
21 , hasHereTable, hasThereTable
22 , justHereTable, justThereTable
22 , justHereTable, justThereTable
23+ , alignMaybeTable
23 , aggregateTheseTable
24 , aggregateTheseTable
24 , nameTheseTable
25 , nameTheseTable
25 )
26 )
@@ -29,13 +30,13 @@ where
29import Data.Bifunctor ( Bifunctor, bimap )
30import Data.Bifunctor ( Bifunctor, bimap )
30import Data.Kind ( Type )
31import Data.Kind ( Type )
31import Data.Maybe ( isJust )
32import Data.Maybe ( isJust )
32−import Prelude hiding ( undefined )
33+import Prelude hiding ( null, undefined )
33
34
34-- rel8
35-- rel8
35import Rel8.Aggregate ( Aggregate )
36import Rel8.Aggregate ( Aggregate )
36import Rel8.Expr ( Expr )
37import Rel8.Expr ( Expr )
37−import Rel8.Expr.Bool ( (&&.), not_ )
38+import Rel8.Expr.Bool ( (&&.), (||.), boolExpr, not_ )
38−import Rel8.Expr.Null ( isNonNull )
39+import Rel8.Expr.Null ( null, isNonNull )
39import Rel8.Kind.Context ( Reifiable )
40import Rel8.Kind.Context ( Reifiable )
40import Rel8.Schema.Context.Nullify ( Nullifiable )
41import Rel8.Schema.Context.Nullify ( Nullifiable )
41import Rel8.Schema.Dict ( Dict( Dict ) )
42import Rel8.Schema.Dict ( Dict( Dict ) )
@@ -288,6 +289,16 @@ justThereTable :: TheseTable context a b -> MaybeTable context b
288justThereTable = there
289justThereTable = there
289
290
290
291
292+-- | Construct a @TheseTable@ from two 'MaybeTable's.
293+alignMaybeTable :: ()
294+ => MaybeTable Expr a
295+ -> MaybeTable Expr b
296+ -> MaybeTable Expr (TheseTable Expr a b)
297+alignMaybeTable a b = MaybeTable tag (pure (TheseTable a b))
298+ where
299+ tag = boolExpr null mempty (isJustTable a ||. isJustTable b)
300+
301+
291-- | Construct a @TheseTable@. Corresponds to 'This'.
302-- | Construct a @TheseTable@. Corresponds to 'This'.
292thisTable :: Table Expr b => a -> TheseTable Expr a b
303thisTable :: Table Expr b => a -> TheseTable Expr a b
293thisTable a = TheseTable (justTable a) nothingTable
304thisTable a = TheseTable (justTable a) nothingTable
src/Rel8/Table/Window.hsnew file+43
@@ -0,0 +1,43 @@
1+{-# language MonoLocalBinds #-}
2+
3+module Rel8.Table.Window
4+ ( cumulative
5+ , cumulative_
6+ , currentRow
7+ )
8+where
9+
10+-- base
11+import Prelude
12+
13+-- opaleye
14+import qualified Opaleye.Window as Opaleye
15+
16+-- rel8
17+import Rel8.Aggregate ( Aggregates )
18+import qualified Rel8.Expr.Window as Expr
19+import Rel8.Schema.HTable ( hfield, htabulateA )
20+import Rel8.Table ( fromColumns, toColumns )
21+import Rel8.Window ( Window( Window ) )
22+
23+
24+-- | 'cumulative' allows the use of aggregation functions in 'Window'
25+-- expressions. In particular, @'cumulative' 'Rel8.sum'@
26+-- (when combined with 'Rel8.Window.orderPartitionBy') gives a running total,
27+-- also known as a \"cumulative sum\", hence the name @cumulative@.
28+cumulative :: Aggregates aggregates exprs => (a -> aggregates) -> Window a exprs
29+cumulative f =
30+ fmap fromColumns $
31+ htabulateA $ \field ->
32+ Expr.cumulative ((`hfield` field) . toColumns . f)
33+
34+
35+-- | A version of 'cumulative' for use with nullary aggregators like
36+-- 'Rel8.Expr.Aggregate.countStar'.
37+cumulative_ :: Aggregates aggregates exprs => aggregates -> Window a exprs
38+cumulative_ = cumulative . const
39+
40+
41+-- | Return every column of the current row of a window query.
42+currentRow :: Window a a
43+currentRow = Window $ Opaleye.over (Opaleye.noWindowFunction id) mempty mempty
src/Rel8/Tabulate.hsnew file+640
@@ -0,0 +1,640 @@
1+{-# language FlexibleContexts #-}
2+{-# language MonoLocalBinds #-}
3+{-# language ScopedTypeVariables #-}
4+{-# language StandaloneKindSignatures #-}
5+{-# language TypeApplications #-}
6+{-# language TupleSections #-}
7+{-# language UndecidableInstances #-}
8+
9+-- | "Rel8.Tabulate" provides an alternative API ('Tabulation') for writing
10+-- queries that complements the main "Rel8" API ('Query').
11+
12+module Rel8.Tabulate
13+ (
14+ Tabulation
15+
16+ -- * Interfacing with 'Query's
17+ , fromQuery
18+ , toQuery
19+ , liftQuery
20+ , through
21+ , lookup
22+
23+ -- * Aggregation and Ordering
24+ , aggregate
25+ , distinct
26+ , order
27+
28+ -- ** Magic 'Tabulation's
29+ -- $magic
30+ , count
31+ , optional
32+ , many
33+ , some
34+ , exists
35+ , present
36+ , absent
37+
38+ -- * Natural joins
39+ , align
40+ , alignWith
41+ , leftAlign
42+ , leftAlignWith
43+ , rightAlign
44+ , rightAlignWith
45+ , zip
46+ , zipWith
47+ , similarity
48+ , difference
49+ )
50+where
51+
52+-- base
53+import Control.Applicative ( (<|>), empty, liftA2 )
54+import Control.Monad ( liftM2 )
55+import Data.Bifunctor ( Bifunctor, bimap, first, second )
56+import Data.Foldable ( traverse_ )
57+import Data.Function ( on )
58+import Data.Functor.Contravariant ( Contravariant, (>$<), contramap )
59+import Data.Int ( Int64 )
60+import Data.Kind ( Type )
61+import Data.Maybe ( fromJust, fromMaybe )
62+import Prelude hiding ( lookup, zip, zipWith )
63+
64+-- bifunctors
65+import Data.Bifunctor.Clown ( Clown( Clown ), runClown )
66+
67+-- comonad
68+import Control.Comonad ( extract )
69+
70+-- opaleye
71+import qualified Opaleye.Aggregate as Opaleye
72+import qualified Opaleye.Order as Opaleye ( orderBy, distinctOnExplicit )
73+
74+-- profunctors
75+import Data.Profunctor ( dimap, lmap )
76+
77+-- product-profunctors
78+import Data.Profunctor.Product
79+ ( ProductProfunctor, (***!)
80+ , SumProfunctor, (+++!)
81+ )
82+import qualified Data.Profunctor.Product as PP
83+
84+-- rel8
85+import Rel8.Aggregate ( Aggregates )
86+import Rel8.Expr ( Expr )
87+import Rel8.Expr.Aggregate ( countStar )
88+import Rel8.Expr.Bool ( true )
89+import Rel8.Order ( Order( Order ) )
90+import Rel8.Query ( Query )
91+import qualified Rel8.Query.Exists as Q ( exists, present, absent )
92+import Rel8.Query.Filter ( where_ )
93+import Rel8.Query.List ( catNonEmptyTable )
94+import qualified Rel8.Query.Maybe as Q ( optional )
95+import Rel8.Query.Opaleye ( mapOpaleye, unsafePeekQuery )
96+import Rel8.Query.Rebind ( rebind )
97+import Rel8.Query.These ( alignBy )
98+import Rel8.Table ( Table, fromColumns, toColumns )
99+import Rel8.Table.Aggregate ( hgroupBy, listAgg, nonEmptyAgg )
100+import Rel8.Table.Alternative
101+ ( AltTable, (<|>:)
102+ , AlternativeTable, emptyTable
103+ )
104+import Rel8.Table.Cols ( fromCols, toCols )
105+import Rel8.Table.Eq ( EqTable, (==:), eqTable )
106+import Rel8.Table.List ( ListTable( ListTable ) )
107+import Rel8.Table.Maybe ( MaybeTable( MaybeTable ), maybeTable )
108+import Rel8.Table.NonEmpty ( NonEmptyTable( NonEmptyTable ) )
109+import Rel8.Table.Opaleye ( aggregator, unpackspec )
110+import Rel8.Table.Ord ( OrdTable )
111+import Rel8.Table.Order ( ascTable )
112+import Rel8.Table.Projection
113+ ( Biprojectable, biproject
114+ , Projectable, project
115+ , apply
116+ )
117+import Rel8.Table.These ( TheseTable( TheseTable ), theseTable )
118+
119+-- semigroupoids
120+import Data.Functor.Apply ( Apply, liftF2 )
121+import Data.Functor.Bind ( Bind, (>>-) )
122+
123+
124+type Key :: Type -> Type
125+type Key = Maybe
126+
127+
128+cat :: Table Expr k => Key k -> Query k
129+cat = maybe emptyTable pure
130+
131+
132+key :: (ProductProfunctor p, SumProfunctor p)
133+ => p a b -> p (Key a) (Key b)
134+key a = dimap from to (PP.empty +++! a)
135+ where
136+ from = maybe (Left ()) Right
137+ to = either (const Nothing) Just
138+
139+
140+keyed :: (ProductProfunctor p, SumProfunctor p)
141+ => p k l -> p a b -> p (Key k, a) (Key l, b)
142+keyed k a = key k ***! a
143+
144+
145+type Predicate :: Type -> Type
146+newtype Predicate a = Predicate (Maybe (a -> Expr Bool))
147+
148+
149+instance Contravariant Predicate where
150+ contramap f (Predicate a) = Predicate (lmap f <$> a)
151+
152+
153+instance Semigroup (Predicate k) where
154+ Predicate ma <> Predicate mb = Predicate $ ma <|> mb
155+
156+
157+instance Monoid (Predicate k) where
158+ mempty = Predicate Nothing
159+
160+
161+match :: EqTable k => Key k -> Predicate k
162+match = Predicate . fmap (==:)
163+
164+
165+ensure :: Predicate k -> Key k -> Query ()
166+ensure (Predicate mp) = traverse_ (\k -> traverse_ (\p -> where_ (p k)) mp)
167+
168+
169+-- | A @'Tabulation' k a@ is like a @'Query' a@, except that each row also
170+-- has a key @k@ in addition to the value @a@. 'Tabulation's can be composed
171+-- monadically just like 'Query's, but the resulting join is more like a
172+-- @NATURAL JOIN@ (based on the common key column(s) @k@) than the
173+-- @CROSS JOIN@ given by 'Query'.
174+--
175+-- Another way to think of @'Tabulation' k a@ is as analogous to @Map k a@ in
176+-- the same way @'Query' a@ is analogous to @[a]@. However, there's nothing
177+-- stopping a 'Tabulation' from containing multiple rows with the same key, so
178+-- technically @Map k (NonEmpty a)@ is more accurate.
179+--
180+-- 'Tabulation's can be created from 'Query's with 'fromQuery' and 'liftQuery'
181+-- and converted back to 'Query's with 'lookup' and 'toQuery' (though note the
182+-- caveats that come with the latter).
183+type Tabulation :: Type -> Type -> Type
184+newtype Tabulation k a = Tabulation (Predicate k -> Query (Key k, a))
185+
186+
187+instance Biprojectable Tabulation where
188+ biproject f g =
189+ bimap
190+ (fromColumns . apply f . toColumns)
191+ (fromColumns . apply g . toColumns)
192+
193+
194+instance Bifunctor Tabulation where
195+ bimap f g (Tabulation a) = Tabulation $ \p ->
196+ bimap (fmap f) g <$> a (f >$< p)
197+
198+
199+instance Functor (Tabulation k) where
200+ fmap = second
201+
202+
203+instance Projectable (Tabulation k) where
204+ project f = fmap (fromColumns . apply f . toColumns)
205+
206+
207+-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<.>)@ is
208+-- @intersectionWith (liftA2 (<*>))@
209+instance EqTable k => Apply (Tabulation k) where
210+ liftF2 = liftA2
211+
212+
213+-- | @pure = 'liftQuery' . pure@
214+instance EqTable k => Applicative (Tabulation k) where
215+ pure = liftQuery . pure
216+ liftA2 = liftM2
217+
218+
219+instance EqTable k => Bind (Tabulation k) where
220+ Tabulation as >>- f = Tabulation $ \p -> do
221+ (k, a) <- as p
222+ case f a of
223+ Tabulation bs -> do
224+ let p' = match k
225+ (k', b) <- bs (p' <> p)
226+ ensure p' k'
227+ pure (k' <|> k, b)
228+
229+
230+instance EqTable k => Monad (Tabulation k) where
231+ (>>=) = (>>-)
232+
233+
234+-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<|>:)@ is
235+-- @unionWith (<>)@.
236+instance EqTable k => AltTable (Tabulation k) where
237+ tas <|>: tbs = do
238+ eas <- peek tas
239+ ebs <- peek tbs
240+ case (eas, ebs) of
241+ (Left as, Left bs) -> liftQuery $ as <|>: bs
242+ (Right as, Right bs) -> fromQuery $ as <|>: bs
243+ _ -> catNonEmptyTable `through` ((<>) `on` some) tas tbs
244+
245+
246+instance EqTable k => AlternativeTable (Tabulation k) where
247+ emptyTable = Tabulation $ const $ fmap (empty,) emptyTable
248+
249+
250+-- | If @'Tabulation' k a@ is @Map k (NonEmpty a)@, then @(<>)@ is
251+-- @unionWith (liftA2 (<>))@.
252+instance (EqTable k, Table Expr a, Semigroup a) => Semigroup (Tabulation k a)
253+ where
254+ (<>) = alignWith (theseTable id id (<>))
255+
256+
257+instance (EqTable k, Table Expr a, Semigroup a) => Monoid (Tabulation k a)
258+ where
259+ mempty = emptyTable
260+
261+
262+-- | Any 'Query' of key-value pairs @(k, a)@ can be a @'Tabulation' k a@.
263+fromQuery :: Query (k, a) -> Tabulation k a
264+fromQuery = Tabulation . const . fmap (first pure)
265+
266+
267+-- | Convert a @'Tabulation' k a@ back into a 'Query' of key-value pairs.
268+--
269+-- Note that the result of a 'toQuery' is undefined (will always return zero
270+-- rows) on 'Tabulation's constructed with 'liftQuery' or 'pure'. So while
271+-- @toQuery . fromQuery@ is always @id@, @fromQuery . toQuery@ is not.
272+--
273+-- A safer, more predictable alternative to 'toQuery' is to use 'lookup' with
274+-- an explicit set of keys:
275+--
276+-- @
277+-- do
278+-- k <- keys
279+-- a <- lookup k tabulation
280+-- pure (k, a)
281+-- @
282+--
283+-- Having said that, in practice, most legitimate uses of 'Tabulation' will
284+-- have a well-defined 'toQuery'. It would be possible in theory to encode
285+-- the necessary invariants at the type level using an indexed monad, but we
286+-- would lose the ability to use @do@-notation, which is the main benefit
287+-- of having 'Tabulation' as a monad in the first place.
288+--
289+-- In particular, @'toQuery' t@ is well-defined for any 'Tabulation' @t@
290+-- defined as @t = fromQuery _@. @'toQuery' t@ is also well-defined for any
291+-- 'Tabulation' @t@ defined as @t = t' >>= _@ or @t = t' *> _@ where
292+-- @'toQuery' t'@ is well-defined. There are other valid permutations too.
293+-- Generally, anything that uses 'fromQuery' at some point, unless wrapped in
294+-- a top-level 'present' or 'absent', will have a well-defined 'toQuery'.
295+toQuery :: Table Expr k => Tabulation k a -> Query (k, a)
296+toQuery (Tabulation f) = do
297+ (mk, a) <- f mempty
298+ k <- cat mk
299+ pure (k, a)
300+
301+
302+-- | A @'Query' a@ can be treated as a @'Tabulation' k a@ where the given @a@
303+-- values exist at every possible key @k@.
304+liftQuery :: Query a -> Tabulation k a
305+liftQuery = Tabulation . const . fmap (empty,)
306+
307+
308+-- | Run a Kleisli arrow in the the 'Query' monad \"through\" a 'Tabulation'.
309+-- Useful for 'Rel8.filter'ing a 'Tabulation'.
310+--
311+-- @
312+-- 'Rel8.filter' ((>=. 30) . userAge) `'through'` usersById
313+-- @
314+through :: (a -> Query b) -> Tabulation k a -> Tabulation k b
315+through f (Tabulation as) = Tabulation $ \p -> do
316+ (k, a) <- as p
317+ b <- f a
318+ pure (k, b)
319+infixr 1 `through`
320+
321+
322+-- | @'lookup' k t@ returns the value(s) at the key @k@ in the tabulation @t@.
323+lookup :: EqTable k => k -> Tabulation k a -> Query a
324+lookup k (Tabulation f) = do
325+ (k', a) <- f p
326+ ensure p k'
327+ pure a
328+ where
329+ p = match (pure k)
330+
331+
332+-- | 'aggregate' aggregates the values within each key of a
333+-- 'Tabulation'. There is an implicit @GROUP BY@ on all the key columns.
334+aggregate :: forall k aggregates exprs.
335+ ( EqTable k
336+ , Aggregates aggregates exprs
337+ )
338+ => Tabulation k aggregates -> Tabulation k exprs
339+aggregate (Tabulation f) = Tabulation $
340+ mapOpaleye (Opaleye.aggregate (keyed haggregator aggregator)) .
341+ fmap (first (fmap (hgroupBy (eqTable @k) . toColumns))) .
342+ f
343+ where
344+ haggregator = dimap fromColumns fromCols aggregator
345+
346+
347+-- | 'distinct' ensures a 'Tabulation' has at most one value for
348+-- each key, i.e., it drops duplicates. In general it keeps only the
349+-- \"first\" value it encounters for each key, but note that \"first\" is
350+-- undefined unless you first call 'order'.
351+distinct :: EqTable k => Tabulation k a -> Tabulation k a
352+distinct (Tabulation f) = Tabulation $
353+ mapOpaleye (Opaleye.distinctOnExplicit (key unpackspec) fst) . f
354+
355+
356+-- | 'order' orders the /values/ of a 'Tabulation' within their
357+-- respective keys. This specifies a defined order for 'distinct'.
358+-- It also defines the order of the lists produced by 'many' and
359+-- 'some'.
360+order :: OrdTable k => Order a -> Tabulation k a -> Tabulation k a
361+order ordering (Tabulation f) =
362+ Tabulation $ mapOpaleye (Opaleye.orderBy ordering') . f
363+ where
364+ Order ordering' = runClown (keyed (Clown ascTable) (Clown ordering))
365+
366+
367+-- $magic
368+--
369+-- Some of the following combinators produce \"magic\" 'Tabulation's. Let's
370+-- use 'count' as an example to demonstrate this concept. Consider
371+-- the following:
372+--
373+-- @
374+-- count $ fromQuery $ values
375+-- [ (lit 'a', lit True)
376+-- , (lit 'a', lit False)
377+-- , (lit 'b', lit True)
378+-- ]
379+-- @
380+--
381+-- You might expect this to be equivalent to the following 'Tabulation':
382+--
383+-- @
384+-- fromQuery $ values
385+-- [ (lit 'a', 2)
386+-- , (lit 'b', 1)
387+-- ]
388+-- @
389+--
390+-- However, it isn't quite. While the resulting 'Tabulation' does effectively
391+-- contain the above entries, it also behaves as though it contained the value
392+-- @0@ at every other possible key.
393+--
394+-- This means you can do:
395+--
396+-- @
397+-- do
398+-- user <- usersById
399+-- orderCount <- count ordersByUserId
400+-- @
401+--
402+-- To see how many orders a user has (getting @0@ if they have no orders).
403+
404+
405+-- | 'count' returns a count of how many entries are in the given
406+-- 'Tabulation' at each key.
407+--
408+-- The resulting 'Tabulation' is \"magic\" in that the value @0@ exists at
409+-- every possible key that wasn't in the given 'Tabulation'.
410+count :: EqTable k => Tabulation k a -> Tabulation k (Expr Int64)
411+count =
412+ fmap (maybeTable 0 id) .
413+ optional .
414+ aggregate .
415+ fmap (const countStar)
416+
417+
418+-- | 'optional' produces a \"magic\" 'Tabulation' whereby each
419+-- entry in the given 'Tabulation' is wrapped in 'Rel8.justTable', and every
420+-- other possible key contains a single 'Rel8.nothingTable'.
421+--
422+-- This is used to implement 'leftAlignWith'.
423+optional :: Tabulation k a -> Tabulation k (MaybeTable Expr a)
424+optional (Tabulation f) = Tabulation $ \p -> case p of
425+ Predicate Nothing -> fmap pure <$> f p
426+ _ -> fmap (\m -> (empty, snd <$> m)) $ Q.optional $ do
427+ (k, a) <- f p
428+ ensure p k
429+ pure (k, a)
430+
431+
432+-- | 'many' aggregates each entry with a particular key into a
433+-- single entry with all of the values contained in a 'ListTable'.
434+--
435+-- 'order' can be used to give this 'ListTable' a defined order.
436+--
437+-- The resulting 'Tabulation' is \"magic\" in that the value
438+-- @'Rel8.listTable []'@ exists at every possible key that wasn't in the given
439+-- 'Tabulation'.
440+many :: (EqTable k, Table Expr a)
441+ => Tabulation k a -> Tabulation k (ListTable Expr a)
442+many =
443+ fmap (maybeTable mempty (\(ListTable a) -> ListTable a)) .
444+ optional .
445+ aggregate .
446+ fmap (listAgg . toCols)
447+
448+
449+-- | 'some' aggregates each entry with a particular key into a
450+-- single entry with all of the values contained in a 'NonEmptyTable'.
451+--
452+-- 'order' can be used to give this 'NonEmptyTable' a defined order.
453+some :: (EqTable k, Table Expr a)
454+ => Tabulation k a -> Tabulation k (NonEmptyTable Expr a)
455+some =
456+ fmap (\(NonEmptyTable a) -> NonEmptyTable a) .
457+ aggregate .
458+ fmap (nonEmptyAgg . toCols)
459+
460+
461+-- | 'exists' produces a \"magic\" 'Tabulation' which contains the
462+-- value 'Rel8.true' at each key in the given 'Tabulation', and the value
463+-- 'Rel8.false' at every other possible key.
464+exists :: Tabulation k a -> Tabulation k (Expr Bool)
465+exists (Tabulation f) = Tabulation $ \p -> case p of
466+ Predicate Nothing -> (true <$) <$> f p
467+ _ -> fmap (empty,) $ Q.exists $ do
468+ (k, _) <- f p
469+ ensure p k
470+
471+
472+-- | 'present' produces a 'Tabulation' where a single @()@ row
473+-- exists for every key that was present in the given 'Tabulation'.
474+--
475+-- This is used to implement 'similarity'.
476+present :: Tabulation k a -> Tabulation k ()
477+present (Tabulation f) = Tabulation $ \p -> do
478+ Q.present $ do
479+ (k, _) <- f p
480+ ensure p k
481+ pure (empty, ())
482+
483+
484+-- | 'absent' produces a 'Tabulation' where a single @()@ row exists
485+-- at every possible key that absent from the given 'Tabulation'.
486+--
487+-- This is used to implement 'difference'.
488+absent :: Tabulation k a -> Tabulation k ()
489+absent (Tabulation f) = Tabulation $ \p -> do
490+ Q.absent $ do
491+ (k, _) <- f p
492+ ensure p k
493+ pure (empty, ())
494+
495+
496+-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
497+--
498+-- Analogous to 'Data.Semialign.align'.
499+align :: EqTable k
500+ => Tabulation k a -> Tabulation k b -> Tabulation k (TheseTable Expr a b)
501+align = alignWith id
502+
503+
504+-- | Performs a @NATURAL FULL OUTER JOIN@ based on the common key columns.
505+--
506+-- Analogous to 'Data.Semialign.alignWith'.
507+alignWith :: EqTable k
508+ => (TheseTable Expr a b -> c)
509+ -> Tabulation k a -> Tabulation k b -> Tabulation k c
510+alignWith f (Tabulation as) (Tabulation bs) = Tabulation $ \p -> do
511+ tkab <- liftF2 (alignBy condition) as bs p
512+ k <- traverse (rebind "key") $ recover $ bimap fst fst tkab
513+ let
514+ tab = bimap snd snd tkab
515+ pure (k, f tab)
516+ where
517+ condition (k, _) (k', _) = fromMaybe true (liftA2 (==:) k k')
518+ recover (TheseTable mma@(MaybeTable _ ma) mmb@(MaybeTable _ mb)) =
519+ case extract ma of
520+ Nothing -> extract mb
521+ Just a -> case extract mb of
522+ Nothing -> extract ma
523+ Just b -> case a <$ mma <|>: b <$ mmb of
524+ MaybeTable _ c -> pure (extract c)
525+
526+
527+-- | Performs a @NATURAL LEFT OUTER JOIN@ based on the common key columns.
528+--
529+-- Analogous to 'Data.Semialign.rpadZip'.
530+--
531+-- Note that you can achieve the same effect with 'optional' and the
532+-- 'Applicative' instance for 'Tabulation', i.e., this is just
533+-- @\left right -> liftA2 (,) left (optional right). You can also
534+-- use @do@-notation.
535+leftAlign :: EqTable k
536+ => Tabulation k a -> Tabulation k b -> Tabulation k (a, MaybeTable Expr b)
537+leftAlign = leftAlignWith (,)
538+
539+
540+-- | Performs a @NATURAL LEFT OUTER JOIN@ based on the common key columns.
541+--
542+-- Analogous to 'Data.Semialign.rpadZipWith'.
543+--
544+-- Note that you can achieve the same effect with 'optional' and the
545+-- 'Applicative' instance for 'Tabulation', i.e., this is just
546+-- @\f left right -> liftA2 f left (optional right). You can also
547+-- use @do@-notation.
548+leftAlignWith :: EqTable k
549+ => (a -> MaybeTable Expr b -> c)
550+ -> Tabulation k a -> Tabulation k b -> Tabulation k c
551+leftAlignWith f left right = liftA2 f left (optional right)
552+
553+
554+-- | Performs a @NATURAL RIGHT OUTER JOIN@ based on the common key columns.
555+--
556+-- Analogous to 'Data.Semialign.lpadZip'.
557+--
558+-- Note that you can achieve the same effect with 'optional' and the
559+-- 'Applicative' instance for 'Tabulation', i.e., this is just
560+-- @\left right -> liftA2 (flip (,)) right (optional left). You can
561+-- also use @do@-notation.
562+rightAlign :: EqTable k
563+ => Tabulation k a -> Tabulation k b -> Tabulation k (MaybeTable Expr a, b)
564+rightAlign = rightAlignWith (,)
565+
566+
567+-- | Performs a @NATURAL RIGHT OUTER JOIN@ based on the common key columns.
568+--
569+-- Analogous to 'Data.Semialign.lpadZipWith'.
570+--
571+-- Note that you can achieve the same effect with 'optional' and the
572+-- 'Applicative' instance for 'Tabulation', i.e., this is just
573+-- @\f left right -> liftA2 (flip f) right (optional left). You can
574+-- also use @do@-notation.
575+rightAlignWith :: EqTable k
576+ => (MaybeTable Expr a -> b -> c)
577+ -> Tabulation k a -> Tabulation k b -> Tabulation k c
578+rightAlignWith f left right = liftA2 (flip f) right (optional left)
579+
580+
581+-- | Performs a @NATURAL INNER JOIN@ based on the common key columns.
582+--
583+-- Analagous to 'Data.Semialign.zip'.
584+--
585+-- Note that you can achieve the same effect with the 'Applicative' instance
586+-- of 'Tabulation', i.e., this is just @'liftA2 (,)'@. You can also use
587+-- @do@-notation.
588+zip :: EqTable k
589+ => Tabulation k a -> Tabulation k b -> Tabulation k (a, b)
590+zip = zipWith (,)
591+
592+
593+-- | Performs a @NATURAL INNER JOIN@ based on the common key columns.
594+--
595+-- Analagous to 'Data.Semialign.zipWith'.
596+--
597+-- Note that you can achieve the same effect with the 'Applicative' instance
598+-- of 'Tabulation', i.e., this is just @'liftA2'@. You can also use
599+-- @do@-notation.
600+zipWith :: EqTable k
601+ => (a -> b -> c) -> Tabulation k a -> Tabulation k b -> Tabulation k c
602+zipWith = liftA2
603+
604+
605+-- | Performs a [@NATURAL SEMI JOIN@](https://en.wikipedia.org/wiki/Relational_algebra#Semijoin_%28%E2%8B%89%29%28%E2%8B%8A%29)
606+-- based on the common key columns.
607+--
608+-- The result is a subset of the left tabulation where only entries which have
609+-- a corresponding entry in the right tabulation are kept.
610+--
611+-- Note that you can achieve a similar effect with 'present' and the
612+-- 'Applicative' instance of 'Tabulation', i.e., this is just
613+-- @\left right -> left <* present right@. You can also use
614+-- @do@-notation.
615+similarity :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
616+similarity a b = a <* present b
617+
618+
619+-- | Performs a [@NATURAL ANTI JOIN@](https://en.wikipedia.org/wiki/Relational_algebra#Antijoin_%28%E2%96%B7%29)
620+-- based on the common key columns.
621+--
622+-- The result is a subset of the left tabulation where only entries which do
623+-- not have a corresponding entry in the right tabulation are kept.
624+--
625+-- Note that you can achieve a similar effect with 'absent' and the
626+-- 'Applicative' instance of 'Tabulation', i.e., this is just
627+-- @\left right -> left <* absent right@. You can also use
628+-- @do@-notation.
629+difference :: EqTable k => Tabulation k a -> Tabulation k b -> Tabulation k a
630+difference a b = a <* absent b
631+
632+
633+-- | 'Tabulation's can be produced with either 'fromQuery' or 'liftQuery', and
634+-- in some cases we might want to treat these differently. 'peek' uses
635+-- 'unsafePeekQuery' to determine which type of 'Tabulation' we have.
636+peek :: Tabulation k a -> Tabulation k (Either (Query a) (Query (k, a)))
637+peek (Tabulation f) = Tabulation $ \p ->
638+ pure $ (empty,) $ case unsafePeekQuery (f p) of
639+ (Nothing, _) -> Left $ fmap snd (f p)
640+ (Just _, _) -> Right $ fmap (first fromJust) (f p)
src/Rel8/Type.hs+9−8
@@ -1,3 +1,4 @@
1+{-# language FlexibleContexts #-}
1{-# language FlexibleInstances #-}
2{-# language FlexibleInstances #-}
2{-# language MonoLocalBinds #-}
3{-# language MonoLocalBinds #-}
3{-# language MultiWayIf #-}
4{-# language MultiWayIf #-}
@@ -127,10 +128,10 @@ instance DBType Int64 where
127instance DBType Float where
128instance DBType Float where
128 typeInformation = TypeInformation
129 typeInformation = TypeInformation
129 { encode = \x -> Opaleye.ConstExpr
130 { encode = \x -> Opaleye.ConstExpr
130− if | x == (1 /0) -> Opaleye.OtherLit "'Infinity'"
131+ if | x == (1 / 0) -> Opaleye.OtherLit "'Infinity'"
131− | isNaN x -> Opaleye.OtherLit "'NaN'"
132+ | isNaN x -> Opaleye.OtherLit "'NaN'"
132− | x == (-1/0) -> Opaleye.OtherLit "'-Infinity'"
133+ | x == (-1 / 0) -> Opaleye.OtherLit "'-Infinity'"
133− | otherwise -> Opaleye.NumericLit $ realToFrac x
134+ | otherwise -> Opaleye.NumericLit $ realToFrac x
134 , decode = Hasql.float4
135 , decode = Hasql.float4
135 , typeName = "float4"
136 , typeName = "float4"
136 }
137 }
@@ -140,10 +141,10 @@ instance DBType Float where
140instance DBType Double where
141instance DBType Double where
141 typeInformation = TypeInformation
142 typeInformation = TypeInformation
142 { encode = \x -> Opaleye.ConstExpr
143 { encode = \x -> Opaleye.ConstExpr
143− if | x == (1 /0) -> Opaleye.OtherLit "'Infinity'"
144+ if | x == (1 / 0) -> Opaleye.OtherLit "'Infinity'"
144− | isNaN x -> Opaleye.OtherLit "'NaN'"
145+ | isNaN x -> Opaleye.OtherLit "'NaN'"
145− | x == (-1/0) -> Opaleye.OtherLit "'-Infinity'"
146+ | x == (-1 / 0) -> Opaleye.OtherLit "'-Infinity'"
146− | otherwise -> Opaleye.NumericLit $ realToFrac x
147+ | otherwise -> Opaleye.NumericLit $ realToFrac x
147 , decode = Hasql.float8
148 , decode = Hasql.float8
148 , typeName = "float8"
149 , typeName = "float8"
149 }
150 }
src/Rel8/Type/Enum.hs+1
@@ -106,6 +106,7 @@ class (DBType a, Enumable a) => DBEnum a where
106
106
107-- | Types that are sum types, where each constructor is unary (that is, has no
107-- | Types that are sum types, where each constructor is unary (that is, has no
108-- fields).
108-- fields).
109+type Enumable :: Type -> Constraint
109class (Generic a, GEnumable (Rep a)) => Enumable a
110class (Generic a, GEnumable (Rep a)) => Enumable a
110instance (Generic a, GEnumable (Rep a)) => Enumable a
111instance (Generic a, GEnumable (Rep a)) => Enumable a
111
112
src/Rel8/Type/JSONBEncoded.hs+4
@@ -1,3 +1,5 @@
1+{-# language StandaloneKindSignatures #-}
2+
1module Rel8.Type.JSONBEncoded ( JSONBEncoded(..) ) where
3module Rel8.Type.JSONBEncoded ( JSONBEncoded(..) ) where
2
4
3-- aeson
5-- aeson
@@ -6,6 +8,7 @@ import Data.Aeson.Types ( parseEither )
6
8
7-- base
9-- base
8import Data.Bifunctor ( first )
10import Data.Bifunctor ( first )
11+import Data.Kind ( Type )
9import Prelude
12import Prelude
10
13
11-- hasql
14-- hasql
@@ -20,6 +23,7 @@ import Data.Text ( pack )
20
23
21
24
22-- | Like 'Rel8.JSONEncoded', but works for @jsonb@ columns.
25-- | Like 'Rel8.JSONEncoded', but works for @jsonb@ columns.
26+type JSONBEncoded :: Type -> Type
23newtype JSONBEncoded a = JSONBEncoded { fromJSONBEncoded :: a }
27newtype JSONBEncoded a = JSONBEncoded { fromJSONBEncoded :: a }
24
28
25
29
src/Rel8/Type/JSONEncoded.hs+4
@@ -1,3 +1,5 @@
1+{-# language StandaloneKindSignatures #-}
2+
1module Rel8.Type.JSONEncoded ( JSONEncoded(..) ) where
3module Rel8.Type.JSONEncoded ( JSONEncoded(..) ) where
2
4
3-- aeson
5-- aeson
@@ -5,6 +7,7 @@ import Data.Aeson ( FromJSON, ToJSON, parseJSON, toJSON )
5import Data.Aeson.Types ( parseEither )
7import Data.Aeson.Types ( parseEither )
6
8
7-- base
9-- base
10+import Data.Kind ( Type )
8import Prelude
11import Prelude
9
12
10-- rel8
13-- rel8
@@ -15,6 +18,7 @@ import Rel8.Type.Information ( parseTypeInformation )
15-- | A deriving-via helper type for column types that store a Haskell value
18-- | A deriving-via helper type for column types that store a Haskell value
16-- using a JSON encoding described by @aeson@'s 'ToJSON' and 'FromJSON' type
19-- using a JSON encoding described by @aeson@'s 'ToJSON' and 'FromJSON' type
17-- classes.
20-- classes.
21+type JSONEncoded :: Type -> Type
18newtype JSONEncoded a = JSONEncoded { fromJSONEncoded :: a }
22newtype JSONEncoded a = JSONEncoded { fromJSONEncoded :: a }
19
23
20
24
src/Rel8/Type/Num.hs+4−1
@@ -18,6 +18,7 @@ import Prelude
18
18
19-- rel8
19-- rel8
20import Rel8.Type ( DBType )
20import Rel8.Type ( DBType )
21+import Rel8.Type.Ord ( DBOrd )
21
22
22-- scientific
23-- scientific
23import Data.Scientific ( Scientific )
24import Data.Scientific ( Scientific )
@@ -39,13 +40,14 @@ instance DBNum Scientific
39-- expressions. This is a Rel8 concept, and allows us to provide
40-- expressions. This is a Rel8 concept, and allows us to provide
40-- 'fromIntegral'.
41-- 'fromIntegral'.
41type DBIntegral :: Type -> Constraint
42type DBIntegral :: Type -> Constraint
42−class DBNum a => DBIntegral a
43+class (DBNum a, DBOrd a) => DBIntegral a
43instance DBIntegral Int16
44instance DBIntegral Int16
44instance DBIntegral Int32
45instance DBIntegral Int32
45instance DBIntegral Int64
46instance DBIntegral Int64
46
47
47
48
48-- | The class of database types that support the @/@ operator.
49-- | The class of database types that support the @/@ operator.
50+type DBFractional :: Type -> Constraint
49class DBNum a => DBFractional a
51class DBNum a => DBFractional a
50instance DBFractional Float
52instance DBFractional Float
51instance DBFractional Double
53instance DBFractional Double
@@ -53,6 +55,7 @@ instance DBFractional Scientific
53
55
54
56
55-- | The class of database types that support the @/@ operator.
57-- | The class of database types that support the @/@ operator.
58+type DBFloating :: Type -> Constraint
56class DBFractional a => DBFloating a
59class DBFractional a => DBFloating a
57instance DBFloating Float
60instance DBFloating Float
58instance DBFloating Double
61instance DBFloating Double
src/Rel8/Type/Ord.hs−2
@@ -75,7 +75,6 @@ instance Sql DBOrd a => DBOrd (NonEmpty a)
75-- | The class of database types that support the @max@ aggregation function.
75-- | The class of database types that support the @max@ aggregation function.
76type DBMax :: Type -> Constraint
76type DBMax :: Type -> Constraint
77class DBOrd a => DBMax a
77class DBOrd a => DBMax a
78−instance DBMax Bool
79instance DBMax Char
78instance DBMax Char
80instance DBMax Int16
79instance DBMax Int16
81instance DBMax Int32
80instance DBMax Int32
@@ -101,7 +100,6 @@ instance Sql DBMax a => DBMax (NonEmpty a)
101-- | The class of database types that support the @min@ aggregation function.
100-- | The class of database types that support the @min@ aggregation function.
102type DBMin :: Type -> Constraint
101type DBMin :: Type -> Constraint
103class DBOrd a => DBMin a
102class DBOrd a => DBMin a
104−instance DBMin Bool
105instance DBMin Char
103instance DBMin Char
106instance DBMin Int16
104instance DBMin Int16
107instance DBMin Int32
105instance DBMin Int32
src/Rel8/Type/ReadShow.hs+3
@@ -1,10 +1,12 @@
1{-# language ScopedTypeVariables #-}
1{-# language ScopedTypeVariables #-}
2+{-# language StandaloneKindSignatures #-}
2{-# language TypeApplications #-}
3{-# language TypeApplications #-}
3{-# language ViewPatterns #-}
4{-# language ViewPatterns #-}
4
5
5module Rel8.Type.ReadShow ( ReadShow(..) ) where
6module Rel8.Type.ReadShow ( ReadShow(..) ) where
6
7
7-- base
8-- base
9+import Data.Kind ( Type )
8import Data.Proxy ( Proxy( Proxy ) )
10import Data.Proxy ( Proxy( Proxy ) )
9import Data.Typeable ( Typeable, typeRep )
11import Data.Typeable ( Typeable, typeRep )
10import Prelude
12import Prelude
@@ -20,6 +22,7 @@ import qualified Data.Text as Text
20
22
21-- | A deriving-via helper type for column types that store a Haskell value
23-- | A deriving-via helper type for column types that store a Haskell value
22-- using a Haskell's 'Read' and 'Show' type classes.
24-- using a Haskell's 'Read' and 'Show' type classes.
25+type ReadShow :: Type -> Type
23newtype ReadShow a = ReadShow { fromReadShow :: a }
26newtype ReadShow a = ReadShow { fromReadShow :: a }
24
27
25
28
src/Rel8/Type/Tag.hs+1
@@ -90,6 +90,7 @@ instance DBMonoid MaybeTag where
90 memptyExpr = litExpr mempty
90 memptyExpr = litExpr mempty
91
91
92
92
93+type Tag :: Type
93newtype Tag = Tag Text
94newtype Tag = Tag Text
94 deriving newtype
95 deriving newtype
95 ( Eq, Ord, Read, Show
96 ( Eq, Ord, Read, Show
src/Rel8/Window.hsnew file+97
@@ -0,0 +1,97 @@
1+{-# language DerivingVia #-}
2+{-# language FlexibleContexts #-}
3+{-# language GeneralizedNewtypeDeriving #-}
4+{-# language ScopedTypeVariables #-}
5+{-# language StandaloneKindSignatures #-}
6+{-# language TypeApplications #-}
7+
8+module Rel8.Window
9+ ( Window(..)
10+ , Partition
11+ , over
12+ , partitionBy
13+ , orderPartitionBy
14+ )
15+where
16+
17+-- base
18+import Data.Functor.Const ( Const( Const ), getConst )
19+import Data.Functor.Contravariant ( Contravariant, contramap )
20+import Data.Kind ( Type )
21+import Prelude
22+
23+-- opaleye
24+import qualified Opaleye.Internal.Window as Opaleye
25+import qualified Opaleye.Internal.PackMap as Opaleye
26+
27+-- profunctors
28+import Data.Profunctor ( Profunctor )
29+
30+-- product-profunctors
31+import Data.Profunctor.Product ( ProductProfunctor, (****), purePP )
32+
33+-- rel8
34+import Rel8.Expr.Opaleye ( toColumn, toPrimExpr )
35+import Rel8.Order( Order( Order ) )
36+import Rel8.Schema.HTable ( hfield, htabulateA )
37+import Rel8.Table ( Columns, toColumns )
38+import Rel8.Table.Eq ( EqTable )
39+
40+-- semigroupoids
41+import Data.Functor.Apply ( Apply, WrappedApplicative(..) )
42+
43+
44+-- | 'Window' is an applicative functor that represents expressions that
45+-- contain
46+-- [window functions](https://www.postgresql.org/docs/current/tutorial-window.html).
47+-- 'Rel8.Query.Window.window' can be used to
48+-- evaluate these expressions over a particular query.
49+type Window :: Type -> Type -> Type
50+newtype Window a b = Window (Opaleye.Windows a b)
51+ deriving newtype (Profunctor)
52+ deriving newtype (Functor, Applicative)
53+ deriving (Apply) via (WrappedApplicative (Window a))
54+
55+
56+instance ProductProfunctor Window where
57+ purePP = pure
58+ (****) = (<*>)
59+
60+
61+-- | In PostgreSQL, window functions must specify the \"window\" or
62+-- \"partition\" over which they operate. The syntax for this looks like:
63+-- @SUM(salary) OVER (PARTITION BY department)@. The Rel8 type 'Partition'
64+-- represents everything that comes after @OVER@.
65+--
66+-- 'Partition' is a 'Monoid', so 'Window's created with 'partitionBy' and
67+-- 'orderWindowBy' can be combined using '<>'.
68+type Partition :: Type -> Type
69+newtype Partition a = Partition (Opaleye.Window a)
70+ deriving newtype (Contravariant, Semigroup, Monoid)
71+
72+
73+-- | 'over' adds a 'Partition' to a 'Window' expression.
74+--
75+-- @@@
76+-- 'Rel8.Table.Window.cumulative' ('Rel8.Expr.Aggregate.sum' . salary) `over` 'partitionBy' department <> 'orderPartitionBy' (salary >$< 'Rel8.desc')
77+-- @@@
78+over :: Window a b -> Partition a -> Window a b
79+over (Window (Opaleye.Windows (Opaleye.PackMap w))) (Partition p) =
80+ Window $ Opaleye.Windows $ Opaleye.PackMap $ \f ->
81+ w (\(o, p') -> f (o, p' <> p))
82+infixl 1 `over`
83+
84+
85+-- | Restricts a window function to operate only the group of rows that share
86+-- the same value(s) for the given expression(s).
87+partitionBy :: forall b a. EqTable b => (a -> b) -> Partition a
88+partitionBy f =
89+ Partition $ contramap (toColumns . f) $ getConst $
90+ htabulateA @(Columns b) $ \field ->
91+ Const $ Opaleye.partitionBy (toColumn . toPrimExpr . (`hfield` field))
92+
93+
94+-- | Controls the order in which rows are processed by window functions. This
95+-- does not need to match the ordering of the overall query.
96+orderPartitionBy :: Order a -> Partition a
97+orderPartitionBy (Order ordering) = Partition $ Opaleye.orderPartitionBy ordering
tests/Main.hs+273−164
@@ -1,12 +1,11 @@
1+{-# language BangPatterns #-}
1{-# language BlockArguments #-}
2{-# language BlockArguments #-}
2−{-# language DataKinds #-}
3{-# language DeriveAnyClass #-}
3{-# language DeriveAnyClass #-}
4{-# language DeriveGeneric #-}
4{-# language DeriveGeneric #-}
5{-# language DerivingStrategies #-}
5{-# language DerivingStrategies #-}
6−{-# language DisambiguateRecordFields #-}
7{-# language FlexibleContexts #-}
6{-# language FlexibleContexts #-}
8{-# language FlexibleInstances #-}
7{-# language FlexibleInstances #-}
9−{-# language GADTs #-}
8+{-# language MonoLocalBinds #-}
10{-# language NamedFieldPuns #-}
9{-# language NamedFieldPuns #-}
11{-# language OverloadedStrings #-}
10{-# language OverloadedStrings #-}
12{-# language RecordWildCards #-}
11{-# language RecordWildCards #-}
@@ -14,17 +13,15 @@
14{-# language StandaloneDeriving #-}
13{-# language StandaloneDeriving #-}
15{-# language TypeApplications #-}
14{-# language TypeApplications #-}
16
15
17−{-# options_ghc -fno-warn-redundant-constraints #-}
18−
19module Main
16module Main
20 ( main
17 ( main
21 )
18 )
22where
19where
23
20
24-- base
21-- base
25−import Control.Applicative ( liftA2, liftA3 )
22+import Control.Applicative ( empty, liftA2, liftA3 )
26−import Control.Monad (void)
23+import Control.Exception ( bracket, throwIO )
27−import Control.Monad.IO.Class ( MonadIO, liftIO )
24+import Control.Monad ( (>=>), void )
28import Data.Bifunctor ( bimap )
25import Data.Bifunctor ( bimap )
29import Data.Foldable ( for_ )
26import Data.Foldable ( for_ )
30import Data.Int ( Int32, Int64 )
27import Data.Int ( Int32, Int64 )
@@ -38,25 +35,27 @@ import GHC.Generics ( Generic )
38import qualified Data.ByteString.Lazy
35import qualified Data.ByteString.Lazy
39
36
40-- case-insensitive
37-- case-insensitive
41−import Data.CaseInsensitive (mk)
38+import Data.CaseInsensitive ( mk )
42
39
43-- containers
40-- containers
41+import Data.Containers.ListUtils ( nubOrdOn )
44import qualified Data.Map.Strict as Map
42import qualified Data.Map.Strict as Map
45
43
46-- hasql
44-- hasql
47−import Hasql.Connection ( Connection, acquire, release )
45+import Hasql.Connection ( Connection, acquire, release )
48import Hasql.Session ( sql, run )
46import Hasql.Session ( sql, run )
49
47
48+-- hasql-transaction
49+import Hasql.Transaction ( Transaction, condemn, statement )
50+import qualified Hasql.Transaction.Sessions as Hasql
51+
50-- hedgehog
52-- hedgehog
51import Hedgehog ( property, (===), forAll, cover, diff, evalM, PropertyT, TestT, test, Gen )
53import Hedgehog ( property, (===), forAll, cover, diff, evalM, PropertyT, TestT, test, Gen )
52import qualified Hedgehog.Gen as Gen
54import qualified Hedgehog.Gen as Gen
53import qualified Hedgehog.Range as Range
55import qualified Hedgehog.Range as Range
54
56
55−-- lifted-base
57+-- mmorph
56−import Control.Exception.Lifted ( bracket, throwIO, bracket_ )
57−
58−-- monad-control
59−import Control.Monad.Trans.Control ( MonadBaseControl )
58+import Control.Monad.Morph ( hoist )
60
59
61-- rel8
60-- rel8
62import Rel8 ( Result )
61import Rel8 ( Result )
@@ -72,12 +71,16 @@ import Test.Tasty
72import Test.Tasty.Hedgehog ( testProperty )
71import Test.Tasty.Hedgehog ( testProperty )
73
72
74-- text
73-- text
75−import Data.Text (Text, pack)
74+import Data.Text ( Text, pack, unpack )
76import qualified Data.Text.Lazy
75import qualified Data.Text.Lazy
76+import Data.Text.Encoding ( decodeUtf8 )
77
77
78-- time
78-- time
79import Data.Time
79import Data.Time
80
80
81+-- transformers
82+import Control.Monad.Trans.Class ( lift )
83+
81-- tmp-postgres
84-- tmp-postgres
82import qualified Database.Postgres.Temp as TmpPostgres
85import qualified Database.Postgres.Temp as TmpPostgres
83
86
@@ -113,11 +116,13 @@ tests =
113 , testCatMaybeTable getTestDatabase
116 , testCatMaybeTable getTestDatabase
114 , testCatMaybe getTestDatabase
117 , testCatMaybe getTestDatabase
115 , testMaybeTable getTestDatabase
118 , testMaybeTable getTestDatabase
119+ , testAggregateMaybeTable getTestDatabase
116 , testNestedTables getTestDatabase
120 , testNestedTables getTestDatabase
117 , testMaybeTableApplicative getTestDatabase
121 , testMaybeTableApplicative getTestDatabase
118 , testLogicalFixities getTestDatabase
122 , testLogicalFixities getTestDatabase
119 , testUpdate getTestDatabase
123 , testUpdate getTestDatabase
120 , testDelete getTestDatabase
124 , testDelete getTestDatabase
125+ , testUpsert getTestDatabase
121 , testSelectNestedPairs getTestDatabase
126 , testSelectNestedPairs getTestDatabase
122 , testSelectArray getTestDatabase
127 , testSelectArray getTestDatabase
123 , testNestedMaybeTable getTestDatabase
128 , testNestedMaybeTable getTestDatabase
@@ -133,6 +138,7 @@ tests =
133 flip run conn do
138 flip run conn do
134 sql "CREATE EXTENSION citext"
139 sql "CREATE EXTENSION citext"
135 sql "CREATE TABLE test_table ( column1 text not null, column2 bool not null )"
140 sql "CREATE TABLE test_table ( column1 text not null, column2 bool not null )"
141+ sql "CREATE TABLE unique_table ( \"key\" text not null unique, \"value\" text not null )"
136 sql "CREATE SEQUENCE test_seq"
142 sql "CREATE SEQUENCE test_seq"
137
143
138 return db
144 return db
@@ -140,19 +146,21 @@ tests =
140 stopTestDatabase = TmpPostgres.stop
146 stopTestDatabase = TmpPostgres.stop
141
147
142
148
149+connect :: TmpPostgres.DB -> IO Connection
150+connect = acquire . TmpPostgres.toConnectionString >=> either (maybe empty (fail . unpack . decodeUtf8)) pure
151+
152+
143databasePropertyTest
153databasePropertyTest
144 :: TestName
154 :: TestName
145− -> (((Connection -> TestT IO ()) -> PropertyT IO ()) -> PropertyT IO ())
155+ -> ((TestT Transaction () -> PropertyT IO ()) -> PropertyT IO ())
146 -> IO TmpPostgres.DB -> TestTree
156 -> IO TmpPostgres.DB -> TestTree
147databasePropertyTest testName f getTestDatabase =
157databasePropertyTest testName f getTestDatabase =
148− withResource connect release $ \c ->
158+ withResource (connect =<< getTestDatabase) release $ \c ->
149 testProperty testName $ property do
159 testProperty testName $ property do
150− connection <- liftIO c
160+ connection <- lift c
151− f \g -> test $ rollingBack connection $ g connection
161+ f $ test . hoist \m -> do
152−
162+ e <- run (Hasql.transaction Hasql.Serializable Hasql.Write (m <* condemn)) connection
153− where
163+ either throwIO pure e
154−
155− connect = either (error . show) return =<< acquire . TmpPostgres.toConnectionString =<< getTestDatabase
156
164
157
165
158data TestTable f = TestTable
166data TestTable f = TestTable
@@ -184,17 +192,17 @@ testSelectTestTable :: IO TmpPostgres.DB -> TestTree
184testSelectTestTable = databasePropertyTest "Can SELECT TestTable" \transaction -> do
192testSelectTestTable = databasePropertyTest "Can SELECT TestTable" \transaction -> do
185 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
193 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
186
194
187− transaction \connection -> do
195+ transaction do
188− liftIO $ Rel8.insert connection
196+ selected <- lift do
189− Rel8.Insert
197+ statement () $ Rel8.insert Rel8.Insert
190 { into = testTableSchema
198 { into = testTableSchema
191 , rows = Rel8.values $ map Rel8.lit rows
199 , rows = Rel8.values $ map Rel8.lit rows
192 , onConflict = Rel8.DoNothing
200 , onConflict = Rel8.DoNothing
193 , returning = pure ()
201 , returning = pure ()
194 }
202 }
195
203
196− selected <- liftIO $ Rel8.select connection do
204+ statement () $ Rel8.select do
197− Rel8.each testTableSchema
205+ Rel8.each testTableSchema
198
206
199 sort selected === sort rows
207 sort selected === sort rows
200
208
@@ -211,11 +219,12 @@ testWhere_ = databasePropertyTest "WHERE (Rel8.where_)" \transaction -> do
211
219
212 let expected = filter (\t -> testTableColumn2 t == magicBool) rows
220 let expected = filter (\t -> testTableColumn2 t == magicBool) rows
213
221
214− transaction \connection -> do
222+ transaction do
223+ selected <- lift do
215− selected <- liftIO $ Rel8.select connection do
224+ statement () $ Rel8.select do
216− t <- Rel8.values $ Rel8.lit <$> rows
225+ t <- Rel8.values $ Rel8.lit <$> rows
217− Rel8.where_ $ testTableColumn2 t Rel8.==. Rel8.lit magicBool
226+ Rel8.where_ $ testTableColumn2 t Rel8.==. Rel8.lit magicBool
218− return t
227+ return t
219
228
220 sort selected === sort expected
229 sort selected === sort expected
221
230
@@ -228,11 +237,12 @@ testFilter :: IO TmpPostgres.DB -> TestTree
228testFilter = databasePropertyTest "filter" \transaction -> do
237testFilter = databasePropertyTest "filter" \transaction -> do
229 rows <- forAll $ Gen.list (Range.linear 1 10) genTestTable
238 rows <- forAll $ Gen.list (Range.linear 1 10) genTestTable
230
239
231− transaction \connection -> do
240+ transaction do
232 let expected = filter testTableColumn2 rows
241 let expected = filter testTableColumn2 rows
233
242
234− selected <- liftIO $ Rel8.select connection
243+ selected <- lift do
244+ statement () $ Rel8.select do
235− $ Rel8.filter testTableColumn2 =<< Rel8.values (Rel8.lit <$> rows)
245+ Rel8.filter testTableColumn2 =<< Rel8.values (Rel8.lit <$> rows)
236
246
237 sort selected === sort expected
247 sort selected === sort expected
238
248
@@ -247,9 +257,10 @@ testLimit = databasePropertyTest "LIMIT (Rel8.limit)" \transaction -> do
247
257
248 n <- forAll $ Gen.integral (Range.linear 0 10)
258 n <- forAll $ Gen.integral (Range.linear 0 10)
249
259
250− transaction \connection -> do
260+ transaction do
261+ selected <- lift do
251− selected <- liftIO $ Rel8.select connection do
262+ statement () $ Rel8.select do
252− Rel8.limit n $ Rel8.values (Rel8.lit <$> rows)
263+ Rel8.limit n $ Rel8.values (Rel8.lit <$> rows)
253
264
254 diff (length selected) (<=) (fromIntegral n)
265 diff (length selected) (<=) (fromIntegral n)
255
266
@@ -267,9 +278,10 @@ testUnion = databasePropertyTest "UNION (Rel8.union)" \transaction -> evalM do
267 left <- forAll $ Gen.list (Range.linear 0 10) genTestTable
278 left <- forAll $ Gen.list (Range.linear 0 10) genTestTable
268 right <- forAll $ Gen.list (Range.linear 0 10) genTestTable
279 right <- forAll $ Gen.list (Range.linear 0 10) genTestTable
269
280
270− transaction \connection -> do
281+ transaction do
282+ selected <- lift do
271− selected <- liftIO $ Rel8.select connection do
283+ statement () $ Rel8.select do
272− Rel8.values (Rel8.lit <$> nub left) `Rel8.union` Rel8.values (Rel8.lit <$> nub right)
284+ Rel8.values (Rel8.lit <$> nub left) `Rel8.union` Rel8.values (Rel8.lit <$> nub right)
273
285
274 sort selected === sort (nub (left ++ right))
286 sort selected === sort (nub (left ++ right))
275
287
@@ -278,9 +290,11 @@ testDistinct :: IO TmpPostgres.DB -> TestTree
278testDistinct = databasePropertyTest "DISTINCT (Rel8.distinct)" \transaction -> do
290testDistinct = databasePropertyTest "DISTINCT (Rel8.distinct)" \transaction -> do
279 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
291 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
280
292
281− transaction \connection -> do
293+ transaction do
294+ selected <- lift do
295+ statement () $ Rel8.select do
282− selected <- liftIO $ Rel8.select connection $ Rel8.distinct do
296+ Rel8.distinct do
283− Rel8.values (Rel8.lit <$> rows)
297+ Rel8.values (Rel8.lit <$> rows)
284
298
285 sort selected === nub (sort rows)
299 sort selected === nub (sort rows)
286
300
@@ -293,8 +307,10 @@ testExists :: IO TmpPostgres.DB -> TestTree
293testExists = databasePropertyTest "EXISTS (Rel8.exists)" \transaction -> do
307testExists = databasePropertyTest "EXISTS (Rel8.exists)" \transaction -> do
294 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
308 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
295
309
296− transaction \connection -> do
310+ transaction do
311+ exists <- lift do
312+ statement () $ Rel8.select do
297− exists <- liftIO $ Rel8.select connection $ Rel8.exists $ Rel8.values $ Rel8.lit <$> rows
313+ Rel8.exists $ Rel8.values $ Rel8.lit <$> rows
298
314
299 case rows of
315 case rows of
300 [] -> exists === [False]
316 [] -> exists === [False]
@@ -305,9 +321,10 @@ testOptional :: IO TmpPostgres.DB -> TestTree
305testOptional = databasePropertyTest "Rel8.optional" \transaction -> do
321testOptional = databasePropertyTest "Rel8.optional" \transaction -> do
306 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
322 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
307
323
308− transaction \connection -> do
324+ transaction do
325+ selected <- lift do
309− selected <- liftIO $ Rel8.select connection do
326+ statement () $ Rel8.select do
310− Rel8.optional $ Rel8.values (Rel8.lit <$> rows)
327+ Rel8.optional $ Rel8.values (Rel8.lit <$> rows)
311
328
312 case rows of
329 case rows of
313 [] -> selected === [Nothing]
330 [] -> selected === [Nothing]
@@ -318,9 +335,10 @@ testAnd :: IO TmpPostgres.DB -> TestTree
318testAnd = databasePropertyTest "AND (&&.)" \transaction -> do
335testAnd = databasePropertyTest "AND (&&.)" \transaction -> do
319 (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool
336 (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool
320
337
321− transaction \connection -> do
338+ transaction do
322− [result] <- liftIO $ Rel8.select connection $ pure $
339+ [result] <- lift do
340+ statement () $ Rel8.select do
323− Rel8.lit x Rel8.&&. Rel8.lit y
341+ pure $ Rel8.lit x Rel8.&&. Rel8.lit y
324
342
325 result === (x && y)
343 result === (x && y)
326
344
@@ -329,9 +347,10 @@ testOr :: IO TmpPostgres.DB -> TestTree
329testOr = databasePropertyTest "OR (||.)" \transaction -> do
347testOr = databasePropertyTest "OR (||.)" \transaction -> do
330 (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool
348 (x, y) <- forAll $ liftA2 (,) Gen.bool Gen.bool
331
349
332− transaction \connection -> do
350+ transaction do
351+ [result] <- lift do
333− [result] <- liftIO $ Rel8.select connection $ pure $
352+ statement () $ Rel8.select $ pure $
334− Rel8.lit x Rel8.||. Rel8.lit y
353+ Rel8.lit x Rel8.||. Rel8.lit y
335
354
336 result === (x || y)
355 result === (x || y)
337
356
@@ -340,9 +359,10 @@ testLogicalFixities :: IO TmpPostgres.DB -> TestTree
340testLogicalFixities = databasePropertyTest "Logical operator fixities" \transaction -> do
359testLogicalFixities = databasePropertyTest "Logical operator fixities" \transaction -> do
341 (u, v, w, x) <- forAll $ (,,,) <$> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool
360 (u, v, w, x) <- forAll $ (,,,) <$> Gen.bool <*> Gen.bool <*> Gen.bool <*> Gen.bool
342
361
343− transaction \connection -> do
362+ transaction do
344− [result] <- liftIO $ Rel8.select connection $ pure $
363+ [result] <- lift do
364+ statement () $ Rel8.select do
345− Rel8.lit u Rel8.||. Rel8.lit v Rel8.&&. Rel8.lit w Rel8.==. Rel8.lit x
365+ pure $ Rel8.lit u Rel8.||. Rel8.lit v Rel8.&&. Rel8.lit w Rel8.==. Rel8.lit x
346
366
347 result === (u || v && w == x)
367 result === (u || v && w == x)
348
368
@@ -351,9 +371,10 @@ testNot :: IO TmpPostgres.DB -> TestTree
351testNot = databasePropertyTest "NOT (not_)" \transaction -> do
371testNot = databasePropertyTest "NOT (not_)" \transaction -> do
352 x <- forAll Gen.bool
372 x <- forAll Gen.bool
353
373
354− transaction \connection -> do
374+ transaction do
355− [result] <- liftIO $ Rel8.select connection $ pure $
375+ [result] <- lift do
376+ statement () $ Rel8.select do
356− Rel8.not_ $ Rel8.lit x
377+ pure $ Rel8.not_ $ Rel8.lit x
357
378
358 result === not x
379 result === not x
359
380
@@ -362,9 +383,10 @@ testBool :: IO TmpPostgres.DB -> TestTree
362testBool = databasePropertyTest "ifThenElse_" \transaction -> do
383testBool = databasePropertyTest "ifThenElse_" \transaction -> do
363 (x, y, z) <- forAll $ liftA3 (,,) Gen.bool Gen.bool Gen.bool
384 (x, y, z) <- forAll $ liftA3 (,,) Gen.bool Gen.bool Gen.bool
364
385
365− transaction \connection -> do
386+ transaction do
366− [result] <- liftIO $ Rel8.select connection $ pure $
387+ [result] <- lift do
388+ statement () $ Rel8.select do
367− Rel8.bool (Rel8.lit z) (Rel8.lit y) (Rel8.lit x)
389+ pure $ Rel8.bool (Rel8.lit z) (Rel8.lit y) (Rel8.lit x)
368
390
369 result === if x then y else z
391 result === if x then y else z
370
392
@@ -376,9 +398,10 @@ testAp = databasePropertyTest "Cartesian product (<*>)" \transaction -> do
376 (Gen.list (Range.linear 1 10) genTestTable)
398 (Gen.list (Range.linear 1 10) genTestTable)
377 (Gen.list (Range.linear 1 10) genTestTable)
399 (Gen.list (Range.linear 1 10) genTestTable)
378
400
379− transaction \connection -> do
401+ transaction do
402+ result <- lift do
380− result <- liftIO $ Rel8.select connection $ do
403+ statement () $ Rel8.select do
381− liftA2 (,) (Rel8.values (Rel8.lit <$> rows1)) (Rel8.values (Rel8.lit <$> rows2))
404+ liftA2 (,) (Rel8.values (Rel8.lit <$> rows1)) (Rel8.values (Rel8.lit <$> rows2))
382
405
383 sort result === sort (liftA2 (,) rows1 rows2)
406 sort result === sort (liftA2 (,) rows1 rows2)
384
407
@@ -407,21 +430,22 @@ testDBType getTestDatabase = testGroup "DBType instances"
407 where
430 where
408 dbTypeTest :: (Eq a, Show a, Rel8.DBType a) => TestName -> Gen a -> TestTree
431 dbTypeTest :: (Eq a, Show a, Rel8.DBType a) => TestName -> Gen a -> TestTree
409 dbTypeTest name generator = testGroup name
432 dbTypeTest name generator = testGroup name
410− [ databasePropertyTest name (t (==) generator) getTestDatabase
433+ [ databasePropertyTest name (t generator) getTestDatabase
411− , databasePropertyTest ("Maybe " <> name) (t (==) (Gen.maybe generator)) getTestDatabase
434+ , databasePropertyTest ("Maybe " <> name) (t (Gen.maybe generator)) getTestDatabase
412 ]
435 ]
413
436
414− t :: forall a b. (Show a, Rel8.Sql Rel8.DBType a)
437+ t :: forall a b. (Eq a, Show a, Rel8.Sql Rel8.DBType a)
415− => (a -> a -> Bool)
416− -> Gen a
438+ => Gen a
417− -> ((Connection -> TestT IO ()) -> PropertyT IO b)
439+ -> (TestT Transaction () -> PropertyT IO b)
418 -> PropertyT IO b
440 -> PropertyT IO b
419− t eq generator transaction = do
441+ t generator transaction = do
420 x <- forAll generator
442 x <- forAll generator
421
443
422− transaction \connection -> do
444+ transaction do
445+ [res] <- lift do
446+ statement () $ Rel8.select do
423− [res] <- liftIO $ Rel8.select connection $ pure (Rel8.litExpr x)
447+ pure (Rel8.litExpr x)
424− diff res eq x
448+ diff res (==) x
425
449
426 genDay :: Gen Day
450 genDay :: Gen Day
427 genDay = do
451 genDay = do
@@ -463,13 +487,15 @@ testDBEq getTestDatabase = testGroup "DBEq instances"
463
487
464 t :: forall a. (Eq a, Show a, Rel8.Sql Rel8.DBEq a)
488 t :: forall a. (Eq a, Show a, Rel8.Sql Rel8.DBEq a)
465 => Gen a
489 => Gen a
466− -> ((Connection -> TestT IO ()) -> PropertyT IO ())
490+ -> (TestT Transaction () -> PropertyT IO ())
467 -> PropertyT IO ()
491 -> PropertyT IO ()
468 t generator transaction = do
492 t generator transaction = do
469 (x, y) <- forAll (liftA2 (,) generator generator)
493 (x, y) <- forAll (liftA2 (,) generator generator)
470
494
471− transaction \connection -> do
495+ transaction do
496+ [res] <- lift do
497+ statement () $ Rel8.select do
472− [res] <- liftIO $ Rel8.select connection $ pure $ Rel8.litExpr x Rel8.==. Rel8.litExpr y
498+ pure $ Rel8.litExpr x Rel8.==. Rel8.litExpr y
473 res === (x == y)
499 res === (x == y)
474
500
475
501
@@ -477,9 +503,10 @@ testTableEquality :: IO TmpPostgres.DB -> TestTree
477testTableEquality = databasePropertyTest "TestTable equality" \transaction -> do
503testTableEquality = databasePropertyTest "TestTable equality" \transaction -> do
478 (x, y) <- forAll $ liftA2 (,) genTestTable genTestTable
504 (x, y) <- forAll $ liftA2 (,) genTestTable genTestTable
479
505
480− transaction \connection -> do
506+ transaction do
507+ [eq] <- lift do
481− [eq] <- liftIO $ Rel8.select connection do
508+ statement () $ Rel8.select do
482− pure $ Rel8.lit x Rel8.==: Rel8.lit y
509+ pure $ Rel8.lit x Rel8.==: Rel8.lit y
483
510
484 eq === (x == y)
511 eq === (x == y)
485
512
@@ -488,8 +515,10 @@ testFromString :: IO TmpPostgres.DB -> TestTree
488testFromString = databasePropertyTest "FromString" \transaction -> do
515testFromString = databasePropertyTest "FromString" \transaction -> do
489 str <- forAll $ Gen.list (Range.linear 0 10) Gen.unicode
516 str <- forAll $ Gen.list (Range.linear 0 10) Gen.unicode
490
517
491− transaction \connection -> do
518+ transaction do
519+ [result] <- lift do
520+ statement () $ Rel8.select do
492− [result] <- liftIO $ Rel8.select connection $ pure $ fromString str
521+ pure $ fromString str
493 result === pack str
522 result === pack str
494
523
495
524
@@ -497,10 +526,11 @@ testCatMaybeTable :: IO TmpPostgres.DB -> TestTree
497testCatMaybeTable = databasePropertyTest "catMaybeTable" \transaction -> do
526testCatMaybeTable = databasePropertyTest "catMaybeTable" \transaction -> do
498 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
527 rows <- forAll $ Gen.list (Range.linear 0 10) genTestTable
499
528
500− transaction \connection -> do
529+ transaction do
530+ selected <- lift do
501− selected <- liftIO $ Rel8.select connection do
531+ statement () $ Rel8.select do
502− testTable <- Rel8.values $ Rel8.lit <$> rows
532+ testTable <- Rel8.values $ Rel8.lit <$> rows
503− Rel8.catMaybeTable $ Rel8.bool Rel8.nothingTable (pure testTable) (testTableColumn2 testTable)
533+ Rel8.catMaybeTable $ Rel8.bool Rel8.nothingTable (pure testTable) (testTableColumn2 testTable)
504
534
505 sort selected === sort (filter testTableColumn2 rows)
535 sort selected === sort (filter testTableColumn2 rows)
506
536
@@ -509,9 +539,10 @@ testCatMaybe :: IO TmpPostgres.DB -> TestTree
509testCatMaybe = databasePropertyTest "catMaybe" \transaction -> evalM do
539testCatMaybe = databasePropertyTest "catMaybe" \transaction -> evalM do
510 rows <- forAll $ Gen.list (Range.linear 0 10) $ Gen.maybe Gen.bool
540 rows <- forAll $ Gen.list (Range.linear 0 10) $ Gen.maybe Gen.bool
511
541
512− transaction \connection -> do
542+ transaction do
513− selected <- evalM $ liftIO $ Rel8.select connection do
543+ selected <- lift do
544+ statement () $ Rel8.select do
514− Rel8.catNull =<< Rel8.values (map Rel8.lit rows)
545+ Rel8.catNull =<< Rel8.values (map Rel8.lit rows)
515
546
516 sort selected === sort (catMaybes rows)
547 sort selected === sort (catMaybes rows)
517
548
@@ -520,15 +551,38 @@ testMaybeTable :: IO TmpPostgres.DB -> TestTree
520testMaybeTable = databasePropertyTest "maybeTable" \transaction -> evalM do
551testMaybeTable = databasePropertyTest "maybeTable" \transaction -> evalM do
521 (rows, def) <- forAll $ liftA2 (,) (Gen.list (Range.linear 0 10) genTestTable) genTestTable
552 (rows, def) <- forAll $ liftA2 (,) (Gen.list (Range.linear 0 10) genTestTable) genTestTable
522
553
523− transaction \connection -> do
554+ transaction do
524− selected <- liftIO $ Rel8.select connection $
555+ selected <- lift do
556+ statement () $ Rel8.select do
525− Rel8.maybeTable (Rel8.lit def) id <$> Rel8.optional (Rel8.values (Rel8.lit <$> rows))
557+ Rel8.maybeTable (Rel8.lit def) id <$> Rel8.optional (Rel8.values (Rel8.lit <$> rows))
526
558
527 case rows of
559 case rows of
528 [] -> selected === [def]
560 [] -> selected === [def]
529 _ -> sort selected === sort rows
561 _ -> sort selected === sort rows
530
562
531
563
564+testAggregateMaybeTable :: IO TmpPostgres.DB -> TestTree
565+testAggregateMaybeTable = databasePropertyTest "aggregateMaybeTable" \transaction -> evalM do
566+ rows <- forAll $ Gen.list (Range.linear 0 10) (Gen.maybe (Gen.int64 (Range.linear 0 10)))
567+
568+ let
569+ aggregate = go 0 False False
570+ where
571+ go !n nothing _ (Just a : as) = go (n + a) nothing True as
572+ go n _ just (Nothing : as) = go n True just as
573+ go _ False False [] = []
574+ go _ True False [] = [Nothing]
575+ go n False True [] = [Just n]
576+ go n True True [] = [Nothing, Just n]
577+
578+ transaction do
579+ selected <- lift do
580+ statement () $ Rel8.select do
581+ Rel8.aggregate $ Rel8.aggregateMaybeTable Rel8.sum <$> Rel8.values (Rel8.lit <$> rows)
582+
583+ sort selected === aggregate rows
584+
585+
532data TwoTestTables f =
586data TwoTestTables f =
533 TwoTestTables
587 TwoTestTables
534 { testTable1 :: TestTable f
588 { testTable1 :: TestTable f
@@ -549,9 +603,10 @@ testNestedTables = databasePropertyTest "Nested TestTables" \transaction -> eval
549 Gen.list (Range.linear 0 10) $
603 Gen.list (Range.linear 0 10) $
550 liftA2 TwoTestTables genTestTable genTestTable
604 liftA2 TwoTestTables genTestTable genTestTable
551
605
552− transaction \connection -> do
606+ transaction do
607+ selected <- lift do
553− selected <- liftIO $ Rel8.select connection do
608+ statement () $ Rel8.select do
554− Rel8.values (Rel8.lit <$> rows)
609+ Rel8.values (Rel8.lit <$> rows)
555
610
556 sort selected === sort rows
611 sort selected === sort rows
557
612
@@ -561,11 +616,12 @@ testMaybeTableApplicative = databasePropertyTest "MaybeTable (<*>)" \transaction
561 rows1 <- genRows
616 rows1 <- genRows
562 rows2 <- genRows
617 rows2 <- genRows
563
618
564− transaction \connection -> do
619+ transaction do
620+ selected <- lift do
565− selected <- liftIO $ Rel8.select connection do
621+ statement () $ Rel8.select do
566− as <- Rel8.optional (Rel8.values (Rel8.lit <$> rows1))
622+ as <- Rel8.optional (Rel8.values (Rel8.lit <$> rows1))
567− bs <- Rel8.optional (Rel8.values (Rel8.lit <$> rows2))
623+ bs <- Rel8.optional (Rel8.values (Rel8.lit <$> rows2))
568− pure $ liftA2 (,) as bs
624+ pure $ liftA2 (,) as bs
569
625
570 case (rows1, rows2) of
626 case (rows1, rows2) of
571 ([], []) -> selected === [Nothing]
627 ([], []) -> selected === [Nothing]
@@ -577,14 +633,6 @@ testMaybeTableApplicative = databasePropertyTest "MaybeTable (<*>)" \transaction
577 genRows = forAll do
633 genRows = forAll do
578 Gen.list (Range.linear 0 10) $ liftA2 TestTable (Gen.text (Range.linear 0 10) Gen.unicode) (pure True)
634 Gen.list (Range.linear 0 10) $ liftA2 TestTable (Gen.text (Range.linear 0 10) Gen.unicode) (pure True)
579
635
580−rollingBack
581− :: (MonadBaseControl IO m, MonadIO m)
582− => Connection -> m a -> m a
583−rollingBack connection =
584− bracket_
585− (liftIO (run (sql "BEGIN") connection))
586− (liftIO (run (sql "ROLLBACK") connection))
587−
588
636
589genTestTable :: Gen (TestTable Result)
637genTestTable :: Gen (TestTable Result)
590genTestTable = do
638genTestTable = do
@@ -597,17 +645,16 @@ testUpdate :: IO TmpPostgres.DB -> TestTree
597testUpdate = databasePropertyTest "Can UPDATE TestTable" \transaction -> do
645testUpdate = databasePropertyTest "Can UPDATE TestTable" \transaction -> do
598 rows <- forAll $ Gen.map (Range.linear 0 5) $ liftA2 (,) genTestTable genTestTable
646 rows <- forAll $ Gen.map (Range.linear 0 5) $ liftA2 (,) genTestTable genTestTable
599
647
600− transaction \connection -> do
648+ transaction do
601− liftIO $ Rel8.insert connection
649+ selected <- lift do
602− Rel8.Insert
650+ statement () $ Rel8.insert Rel8.Insert
603 { into = testTableSchema
651 { into = testTableSchema
604 , rows = Rel8.values $ map Rel8.lit $ Map.keys rows
652 , rows = Rel8.values $ map Rel8.lit $ Map.keys rows
605 , onConflict = Rel8.DoNothing
653 , onConflict = Rel8.DoNothing
606 , returning = pure ()
654 , returning = pure ()
607 }
655 }
608
656
609− liftIO $ Rel8.update connection
610− Rel8.Update
657+ statement () $ Rel8.update Rel8.Update
611 { target = testTableSchema
658 { target = testTableSchema
612 , from = pure ()
659 , from = pure ()
613 , set = \_ r ->
660 , set = \_ r ->
@@ -628,8 +675,8 @@ testUpdate = databasePropertyTest "Can UPDATE TestTable" \transaction -> do
628 , returning = pure ()
675 , returning = pure ()
629 }
676 }
630
677
631− selected <- liftIO $ Rel8.select connection do
678+ statement () $ Rel8.select do
632− Rel8.each testTableSchema
679+ Rel8.each testTableSchema
633
680
634 sort selected === sort (Map.elems rows)
681 sort selected === sort (Map.elems rows)
635
682
@@ -642,30 +689,96 @@ testDelete :: IO TmpPostgres.DB -> TestTree
642testDelete = databasePropertyTest "Can DELETE TestTable" \transaction -> do
689testDelete = databasePropertyTest "Can DELETE TestTable" \transaction -> do
643 rows <- forAll $ Gen.list (Range.linear 0 5) genTestTable
690 rows <- forAll $ Gen.list (Range.linear 0 5) genTestTable
644
691
645− transaction \connection -> do
692+ transaction do
646− liftIO $ Rel8.insert connection
693+ (deleted, selected) <- lift do
647− Rel8.Insert
694+ statement () $ Rel8.insert Rel8.Insert
648 { into = testTableSchema
695 { into = testTableSchema
649 , rows = Rel8.values $ map Rel8.lit rows
696 , rows = Rel8.values $ map Rel8.lit rows
650 , onConflict = Rel8.DoNothing
697 , onConflict = Rel8.DoNothing
651 , returning = pure ()
698 , returning = pure ()
652 }
699 }
653
700
654− deleted <-
655− liftIO $ Rel8.delete connection
656− Rel8.Delete
701+ deleted <- statement () $ Rel8.delete Rel8.Delete
657 { from = testTableSchema
702 { from = testTableSchema
658 , using = pure ()
703 , using = pure ()
659 , deleteWhere = const testTableColumn2
704 , deleteWhere = const testTableColumn2
660 , returning = Rel8.Projection id
705 , returning = Rel8.Projection id
661 }
706 }
662
707
663− selected <- liftIO $ Rel8.select connection do
708+ selected <- statement () $ Rel8.select do
664− Rel8.each testTableSchema
709+ Rel8.each testTableSchema
710+
711+ pure (deleted, selected)
665
712
666 sort (deleted <> selected) === sort rows
713 sort (deleted <> selected) === sort rows
667
714
668
715
716+data UniqueTable f = UniqueTable
717+ { uniqueTableKey :: Rel8.Column f Text
718+ , uniqueTableValue :: Rel8.Column f Text
719+ }
720+ deriving stock Generic
721+ deriving anyclass Rel8.Rel8able
722+
723+
724+deriving stock instance Eq (UniqueTable Result)
725+deriving stock instance Ord (UniqueTable Result)
726+deriving stock instance Show (UniqueTable Result)
727+
728+
729+uniqueTableSchema :: Rel8.TableSchema (UniqueTable Rel8.Name)
730+uniqueTableSchema =
731+ Rel8.TableSchema
732+ { name = "unique_table"
733+ , schema = Nothing
734+ , columns = UniqueTable
735+ { uniqueTableKey = "key"
736+ , uniqueTableValue = "value"
737+ }
738+ }
739+
740+
741+genUniqueTable :: Gen (UniqueTable Result)
742+genUniqueTable = do
743+ uniqueTableKey <- Gen.text (Range.linear 0 5) Gen.alphaNum
744+ uniqueTableValue <- Gen.text (Range.linear 0 5) Gen.alphaNum
745+ pure UniqueTable {..}
746+
747+
748+testUpsert :: IO TmpPostgres.DB -> TestTree
749+testUpsert = databasePropertyTest "Can UPSERT UniqueTable" \transaction -> do
750+ as <- unique $ forAll $ Gen.list (Range.linear 0 20) genUniqueTable
751+ bs <- unique $ forAll $ Gen.list (Range.linear 0 20) genUniqueTable
752+
753+ transaction do
754+ selected <- lift do
755+ statement () $ Rel8.insert Rel8.Insert
756+ { into = uniqueTableSchema
757+ , rows = Rel8.values $ Rel8.lit <$> as
758+ , onConflict = Rel8.DoNothing
759+ , returning = pure ()
760+ }
761+
762+ statement () $ Rel8.insert Rel8.Insert
763+ { into = uniqueTableSchema
764+ , rows = Rel8.values $ Rel8.lit <$> bs
765+ , onConflict = Rel8.DoUpdate Rel8.Upsert
766+ { index = uniqueTableKey
767+ , set = \UniqueTable {uniqueTableValue} old -> old {uniqueTableValue}
768+ , updateWhere = \_ _ -> Rel8.true
769+ }
770+ , returning = pure ()
771+ }
772+
773+ statement () $ Rel8.select do
774+ Rel8.each uniqueTableSchema
775+
776+ fromUniqueTables selected === fromUniqueTables bs <> fromUniqueTables as
777+ where
778+ unique = fmap (nubOrdOn uniqueTableKey)
779+ fromUniqueTables = Map.fromList . map \(UniqueTable key value) -> (key, value)
780+
781+
669newtype HKNestedPair f = HKNestedPair { pairOne :: (TestTable f, TestTable f) }
782newtype HKNestedPair f = HKNestedPair { pairOne :: (TestTable f, TestTable f) }
670 deriving stock Generic
783 deriving stock Generic
671 deriving anyclass Rel8.Rel8able
784 deriving anyclass Rel8.Rel8able
@@ -679,9 +792,10 @@ testSelectNestedPairs :: IO TmpPostgres.DB -> TestTree
679testSelectNestedPairs = databasePropertyTest "Can SELECT nested pairs" \transaction -> do
792testSelectNestedPairs = databasePropertyTest "Can SELECT nested pairs" \transaction -> do
680 rows <- forAll $ Gen.list (Range.linear 0 10) $ HKNestedPair <$> liftA2 (,) genTestTable genTestTable
793 rows <- forAll $ Gen.list (Range.linear 0 10) $ HKNestedPair <$> liftA2 (,) genTestTable genTestTable
681
794
682− transaction \connection -> do
795+ transaction do
796+ selected <- lift do
683− selected <- liftIO $ Rel8.select connection do
797+ statement () $ Rel8.select do
684− Rel8.values $ map Rel8.lit rows
798+ Rel8.values $ map Rel8.lit rows
685
799
686 sort selected === sort rows
800 sort selected === sort rows
687
801
@@ -690,18 +804,20 @@ testSelectArray :: IO TmpPostgres.DB -> TestTree
690testSelectArray = databasePropertyTest "Can SELECT Arrays (with aggregation)" \transaction -> do
804testSelectArray = databasePropertyTest "Can SELECT Arrays (with aggregation)" \transaction -> do
691 rows <- forAll $ Gen.list (Range.linear 1 10) Gen.bool
805 rows <- forAll $ Gen.list (Range.linear 1 10) Gen.bool
692
806
693− transaction \connection -> do
807+ transaction do
808+ selected <- lift do
694− selected <- liftIO $ Rel8.select connection do
809+ statement () $ Rel8.select do
695− Rel8.many $ Rel8.values (map Rel8.lit rows)
810+ Rel8.many $ Rel8.values (map Rel8.lit rows)
696
811
697 selected === [foldMap pure rows]
812 selected === [foldMap pure rows]
698
813
814+ selected' <- lift do
699− selected' <- liftIO $ Rel8.select connection $ do
815+ statement () $ Rel8.select do
700− a <- Rel8.catListTable =<< do
816+ a <- Rel8.catListTable =<< do
701− Rel8.many $ Rel8.values (map Rel8.lit rows)
817+ Rel8.many $ Rel8.values (map Rel8.lit rows)
702− b <- Rel8.catListTable =<< do
818+ b <- Rel8.catListTable =<< do
703− Rel8.many $ Rel8.values (map Rel8.lit rows)
819+ Rel8.many $ Rel8.values (map Rel8.lit rows)
704− pure (a, b)
820+ pure (a, b)
705
821
706 selected' === liftA2 (,) rows rows
822 selected' === liftA2 (,) rows rows
707
823
@@ -723,10 +839,11 @@ testNestedMaybeTable :: IO TmpPostgres.DB -> TestTree
723testNestedMaybeTable = databasePropertyTest "Can nest MaybeTable within other tables" \transaction -> do
839testNestedMaybeTable = databasePropertyTest "Can nest MaybeTable within other tables" \transaction -> do
724 let example = NestedMaybeTable { nmt1 = True, nmt2 = Just (TestTable "Hi" True) }
840 let example = NestedMaybeTable { nmt1 = True, nmt2 = Just (TestTable "Hi" True) }
725
841
726− transaction \connection -> do
842+ transaction do
843+ selected <- lift do
727− selected <- liftIO $ Rel8.select connection do
844+ statement () $ Rel8.select do
728− x <- Rel8.values [Rel8.lit example]
845+ x <- Rel8.values [Rel8.lit example]
729− pure $ Rel8.maybeTable (Rel8.lit False) (\_ -> Rel8.lit True) (nmt2 x)
846+ pure $ Rel8.maybeTable (Rel8.lit False) (\_ -> Rel8.lit True) (nmt2 x)
730
847
731 selected === [True]
848 selected === [True]
732
849
@@ -734,11 +851,12 @@ testNestedMaybeTable = databasePropertyTest "Can nest MaybeTable within other ta
734testEvaluate :: IO TmpPostgres.DB -> TestTree
851testEvaluate :: IO TmpPostgres.DB -> TestTree
735testEvaluate = databasePropertyTest "evaluate has the evaluation order we expect" \transaction -> do
852testEvaluate = databasePropertyTest "evaluate has the evaluation order we expect" \transaction -> do
736
853
737− transaction \connection -> do
854+ transaction do
855+ selected <- lift do
738− selected <- liftIO $ Rel8.select connection do
856+ statement () $ Rel8.select do
739− x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])
857+ x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])
740− y <- Rel8.evaluate (Rel8.nextval "test_seq")
858+ y <- Rel8.evaluate (Rel8.nextval "test_seq")
741− pure (x, (y, y))
859+ pure (x, (y, y))
742
860
743 normalize selected ===
861 normalize selected ===
744 [ ('a', (0, 0))
862 [ ('a', (0, 0))
@@ -746,11 +864,12 @@ testEvaluate = databasePropertyTest "evaluate has the evaluation order we expect
746 , ('c', (2, 2))
864 , ('c', (2, 2))
747 ]
865 ]
748
866
867+ selected' <- lift do
749− selected' <- liftIO $ Rel8.select connection do
868+ statement () $ Rel8.select do
750− x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])
869+ x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])
751− y <- Rel8.values (Rel8.lit <$> ['d', 'e', 'f'])
870+ y <- Rel8.values (Rel8.lit <$> ['d', 'e', 'f'])
752− z <- Rel8.evaluate (Rel8.nextval "test_seq")
871+ z <- Rel8.evaluate (Rel8.nextval "test_seq")
753− pure ((x, y), (z, z))
872+ pure ((x, y), (z, z))
754
873
755 normalize selected' ===
874 normalize selected' ===
756 [ (('a', 'd'), (0, 0))
875 [ (('a', 'd'), (0, 0))
@@ -768,13 +887,3 @@ testEvaluate = databasePropertyTest "evaluate has the evaluation order we expect
768 normalize :: [(x, (Int64, Int64))] -> [(x, (Int64, Int64))]
887 normalize :: [(x, (Int64, Int64))] -> [(x, (Int64, Int64))]
769 normalize [] = []
888 normalize [] = []
770 normalize xs@((_, (i, _)) : _) = map (fmap (\(a, b) -> (a - i, b - i))) xs
889 normalize xs@((_, (i, _)) : _) = map (fmap (\(a, b) -> (a - i, b - i))) xs
771−
772−{-
773− selected <- liftIO $ Rel8.select connection do
774− x <- Rel8.values (Rel8.lit <$> ['a', 'b', 'c'])
775− x <- Rel8.values (Rel8.lit <$> ['d', 'e', 'f'])
776− y <- Rel8.evaluate (Rel8.nextval "test_seq")
777− pure (x, y, y)
778−
779− selected === [('a', 1, 1), ('b', 2, 2), ('c', 3, 3)]
780−-}
tests/Rel8/Generic/Rel8able/Test.hs+10−1
@@ -5,6 +5,7 @@
5{-# language DuplicateRecordFields #-}
5{-# language DuplicateRecordFields #-}
6{-# language FlexibleInstances #-}
6{-# language FlexibleInstances #-}
7{-# language MultiParamTypeClasses #-}
7{-# language MultiParamTypeClasses #-}
8+{-# language StandaloneKindSignatures #-}
8{-# language TypeFamilies #-}
9{-# language TypeFamilies #-}
9{-# language UndecidableInstances #-}
10{-# language UndecidableInstances #-}
10
11
@@ -171,9 +172,17 @@ data NestedTableTestB f = NestedTableTestB
171 deriving anyclass Rel8able
172 deriving anyclass Rel8able
172
173
173
174
174−
175newtype IdRecord a f = IdRecord { recordId :: Column f a }
175newtype IdRecord a f = IdRecord { recordId :: Column f a }
176 deriving stock Generic
176 deriving stock Generic
177
177
178
178
179instance DBType a => Rel8able (IdRecord a)
179instance DBType a => Rel8able (IdRecord a)
180+
181+
182+type Nest :: KRel8able -> KRel8able -> KRel8able
183+data Nest t u f = Nest
184+ { foo :: t f
185+ , bar :: u f
186+ }
187+ deriving stock Generic
188+ deriving anyclass Rel8able