on dev help sign in
Merge pull request #23 from morphismtech/0.2
1ac00caEitan Chatav
.circleci/config.ymlnew file+23
@@ -0,0 +1,23 @@
1+version: 2
2+jobs:
3+ build:
4+ docker:
5+ - image: haskell:latest
6+ - image: circleci/postgres:latest
7+ environment:
8+ POSTGRES_DB: exampledb
9+ steps:
10+ - checkout
11+ - restore_cache:
12+ keys:
13+ - dependency-cache
14+ - run: apt-get update && apt-get install -y libpq-dev xz-utils make
15+ - run: stack upgrade && stack update
16+ - run: stack build
17+ - run: stack test
18+ - run: stack haddock
19+ - save_cache:
20+ key: dependency-cache
21+ paths:
22+ - ".stack-work"
23+ - "/root/.stack/"
.gitignore+1
@@ -17,3 +17,4 @@ cabal.sandbox.config
17*.eventlog
17*.eventlog
18.stack-work/
18.stack-work/
19cabal.project.local
19cabal.project.local
20+.DS_Store
README.md+162−97
@@ -2,7 +2,7 @@
2
2
3![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
3![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
4
4
5−[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/echatav/squeal)
5+[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/morphismtech/squeal)
6
6
7[Github](https://github.com/morphismtech/squeal)
7[Github](https://github.com/morphismtech/squeal)
8
8
@@ -17,148 +17,213 @@
17## usage
17## usage
18
18
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
20−
21−First, we need some language extensions because Squeal uses modern GHC features.
20+First, we need some language extensions because Squeal uses modern GHC
22−
21+features.
23```haskell
22```haskell
24−{-# LANGUAGE
23+>>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
25− DataKinds
24+>>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
26− , DeriveGeneric
27− , OverloadedLabels
28− , OverloadedStrings
29− , TypeApplications
30− , TypeOperators
31−#-}
32```
25```
33
26
34−Here comes the Main module and imports.
27+We'll need some imports.
35
28
36```haskell
29```haskell
37−module Main (main) where
30+>>> import Control.Monad (void)
38−
39−import Control.Monad.Base (liftBase)
31+>>> import Control.Monad.Base (liftBase)
40−import Data.Int (Int32)
32+>>> import Data.Int (Int32)
41−import Data.Text (Text)
33+>>> import Data.Text (Text)
42−
43−import Squeal.PostgreSQL
34+>>> import Squeal.PostgreSQL
44```
35```
45
36
46We'll use generics to easily convert between Haskell and PostgreSQL values.
37We'll use generics to easily convert between Haskell and PostgreSQL values.
47
38
48```haskell
39```haskell
49−import qualified Generics.SOP as SOP
40+>>> import qualified Generics.SOP as SOP
50−import qualified GHC.Generics as GHC
41+>>> import qualified GHC.Generics as GHC
51```
42```
52
43
53−The first step is to define the schema of our database. This is where we use DataKinds and TypeOperators. The schema consists of a type-level list of tables, a `:::` pairing of a type level string or `Symbol` and a list a columns, itself a `:::` pairing of a `Symbol` and a `ColumnType`. The `ColumnType` describes the PostgreSQL type of the column as well as whether or not it may contain `NULL` and whether or not inserts and updates can use a `DEFAULT`. For our schema, we'll define two tables, a users table and an emails table.
44+The first step is to define the schema of our database. This is where
45+we use `DataKinds` and `TypeOperators`. The schema consists of a type-level
46+list of tables, a `:::` pairing of a type level string or
47+`Symbol` and a list a columns, itself a `:::` pairing of a
48+`Symbol` and a `ColumnType`. The `ColumnType` describes the
49+PostgreSQL type of the column as well as whether or not it may contain
50+`NULL` and whether or not inserts and updates can use a `DEFAULT`. For our
51+schema, we'll define two tables, a users table and an emails table.
54
52
55```haskell
53```haskell
54+>>> :{
56type Schema =
55type Schema =
57− '[ "users" :::
56+ '[ "users" :::
57+ '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
58− '[ "id" ::: 'Optional ('NotNull 'PGint4)
58+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
59− , "name" ::: 'Required ('NotNull 'PGtext)
59+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
60 ]
60 ]
61 , "emails" :::
61 , "emails" :::
62+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
63+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
64+ ] :=>
62− '[ "id" ::: 'Optional ('NotNull 'PGint4)
65+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
63− , "user_id" ::: 'Required ('NotNull 'PGint4)
66+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
64− , "email" ::: 'Required ('Null 'PGtext)
67+ , "email" ::: 'NoDef :=> 'Null 'PGtext
65 ]
68 ]
66 ]
69 ]
70+:}
67```
71```
68
72
69−Next, we'll write Definitions to set up and tear down the schema. In Squeal, a Definition is a `createTable`, `alterTable` or `dropTable` command and has two type parameters, corresponding to the schema before being run and the schema after. We can compose definitions using `>>>`. Here and in the rest of our commands we make use of overloaded labels to refer to named tables and columns in our schema.
73+Next, we'll write `Definition`s to set up and tear down the schema. In
74+Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
75+command and has two type parameters, corresponding to the schema
76+before being run and the schema after. We can compose definitions using
77+`>>>`. Here and in the rest of our commands we make use of overloaded
78+labels to refer to named tables and columns in our schema.
70
79
71```haskell
80```haskell
81+>>> :{
82+let
72−setup :: Definition '[] Schema
83+ setup :: Definition '[] Schema
73−setup =
84+ setup =
74− createTable #users
85+ createTable #users
75− ( serial `As` #id :*
86+ ( serial `As` #id :*
76− (text & notNull) `As` #name :* Nil )
87+ (text & notNull) `As` #name :* Nil )
77− [ primaryKey (Column #id :* Nil) ]
88+ ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
78− >>>
79− createTable #emails
89+ createTable #emails
80− ( serial `As` #id :*
90+ ( serial `As` #id :*
81− (int & notNull) `As` #user_id :*
91+ (int & notNull) `As` #user_id :*
82− text `As` #email :* Nil )
92+ text `As` #email :* Nil )
83− [ primaryKey (Column #id :* Nil)
93+ ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
84− , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
94+ foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
85− OnDeleteCascade OnUpdateCascade ]
95+ OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
96+:}
86```
97```
87
98
88−Notice that setup starts with an empty schema `'[]` and produces `Schema`. In our `createTable` commands we included `TableConstraint`s to define primary and foreign keys, making them somewhat complex. Our tear down `Definition` is simpler.
99+We can easily see the generated SQL is unsuprising looking.
89
100
90```haskell
101```haskell
91−teardown :: Definition Schema '[]
102+>>> renderDefinition setup
92−teardown = dropTable #emails >>> dropTable #users
103+"CREATE TABLE users (id serial, name text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)); CREATE TABLE emails (id serial, user_id int NOT NULL, email text, CONSTRAINT pk_emails PRIMARY KEY (id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES rs (id) ON DELETE CASCADE ON UPDATE CASCADE);"
93```
104```
94
105
95−Next, we'll write `Manipulation`s to insert data into our two tables. A `Manipulation` is an `insertInto`, `update` or `deleteFrom` command and has three type parameters, the schema it refers to, a list of parameters it can take as input, and a list of columns it produces as output. When we insert into the users table, we will need a parameter for the name field but not for the id field. Since it's optional, we can use a default value. However, since the emails table refers to the users table, we will need to retrieve the user id that the insert generates and insert it into the emails table. Take a careful look at the type and definition of both of our inserts.
106+Notice that `setup` starts with an empty schema `'[]` and produces `Schema`.
107+In our `createTable` commands we included `TableConstraint`s to define
108+primary and foreign keys, making them somewhat complex. Our tear down
109+`Definition` is simpler.
96
110
97```haskell
111```haskell
98−insertUser :: Manipulation Schema
112+>>> :{
99− '[ 'Required ('NotNull 'PGtext)]
113+let
100− '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
114+ teardown :: Definition Schema '[]
101−insertUser = insertInto #users
115+ teardown = dropTable #emails >>> dropTable #users
102− ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
116+:}
103− OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
117+>>> renderDefinition teardown
104−
118+"DROP TABLE emails; DROP TABLE users;"
105−insertEmail :: Manipulation Schema
106− '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
107−insertEmail = insertInto #emails ( Values
108− ( def `As` #id :*
109− param @1 `As` #user_id :*
110− param @2 `As` #email :* Nil) [] )
111− OnConflictDoNothing (Returning Nil)
112```
119```
113
120
114−Next we write a `Query` to retrieve users from the database. We're not interested in the ids here, just the usernames and email addresses. We need to use an inner join to get the right result. A `Query` is like a `Manipulation` with the same kind of type parameters.
121+Next, we'll write `Manipulation`s to insert data into our two tables.
122+A `Manipulation` is a `insertInto`, `update` or `deleteFrom` command and
123+has three type parameters, the schema it refers to, a list of parameters
124+it can take as input, and a list of columns it produces as output. When
125+we insert into the users table, we will need a parameter for the `name`
126+field but not for the `id` field. Since it's optional, we can use a default
127+value. However, since the emails table refers to the users table, we will
128+need to retrieve the user id that the insert generates and insert it into
129+the emails table. Take a careful look at the type and definition of both
130+of our inserts.
115
131
116```haskell
132```haskell
117−getUsers :: Query Schema '[]
133+>>> :{
118− '[ "userName" ::: 'Required ('NotNull 'PGtext)
134+let
119− , "userEmail" ::: 'Required ('Null 'PGtext) ]
135+ insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ]
120−getUsers = select
136+ '[ "fromOnly" ::: 'NotNull 'PGint4 ]
121− (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
137+ insertUser = insertRow #users
122− ( from (Table (#users `As` #u)
138+ (Default `As` #id :* Set (param `1) `As` #name :* Nil)
123− & InnerJoin (Table (#emails `As` #e))
139+ OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
124− (#u ! #id .== #e ! #user_id)) )
140+:}
141+>>> :{
142+let
143+ insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
144+ insertEmail = insertRow #emails
145+ ( Default `As` #id :*
146+ Set (param `1) `As` #user_id :*
147+ Set (param `2) `As` #email :* Nil )
148+ OnConflictDoNothing (Returning Nil)
149+:}
150+>>> renderManipulation insertUser
151+"INSERT INTO users (id, name) VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING URNING id AS fromOnly;"
152+>>> renderManipulation insertEmail
153+"INSERT INTO emails (id, user_id, email) VALUES (DEFAULT, ($1 :: int4), ($2 :: text)N CONFLICT DO NOTHING;"
125```
154```
126
155
127−Now that we've defined the SQL side of things, we'll need a Haskell type for users. We give the type `Generic` and `HasDatatypeInfo` instances so that we can decode the rows we receive when we run getUsers. Notice that the record fields of the `User` type match the column names of `getUsers`.
156+Next we write a `Query` to retrieve users from the database. We're not
157+interested in the ids here, just the usernames and email addresses. We
158+need to use an inner join to get the right result. A `Query` is like a
159+`Manipulation` with the same kind of type parameters.
160+
161+```haskell
162+>>> :{
163+let
164+ getUsers :: Query Schema '[]
165+ '[ "userName" ::: 'NotNull 'PGtext
166+ , "userEmail" ::: 'Null 'PGtext ]
167+ getUsers = select
168+ (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
169+ ( from (table (#users `As` #u)
170+ & innerJoin (table (#emails `As` #e))
171+ (#u ! #id .== #e ! #user_id)) )
172+:}
173+>>> renderQuery getUsers
174+"SELECT u.name AS userName, e.email AS userEmail FROM users AS u INNER JOIN emails e ON (u.id = e.user_id)"
175+```
176+Now that we've defined the SQL side of things, we'll need a Haskell type
177+for users. We give the type `Generics.SOP.Generic` and
178+`Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
179+we receive when we run `getUsers`. Notice that the record fields of the
180+`User` type match the column names of `getUsers`.
128
181
129```haskell
182```haskell
130−data User = User { userName :: Text, userEmail :: Maybe Text }
183+>>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, .Generic)
131− deriving (Show, GHC.Generic)
132−instance SOP.Generic User
184+>>> instance SOP.Generic User
133−instance SOP.HasDatatypeInfo User
185+>>> instance SOP.HasDatatypeInfo User
134```
186```
135
187
136Let's also create some users to add to the database.
188Let's also create some users to add to the database.
137
189
138```haskell
190```haskell
191+>>> :{
192+let
139−users :: [User]
193+ users :: [User]
140−users =
194+ users =
141− [ User "Alice" (Just "alice@gmail.com")
195+ [ User "Alice" (Just "alice`gmail.com")
142− , User "Bob" Nothing
196+ , User "Bob" Nothing
143− , User "Carole" (Just "carole@hotmail.com")
197+ , User "Carole" (Just "carole`hotmail.com")
144− ]
198+ ]
199+:}
145```
200```
146
201
147−Now we can put together all the pieces into a program. The program connects to the database, sets up the schema, inserts the user data (using prepared statements as an optimization), queries the user data and prints it out and finally closes the connection. We can thread the changing schema information through by using the indexed `PQ` monad transformer and when the schema doesn't change we can use `Monad` and `MonadPQ` functionality.
202+Now we can put together all the pieces into a program. The program
203+connects to the database, sets up the schema, inserts the user data
204+(using prepared statements as an optimization), queries the user
205+data and prints it out and finally closes the connection. We can thread
206+the changing schema information through by using the indexed `PQ` monad
207+transformer and when the schema doesn't change we can use `Monad` and
208+`MonadPQ` functionality.
148
209
149```haskell
210```haskell
150−main :: IO ()
211+>>> :{
151−main = void $
212+let
152− withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
213+ session :: PQ Schema Schema IO ()
153− define setup
154− & pqThen session
155− & thenDefine teardown
156− where
157− session = do
214+ session = do
158− idResults <- traversePrepared insertUser (Only . userName <$> users)
215+ idResults <- traversePrepared insertUser (Only . userName <$> users)
159− ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
216+ ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
160− traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
217+ traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
161− usersResult <- runQuery getUsers
218+ usersResult <- runQuery getUsers
162− usersRows <- getRows usersResult
219+ usersRows <- getRows usersResult
163− liftBase $ print (usersRows :: [User])
220+ liftBase $ print (usersRows :: [User])
221+:}
222+>>> :{
223+void . withConnection "host=localhost port=5432 dbname=exampledb" $
224+ define setup
225+ & pqThen session
226+ & thenDefine teardown
227+:}
228+[User {userName = "Alice", userEmail = Just "alice`gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just role`hotmail.com"}]
164```
229```
RELEASE NOTES.mdnew file+134
@@ -0,0 +1,134 @@
1+## RELEASE NOTES
2+
3+### Version 0.2 - March 24, 2018
4+
5+**Changes**
6+- **Constraints** - Type level table constraints like primary and foreign keys and column constraints like having `DEFAULT`.
7+- **Migrations** - Support for linear, invertible migrations tracked in an auxiliary table
8+- **Arrays** - Support for fixed- and variable-length arrays
9+- **Aliases** - Generalized `Has` constraint
10+- **Pools** - Support for pooled connections
11+- **Transactions** - Support for transaction control language
12+- **Queries, Manipulations, Definitions** - Small and large changes to Squeal's DSL
13+
14+Well, a lot of things changed!
15+
16+**Constraints**
17+
18+An important request was to bring constraints to the type level.
19+This means that more of the schema is statically known. In `0.1` column constraints - which boil down
20+to having `DEFAULT` or not - were at the type level, but they were confusingly named.
21+
22+```haskell
23+0.1: 'Optional ('NotNull 'PGInt4)
24+0.2: 'Def :=> 'NotNull 'PGInt4
25+0.1: 'Required ('NotNull 'PGInt4)
26+0.2: 'NoDef :=> 'NotNull 'PGInt4
27+```
28+
29+The `:=>` type operator is intended to helpfully connote a constraint relation.
30+It's also used for table constraints which are new in `0.2`.
31+
32+```haskell
33+"emails" :::
34+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
35+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
36+ ] :=>
37+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
38+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
39+ , "email" ::: 'NoDef :=> 'Null 'PGtext
40+ ]
41+```
42+
43+Another change in the constraint system was the removal of column constraints from
44+query and manipulation results, as result columns don't support a notion of `DEFAULT`.
45+This necessitates a distinction between `TableType`s which have both column and table
46+constraints and `RelationType`s which have neither.
47+
48+**Migrations**
49+
50+Migrations are a hot topic and many people have requested this feature. Squeal `0.2`
51+adds support for linear, invertible migrations. That is, a migration is a named,
52+invertible, schema-tracking computation:
53+
54+```haskell
55+data Migration io schema0 schema1 = Migration
56+ { name :: Text -- ^ The `name` of a `Migration`.
57+ -- Each `name` in a `Migration` should be unique.
58+ , up :: PQ schema0 schema1 io () -- ^ The `up` instruction of a `Migration`.
59+ , down :: PQ schema1 schema0 io () -- ^ The `down` instruction of a `Migration`.
60+ }
61+```
62+
63+And, `Migration`s can be put together in an "aligned" list:
64+
65+```haskell
66+data AlignedList p x0 x1 where
67+ Done :: AlignedList p x x
68+ (:>>) :: p x0 x1 -> AlignedList p x1 x2 -> AlignedList p x0 x2
69+```
70+
71+These aligned lists are free categories and might look familiar from
72+the [reflections without remorse](http://okmij.org/ftp/Haskell/zseq.pdf) technique,
73+which uses their cousins, aligned sequences.
74+
75+In the context of migration, they allow one to chain new migrations as a
76+schema evolves over time. `Migration`s' execution is tracked in an auxiliary
77+migrations table. Migration lists can then be run or rewinded.
78+
79+```haskell
80+migrateUp
81+ :: MonadBaseControl IO io
82+ => AlignedList (Migration io) schema0 schema1 -- ^ migrations to run
83+ -> PQ
84+ ("schema_migrations" ::: MigrationsTable ': schema0)
85+ ("schema_migrations" ::: MigrationsTable ': schema1)
86+ io ()
87+
88+migrateDown
89+ :: MonadBaseControl IO io
90+ => AlignedList (Migration io) schema0 schema1 -- ^ migrations to rewind
91+ -> PQ
92+ ("schema_migrations" ::: MigrationsTable ': schema1)
93+ ("schema_migrations" ::: MigrationsTable ': schema0)
94+ io ()
95+```
96+
97+**Aliases**
98+
99+In Squeal `0.1`, we had different typeclasses `HasColumn` and `HasTable` to indicate
100+that a table has a column or that a schema has a table. In Squeal `0.2` this has been
101+unified to a single typeclass,
102+
103+```haskell
104+class KnownSymbol alias =>
105+ Has (alias :: Symbol) (fields :: [(Symbol,kind)]) (field :: kind)
106+ | alias fields -> field where
107+```
108+
109+**Arrays**
110+
111+Support for array types has been added to Squeal `0.2` through
112+the `'PGfixarray` and `'PGvararray` `PGType`s. Array values can be
113+constructed using the `array` function and can be encoded from and decoded to
114+Haskell `Vector`s.
115+
116+**Pools**
117+
118+Squeal `0.2` provides a monad transformer `PoolPQ` that's an instance of `MonadPQ`.
119+The `resource-pool` library is leveraged to provide striped pools of `Connection`s.
120+`PoolPQ` should be a drop in replacement for running `Manipulation`s and `Query`s with
121+`PQ`.
122+
123+**Transactions**
124+
125+Squeal `0.2` supports a simple transaction control language. A computation in
126+`MonadPQ` can be called `transactionally` with different levels of isolation.
127+Additionally, a schema changing computation, a data definition, can be run in a
128+transaction. Running a computation in a transaction means that all SQL statements
129+will be rolled back if an exception is encountered.
130+
131+**Queries, Manipulations and Definitions**
132+
133+The above changes required major and minor changes to Squeal DSL functions.
134+Please consult the documentation.
circle.ymldeleted−12
@@ -1,12 +0,0 @@
1−dependencies:
2− cache_directories:
3− - "~/.stack"
4− - ".stack-work"
5− override:
6− - stack upgrade
7− - stack update
8− - stack setup
9− - stack test --no-run-tests
10−test:
11− override:
12− - stack test
squeal-postgresql/README.md+162−97
@@ -2,7 +2,7 @@
2
2
3![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
3![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
4
4
5−[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/echatav/squeal)
5+[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/morphismtech/squeal)
6
6
7[Github](https://github.com/morphismtech/squeal)
7[Github](https://github.com/morphismtech/squeal)
8
8
@@ -17,148 +17,213 @@
17## usage
17## usage
18
18
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
19Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
20−
21−First, we need some language extensions because Squeal uses modern GHC features.
20+First, we need some language extensions because Squeal uses modern GHC
22−
21+features.
23```haskell
22```haskell
24−{-# LANGUAGE
23+>>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
25− DataKinds
24+>>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
26− , DeriveGeneric
27− , OverloadedLabels
28− , OverloadedStrings
29− , TypeApplications
30− , TypeOperators
31−#-}
32```
25```
33
26
34−Here comes the Main module and imports.
27+We'll need some imports.
35
28
36```haskell
29```haskell
37−module Main (main) where
30+>>> import Control.Monad (void)
38−
39−import Control.Monad.Base (liftBase)
31+>>> import Control.Monad.Base (liftBase)
40−import Data.Int (Int32)
32+>>> import Data.Int (Int32)
41−import Data.Text (Text)
33+>>> import Data.Text (Text)
42−
43−import Squeal.PostgreSQL
34+>>> import Squeal.PostgreSQL
44```
35```
45
36
46We'll use generics to easily convert between Haskell and PostgreSQL values.
37We'll use generics to easily convert between Haskell and PostgreSQL values.
47
38
48```haskell
39```haskell
49−import qualified Generics.SOP as SOP
40+>>> import qualified Generics.SOP as SOP
50−import qualified GHC.Generics as GHC
41+>>> import qualified GHC.Generics as GHC
51```
42```
52
43
53−The first step is to define the schema of our database. This is where we use DataKinds and TypeOperators. The schema consists of a type-level list of tables, a `:::` pairing of a type level string or `Symbol` and a list a columns, itself a `:::` pairing of a `Symbol` and a `ColumnType`. The `ColumnType` describes the PostgreSQL type of the column as well as whether or not it may contain `NULL` and whether or not inserts and updates can use a `DEFAULT`. For our schema, we'll define two tables, a users table and an emails table.
44+The first step is to define the schema of our database. This is where
45+we use `DataKinds` and `TypeOperators`. The schema consists of a type-level
46+list of tables, a `:::` pairing of a type level string or
47+`Symbol` and a list a columns, itself a `:::` pairing of a
48+`Symbol` and a `ColumnType`. The `ColumnType` describes the
49+PostgreSQL type of the column as well as whether or not it may contain
50+`NULL` and whether or not inserts and updates can use a `DEFAULT`. For our
51+schema, we'll define two tables, a users table and an emails table.
54
52
55```haskell
53```haskell
54+>>> :{
56type Schema =
55type Schema =
57− '[ "users" :::
56+ '[ "users" :::
57+ '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
58− '[ "id" ::: 'Optional ('NotNull 'PGint4)
58+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
59− , "name" ::: 'Required ('NotNull 'PGtext)
59+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
60 ]
60 ]
61 , "emails" :::
61 , "emails" :::
62+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
63+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
64+ ] :=>
62− '[ "id" ::: 'Optional ('NotNull 'PGint4)
65+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
63− , "user_id" ::: 'Required ('NotNull 'PGint4)
66+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
64− , "email" ::: 'Required ('Null 'PGtext)
67+ , "email" ::: 'NoDef :=> 'Null 'PGtext
65 ]
68 ]
66 ]
69 ]
70+:}
67```
71```
68
72
69−Next, we'll write Definitions to set up and tear down the schema. In Squeal, a Definition is a `createTable`, `alterTable` or `dropTable` command and has two type parameters, corresponding to the schema before being run and the schema after. We can compose definitions using `>>>`. Here and in the rest of our commands we make use of overloaded labels to refer to named tables and columns in our schema.
73+Next, we'll write `Definition`s to set up and tear down the schema. In
74+Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
75+command and has two type parameters, corresponding to the schema
76+before being run and the schema after. We can compose definitions using
77+`>>>`. Here and in the rest of our commands we make use of overloaded
78+labels to refer to named tables and columns in our schema.
70
79
71```haskell
80```haskell
81+>>> :{
82+let
72−setup :: Definition '[] Schema
83+ setup :: Definition '[] Schema
73−setup =
84+ setup =
74− createTable #users
85+ createTable #users
75− ( serial `As` #id :*
86+ ( serial `As` #id :*
76− (text & notNull) `As` #name :* Nil )
87+ (text & notNull) `As` #name :* Nil )
77− [ primaryKey (Column #id :* Nil) ]
88+ ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
78− >>>
79− createTable #emails
89+ createTable #emails
80− ( serial `As` #id :*
90+ ( serial `As` #id :*
81− (int & notNull) `As` #user_id :*
91+ (int & notNull) `As` #user_id :*
82− text `As` #email :* Nil )
92+ text `As` #email :* Nil )
83− [ primaryKey (Column #id :* Nil)
93+ ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
84− , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
94+ foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
85− OnDeleteCascade OnUpdateCascade ]
95+ OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
96+:}
86```
97```
87
98
88−Notice that setup starts with an empty schema `'[]` and produces `Schema`. In our `createTable` commands we included `TableConstraint`s to define primary and foreign keys, making them somewhat complex. Our tear down `Definition` is simpler.
99+We can easily see the generated SQL is unsuprising looking.
89
100
90```haskell
101```haskell
91−teardown :: Definition Schema '[]
102+>>> renderDefinition setup
92−teardown = dropTable #emails >>> dropTable #users
103+"CREATE TABLE users (id serial, name text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)); CREATE TABLE emails (id serial, user_id int NOT NULL, email text, CONSTRAINT pk_emails PRIMARY KEY (id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES rs (id) ON DELETE CASCADE ON UPDATE CASCADE);"
93```
104```
94
105
95−Next, we'll write `Manipulation`s to insert data into our two tables. A `Manipulation` is an `insertInto`, `update` or `deleteFrom` command and has three type parameters, the schema it refers to, a list of parameters it can take as input, and a list of columns it produces as output. When we insert into the users table, we will need a parameter for the name field but not for the id field. Since it's optional, we can use a default value. However, since the emails table refers to the users table, we will need to retrieve the user id that the insert generates and insert it into the emails table. Take a careful look at the type and definition of both of our inserts.
106+Notice that `setup` starts with an empty schema `'[]` and produces `Schema`.
107+In our `createTable` commands we included `TableConstraint`s to define
108+primary and foreign keys, making them somewhat complex. Our tear down
109+`Definition` is simpler.
96
110
97```haskell
111```haskell
98−insertUser :: Manipulation Schema
112+>>> :{
99− '[ 'Required ('NotNull 'PGtext)]
113+let
100− '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
114+ teardown :: Definition Schema '[]
101−insertUser = insertInto #users
115+ teardown = dropTable #emails >>> dropTable #users
102− ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
116+:}
103− OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
117+>>> renderDefinition teardown
104−
118+"DROP TABLE emails; DROP TABLE users;"
105−insertEmail :: Manipulation Schema
106− '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
107−insertEmail = insertInto #emails ( Values
108− ( def `As` #id :*
109− param @1 `As` #user_id :*
110− param @2 `As` #email :* Nil) [] )
111− OnConflictDoNothing (Returning Nil)
112```
119```
113
120
114−Next we write a `Query` to retrieve users from the database. We're not interested in the ids here, just the usernames and email addresses. We need to use an inner join to get the right result. A `Query` is like a `Manipulation` with the same kind of type parameters.
121+Next, we'll write `Manipulation`s to insert data into our two tables.
122+A `Manipulation` is a `insertInto`, `update` or `deleteFrom` command and
123+has three type parameters, the schema it refers to, a list of parameters
124+it can take as input, and a list of columns it produces as output. When
125+we insert into the users table, we will need a parameter for the `name`
126+field but not for the `id` field. Since it's optional, we can use a default
127+value. However, since the emails table refers to the users table, we will
128+need to retrieve the user id that the insert generates and insert it into
129+the emails table. Take a careful look at the type and definition of both
130+of our inserts.
115
131
116```haskell
132```haskell
117−getUsers :: Query Schema '[]
133+>>> :{
118− '[ "userName" ::: 'Required ('NotNull 'PGtext)
134+let
119− , "userEmail" ::: 'Required ('Null 'PGtext) ]
135+ insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ]
120−getUsers = select
136+ '[ "fromOnly" ::: 'NotNull 'PGint4 ]
121− (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
137+ insertUser = insertRow #users
122− ( from (Table (#users `As` #u)
138+ (Default `As` #id :* Set (param `1) `As` #name :* Nil)
123− & InnerJoin (Table (#emails `As` #e))
139+ OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
124− (#u ! #id .== #e ! #user_id)) )
140+:}
141+>>> :{
142+let
143+ insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
144+ insertEmail = insertRow #emails
145+ ( Default `As` #id :*
146+ Set (param `1) `As` #user_id :*
147+ Set (param `2) `As` #email :* Nil )
148+ OnConflictDoNothing (Returning Nil)
149+:}
150+>>> renderManipulation insertUser
151+"INSERT INTO users (id, name) VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING URNING id AS fromOnly;"
152+>>> renderManipulation insertEmail
153+"INSERT INTO emails (id, user_id, email) VALUES (DEFAULT, ($1 :: int4), ($2 :: text)N CONFLICT DO NOTHING;"
125```
154```
126
155
127−Now that we've defined the SQL side of things, we'll need a Haskell type for users. We give the type `Generic` and `HasDatatypeInfo` instances so that we can decode the rows we receive when we run getUsers. Notice that the record fields of the `User` type match the column names of `getUsers`.
156+Next we write a `Query` to retrieve users from the database. We're not
157+interested in the ids here, just the usernames and email addresses. We
158+need to use an inner join to get the right result. A `Query` is like a
159+`Manipulation` with the same kind of type parameters.
160+
161+```haskell
162+>>> :{
163+let
164+ getUsers :: Query Schema '[]
165+ '[ "userName" ::: 'NotNull 'PGtext
166+ , "userEmail" ::: 'Null 'PGtext ]
167+ getUsers = select
168+ (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
169+ ( from (table (#users `As` #u)
170+ & innerJoin (table (#emails `As` #e))
171+ (#u ! #id .== #e ! #user_id)) )
172+:}
173+>>> renderQuery getUsers
174+"SELECT u.name AS userName, e.email AS userEmail FROM users AS u INNER JOIN emails e ON (u.id = e.user_id)"
175+```
176+Now that we've defined the SQL side of things, we'll need a Haskell type
177+for users. We give the type `Generics.SOP.Generic` and
178+`Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
179+we receive when we run `getUsers`. Notice that the record fields of the
180+`User` type match the column names of `getUsers`.
128
181
129```haskell
182```haskell
130−data User = User { userName :: Text, userEmail :: Maybe Text }
183+>>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, .Generic)
131− deriving (Show, GHC.Generic)
132−instance SOP.Generic User
184+>>> instance SOP.Generic User
133−instance SOP.HasDatatypeInfo User
185+>>> instance SOP.HasDatatypeInfo User
134```
186```
135
187
136Let's also create some users to add to the database.
188Let's also create some users to add to the database.
137
189
138```haskell
190```haskell
191+>>> :{
192+let
139−users :: [User]
193+ users :: [User]
140−users =
194+ users =
141− [ User "Alice" (Just "alice@gmail.com")
195+ [ User "Alice" (Just "alice`gmail.com")
142− , User "Bob" Nothing
196+ , User "Bob" Nothing
143− , User "Carole" (Just "carole@hotmail.com")
197+ , User "Carole" (Just "carole`hotmail.com")
144− ]
198+ ]
199+:}
145```
200```
146
201
147−Now we can put together all the pieces into a program. The program connects to the database, sets up the schema, inserts the user data (using prepared statements as an optimization), queries the user data and prints it out and finally closes the connection. We can thread the changing schema information through by using the indexed `PQ` monad transformer and when the schema doesn't change we can use `Monad` and `MonadPQ` functionality.
202+Now we can put together all the pieces into a program. The program
203+connects to the database, sets up the schema, inserts the user data
204+(using prepared statements as an optimization), queries the user
205+data and prints it out and finally closes the connection. We can thread
206+the changing schema information through by using the indexed `PQ` monad
207+transformer and when the schema doesn't change we can use `Monad` and
208+`MonadPQ` functionality.
148
209
149```haskell
210```haskell
150−main :: IO ()
211+>>> :{
151−main = void $
212+let
152− withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
213+ session :: PQ Schema Schema IO ()
153− define setup
154− & pqThen session
155− & thenDefine teardown
156− where
157− session = do
214+ session = do
158− idResults <- traversePrepared insertUser (Only . userName <$> users)
215+ idResults <- traversePrepared insertUser (Only . userName <$> users)
159− ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
216+ ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
160− traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
217+ traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
161− usersResult <- runQuery getUsers
218+ usersResult <- runQuery getUsers
162− usersRows <- getRows usersResult
219+ usersRows <- getRows usersResult
163− liftBase $ print (usersRows :: [User])
220+ liftBase $ print (usersRows :: [User])
221+:}
222+>>> :{
223+void . withConnection "host=localhost port=5432 dbname=exampledb" $
224+ define setup
225+ & pqThen session
226+ & thenDefine teardown
227+:}
228+[User {userName = "Alice", userEmail = Just "alice`gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just role`hotmail.com"}]
164```
229```
squeal-postgresql/exe/Example.hs+43−36
@@ -4,6 +4,7 @@
4 , FlexibleContexts
4 , FlexibleContexts
5 , OverloadedLabels
5 , OverloadedLabels
6 , OverloadedStrings
6 , OverloadedStrings
7+ , OverloadedLists
7 , TypeApplications
8 , TypeApplications
8 , TypeOperators
9 , TypeOperators
9#-}
10#-}
@@ -12,9 +13,10 @@ module Main (main, main2) where
12
13
13import Control.Monad (void)
14import Control.Monad (void)
14import Control.Monad.Base (liftBase, MonadBase)
15import Control.Monad.Base (liftBase, MonadBase)
15−import Data.Int (Int32)
16+import Data.Int (Int16, Int32)
16import Data.Monoid ((<>))
17import Data.Monoid ((<>))
17import Data.Text (Text)
18import Data.Text (Text)
19+import Data.Vector (Vector)
18
20
19import Squeal.PostgreSQL
21import Squeal.PostgreSQL
20
22
@@ -24,13 +26,18 @@ import qualified GHC.Generics as GHC
24
26
25type Schema =
27type Schema =
26 '[ "users" :::
28 '[ "users" :::
29+ '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
27− '[ "id" ::: 'Optional ('NotNull 'PGint4)
30+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
28− , "name" ::: 'Required ('NotNull 'PGtext)
31+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
32+ , "vec" ::: 'NoDef :=> 'NotNull ('PGvararray 'PGint2)
29 ]
33 ]
30 , "emails" :::
34 , "emails" :::
35+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
36+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
37+ ] :=>
31− '[ "id" ::: 'Optional ('NotNull 'PGint4)
38+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
32− , "user_id" ::: 'Required ('NotNull 'PGint4)
39+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
33− , "email" ::: 'Required ('Null 'PGtext)
40+ , "email" ::: 'NoDef :=> 'Null 'PGtext
34 ]
41 ]
35 ]
42 ]
36
43
@@ -38,61 +45,61 @@ setup :: Definition '[] Schema
38setup =
45setup =
39 createTable #users
46 createTable #users
40 ( serial `As` #id :*
47 ( serial `As` #id :*
41− (text & notNull) `As` #name :* Nil )
48+ (text & notNull) `As` #name :*
49+ (vararray int2 & notNull) `As` #vec :* Nil )
42− [ primaryKey (Column #id :* Nil) ]
50+ ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil )
43 >>>
51 >>>
44 createTable #emails
52 createTable #emails
45 ( serial `As` #id :*
53 ( serial `As` #id :*
46 (int & notNull) `As` #user_id :*
54 (int & notNull) `As` #user_id :*
47 text `As` #email :* Nil )
55 text `As` #email :* Nil )
48− [ primaryKey (Column #id :* Nil)
56+ ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
49− , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
57+ foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
50− OnDeleteCascade OnUpdateCascade ]
58+ OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
51
59
52teardown :: Definition Schema '[]
60teardown :: Definition Schema '[]
53teardown = dropTable #emails >>> dropTable #users
61teardown = dropTable #emails >>> dropTable #users
54
62
55−insertUser :: Manipulation Schema
63+insertUser :: Manipulation Schema '[ 'NotNull 'PGtext, 'NotNull ('PGvararray 'PGint2)]
56− '[ 'Required ('NotNull 'PGtext)]
57− '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
64+ '[ "fromOnly" ::: 'NotNull 'PGint4 ]
58−insertUser = insertInto #users
65+insertUser = insertRows #users
59− ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
66+ (Default `As` #id :* Set (param @1) `As` #name :* Set (param @2) `As` #vec :* Nil) []
60 OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
67 OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
61
68
62−insertEmail :: Manipulation Schema
69+insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
63− '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
64−insertEmail = insertInto #emails ( Values
70+insertEmail = insertRows #emails
65− ( def `As` #id :*
71+ ( Default `As` #id :*
66− param @1 `As` #user_id :*
72+ Set (param @1) `As` #user_id :*
67− param @2 `As` #email :* Nil) [] )
73+ Set (param @2) `As` #email :* Nil ) []
68 OnConflictDoNothing (Returning Nil)
74 OnConflictDoNothing (Returning Nil)
69
75
70getUsers :: Query Schema '[]
76getUsers :: Query Schema '[]
71− '[ "userName" ::: 'Required ('NotNull 'PGtext)
77+ '[ "userName" ::: 'NotNull 'PGtext
72− , "userEmail" ::: 'Required ('Null 'PGtext) ]
78+ , "userEmail" ::: 'Null 'PGtext
79+ , "userVec" ::: 'NotNull ('PGvararray 'PGint2)]
73getUsers = select
80getUsers = select
74− (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
81+ (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* #u ! #vec `As` #userVec :* Nil)
75− ( from (Table (#users `As` #u)
82+ ( from (table (#users `As` #u)
76− & InnerJoin (Table (#emails `As` #e))
83+ & innerJoin (table (#emails `As` #e))
77 (#u ! #id .== #e ! #user_id)) )
84 (#u ! #id .== #e ! #user_id)) )
78
85
79−data User = User { userName :: Text, userEmail :: Maybe Text }
86+data User = User { userName :: Text, userEmail :: Maybe Text, userVec :: Vector (Maybe Int16) }
80 deriving (Show, GHC.Generic)
87 deriving (Show, GHC.Generic)
81instance SOP.Generic User
88instance SOP.Generic User
82instance SOP.HasDatatypeInfo User
89instance SOP.HasDatatypeInfo User
83
90
84users :: [User]
91users :: [User]
85users =
92users =
86− [ User "Alice" (Just "alice@gmail.com")
93+ [ User "Alice" (Just "alice@gmail.com") [Nothing, Just 1]
87− , User "Bob" Nothing
94+ , User "Bob" Nothing [Just 2, Nothing]
88− , User "Carole" (Just "carole@hotmail.com")
95+ , User "Carole" (Just "carole@hotmail.com") [Just 3]
89 ]
96 ]
90
97
91session :: (MonadBase IO pq, MonadPQ Schema pq) => pq ()
98session :: (MonadBase IO pq, MonadPQ Schema pq) => pq ()
92session = do
99session = do
93 liftBase $ Char8.putStrLn "manipulating"
100 liftBase $ Char8.putStrLn "manipulating"
94− idResults <- traversePrepared insertUser (Only . userName <$> users)
101+ idResults <- traversePrepared insertUser ([(userName user, userVec user) | user <- users])
95− ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
102+ ids <- traverse (fmap fromOnly . getRow 0) idResults
96 traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
103 traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
97 liftBase $ Char8.putStrLn "querying"
104 liftBase $ Char8.putStrLn "querying"
98 usersResult <- runQuery getUsers
105 usersResult <- runQuery getUsers
@@ -114,8 +121,8 @@ main = do
114 finish connection3
121 finish connection3
115
122
116main2 :: IO ()
123main2 :: IO ()
117−main2 = void $
124+main2 =
118− withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
125+ void . withConnection "host=localhost port=5432 dbname=exampledb" $
119 define setup
126 define setup
120 & pqThen session
127 & pqThen session
121− & thenDefine teardown
128+ & pqThen (define teardown)
squeal-postgresql/squeal-postgresql.cabal+35−44
@@ -1,5 +1,5 @@
1name: squeal-postgresql
1name: squeal-postgresql
2−version: 0.1.1.4
2+version: 0.2
3synopsis: Squeal PostgreSQL Library
3synopsis: Squeal PostgreSQL Library
4description: Squeal is a type-safe embedding of PostgreSQL in Haskell
4description: Squeal is a type-safe embedding of PostgreSQL in Haskell
5homepage: https://github.com/morphismtech/squeal
5homepage: https://github.com/morphismtech/squeal
@@ -26,46 +26,36 @@ library
26 Squeal.PostgreSQL.Definition
26 Squeal.PostgreSQL.Definition
27 Squeal.PostgreSQL.Expression
27 Squeal.PostgreSQL.Expression
28 Squeal.PostgreSQL.Manipulation
28 Squeal.PostgreSQL.Manipulation
29+ Squeal.PostgreSQL.Migration
30+ Squeal.PostgreSQL.Pool
29 Squeal.PostgreSQL.PQ
31 Squeal.PostgreSQL.PQ
30− Squeal.PostgreSQL.Prettyprint
32+ Squeal.PostgreSQL.Render
31 Squeal.PostgreSQL.Query
33 Squeal.PostgreSQL.Query
32 Squeal.PostgreSQL.Schema
34 Squeal.PostgreSQL.Schema
35+ Squeal.PostgreSQL.Transaction
33 default-language: Haskell2010
36 default-language: Haskell2010
34 ghc-options: -Wall -fprint-explicit-kinds
37 ghc-options: -Wall -fprint-explicit-kinds
35 build-depends:
38 build-depends:
36− aeson >= 1.2.1.0 && < 2
39+ aeson >= 1.2.4.0
37− , base >= 4.10.0.0 && < 5
40+ , base >= 4.10.1.0
38− , bytestring >= 0.10.8.2 && < 1
41+ , bytestring >= 0.10.8.2
39− , deepseq >= 1.4.3.0 && < 2
42+ , deepseq >= 1.4.3.0
40− , generics-sop >= 0.3.1.0 && < 1
43+ , generics-sop >= 0.3.2.0
41− , lifted-base >= 0.2.3.11 && < 1
44+ , lifted-base >= 0.2.3.12
45+ , mmorph >= 1.1.1
42− , monad-control >= 1.0.2.2 && < 2
46+ , monad-control >= 1.0.2.3
43− , mtl >= 2.2.1 && < 3
47+ , mtl >= 2.2.2
44− , network-ip >= 0.3.0.2 && < 1
48+ , network-ip >= 0.3.0.2
45− , postgresql-binary >= 0.12.1 && < 1
49+ , postgresql-binary >= 0.12.1
46− , postgresql-libpq >= 0.9.3.1 && < 1
50+ , postgresql-libpq >= 0.9.4.1
51+ , resource-pool >= 0.2.3.2
47− , scientific >= 0.3.5.1 && < 1
52+ , scientific >= 0.3.5.3
48− , text >= 1.2.2.2 && < 2
53+ , text >= 1.2.3.0
49− , time >= 1.8.0.2 && < 2
54+ , time >= 1.8.0.2
50− , transformers >= 0.5.2.0 && < 1
55+ , transformers >= 0.5.2.0
51− , transformers-base >= 0.4.4 && < 1
56+ , transformers-base >= 0.4.4
52− , uuid >= 1.3.13 && < 2
57+ , uuid-types >= 1.0.3
53−
58+ , vector >= 0.12.0.1
54−test-suite squeal-postgresql-spec
55− default-language: Haskell2010
56− type: exitcode-stdio-1.0
57− hs-source-dirs: test
58− ghc-options: -Wall
59− main-is: Spec.hs
60− other-modules:
61− Squeal.PostgreSQL.DefinitionSpec
62− Squeal.PostgreSQL.ManipulationSpec
63− Squeal.PostgreSQL.QuerySpec
64− build-depends:
65− base >= 4.10.0.0 && < 5
66− , generics-sop >= 0.3.1.0 && < 1
67− , hspec >= 2.4.4 && < 3
68− , squeal-postgresql
69
59
70test-suite squeal-postgresql-doctest
60test-suite squeal-postgresql-doctest
71 default-language: Haskell2010
61 default-language: Haskell2010
@@ -74,8 +64,8 @@ test-suite squeal-postgresql-doctest
74 ghc-options: -Wall
64 ghc-options: -Wall
75 main-is: DocTest.hs
65 main-is: DocTest.hs
76 build-depends:
66 build-depends:
77− base >= 4.10.0.0 && < 5
67+ base >= 4.10.0.0
78− , doctest >= 0.11.4 && < 1
68+ , doctest >= 0.11.4
79
69
80executable squeal-postgresql-example
70executable squeal-postgresql-example
81 default-language: Haskell2010
71 default-language: Haskell2010
@@ -83,11 +73,12 @@ executable squeal-postgresql-example
83 ghc-options: -Wall
73 ghc-options: -Wall
84 main-is: Example.hs
74 main-is: Example.hs
85 build-depends:
75 build-depends:
86− base >= 4.10.0.0 && < 5
76+ base >= 4.10.0.0
87− , bytestring >= 0.10.8.2 && < 1
77+ , bytestring >= 0.10.8.2
88− , generics-sop >= 0.3.1.0 && < 1
78+ , generics-sop >= 0.3.1.0
89− , mtl >= 2.2.1 && < 3
79+ , mtl >= 2.2.1
90 , squeal-postgresql
80 , squeal-postgresql
91− , text >= 1.2.2.2 && < 2
81+ , text >= 1.2.2.2
92− , transformers >= 0.5.2.0 && < 1
82+ , transformers >= 0.5.2.0
93− , transformers-base >= 0.4.4 && < 1
83+ , transformers-base >= 0.4.4
84+ , vector >= 0.12.0.1
squeal-postgresql/src/Squeal/PostgreSQL.hs+138−102
@@ -4,55 +4,54 @@
4-- Maintainer: eitan@morphism.tech
4-- Maintainer: eitan@morphism.tech
5-- Stability: experimental
5-- Stability: experimental
6--
6--
7−-- Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
7+-- Squeal is a deep embedding of [PostgreSQL](https://www.postgresql.org) in Haskell.
8+-- Let's see an example!
8--
9--
9-- First, we need some language extensions because Squeal uses modern GHC
10-- First, we need some language extensions because Squeal uses modern GHC
10-- features.
11-- features.
11--
12--
12−-- > {-# LANGUAGE
13+-- >>> :set -XDataKinds -XDeriveGeneric -XOverloadedLabels
13−-- > DataKinds
14+-- >>> :set -XOverloadedStrings -XTypeApplications -XTypeOperators
14−-- > , DeriveGeneric
15−-- > , OverloadedLabels
16−-- > , OverloadedStrings
17−-- > , TypeApplications
18−-- > , TypeOperators
19−-- > #-}
20--
15--
21−-- Here comes the @Main@ module and imports.
16+-- We'll need some imports.
22--
17--
23−-- > module Main (main) where
18+-- >>> import Control.Monad (void)
24−-- >
25−-- > import Control.Monad.Base (liftBase)
19+-- >>> import Control.Monad.Base (liftBase)
26−-- > import Data.Int (Int32)
20+-- >>> import Data.Int (Int32)
27−-- > import Data.Text (Text)
21+-- >>> import Data.Text (Text)
28−-- >
29−-- > import Squeal.PostgreSQL
22+-- >>> import Squeal.PostgreSQL
30--
23--
31-- We'll use generics to easily convert between Haskell and PostgreSQL values.
24-- We'll use generics to easily convert between Haskell and PostgreSQL values.
32--
25--
33−-- > import qualified Generics.SOP as SOP
26+-- >>> import qualified Generics.SOP as SOP
34−-- > import qualified GHC.Generics as GHC
27+-- >>> import qualified GHC.Generics as GHC
35--
28--
36-- The first step is to define the schema of our database. This is where
29-- The first step is to define the schema of our database. This is where
37−-- we use @DataKinds@ and @TypeOperators@. The schema consists of a type-level
30+-- we use @DataKinds@ and @TypeOperators@.
38−-- list of tables, a `:::` pairing of a type level string or
31+--
39−-- @Symbol@ and a list a columns, itself a `:::` pairing of a
32+-- >>> :{
40−-- @Symbol@ and a `ColumnType`. The `ColumnType` describes the
41−-- PostgreSQL type of the column as well as whether or not it may contain
42−-- @NULL@ and whether or not inserts and updates can use a @DEFAULT@. For our
43−-- schema, we'll define two tables, a users table and an emails table.
44−--
45−-- > type Schema =
33+-- type Schema =
46−-- > '[ "users" :::
34+-- '[ "users" :::
47−-- > '[ "id" ::: 'Optional ('NotNull 'PGint4)
35+-- '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
48−-- > , "name" ::: 'Required ('NotNull 'PGtext)
36+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
37+-- , "name" ::: 'NoDef :=> 'NotNull 'PGtext
38+-- ]
39+-- , "emails" :::
40+-- '[ "pk_emails" ::: 'PrimaryKey '["id"]
41+-- , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
49−-- > ]
42+-- ] :=>
50−-- > , "emails" :::
51−-- > '[ "id" ::: 'Optional ('NotNull 'PGint4)
43+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
52−-- > , "user_id" ::: 'Required ('NotNull 'PGint4)
44+-- , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
53−-- > , "email" ::: 'Required ('Null 'PGtext)
45+-- , "email" ::: 'NoDef :=> 'Null 'PGtext
54−-- > ]
46+-- ]
55−-- > ]
47+-- ]
48+-- :}
49+--
50+-- Notice the use of type operators. `:::` is used
51+-- to pair an alias `Symbol` with either a `TableType` or a `ColumnType`.
52+-- `:=>` is used to pair a `TableConstraint`s with a `ColumnsType`,
53+-- yielding a `TableType`, or to pair a `ColumnConstraint` with a `NullityType`,
54+-- yielding a `ColumnType`.
56--
55--
57-- Next, we'll write `Definition`s to set up and tear down the schema. In
56-- Next, we'll write `Definition`s to set up and tear down the schema. In
58-- Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
57-- Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
@@ -61,31 +60,45 @@
61-- `>>>`. Here and in the rest of our commands we make use of overloaded
60-- `>>>`. Here and in the rest of our commands we make use of overloaded
62-- labels to refer to named tables and columns in our schema.
61-- labels to refer to named tables and columns in our schema.
63--
62--
63+-- >>> :{
64+-- let
64−-- > setup :: Definition '[] Schema
65+-- setup :: Definition '[] Schema
65−-- > setup =
66+-- setup =
66−-- > createTable #users
67+-- createTable #users
67−-- > ( serial `As` #id :*
68+-- ( serial `As` #id :*
68−-- > (text & notNull) `As` #name :* Nil )
69+-- (text & notNull) `As` #name :* Nil )
69−-- > [ primaryKey (Column #id :* Nil) ]
70+-- ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
70−-- > >>>
71−-- > createTable #emails
71+-- createTable #emails
72−-- > ( serial `As` #id :*
72+-- ( serial `As` #id :*
73−-- > (int & notNull) `As` #user_id :*
73+-- (int & notNull) `As` #user_id :*
74−-- > text `As` #email :* Nil )
74+-- text `As` #email :* Nil )
75−-- > [ primaryKey (Column #id :* Nil)
75+-- ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
76−-- > , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
76+-- foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
77−-- > OnDeleteCascade OnUpdateCascade ]
77+-- OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
78+-- :}
79+--
80+-- We can easily see the generated SQL is unsuprising looking.
81+--
82+-- >>> renderDefinition setup
83+-- "CREATE TABLE users (id serial, name text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)); CREATE TABLE emails (id serial, user_id int NOT NULL, email text, CONSTRAINT pk_emails PRIMARY KEY (id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE);"
78--
84--
79-- Notice that @setup@ starts with an empty schema @'[]@ and produces @Schema@.
85-- Notice that @setup@ starts with an empty schema @'[]@ and produces @Schema@.
80-- In our `createTable` commands we included `TableConstraint`s to define
86-- In our `createTable` commands we included `TableConstraint`s to define
81-- primary and foreign keys, making them somewhat complex. Our tear down
87-- primary and foreign keys, making them somewhat complex. Our tear down
82-- `Definition` is simpler.
88-- `Definition` is simpler.
83--
89--
90+-- >>> :{
91+-- let
84−-- > teardown :: Definition Schema '[]
92+-- teardown :: Definition Schema '[]
85−-- > teardown = dropTable #emails >>> dropTable #users
93+-- teardown = dropTable #emails >>> dropTable #users
94+-- :}
95+--
96+-- >>> renderDefinition teardown
97+-- "DROP TABLE emails; DROP TABLE users;"
86--
98--
87-- Next, we'll write `Manipulation`s to insert data into our two tables.
99-- Next, we'll write `Manipulation`s to insert data into our two tables.
100+-- A `Manipulation` is an `insertRow` (or other inserts), `update`
88−-- A `Manipulation` is a `insertInto`, `update` or `deleteFrom` command and
101+-- or `deleteFrom` command and
89-- has three type parameters, the schema it refers to, a list of parameters
102-- has three type parameters, the schema it refers to, a list of parameters
90-- it can take as input, and a list of columns it produces as output. When
103-- it can take as input, and a list of columns it produces as output. When
91-- we insert into the users table, we will need a parameter for the @name@
104-- we insert into the users table, we will need a parameter for the @name@
@@ -95,34 +108,48 @@
95-- the emails table. Take a careful look at the type and definition of both
108-- the emails table. Take a careful look at the type and definition of both
96-- of our inserts.
109-- of our inserts.
97--
110--
111+-- >>> :{
112+-- let
98−-- > insertUser :: Manipulation Schema
113+-- insertUser :: Manipulation Schema '[ 'NotNull 'PGtext ] '[ "fromOnly" ::: 'NotNull 'PGint4 ]
99−-- > '[ 'Required ('NotNull 'PGtext)]
100−-- > '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
101−-- > insertUser = insertInto #users
114+-- insertUser = insertRow #users
102−-- > ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
115+-- (Default `As` #id :* Set (param @1) `As` #name :* Nil)
103−-- > OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
116+-- OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
104−-- >
117+-- :}
118+--
119+-- >>> :{
120+-- let
105−-- > insertEmail :: Manipulation Schema
121+-- insertEmail :: Manipulation Schema '[ 'NotNull 'PGint4, 'Null 'PGtext] '[]
106−-- > '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
107−-- > insertEmail = insertInto #emails ( Values
122+-- insertEmail = insertRow #emails
108−-- > ( def `As` #id :*
123+-- ( Default `As` #id :*
109−-- > param @1 `As` #user_id :*
124+-- Set (param @1) `As` #user_id :*
110−-- > param @2 `As` #email :* Nil) [] )
125+-- Set (param @2) `As` #email :* Nil )
111−-- > OnConflictDoNothing (Returning Nil)
126+-- OnConflictDoNothing (Returning Nil)
127+-- :}
128+--
129+-- >>> renderManipulation insertUser
130+-- "INSERT INTO users (id, name) VALUES (DEFAULT, ($1 :: text)) ON CONFLICT DO NOTHING RETURNING id AS fromOnly;"
131+-- >>> renderManipulation insertEmail
132+-- "INSERT INTO emails (id, user_id, email) VALUES (DEFAULT, ($1 :: int4), ($2 :: text)) ON CONFLICT DO NOTHING;"
112--
133--
113-- Next we write a `Query` to retrieve users from the database. We're not
134-- Next we write a `Query` to retrieve users from the database. We're not
114-- interested in the ids here, just the usernames and email addresses. We
135-- interested in the ids here, just the usernames and email addresses. We
115-- need to use an inner join to get the right result. A `Query` is like a
136-- need to use an inner join to get the right result. A `Query` is like a
116-- `Manipulation` with the same kind of type parameters.
137-- `Manipulation` with the same kind of type parameters.
117--
138--
139+-- >>> :{
140+-- let
118−-- > getUsers :: Query Schema '[]
141+-- getUsers :: Query Schema '[]
119−-- > '[ "userName" ::: 'Required ('NotNull 'PGtext)
142+-- '[ "userName" ::: 'NotNull 'PGtext
120−-- > , "userEmail" ::: 'Required ('Null 'PGtext) ]
143+-- , "userEmail" ::: 'Null 'PGtext ]
121−-- > getUsers = select
144+-- getUsers = select
122−-- > (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
145+-- (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
123−-- > ( from (Table (#users `As` #u)
146+-- ( from (table (#users `As` #u)
124−-- > & InnerJoin (Table (#emails `As` #e))
147+-- & innerJoin (table (#emails `As` #e))
125−-- > (#u ! #id .== #e ! #user_id)) )
148+-- (#u ! #id .== #e ! #user_id)) )
149+-- :}
150+--
151+-- >>> renderQuery getUsers
152+-- "SELECT u.name AS userName, e.email AS userEmail FROM users AS u INNER JOIN emails AS e ON (u.id = e.user_id)"
126--
153--
127-- Now that we've defined the SQL side of things, we'll need a Haskell type
154-- Now that we've defined the SQL side of things, we'll need a Haskell type
128-- for users. We give the type `Generics.SOP.Generic` and
155-- for users. We give the type `Generics.SOP.Generic` and
@@ -130,19 +157,21 @@
130-- we receive when we run @getUsers@. Notice that the record fields of the
157-- we receive when we run @getUsers@. Notice that the record fields of the
131-- @User@ type match the column names of @getUsers@.
158-- @User@ type match the column names of @getUsers@.
132--
159--
133−-- > data User = User { userName :: Text, userEmail :: Maybe Text }
160+-- >>> data User = User { userName :: Text, userEmail :: Maybe Text } deriving (Show, GHC.Generic)
134−-- > deriving (Show, GHC.Generic)
135−-- > instance SOP.Generic User
161+-- >>> instance SOP.Generic User
136−-- > instance SOP.HasDatatypeInfo User
162+-- >>> instance SOP.HasDatatypeInfo User
137--
163--
138-- Let's also create some users to add to the database.
164-- Let's also create some users to add to the database.
139--
165--
166+-- >>> :{
167+-- let
140−-- > users :: [User]
168+-- users :: [User]
141−-- > users =
169+-- users =
142−-- > [ User "Alice" (Just "alice@gmail.com")
170+-- [ User "Alice" (Just "alice@gmail.com")
143−-- > , User "Bob" Nothing
171+-- , User "Bob" Nothing
144−-- > , User "Carole" (Just "carole@hotmail.com")
172+-- , User "Carole" (Just "carole@hotmail.com")
145−-- > ]
173+-- ]
174+-- :}
146--
175--
147-- Now we can put together all the pieces into a program. The program
176-- Now we can put together all the pieces into a program. The program
148-- connects to the database, sets up the schema, inserts the user data
177-- connects to the database, sets up the schema, inserts the user data
@@ -151,21 +180,26 @@
151-- the changing schema information through by using the indexed `PQ` monad
180-- the changing schema information through by using the indexed `PQ` monad
152-- transformer and when the schema doesn't change we can use `Monad` and
181-- transformer and when the schema doesn't change we can use `Monad` and
153-- `MonadPQ` functionality.
182-- `MonadPQ` functionality.
154−--
183+--
155−-- > main :: IO ()
184+-- >>> :{
156−-- > main = void $
185+-- let
157−-- > withConnection "host=localhost port=5432 dbname=exampledb" . runPQ $
186+-- session :: PQ Schema Schema IO ()
158−-- > define setup
159−-- > & pqThen session
160−-- > & thenDefine teardown
161−-- > where
162−-- > session = do
187+-- session = do
163−-- > idResults <- traversePrepared insertUser (Only . userName <$> users)
188+-- idResults <- traversePrepared insertUser (Only . userName <$> users)
164−-- > ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
189+-- ids <- traverse (fmap fromOnly . getRow 0) idResults
165−-- > traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
190+-- traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
166−-- > usersResult <- runQuery getUsers
191+-- usersResult <- runQuery getUsers
167−-- > usersRows <- getRows usersResult
192+-- usersRows <- getRows usersResult
168−-- > liftBase $ print (usersRows :: [User])
193+-- liftBase $ print (usersRows :: [User])
194+-- :}
195+--
196+-- >>> :{
197+-- void . withConnection "host=localhost port=5432 dbname=exampledb" $
198+-- define setup
199+-- & pqThen session
200+-- & pqThen (define teardown)
201+-- :}
202+-- [User {userName = "Alice", userEmail = Just "alice@gmail.com"},User {userName = "Bob", userEmail = Nothing},User {userName = "Carole", userEmail = Just "carole@hotmail.com"}]
169
203
170module Squeal.PostgreSQL
204module Squeal.PostgreSQL
171 ( module Squeal.PostgreSQL.Binary
205 ( module Squeal.PostgreSQL.Binary
@@ -175,6 +209,7 @@ module Squeal.PostgreSQL
175 , module Squeal.PostgreSQL.PQ
209 , module Squeal.PostgreSQL.PQ
176 , module Squeal.PostgreSQL.Query
210 , module Squeal.PostgreSQL.Query
177 , module Squeal.PostgreSQL.Schema
211 , module Squeal.PostgreSQL.Schema
212+ , module Squeal.PostgreSQL.Transaction
178 ) where
213 ) where
179
214
180import Squeal.PostgreSQL.Binary
215import Squeal.PostgreSQL.Binary
@@ -184,3 +219,4 @@ import Squeal.PostgreSQL.Manipulation
184import Squeal.PostgreSQL.PQ
219import Squeal.PostgreSQL.PQ
185import Squeal.PostgreSQL.Query
220import Squeal.PostgreSQL.Query
186import Squeal.PostgreSQL.Schema
221import Squeal.PostgreSQL.Schema
222+import Squeal.PostgreSQL.Transaction
squeal-postgresql/src/Squeal/PostgreSQL/Binary.hs+46−33
@@ -48,7 +48,8 @@ import Data.Int
48import Data.Kind
48import Data.Kind
49import Data.Scientific
49import Data.Scientific
50import Data.Time
50import Data.Time
51−import Data.UUID
51+import Data.UUID.Types
52+import Data.Vector (Vector)
52import Data.Word
53import Data.Word
53import Generics.SOP
54import Generics.SOP
54import GHC.TypeLits
55import GHC.TypeLits
@@ -58,6 +59,7 @@ import qualified Data.ByteString.Lazy as Lazy
58import qualified Data.ByteString as Strict hiding (unpack)
59import qualified Data.ByteString as Strict hiding (unpack)
59import qualified Data.Text.Lazy as Lazy
60import qualified Data.Text.Lazy as Lazy
60import qualified Data.Text as Strict
61import qualified Data.Text as Strict
62+import qualified Data.Vector as Vector
61import qualified GHC.Generics as GHC
63import qualified GHC.Generics as GHC
62import qualified PostgreSQL.Binary.Decoding as Decoding
64import qualified PostgreSQL.Binary.Decoding as Decoding
63import qualified PostgreSQL.Binary.Encoding as Encoding
65import qualified PostgreSQL.Binary.Encoding as Encoding
@@ -113,39 +115,43 @@ instance ToParam UTCTime 'PGtimestamptz where
113instance ToParam DiffTime 'PGinterval where toParam = K . Encoding.interval_int
115instance ToParam DiffTime 'PGinterval where toParam = K . Encoding.interval_int
114instance ToParam Value 'PGjson where toParam = K . Encoding.json_ast
116instance ToParam Value 'PGjson where toParam = K . Encoding.json_ast
115instance ToParam Value 'PGjsonb where toParam = K . Encoding.jsonb_ast
117instance ToParam Value 'PGjsonb where toParam = K . Encoding.jsonb_ast
118+instance (HasOid pg, ToParam x pg)
119+ => ToParam (Vector (Maybe x)) ('PGvararray pg) where
120+ toParam = K . Encoding.nullableArray_vector
121+ (oid @pg) (unK . toParam @x @pg)
116
122
117-- | A `ToColumnParam` constraint lifts the `ToParam` encoding
123-- | A `ToColumnParam` constraint lifts the `ToParam` encoding
118-- of a `Type` to a `ColumnType`, encoding `Maybe`s to `Null`s. You should
124-- of a `Type` to a `ColumnType`, encoding `Maybe`s to `Null`s. You should
119-- not define instances of `ToColumnParam`, just use the provided instances.
125-- not define instances of `ToColumnParam`, just use the provided instances.
120−class ToColumnParam (x :: Type) (ty :: ColumnType) where
126+class ToColumnParam (x :: Type) (ty :: NullityType) where
121− -- | >>> toColumnParam @Int16 @('Required ('NotNull 'PGint2)) 0
127+ -- | >>> toColumnParam @Int16 @('NotNull 'PGint2) 0
122 -- K (Just "\NUL\NUL")
128 -- K (Just "\NUL\NUL")
123 --
129 --
124− -- >>> toColumnParam @(Maybe Int16) @('Required ('Null 'PGint2)) (Just 0)
130+ -- >>> toColumnParam @(Maybe Int16) @('Null 'PGint2) (Just 0)
125 -- K (Just "\NUL\NUL")
131 -- K (Just "\NUL\NUL")
126 --
132 --
127− -- >>> toColumnParam @(Maybe Int16) @('Required ('Null 'PGint2)) Nothing
133+ -- >>> toColumnParam @(Maybe Int16) @('Null 'PGint2) Nothing
128 -- K Nothing
134 -- K Nothing
129 toColumnParam :: x -> K (Maybe Strict.ByteString) ty
135 toColumnParam :: x -> K (Maybe Strict.ByteString) ty
130−instance ToParam x pg => ToColumnParam x (optionality ('NotNull pg)) where
136+instance ToParam x pg => ToColumnParam x ('NotNull pg) where
131 toColumnParam = K . Just . Encoding.encodingBytes . unK . toParam @x @pg
137 toColumnParam = K . Just . Encoding.encodingBytes . unK . toParam @x @pg
132−instance ToParam x pg => ToColumnParam (Maybe x) (optionality ('Null pg)) where
138+instance ToParam x pg => ToColumnParam (Maybe x) ('Null pg) where
133 toColumnParam = K . fmap (Encoding.encodingBytes . unK . toParam @x @pg)
139 toColumnParam = K . fmap (Encoding.encodingBytes . unK . toParam @x @pg)
134
140
135-- | A `ToParams` constraint generically sequences the encodings of `Type`s
141-- | A `ToParams` constraint generically sequences the encodings of `Type`s
136-- of the fields of a tuple or record to a row of `ColumnType`s. You should
142-- of the fields of a tuple or record to a row of `ColumnType`s. You should
137-- not define instances of `ToParams`. Instead define `Generic` instances
143-- not define instances of `ToParams`. Instead define `Generic` instances
138-- which in turn provide `ToParams` instances.
144-- which in turn provide `ToParams` instances.
139−class SListI tys => ToParams (x :: Type) (tys :: [ColumnType]) where
145+class SListI tys => ToParams (x :: Type) (tys :: [NullityType]) where
140− -- | >>> type PGparams = '[ 'Required ('NotNull 'PGbool), 'Required ('Null 'PGint2)]
146+ -- | >>> type Params = '[ 'NotNull 'PGbool, 'Null 'PGint2]
141− -- >>> toParams @(Bool, Maybe Int16) @PGparams (False, Just 0)
147+ -- >>> toParams @(Bool, Maybe Int16) @'[ 'NotNull 'PGbool, 'Null 'PGint2] (False, Just 0)
142− -- K (Just "\NUL") :* (K (Just "\NUL\NUL") :* Nil)
148+ -- K (Just "\NUL") :* K (Just "\NUL\NUL") :* Nil
143 --
149 --
144 -- >>> :set -XDeriveGeneric
150 -- >>> :set -XDeriveGeneric
145− -- >>> data Hparams = Hparams { col1 :: Bool, col2 :: Maybe Int16} deriving GHC.Generic
151+ -- >>> data Tuple = Tuple { p1 :: Bool, p2 :: Maybe Int16} deriving GHC.Generic
146− -- >>> instance Generic Hparams
152+ -- >>> instance Generic Tuple
147− -- >>> toParams @Hparams @PGparams (Hparams False (Just 0))
153+ -- >>> toParams @Tuple @Params (Tuple False (Just 0))
148− -- K (Just "\NUL") :* (K (Just "\NUL\NUL") :* Nil)
154+ -- K (Just "\NUL") :* K (Just "\NUL\NUL") :* Nil
149 toParams :: x -> NP (K (Maybe Strict.ByteString)) tys
155 toParams :: x -> NP (K (Maybe Strict.ByteString)) tys
150instance (SListI tys, IsProductType x xs, AllZip ToColumnParam xs tys)
156instance (SListI tys, IsProductType x xs, AllZip ToColumnParam xs tys)
151 => ToParams x tys where
157 => ToParams x tys where
@@ -187,23 +193,31 @@ instance FromValue 'PGinterval DiffTime where
187 fromValue _ = Decoding.interval_int
193 fromValue _ = Decoding.interval_int
188instance FromValue 'PGjson Value where fromValue _ = Decoding.json_ast
194instance FromValue 'PGjson Value where fromValue _ = Decoding.json_ast
189instance FromValue 'PGjsonb Value where fromValue _ = Decoding.jsonb_ast
195instance FromValue 'PGjsonb Value where fromValue _ = Decoding.jsonb_ast
196+instance FromValue pg y => FromValue ('PGvararray pg) (Vector (Maybe y)) where
197+ fromValue _ = Decoding.array
198+ (Decoding.dimensionArray Vector.replicateM
199+ (Decoding.nullableValueArray (fromValue (Proxy @pg))))
200+instance FromValue pg y => FromValue ('PGfixarray n pg) (Vector (Maybe y)) where
201+ fromValue _ = Decoding.array
202+ (Decoding.dimensionArray Vector.replicateM
203+ (Decoding.nullableValueArray (fromValue (Proxy @pg))))
190
204
191-- | A `FromColumnValue` constraint lifts the `FromValue` parser
205-- | A `FromColumnValue` constraint lifts the `FromValue` parser
192-- to a decoding of a @(Symbol, ColumnType)@ to a `Type`,
206-- to a decoding of a @(Symbol, ColumnType)@ to a `Type`,
193-- decoding `Null`s to `Maybe`s. You should not define instances for
207-- decoding `Null`s to `Maybe`s. You should not define instances for
194-- `FromColumnValue`, just use the provided instances.
208-- `FromColumnValue`, just use the provided instances.
195−class FromColumnValue (colty :: (Symbol,ColumnType)) (y :: Type) where
209+class FromColumnValue (colty :: (Symbol,NullityType)) (y :: Type) where
196 -- | >>> :set -XTypeOperators -XOverloadedStrings
210 -- | >>> :set -XTypeOperators -XOverloadedStrings
197 -- >>> newtype Id = Id { getId :: Int16 } deriving Show
211 -- >>> newtype Id = Id { getId :: Int16 } deriving Show
198 -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
212 -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
199− -- >>> fromColumnValue @("col" ::: 'Required ('NotNull 'PGint2)) @Id (K (Just "\NUL\SOH"))
213+ -- >>> fromColumnValue @("col" ::: 'NotNull 'PGint2) @Id (K (Just "\NUL\SOH"))
200 -- Id {getId = 1}
214 -- Id {getId = 1}
201 --
215 --
202− -- >>> fromColumnValue @("col" ::: 'Required ('Null 'PGint2)) @(Maybe Id) (K (Just "\NUL\SOH"))
216+ -- >>> fromColumnValue @("col" ::: 'Null 'PGint2) @(Maybe Id) (K (Just "\NUL\SOH"))
203 -- Just (Id {getId = 1})
217 -- Just (Id {getId = 1})
204 fromColumnValue :: K (Maybe Strict.ByteString) colty -> y
218 fromColumnValue :: K (Maybe Strict.ByteString) colty -> y
205instance FromValue pg y
219instance FromValue pg y
206− => FromColumnValue (column ::: ('Required ('NotNull pg))) y where
220+ => FromColumnValue (column ::: ('NotNull pg)) y where
207 fromColumnValue = \case
221 fromColumnValue = \case
208 K Nothing -> error "fromColumnValue: saw NULL when expecting NOT NULL"
222 K Nothing -> error "fromColumnValue: saw NULL when expecting NOT NULL"
209 K (Just bytes) ->
223 K (Just bytes) ->
@@ -214,7 +228,7 @@ instance FromValue pg y
214 in
228 in
215 either err id errOrValue
229 either err id errOrValue
216instance FromValue pg y
230instance FromValue pg y
217− => FromColumnValue (column ::: ('Required ('Null pg))) (Maybe y) where
231+ => FromColumnValue (column ::: ('Null pg)) (Maybe y) where
218 fromColumnValue (K nullOrBytes)
232 fromColumnValue (K nullOrBytes)
219 = either err id
233 = either err id
220 . Decoding.valueParser (fromValue @pg @y Proxy)
234 . Decoding.valueParser (fromValue @pg @y Proxy)
@@ -223,21 +237,21 @@ instance FromValue pg y
223 err str = error $ "fromColumnValue: " ++ Strict.unpack str
237 err str = error $ "fromColumnValue: " ++ Strict.unpack str
224
238
225-- | A `FromRow` constraint generically sequences the parsings of the columns
239-- | A `FromRow` constraint generically sequences the parsings of the columns
226−-- of a `ColumnsType` into the fields of a record `Type` provided they have
240+-- of a `RelationType` into the fields of a record `Type` provided they have
227-- the same field names. You should not define instances of `FromRow`.
241-- the same field names. You should not define instances of `FromRow`.
228-- Instead define `Generic` and `HasDatatypeInfo` instances which in turn
242-- Instead define `Generic` and `HasDatatypeInfo` instances which in turn
229-- provide `FromRow` instances.
243-- provide `FromRow` instances.
230−class SListI results => FromRow (results :: ColumnsType) y where
244+class SListI results => FromRow (results :: RelationType) y where
231 -- | >>> :set -XOverloadedStrings
245 -- | >>> :set -XOverloadedStrings
232 -- >>> import Data.Text
246 -- >>> import Data.Text
233− -- >>> newtype Id = Id { getId :: Int16 } deriving Show
247+ -- >>> newtype UserId = UserId { getUserId :: Int16 } deriving Show
234− -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
248+ -- >>> instance FromValue 'PGint2 UserId where fromValue = fmap UserId . fromValue
235− -- >>> data Hrow = Hrow { userId :: Id, userName :: Maybe Text } deriving (Show, GHC.Generic)
249+ -- >>> data UserRow = UserRow { userId :: UserId, userName :: Maybe Text } deriving (Show, GHC.Generic)
236− -- >>> instance Generic Hrow
250+ -- >>> instance Generic UserRow
237− -- >>> instance HasDatatypeInfo Hrow
251+ -- >>> instance HasDatatypeInfo UserRow
238− -- >>> type PGrow = '["userId" ::: 'Required ('NotNull 'PGint2), "userName" ::: 'Required ('Null 'PGtext)]
252+ -- >>> type User = '["userId" ::: 'NotNull 'PGint2, "userName" ::: 'Null 'PGtext]
239− -- >>> fromRow @PGrow @Hrow (K (Just "\NUL\SOH") :* K (Just "bloodninja") :* Nil)
253+ -- >>> fromRow @User @UserRow (K (Just "\NUL\SOH") :* K (Just "bloodninja") :* Nil)
240− -- Hrow {userId = Id {getId = 1}, userName = Just "bloodninja"}
254+ -- UserRow {userId = UserId {getUserId = 1}, userName = Just "bloodninja"}
241 fromRow :: NP (K (Maybe Strict.ByteString)) results -> y
255 fromRow :: NP (K (Maybe Strict.ByteString)) results -> y
242instance
256instance
243 ( SListI results
257 ( SListI results
@@ -252,11 +266,10 @@ instance
252-- `toParams` or decoding a single value with `fromRow`.
266-- `toParams` or decoding a single value with `fromRow`.
253--
267--
254-- >>> import Data.Text
268-- >>> import Data.Text
255−-- >>> toParams @(Only (Maybe Text)) @'[ 'Required ('Null 'PGtext)] (Only (Just "foo"))
269+-- >>> toParams @(Only (Maybe Text)) @'[ 'Null 'PGtext] (Only (Just "foo"))
256-- K (Just "foo") :* Nil
270-- K (Just "foo") :* Nil
257--
271--
258−-- >>> type PGShortRow = '["fromOnly" ::: 'Required ('Null 'PGtext)]
259−-- >>> fromRow @PGShortRow @(Only (Maybe Text)) (K (Just "bar") :* Nil)
272+-- >>> fromRow @'["fromOnly" ::: 'Null 'PGtext] @(Only (Maybe Text)) (K (Just "bar") :* Nil)
260-- Only {fromOnly = Just "bar"}
273-- Only {fromOnly = Just "bar"}
261newtype Only x = Only { fromOnly :: x }
274newtype Only x = Only { fromOnly :: x }
262 deriving (Functor,Foldable,Traversable,Eq,Ord,Read,Show,GHC.Generic)
275 deriving (Functor,Foldable,Traversable,Eq,Ord,Read,Show,GHC.Generic)
squeal-postgresql/src/Squeal/PostgreSQL/Definition.hs+270−183
@@ -9,16 +9,22 @@ Squeal data definition language.
9-}
9-}
10
10
11{-# LANGUAGE
11{-# LANGUAGE
12+ ConstraintKinds
12− DataKinds
13+ , DataKinds
13 , DeriveDataTypeable
14 , DeriveDataTypeable
14 , DeriveGeneric
15 , DeriveGeneric
16+ , FlexibleContexts
17+ , FlexibleInstances
15 , GADTs
18 , GADTs
16 , GeneralizedNewtypeDeriving
19 , GeneralizedNewtypeDeriving
17 , KindSignatures
20 , KindSignatures
18 , LambdaCase
21 , LambdaCase
22+ , MultiParamTypeClasses
19 , OverloadedStrings
23 , OverloadedStrings
20 , RankNTypes
24 , RankNTypes
25+ , ScopedTypeVariables
21 , StandaloneDeriving
26 , StandaloneDeriving
27+ , TypeApplications
22 , TypeInType
28 , TypeInType
23 , TypeOperators
29 , TypeOperators
24#-}
30#-}
@@ -29,11 +35,14 @@ module Squeal.PostgreSQL.Definition
29 , (>>>)
35 , (>>>)
30 -- * Create
36 -- * Create
31 , createTable
37 , createTable
32− , TableConstraint (UnsafeTableConstraint, renderTableConstraint)
38+ , createTableIfNotExists
39+ , TableConstraintExpression (..)
40+ , Column (..)
33 , check
41 , check
34 , unique
42 , unique
35 , primaryKey
43 , primaryKey
36 , foreignKey
44 , foreignKey
45+ , ForeignKeyed
37 , OnDeleteClause (OnDeleteNoAction, OnDeleteRestrict, OnDeleteCascade)
46 , OnDeleteClause (OnDeleteNoAction, OnDeleteRestrict, OnDeleteCascade)
38 , renderOnDeleteClause
47 , renderOnDeleteClause
39 , OnUpdateClause (OnUpdateNoAction, OnUpdateRestrict, OnUpdateCascade)
48 , OnUpdateClause (OnUpdateNoAction, OnUpdateRestrict, OnUpdateCascade)
@@ -43,12 +52,11 @@ module Squeal.PostgreSQL.Definition
43 -- * Alter
52 -- * Alter
44 , alterTable
53 , alterTable
45 , alterTableRename
54 , alterTableRename
46− , alterTableAddConstraint
55+ , AlterTable (UnsafeAlterTable, renderAlterTable)
47− , AlterColumns (UnsafeAlterColumns, renderAlterColumns)
56+ , addConstraint
48− , addColumnDefault
57+ , dropConstraint
49− , addColumnNull
58+ , AddColumn (addColumn)
50 , dropColumn
59 , dropColumn
51− , dropColumnCascade
52 , renameColumn
60 , renameColumn
53 , alterColumn
61 , alterColumn
54 , AlterColumn (UnsafeAlterColumn, renderAlterColumn)
62 , AlterColumn (UnsafeAlterColumn, renderAlterColumn)
@@ -70,7 +78,7 @@ import qualified Generics.SOP as SOP
70import qualified GHC.Generics as GHC
78import qualified GHC.Generics as GHC
71
79
72import Squeal.PostgreSQL.Expression
80import Squeal.PostgreSQL.Expression
73−import Squeal.PostgreSQL.Prettyprint
81+import Squeal.PostgreSQL.Render
74import Squeal.PostgreSQL.Schema
82import Squeal.PostgreSQL.Schema
75
83
76{-----------------------------------------
84{-----------------------------------------
@@ -100,31 +108,77 @@ CREATE statements
100-- >>> :set -XOverloadedLabels
108-- >>> :set -XOverloadedLabels
101-- >>> :{
109-- >>> :{
102-- renderDefinition $
110-- renderDefinition $
103−-- createTable #tab (int `As` #a :* real `As` #b :* Nil) []
111+-- createTable #tab (int `As` #a :* real `As` #b :* Nil) Nil
104-- :}
112-- :}
105-- "CREATE TABLE tab (a int, b real);"
113-- "CREATE TABLE tab (a int, b real);"
106createTable
114createTable
107− :: (KnownSymbol table, SOP.SListI columns)
115+ :: ( KnownSymbol table
116+ , columns ~ (col ': cols)
117+ , SOP.SListI columns
118+ , SOP.SListI constraints )
108 => Alias table -- ^ the name of the table to add
119 => Alias table -- ^ the name of the table to add
109− -> NP (Aliased TypeExpression) (column ': columns)
120+ -> NP (Aliased TypeExpression) columns
110 -- ^ the names and datatype of each column
121 -- ^ the names and datatype of each column
111− -> [TableConstraint schema (column ': columns)]
122+ -> NP (Aliased (TableConstraintExpression schema columns)) constraints
112 -- ^ constraints that must hold for the table
123 -- ^ constraints that must hold for the table
113− -> Definition schema (Create table (column ': columns) schema)
124+ -> Definition schema (Create table (constraints :=> columns) schema)
114createTable table columns constraints = UnsafeDefinition $
125createTable table columns constraints = UnsafeDefinition $
126+ "CREATE TABLE" <+> renderCreation table columns constraints
127+
128+-- | `createTableIfNotExists` creates a table if it doesn't exist, but does not add it to the schema.
129+-- Instead, the schema already has the table so if the table did not yet exist, the schema was wrong.
130+-- `createTableIfNotExists` fixes this. Interestingly, this property makes it an idempotent in the `Category` `Definition`.
131+--
132+-- >>> :set -XOverloadedLabels -XTypeApplications
133+-- >>> type Table = '[] :=> '["a" ::: 'NoDef :=> 'Null 'PGint4, "b" ::: 'NoDef :=> 'Null 'PGfloat4]
134+-- >>> type Schema = '["tab" ::: Table]
135+-- >>> :{
136+-- renderDefinition
137+-- (createTableIfNotExists #tab (int `As` #a :* real `As` #b :* Nil) Nil :: Definition Schema Schema)
138+-- :}
139+-- "CREATE TABLE IF NOT EXISTS tab (a int, b real);"
140+createTableIfNotExists
141+ :: ( Has table schema (constraints :=> columns)
142+ , SOP.SListI columns
143+ , SOP.SListI constraints )
144+ => Alias table -- ^ the name of the table to add
145+ -> NP (Aliased TypeExpression) columns
146+ -- ^ the names and datatype of each column
147+ -> NP (Aliased (TableConstraintExpression schema columns)) constraints
148+ -- ^ constraints that must hold for the table
149+ -> Definition schema schema
150+createTableIfNotExists table columns constraints = UnsafeDefinition $
151+ "CREATE TABLE IF NOT EXISTS"
152+ <+> renderCreation table columns constraints
153+
154+-- helper function for `createTable` and `createTableIfNotExists`
155+renderCreation
156+ :: ( KnownSymbol table
157+ , SOP.SListI columns
158+ , SOP.SListI constraints )
159+ => Alias table -- ^ the name of the table to add
160+ -> NP (Aliased TypeExpression) columns
161+ -- ^ the names and datatype of each column
162+ -> NP (Aliased (TableConstraintExpression schema columns)) constraints
163+ -- ^ constraints that must hold for the table
164+ -> ByteString
115− "CREATE TABLE" <+> renderAlias table
165+renderCreation table columns constraints = renderAlias table
116 <+> parenthesized
166 <+> parenthesized
117 ( renderCommaSeparated renderColumnDef columns
167 ( renderCommaSeparated renderColumnDef columns
118− <> renderConstraints constraints )
168+ <> ( case constraints of
169+ Nil -> ""
170+ _ -> ", " <>
171+ renderCommaSeparated renderConstraint constraints ) )
119 <> ";"
172 <> ";"
120 where
173 where
121 renderColumnDef :: Aliased TypeExpression x -> ByteString
174 renderColumnDef :: Aliased TypeExpression x -> ByteString
122 renderColumnDef (ty `As` column) =
175 renderColumnDef (ty `As` column) =
123 renderAlias column <+> renderTypeExpression ty
176 renderAlias column <+> renderTypeExpression ty
124− renderConstraints :: [TableConstraint schema columns] -> ByteString
177+ renderConstraint
125− renderConstraints = \case
178+ :: Aliased (TableConstraintExpression schema columns) constraint
126− [] -> ""
179+ -> ByteString
127− _ -> ", " <> commaSeparated (renderTableConstraint <$> constraints)
180+ renderConstraint (constraint `As` alias) =
181+ "CONSTRAINT" <+> renderAlias alias <+> renderTableConstraintExpression constraint
128
182
129-- | Data types are a way to limit the kind of data that can be stored in a
183-- | Data types are a way to limit the kind of data that can be stored in a
130-- table. For many applications, however, the constraint they provide is
184-- table. For many applications, however, the constraint they provide is
@@ -138,12 +192,28 @@ createTable table columns constraints = UnsafeDefinition $
138-- as you wish. If a user attempts to store data in a column that would
192-- as you wish. If a user attempts to store data in a column that would
139-- violate a constraint, an error is raised. This applies
193-- violate a constraint, an error is raised. This applies
140-- even if the value came from the default value definition.
194-- even if the value came from the default value definition.
141−newtype TableConstraint
195+newtype TableConstraintExpression
142 (schema :: TablesType)
196 (schema :: TablesType)
143 (columns :: ColumnsType)
197 (columns :: ColumnsType)
144− = UnsafeTableConstraint { renderTableConstraint :: ByteString }
198+ (tableConstraint :: TableConstraint)
199+ = UnsafeTableConstraintExpression
200+ { renderTableConstraintExpression :: ByteString }
145 deriving (GHC.Generic,Show,Eq,Ord,NFData)
201 deriving (GHC.Generic,Show,Eq,Ord,NFData)
146
202
203+-- | @Column columns column@ is a witness that `column` is in `columns`.
204+data Column
205+ (columns :: ColumnsType)
206+ (column :: (Symbol,ColumnType))
207+ where
208+ Column
209+ :: Has column columns ty
210+ => Alias column
211+ -> Column columns (column ::: ty)
212+
213+-- | Render a `Column`.
214+renderColumn :: Column columns column -> ByteString
215+renderColumn (Column column) = renderAlias column
216+
147-- | A `check` constraint is the most generic `TableConstraint` type.
217-- | A `check` constraint is the most generic `TableConstraint` type.
148-- It allows you to specify that the value in a certain column must satisfy
218-- It allows you to specify that the value in a certain column must satisfy
149-- a Boolean (truth-value) expression.
219-- a Boolean (truth-value) expression.
@@ -153,14 +223,15 @@ newtype TableConstraint
153-- createTable #tab
223-- createTable #tab
154-- ( (int & notNull) `As` #a :*
224-- ( (int & notNull) `As` #a :*
155-- (int & notNull) `As` #b :* Nil )
225-- (int & notNull) `As` #b :* Nil )
156−-- [ check (#a .> #b) ]
226+-- ( check (Column #a :* Column #b :* Nil) (#a .> #b) `As` #inequality :* Nil )
157-- :}
227-- :}
158−-- "CREATE TABLE tab (a int NOT NULL, b int NOT NULL, CHECK ((a > b)));"
228+-- "CREATE TABLE tab (a int NOT NULL, b int NOT NULL, CONSTRAINT inequality CHECK ((a > b)));"
159check
229check
230+ :: NP (Column columns) subcolumns
160− :: Condition '[table ::: columns] 'Ungrouped '[]
231+ -> Condition '[table ::: ColumnsToRelation subcolumns] 'Ungrouped '[]
161 -- ^ condition to check
232 -- ^ condition to check
162− -> TableConstraint schema columns
233+ -> TableConstraintExpression schema columns ('Check (AliasesOf subcolumns))
163−check condition = UnsafeTableConstraint $
234+check _cols condition = UnsafeTableConstraintExpression $
164 "CHECK" <+> parenthesized (renderExpression condition)
235 "CHECK" <+> parenthesized (renderExpression condition)
165
236
166-- | A `unique` constraint ensure that the data contained in a column,
237-- | A `unique` constraint ensure that the data contained in a column,
@@ -171,15 +242,15 @@ check condition = UnsafeTableConstraint $
171-- createTable #tab
242-- createTable #tab
172-- ( int `As` #a :*
243-- ( int `As` #a :*
173-- int `As` #b :* Nil )
244-- int `As` #b :* Nil )
174−-- [ unique (Column #a :* Column #b :* Nil) ]
245+-- ( unique (Column #a :* Column #b :* Nil) `As` #uq_a_b :* Nil )
175-- :}
246-- :}
176−-- "CREATE TABLE tab (a int, b int, UNIQUE (a, b));"
247+-- "CREATE TABLE tab (a int, b int, CONSTRAINT uq_a_b UNIQUE (a, b));"
177unique
248unique
178 :: SOP.SListI subcolumns
249 :: SOP.SListI subcolumns
179 => NP (Column columns) subcolumns
250 => NP (Column columns) subcolumns
180 -- ^ unique column or group of columns
251 -- ^ unique column or group of columns
181− -> TableConstraint schema columns
252+ -> TableConstraintExpression schema columns ('Unique (AliasesOf subcolumns))
182−unique columns = UnsafeTableConstraint $
253+unique columns = UnsafeTableConstraintExpression $
183 "UNIQUE" <+> parenthesized (renderCommaSeparated renderColumn columns)
254 "UNIQUE" <+> parenthesized (renderCommaSeparated renderColumn columns)
184
255
185-- | A `primaryKey` constraint indicates that a column, or group of columns,
256-- | A `primaryKey` constraint indicates that a column, or group of columns,
@@ -191,15 +262,15 @@ unique columns = UnsafeTableConstraint $
191-- createTable #tab
262-- createTable #tab
192-- ( serial `As` #id :*
263-- ( serial `As` #id :*
193-- (text & notNull) `As` #name :* Nil )
264-- (text & notNull) `As` #name :* Nil )
194−-- [ primaryKey (Column #id :* Nil) ]
265+-- ( primaryKey (Column #id :* Nil) `As` #pk_id :* Nil )
195-- :}
266-- :}
196−-- "CREATE TABLE tab (id serial, name text NOT NULL, PRIMARY KEY (id));"
267+-- "CREATE TABLE tab (id serial, name text NOT NULL, CONSTRAINT pk_id PRIMARY KEY (id));"
197primaryKey
268primaryKey
198 :: (SOP.SListI subcolumns, AllNotNull subcolumns)
269 :: (SOP.SListI subcolumns, AllNotNull subcolumns)
199 => NP (Column columns) subcolumns
270 => NP (Column columns) subcolumns
200 -- ^ identifying column or group of columns
271 -- ^ identifying column or group of columns
201− -> TableConstraint schema columns
272+ -> TableConstraintExpression schema columns ('PrimaryKey (AliasesOf subcolumns))
202−primaryKey columns = UnsafeTableConstraint $
273+primaryKey columns = UnsafeTableConstraintExpression $
203 "PRIMARY KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
274 "PRIMARY KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
204
275
205-- | A `foreignKey` specifies that the values in a column
276-- | A `foreignKey` specifies that the values in a column
@@ -208,59 +279,72 @@ primaryKey columns = UnsafeTableConstraint $
208-- between two related tables.
279-- between two related tables.
209--
280--
210-- >>> :{
281-- >>> :{
211−-- let
282+-- type Schema =
212−-- definition :: Definition '[]
213−-- '[ "users" :::
283+-- '[ "users" :::
284+-- '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
214−-- '[ "id" ::: 'Optional ('NotNull 'PGint4)
285+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
215−-- , "username" ::: 'Required ('NotNull 'PGtext)
286+-- , "name" ::: 'NoDef :=> 'NotNull 'PGtext
216-- ]
287-- ]
217−-- , "emails" :::
288+-- , "emails" :::
289+-- '[ "pk_emails" ::: 'PrimaryKey '["id"]
290+-- , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
291+-- ] :=>
218−-- '[ "id" ::: 'Optional ('NotNull 'PGint4)
292+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
219−-- , "userid" ::: 'Required ('NotNull 'PGint4)
293+-- , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
220−-- , "email" ::: 'Required ('NotNull 'PGtext)
294+-- , "email" ::: 'NoDef :=> 'Null 'PGtext
221-- ]
295-- ]
222−-- ]
296+-- ]
223−-- definition =
297+-- :}
298+--
299+-- >>> :{
300+-- let
301+-- setup :: Definition '[] Schema
302+-- setup =
224−-- createTable #users
303+-- createTable #users
304+-- ( serial `As` #id :*
225−-- (serial `As` #id :* (text & notNull) `As` #username :* Nil)
305+-- (text & notNull) `As` #name :* Nil )
226−-- [primaryKey (Column #id :* Nil)] >>>
306+-- ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil ) >>>
227−-- createTable #emails
307+-- createTable #emails
228−-- ( serial `As` #id :*
308+-- ( serial `As` #id :*
229−-- (integer & notNull) `As` #userid :*
309+-- (int & notNull) `As` #user_id :*
230−-- (text & notNull) `As` #email :* Nil )
310+-- text `As` #email :* Nil )
231−-- [ primaryKey (Column #id :* Nil)
311+-- ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
232−-- , foreignKey (Column #userid :* Nil) #users (Column #id :* Nil)
312+-- foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
233−-- OnDeleteCascade OnUpdateRestrict
313+-- OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
234−-- ]
235−-- in renderDefinition definition
314+-- in renderDefinition setup
236-- :}
315-- :}
237−-- "CREATE TABLE users (id serial, username text NOT NULL, PRIMARY KEY (id)); CREATE TABLE emails (id serial, userid integer NOT NULL, email text NOT NULL, PRIMARY KEY (id), FOREIGN KEY (userid) REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT);"
316+-- "CREATE TABLE users (id serial, name text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)); CREATE TABLE emails (id serial, user_id int NOT NULL, email text, CONSTRAINT pk_emails PRIMARY KEY (id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE CASCADE);"
238foreignKey
317foreignKey
239− :: ( HasTable reftable schema refcolumns
240− , SameTypes subcolumns refsubcolumns
318+ :: ForeignKeyed schema table reftable subcolumns refsubcolumns
241− , AllNotNull subcolumns
242− , SOP.SListI subcolumns
243− , SOP.SListI refsubcolumns)
244 => NP (Column columns) subcolumns
319 => NP (Column columns) subcolumns
245 -- ^ column or columns in the table
320 -- ^ column or columns in the table
246− -> Alias reftable
321+ -> Alias table
247 -- ^ reference table
322 -- ^ reference table
248− -> NP (Column refcolumns) refsubcolumns
323+ -> NP (Column (TableToColumns reftable)) refsubcolumns
249 -- ^ reference column or columns in the reference table
324 -- ^ reference column or columns in the reference table
250 -> OnDeleteClause
325 -> OnDeleteClause
251 -- ^ what to do when reference is deleted
326 -- ^ what to do when reference is deleted
252 -> OnUpdateClause
327 -> OnUpdateClause
253 -- ^ what to do when reference is updated
328 -- ^ what to do when reference is updated
254− -> TableConstraint schema columns
329+ -> TableConstraintExpression schema columns
330+ ('ForeignKey (AliasesOf subcolumns) table (AliasesOf refsubcolumns))
255foreignKey columns reftable refcolumns onDelete onUpdate =
331foreignKey columns reftable refcolumns onDelete onUpdate =
256− UnsafeTableConstraint $
332+ UnsafeTableConstraintExpression $
257 "FOREIGN KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
333 "FOREIGN KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
258 <+> "REFERENCES" <+> renderAlias reftable
334 <+> "REFERENCES" <+> renderAlias reftable
259 <+> parenthesized (renderCommaSeparated renderColumn refcolumns)
335 <+> parenthesized (renderCommaSeparated renderColumn refcolumns)
260 <+> renderOnDeleteClause onDelete
336 <+> renderOnDeleteClause onDelete
261 <+> renderOnUpdateClause onUpdate
337 <+> renderOnUpdateClause onUpdate
262
338
339+-- | A type synonym for constraints on a table with a foreign key.
340+type ForeignKeyed schema table reftable subcolumns refsubcolumns
341+ = ( Has table schema reftable
342+ , SameTypes subcolumns refsubcolumns
343+ , AllNotNull subcolumns
344+ , SOP.SListI subcolumns
345+ , SOP.SListI refsubcolumns )
346+
263−-- | `OnDelete` indicates what to do with rows that reference a deleted row.
347+-- | `OnDeleteClause` indicates what to do with rows that reference a deleted row.
264data OnDeleteClause
348data OnDeleteClause
265 = OnDeleteNoAction
349 = OnDeleteNoAction
266 -- ^ if any referencing rows still exist when the constraint is checked,
350 -- ^ if any referencing rows still exist when the constraint is checked,
@@ -279,7 +363,7 @@ renderOnDeleteClause = \case
279 OnDeleteRestrict -> "ON DELETE RESTRICT"
363 OnDeleteRestrict -> "ON DELETE RESTRICT"
280 OnDeleteCascade -> "ON DELETE CASCADE"
364 OnDeleteCascade -> "ON DELETE CASCADE"
281
365
282−-- | Analagous to `OnDelete` there is also `OnUpdate` which is invoked
366+-- | Analagous to `OnDeleteClause` there is also `OnUpdateClause` which is invoked
283-- when a referenced column is changed (updated).
367-- when a referenced column is changed (updated).
284data OnUpdateClause
368data OnUpdateClause
285 = OnUpdateNoAction
369 = OnUpdateNoAction
@@ -319,14 +403,14 @@ ALTER statements
319
403
320-- | `alterTable` changes the definition of a table from the schema.
404-- | `alterTable` changes the definition of a table from the schema.
321alterTable
405alterTable
322− :: HasTable table schema columns0
406+ :: Has tab schema table0
323− => Alias table -- ^ table to alter
407+ => Alias tab -- ^ table to alter
324− -> AlterColumns columns0 columns1 -- ^ alteration to perform
408+ -> AlterTable schema table0 table1 -- ^ alteration to perform
325− -> Definition schema (Alter table schema columns1)
409+ -> Definition schema (Alter tab schema table1)
326alterTable table alteration = UnsafeDefinition $
410alterTable table alteration = UnsafeDefinition $
327 "ALTER TABLE"
411 "ALTER TABLE"
328 <+> renderAlias table
412 <+> renderAlias table
329− <+> renderAlterColumns alteration
413+ <+> renderAlterTable alteration
330 <> ";"
414 <> ";"
331
415
332-- | `alterTableRename` changes the name of a table from the schema.
416-- | `alterTableRename` changes the name of a table from the schema.
@@ -342,78 +426,96 @@ alterTableRename table0 table1 = UnsafeDefinition $
342 "ALTER TABLE" <+> renderAlias table0
426 "ALTER TABLE" <+> renderAlias table0
343 <+> "RENAME TO" <+> renderAlias table1 <> ";"
427 <+> "RENAME TO" <+> renderAlias table1 <> ";"
344
428
345−-- | An `alterTableAddConstraint` adds a table constraint.
346−--
347−-- >>> :{
348−-- let
349−-- definition :: Definition
350−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
351−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
352−-- definition = alterTableAddConstraint #tab (check (#col .> 0))
353−-- in renderDefinition definition
354−-- :}
355−-- "ALTER TABLE tab ADD CHECK ((col > 0));"
356−alterTableAddConstraint
357− :: HasTable table schema columns
358− => Alias table -- ^ table to constrain
359− -> TableConstraint schema columns -- ^ what constraint to add
360− -> Definition schema schema
361−alterTableAddConstraint table constraint = UnsafeDefinition $
362− "ALTER TABLE" <+> renderAlias table
363− <+> "ADD" <+> renderTableConstraint constraint <> ";"
364−
365−-- | An `AlterColumns` describes the alteration to perform on the columns
429+-- | An `AlterTable` describes the alteration to perform on the columns
366-- of a table.
430-- of a table.
367−newtype AlterColumns
431+newtype AlterTable
368− (columns0 :: ColumnsType)
432+ (schema :: TablesType)
369− (columns1 :: ColumnsType) =
433+ (table0 :: TableType)
370− UnsafeAlterColumns {renderAlterColumns :: ByteString}
434+ (table1 :: TableType) =
435+ UnsafeAlterTable {renderAlterTable :: ByteString}
371 deriving (GHC.Generic,Show,Eq,Ord,NFData)
436 deriving (GHC.Generic,Show,Eq,Ord,NFData)
372
437
373−-- | An `addColumnDefault` adds a new `Optional` column. The new column is
438+-- | An `addConstraint` adds a table constraint.
374−-- initially filled with whatever default value is given.
375--
439--
376-- >>> :{
440-- >>> :{
377-- let
441-- let
378-- definition :: Definition
442-- definition :: Definition
379−-- '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
443+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
380−-- '["tab" :::
444+-- '["tab" ::: '["positive" ::: Check '["col"]] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
381−-- '[ "col1" ::: 'Required ('Null 'PGint4)
382−-- , "col2" ::: 'Optional ('Null 'PGtext) ]]
383−-- definition = alterTable #tab
445+-- definition = alterTable #tab (addConstraint #positive (check (Column #col :* Nil) (#col .> 0)))
384−-- (addColumnDefault #col2 (text & default_ "foo"))
385-- in renderDefinition definition
446-- in renderDefinition definition
386-- :}
447-- :}
387−-- "ALTER TABLE tab ADD COLUMN col2 text DEFAULT E'foo';"
448+-- "ALTER TABLE tab ADD CONSTRAINT positive CHECK ((col > 0));"
388−addColumnDefault
449+addConstraint
389− :: KnownSymbol column
450+ :: KnownSymbol alias
390− => Alias column -- ^ column to add
451+ => Alias alias
391− -> TypeExpression ('Optional ty) -- ^ type of the new column
452+ -> TableConstraintExpression schema columns constraint
392− -> AlterColumns columns (Create column ('Optional ty) columns)
453+ -- ^ constraint to add
393−addColumnDefault column ty = UnsafeAlterColumns $
454+ -> AlterTable schema (constraints :=> columns)
394− "ADD COLUMN" <+> renderAlias column <+> renderTypeExpression ty
455+ (Create alias constraint constraints :=> columns)
395−
456+addConstraint alias constraint = UnsafeAlterTable $
396−-- | An `addColumnNull` adds a new `Null` column. The new column is initially
457+ "ADD" <+> "CONSTRAINT" <+> renderAlias alias
397−-- filled with @NULL@s.
458+ <+> renderTableConstraintExpression constraint
459+
460+-- | A `dropConstraint` drops a table constraint.
398--
461--
399-- >>> :{
462-- >>> :{
400-- let
463-- let
401-- definition :: Definition
464-- definition :: Definition
402−-- '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
465+-- '["tab" ::: '["positive" ::: Check '["col"]] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
403−-- '["tab" :::
466+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
404−-- '[ "col1" ::: 'Required ('Null 'PGint4)
405−-- , "col2" ::: 'Required ('Null 'PGtext) ]]
406−-- definition = alterTable #tab (addColumnNull #col2 text)
467+-- definition = alterTable #tab (dropConstraint #positive)
407-- in renderDefinition definition
468-- in renderDefinition definition
408-- :}
469-- :}
470+-- "ALTER TABLE tab DROP CONSTRAINT positive;"
471+dropConstraint
472+ :: KnownSymbol constraint
473+ => Alias constraint
474+ -- ^ constraint to drop
475+ -> AlterTable schema
476+ (constraints :=> columns)
477+ (Drop constraint constraints :=> columns)
478+dropConstraint constraint = UnsafeAlterTable $
479+ "DROP" <+> "CONSTRAINT" <+> renderAlias constraint
480+
481+-- | An `AddColumn` is either @NULL@ or has @DEFAULT@.
482+class AddColumn ty where
483+ -- | `addColumn` adds a new column, initially filled with whatever
484+ -- default value is given or with @NULL@.
485+ --
486+ -- >>> :{
487+ -- let
488+ -- definition :: Definition
489+ -- '["tab" ::: '[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4]]
490+ -- '["tab" ::: '[] :=>
491+ -- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4
492+ -- , "col2" ::: 'Def :=> 'Null 'PGtext ]]
493+ -- definition = alterTable #tab (addColumn #col2 (text & default_ "foo"))
494+ -- in renderDefinition definition
495+ -- :}
409−-- "ALTER TABLE tab ADD COLUMN col2 text;"
496+ -- "ALTER TABLE tab ADD COLUMN col2 text DEFAULT E'foo';"
410−addColumnNull
497+ --
498+ -- >>> :{
499+ -- let
500+ -- definition :: Definition
501+ -- '["tab" ::: '[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4]]
502+ -- '["tab" ::: '[] :=>
503+ -- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4
504+ -- , "col2" ::: 'NoDef :=> 'Null 'PGtext ]]
505+ -- definition = alterTable #tab (addColumn #col2 text)
506+ -- in renderDefinition definition
507+ -- :}
508+ -- "ALTER TABLE tab ADD COLUMN col2 text;"
509+ addColumn
411− :: KnownSymbol column
510+ :: KnownSymbol column
412− => Alias column -- ^ column to add
511+ => Alias column -- ^ column to add
413− -> TypeExpression ('Required ('Null ty)) -- ^ type of the new column
512+ -> TypeExpression ty -- ^ type of the new column
513+ -> AlterTable schema (constraints :=> columns)
414− -> AlterColumns columns (Create column ('Required ('Null ty)) columns)
514+ (constraints :=> Create column ty columns)
415−addColumnNull column ty = UnsafeAlterColumns $
515+ addColumn column ty = UnsafeAlterTable $
416− "ADD COLUMN" <+> renderAlias column <+> renderTypeExpression ty
516+ "ADD COLUMN" <+> renderAlias column <+> renderTypeExpression ty
517+instance {-# OVERLAPPING #-} AddColumn ('Def :=> ty)
518+instance {-# OVERLAPPABLE #-} AddColumn ('NoDef :=> 'Null ty)
417
519
418-- | A `dropColumn` removes a column. Whatever data was in the column
520-- | A `dropColumn` removes a column. Whatever data was in the column
419-- disappears. Table constraints involving the column are dropped, too.
521-- disappears. Table constraints involving the column are dropped, too.
@@ -423,10 +525,10 @@ addColumnNull column ty = UnsafeAlterColumns $
423-- >>> :{
525-- >>> :{
424-- let
526-- let
425-- definition :: Definition
527-- definition :: Definition
426−-- '["tab" :::
528+-- '["tab" ::: '[] :=>
427−-- '[ "col1" ::: 'Required ('Null 'PGint4)
529+-- '[ "col1" ::: 'NoDef :=> 'Null 'PGint4
428−-- , "col2" ::: 'Required ('Null 'PGtext) ]]
530+-- , "col2" ::: 'NoDef :=> 'Null 'PGtext ]]
429−-- '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
531+-- '["tab" ::: '[] :=> '["col1" ::: 'NoDef :=> 'Null 'PGint4]]
430-- definition = alterTable #tab (dropColumn #col2)
532-- definition = alterTable #tab (dropColumn #col2)
431-- in renderDefinition definition
533-- in renderDefinition definition
432-- :}
534-- :}
@@ -434,57 +536,40 @@ addColumnNull column ty = UnsafeAlterColumns $
434dropColumn
536dropColumn
435 :: KnownSymbol column
537 :: KnownSymbol column
436 => Alias column -- ^ column to remove
538 => Alias column -- ^ column to remove
437− -> AlterColumns columns (Drop column columns)
539+ -> AlterTable schema
540+ (constraints :=> columns)
541+ (DropIfConstraintsInvolve column constraints :=> Drop column columns)
438−dropColumn column = UnsafeAlterColumns $
542+dropColumn column = UnsafeAlterTable $
439 "DROP COLUMN" <+> renderAlias column
543 "DROP COLUMN" <+> renderAlias column
440
544
441−-- | Like `dropColumn` but authorizes dropping everything that depends
442−-- on the column.
443−--
444−-- >>> :{
445−-- let
446−-- definition :: Definition
447−-- '["tab" :::
448−-- '[ "col1" ::: 'Required ('Null 'PGint4)
449−-- , "col2" ::: 'Required ('Null 'PGtext) ]]
450−-- '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
451−-- definition = alterTable #tab (dropColumnCascade #col2)
452−-- in renderDefinition definition
453−-- :}
454−-- "ALTER TABLE tab DROP COLUMN col2 CASCADE;"
455−dropColumnCascade
456− :: KnownSymbol column
457− => Alias column -- ^ column to remove
458− -> AlterColumns columns (Drop column columns)
459−dropColumnCascade column = UnsafeAlterColumns $
460− "DROP COLUMN" <+> renderAlias column <+> "CASCADE"
461−
462-- | A `renameColumn` renames a column.
545-- | A `renameColumn` renames a column.
463--
546--
464-- >>> :{
547-- >>> :{
465-- let
548-- let
466-- definition :: Definition
549-- definition :: Definition
467−-- '["tab" ::: '["foo" ::: 'Required ('Null 'PGint4)]]
550+-- '["tab" ::: '[] :=> '["foo" ::: 'NoDef :=> 'Null 'PGint4]]
468−-- '["tab" ::: '["bar" ::: 'Required ('Null 'PGint4)]]
551+-- '["tab" ::: '[] :=> '["bar" ::: 'NoDef :=> 'Null 'PGint4]]
469-- definition = alterTable #tab (renameColumn #foo #bar)
552-- definition = alterTable #tab (renameColumn #foo #bar)
470-- in renderDefinition definition
553-- in renderDefinition definition
471-- :}
554-- :}
472-- "ALTER TABLE tab RENAME COLUMN foo TO bar;"
555-- "ALTER TABLE tab RENAME COLUMN foo TO bar;"
473renameColumn
556renameColumn
474 :: (KnownSymbol column0, KnownSymbol column1)
557 :: (KnownSymbol column0, KnownSymbol column1)
475− => Alias column0
558+ => Alias column0 -- ^ column to rename
476− -> Alias column1
559+ -> Alias column1 -- ^ what to rename the column
560+ -> AlterTable schema (constraints :=> columns)
477− -> AlterColumns columns (Rename column0 column1 columns)
561+ (constraints :=> Rename column0 column1 columns)
478−renameColumn column0 column1 = UnsafeAlterColumns $
562+renameColumn column0 column1 = UnsafeAlterTable $
479 "RENAME COLUMN" <+> renderAlias column0 <+> "TO" <+> renderAlias column1
563 "RENAME COLUMN" <+> renderAlias column0 <+> "TO" <+> renderAlias column1
480
564
481-- | An `alterColumn` alters a single column.
565-- | An `alterColumn` alters a single column.
482alterColumn
566alterColumn
483− :: (KnownSymbol column, HasColumn column columns ty0)
567+ :: (KnownSymbol column, Has column columns ty0)
484 => Alias column -- ^ column to alter
568 => Alias column -- ^ column to alter
485 -> AlterColumn ty0 ty1 -- ^ alteration to perform
569 -> AlterColumn ty0 ty1 -- ^ alteration to perform
570+ -> AlterTable schema (constraints :=> columns)
486− -> AlterColumns columns (Alter column columns ty1)
571+ (constraints :=> Alter column columns ty1)
487−alterColumn column alteration = UnsafeAlterColumns $
572+alterColumn column alteration = UnsafeAlterTable $
488 "ALTER COLUMN" <+> renderAlias column <+> renderAlterColumn alteration
573 "ALTER COLUMN" <+> renderAlias column <+> renderAlterColumn alteration
489
574
490-- | An `AlterColumn` describes the alteration to perform on a single column.
575-- | An `AlterColumn` describes the alteration to perform on a single column.
@@ -494,20 +579,20 @@ newtype AlterColumn (ty0 :: ColumnType) (ty1 :: ColumnType) =
494
579
495-- | A `setDefault` sets a new default for a column. Note that this doesn't
580-- | A `setDefault` sets a new default for a column. Note that this doesn't
496-- affect any existing rows in the table, it just changes the default for
581-- affect any existing rows in the table, it just changes the default for
497−-- future `insertTable` and `updateTable` commands.
582+-- future insert and update commands.
498--
583--
499-- >>> :{
584-- >>> :{
500-- let
585-- let
501-- definition :: Definition
586-- definition :: Definition
502−-- '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
587+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
503−-- '["tab" ::: '["col" ::: 'Optional ('Null 'PGint4)]]
588+-- '["tab" ::: '[] :=> '["col" ::: 'Def :=> 'Null 'PGint4]]
504-- definition = alterTable #tab (alterColumn #col (setDefault 5))
589-- definition = alterTable #tab (alterColumn #col (setDefault 5))
505-- in renderDefinition definition
590-- in renderDefinition definition
506-- :}
591-- :}
507-- "ALTER TABLE tab ALTER COLUMN col SET DEFAULT 5;"
592-- "ALTER TABLE tab ALTER COLUMN col SET DEFAULT 5;"
508setDefault
593setDefault
509− :: Expression '[] 'Ungrouped '[] ('Required ty) -- ^ default value to set
594+ :: Expression '[] 'Ungrouped '[] ty -- ^ default value to set
510− -> AlterColumn (optionality ty) ('Optional ty)
595+ -> AlterColumn (constraint :=> ty) ('Def :=> ty)
511setDefault expression = UnsafeAlterColumn $
596setDefault expression = UnsafeAlterColumn $
512 "SET DEFAULT" <+> renderExpression expression
597 "SET DEFAULT" <+> renderExpression expression
513
598
@@ -516,13 +601,13 @@ setDefault expression = UnsafeAlterColumn $
516-- >>> :{
601-- >>> :{
517-- let
602-- let
518-- definition :: Definition
603-- definition :: Definition
519−-- '["tab" ::: '["col" ::: 'Optional ('Null 'PGint4)]]
604+-- '["tab" ::: '[] :=> '["col" ::: 'Def :=> 'Null 'PGint4]]
520−-- '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
605+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
521-- definition = alterTable #tab (alterColumn #col dropDefault)
606-- definition = alterTable #tab (alterColumn #col dropDefault)
522-- in renderDefinition definition
607-- in renderDefinition definition
523-- :}
608-- :}
524-- "ALTER TABLE tab ALTER COLUMN col DROP DEFAULT;"
609-- "ALTER TABLE tab ALTER COLUMN col DROP DEFAULT;"
525−dropDefault :: AlterColumn (optionality ty) ('Required ty)
610+dropDefault :: AlterColumn ('Def :=> ty) ('NoDef :=> ty)
526dropDefault = UnsafeAlterColumn $ "DROP DEFAULT"
611dropDefault = UnsafeAlterColumn $ "DROP DEFAULT"
527
612
528-- | A `setNotNull` adds a @NOT NULL@ constraint to a column.
613-- | A `setNotNull` adds a @NOT NULL@ constraint to a column.
@@ -532,13 +617,14 @@ dropDefault = UnsafeAlterColumn $ "DROP DEFAULT"
532-- >>> :{
617-- >>> :{
533-- let
618-- let
534-- definition :: Definition
619-- definition :: Definition
535−-- '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
620+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
536−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
621+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
537-- definition = alterTable #tab (alterColumn #col setNotNull)
622-- definition = alterTable #tab (alterColumn #col setNotNull)
538-- in renderDefinition definition
623-- in renderDefinition definition
539-- :}
624-- :}
540-- "ALTER TABLE tab ALTER COLUMN col SET NOT NULL;"
625-- "ALTER TABLE tab ALTER COLUMN col SET NOT NULL;"
626+setNotNull
541−setNotNull :: AlterColumn (optionality ('Null ty)) (optionality ('NotNull ty))
627+ :: AlterColumn (constraint :=> 'Null ty) (constraint :=> 'NotNull ty)
542setNotNull = UnsafeAlterColumn $ "SET NOT NULL"
628setNotNull = UnsafeAlterColumn $ "SET NOT NULL"
543
629
544-- | A `dropNotNull` drops a @NOT NULL@ constraint from a column.
630-- | A `dropNotNull` drops a @NOT NULL@ constraint from a column.
@@ -546,13 +632,14 @@ setNotNull = UnsafeAlterColumn $ "SET NOT NULL"
546-- >>> :{
632-- >>> :{
547-- let
633-- let
548-- definition :: Definition
634-- definition :: Definition
549−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
635+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
550−-- '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
636+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
551-- definition = alterTable #tab (alterColumn #col dropNotNull)
637-- definition = alterTable #tab (alterColumn #col dropNotNull)
552-- in renderDefinition definition
638-- in renderDefinition definition
553-- :}
639-- :}
554-- "ALTER TABLE tab ALTER COLUMN col DROP NOT NULL;"
640-- "ALTER TABLE tab ALTER COLUMN col DROP NOT NULL;"
641+dropNotNull
555−dropNotNull :: AlterColumn (optionality ('NotNull ty)) (optionality ('Null ty))
642+ :: AlterColumn (constraint :=> 'NotNull ty) (constraint :=> 'Null ty)
556dropNotNull = UnsafeAlterColumn $ "DROP NOT NULL"
643dropNotNull = UnsafeAlterColumn $ "DROP NOT NULL"
557
644
558-- | An `alterType` converts a column to a different data type.
645-- | An `alterType` converts a column to a different data type.
@@ -562,8 +649,8 @@ dropNotNull = UnsafeAlterColumn $ "DROP NOT NULL"
562-- >>> :{
649-- >>> :{
563-- let
650-- let
564-- definition :: Definition
651-- definition :: Definition
565−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
652+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGint4]]
566−-- '["tab" ::: '["col" ::: 'Required ('NotNull 'PGnumeric)]]
653+-- '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGnumeric]]
567-- definition =
654-- definition =
568-- alterTable #tab (alterColumn #col (alterType (numeric & notNull)))
655-- alterTable #tab (alterColumn #col (alterType (numeric & notNull)))
569-- in renderDefinition definition
656-- in renderDefinition definition
squeal-postgresql/src/Squeal/PostgreSQL/Expression.hs+399−336
@@ -37,13 +37,6 @@ module Squeal.PostgreSQL.Expression
37 ( -- * Expression
37 ( -- * Expression
38 Expression (UnsafeExpression, renderExpression)
38 Expression (UnsafeExpression, renderExpression)
39 , HasParameter (param)
39 , HasParameter (param)
40− , HasColumn (getColumn)
41− , Column (Column)
42− , renderColumn
43− , GroupedBy (getGroup1, getGroup2)
44− -- ** Default
45− , def
46− , unDef
47 -- ** Null
40 -- ** Null
48 , null_
41 , null_
49 , unNull
42 , unNull
@@ -53,7 +46,9 @@ module Squeal.PostgreSQL.Expression
53 , isn'tNull
46 , isn'tNull
54 , matchNull
47 , matchNull
55 , nullIf
48 , nullIf
49+ -- ** Arrays
50+ , array
56− -- ** Functions
51+ -- ** Functions
57 , unsafeBinaryOp
52 , unsafeBinaryOp
58 , unsafeUnaryOp
53 , unsafeUnaryOp
59 , unsafeFunction
54 , unsafeFunction
@@ -104,8 +99,7 @@ module Squeal.PostgreSQL.Expression
104 , max_, maxDistinct, min_, minDistinct
99 , max_, maxDistinct, min_, minDistinct
105 -- * Tables
100 -- * Tables
106 , Table (UnsafeTable, renderTable)
101 , Table (UnsafeTable, renderTable)
107− , HasTable (getTable)
102+ -- * Types
108− -- * TypeExpression
109 , TypeExpression (UnsafeTypeExpression, renderTypeExpression)
103 , TypeExpression (UnsafeTypeExpression, renderTypeExpression)
110 , PGTyped (pgtype)
104 , PGTyped (pgtype)
111 , bool
105 , bool
@@ -143,6 +137,8 @@ module Squeal.PostgreSQL.Expression
143 , inet
137 , inet
144 , json
138 , json
145 , jsonb
139 , jsonb
140+ , vararray
141+ , fixarray
146 , notNull
142 , notNull
147 , default_
143 , default_
148 -- * Re-export
144 -- * Re-export
@@ -164,7 +160,7 @@ import Prelude hiding (id, (.))
164
160
165import qualified GHC.Generics as GHC
161import qualified GHC.Generics as GHC
166
162
167−import Squeal.PostgreSQL.Prettyprint
163+import Squeal.PostgreSQL.Render
168import Squeal.PostgreSQL.Schema
164import Squeal.PostgreSQL.Schema
169
165
170{-----------------------------------------
166{-----------------------------------------
@@ -181,10 +177,10 @@ values from primitive expression using arithmetic, logical,
181and other operations.
177and other operations.
182-}
178-}
183newtype Expression
179newtype Expression
184− (tables :: TablesType)
180+ (relations :: RelationsType)
185 (grouping :: Grouping)
181 (grouping :: Grouping)
186− (params :: [ColumnType])
182+ (params :: [NullityType])
187− (ty :: ColumnType)
183+ (ty :: NullityType)
188 = UnsafeExpression { renderExpression :: ByteString }
184 = UnsafeExpression { renderExpression :: ByteString }
189 deriving (GHC.Generic,Show,Eq,Ord,NFData)
185 deriving (GHC.Generic,Show,Eq,Ord,NFData)
190
186
@@ -196,123 +192,51 @@ supplied externally to a SQL statement.
196separately from the SQL command string, in which case `param`s are used to
192separately from the SQL command string, in which case `param`s are used to
197refer to the out-of-line data values.
193refer to the out-of-line data values.
198-}
194-}
199−class (PGTyped (BaseType ty), KnownNat n)
195+class (PGTyped (PGTypeOf ty), KnownNat n) => HasParameter
196+ (n :: Nat)
197+ (params :: [NullityType])
198+ (ty :: NullityType)
200− => HasParameter (n :: Nat) params ty | n params -> ty where
199+ | n params -> ty where
201− param :: Expression tables grouping params ty
200+ param :: Expression relations grouping params ty
202 param = UnsafeExpression $ parenthesized $
201 param = UnsafeExpression $ parenthesized $
203 "$" <> renderNat (Proxy @n) <+> "::"
202 "$" <> renderNat (Proxy @n) <+> "::"
204− <+> renderTypeExpression (pgtype @(BaseType ty))
203+ <+> renderTypeExpression (pgtype @(PGTypeOf ty))
205−instance {-# OVERLAPPING #-} PGTyped (BaseType ty1)
204+instance {-# OVERLAPPING #-} PGTyped (PGTypeOf ty1)
206 => HasParameter 1 (ty1:tys) ty1
205 => HasParameter 1 (ty1:tys) ty1
207instance {-# OVERLAPPABLE #-} (KnownNat n, HasParameter (n-1) params ty)
206instance {-# OVERLAPPABLE #-} (KnownNat n, HasParameter (n-1) params ty)
208 => HasParameter n (ty' : params) ty
207 => HasParameter n (ty' : params) ty
209
208
210−{- | A `HasColumn` constraint indicates an unqualified column reference.
209+instance (HasUnique relation relations columns, Has column columns ty)
211−`getColumn` can only be unambiguous when the `TableExpression` the column
212−references is unique, in which case the column may be referenced using
213−@-XOverloadedLabels@. Otherwise, combined with a `HasTable` constraint, the
214−qualified column reference operator `!` may be used.
215−-}
216−class KnownSymbol column => HasColumn column columns ty
217− | column columns -> ty where
218− getColumn
219− :: HasUnique table tables columns
220− => Alias column
221− -> Expression tables 'Ungrouped params ty
222− getColumn column = UnsafeExpression $ renderAlias column
223−instance {-# OVERLAPPING #-} KnownSymbol column
224− => HasColumn column ((column ::: optionality ty) ': tys) ('Required ty)
225−instance {-# OVERLAPPABLE #-} (KnownSymbol column, HasColumn column table ty)
226− => HasColumn column (ty' ': table) ty
227−
228−instance (HasColumn column columns ty, HasUnique table tables columns)
229− => IsLabel column (Expression tables 'Ungrouped params ty) where
210+ => IsLabel column (Expression relations 'Ungrouped params ty) where
230− fromLabel = getColumn (Alias @column)
211+ fromLabel = UnsafeExpression $ renderAlias (Alias @column)
231−
212+
232−instance (HasTable table tables columns, HasColumn column columns ty)
213+instance (Has relation relations columns, Has column columns ty)
233− => IsTableColumn table column (Expression tables 'Ungrouped params ty) where
214+ => IsQualified relation column (Expression relations 'Ungrouped params ty) where
234− table ! column = UnsafeExpression $
215+ relation ! column = UnsafeExpression $
235− renderAlias table <> "." <> renderAlias column
216+ renderAlias relation <> "." <> renderAlias column
236−
237−-- | A `Column` is a witness to a `HasColumn` constraint. It's used
238−-- in `Squeel.PostgreSQL.Definition.unique` and other
239−-- `Squeel.PostgreSQL.Definition.TableConstraint`s to witness a
240−-- subcolumns relationship.
241−data Column
242− (columns :: ColumnsType)
243− (columnty :: (Symbol,ColumnType))
244− where
245− Column
246− :: HasColumn column columns ty
247− => Alias column
248− -> Column columns (column ::: ty)
249−deriving instance Show (Column columns columnty)
250−deriving instance Eq (Column columns columnty)
251−deriving instance Ord (Column columns columnty)
252−
253−-- | Render a `Column`.
254−renderColumn :: Column columns columnty -> ByteString
255−renderColumn (Column column) = renderAlias column
256−
257−{- | A `GroupedBy` constraint indicates that a table qualified column is
258−a member of the auxiliary namespace created by @GROUP BY@ clauses and thus,
259−may be called in an output `Expression` without aggregating.
260−-}
261−class (KnownSymbol table, KnownSymbol column)
262− => GroupedBy table column bys where
263− getGroup1
264− :: (HasUnique table tables columns, HasColumn column columns ty)
265− => Alias column
266− -> Expression tables ('Grouped bys) params ty
267− getGroup1 column = UnsafeExpression $ renderAlias column
268− getGroup2
269− :: (HasTable table tables columns, HasColumn column columns ty)
270− => Alias table
271− -> Alias column
272− -> Expression tables ('Grouped bys) params ty
273− getGroup2 table column = UnsafeExpression $
274− renderAlias table <> "." <> renderAlias column
275−instance {-# OVERLAPPING #-} (KnownSymbol table, KnownSymbol column)
276− => GroupedBy table column ('(table,column) ': bys)
277−instance {-# OVERLAPPABLE #-}
278− ( KnownSymbol table
279− , KnownSymbol column
280− , GroupedBy table column bys
281− ) => GroupedBy table column (tabcol ': bys)
282
217
283instance
218instance
284− ( HasUnique table tables columns
219+ ( HasUnique relation relations columns
285− , HasColumn column columns ty
220+ , Has column columns ty
286− , GroupedBy table column bys
221+ , GroupedBy relation column bys
287 ) => IsLabel column
222 ) => IsLabel column
288− (Expression tables ('Grouped bys) params ty) where
223+ (Expression relations ('Grouped bys) params ty) where
289− fromLabel = getGroup1 (Alias @column)
224+ fromLabel = UnsafeExpression $ renderAlias (Alias @column)
290
225
291instance
226instance
292− ( HasTable table tables columns
227+ ( Has relation relations columns
293− , HasColumn column columns ty
228+ , Has column columns ty
294− , GroupedBy table column bys
229+ , GroupedBy relation column bys
295− ) => IsTableColumn table column
230+ ) => IsQualified relation column
296− (Expression tables ('Grouped bys) params ty) where (!) = getGroup2
231+ (Expression relations ('Grouped bys) params ty) where
297−
298−-- | >>> renderExpression def
299−-- "DEFAULT"
300−def :: Expression '[] 'Ungrouped params ('Optional (nullity ty))
301−def = UnsafeExpression "DEFAULT"
232+ relation ! column = UnsafeExpression $
302−
233+ renderAlias relation <> "." <> renderAlias column
303−-- | >>> renderExpression $ unDef false
304−-- "FALSE"
305−unDef
306− :: Expression '[] 'Ungrouped params ('Required (nullity ty))
307− -- ^ not @DEFAULT@
308− -> Expression '[] 'Ungrouped params ('Optional (nullity ty))
309−unDef = UnsafeExpression . renderExpression
310
234
311-- | analagous to `Nothing`
235-- | analagous to `Nothing`
312--
236--
313-- >>> renderExpression $ null_
237-- >>> renderExpression $ null_
314-- "NULL"
238-- "NULL"
315−null_ :: Expression tables grouping params (optionality ('Null ty))
239+null_ :: Expression relations grouping params ('Null ty)
316null_ = UnsafeExpression "NULL"
240null_ = UnsafeExpression "NULL"
317
241
318-- | analagous to `Just`
242-- | analagous to `Just`
@@ -320,9 +244,9 @@ null_ = UnsafeExpression "NULL"
320-- >>> renderExpression $ unNull true
244-- >>> renderExpression $ unNull true
321-- "TRUE"
245-- "TRUE"
322unNull
246unNull
323− :: Expression tables grouping params (optionality ('NotNull ty))
247+ :: Expression relations grouping params ('NotNull ty)
324 -- ^ not @NULL@
248 -- ^ not @NULL@
325− -> Expression tables grouping params (optionality ('Null ty))
249+ -> Expression relations grouping params ('Null ty)
326unNull = UnsafeExpression . renderExpression
250unNull = UnsafeExpression . renderExpression
327
251
328-- | return the leftmost value which is not NULL
252-- | return the leftmost value which is not NULL
@@ -330,40 +254,40 @@ unNull = UnsafeExpression . renderExpression
330-- >>> renderExpression $ coalesce [null_, unNull true] false
254-- >>> renderExpression $ coalesce [null_, unNull true] false
331-- "COALESCE(NULL, TRUE, FALSE)"
255-- "COALESCE(NULL, TRUE, FALSE)"
332coalesce
256coalesce
333− :: [Expression tables grouping params ('Required ('Null ty))]
257+ :: [Expression relations grouping params ('Null ty)]
334 -- ^ @NULL@s may be present
258 -- ^ @NULL@s may be present
335− -> Expression tables grouping params ('Required ('NotNull ty))
259+ -> Expression relations grouping params ('NotNull ty)
336 -- ^ @NULL@ is absent
260 -- ^ @NULL@ is absent
337− -> Expression tables grouping params ('Required ('NotNull ty))
261+ -> Expression relations grouping params ('NotNull ty)
338coalesce nullxs notNullx = UnsafeExpression $
262coalesce nullxs notNullx = UnsafeExpression $
339 "COALESCE" <> parenthesized (commaSeparated
263 "COALESCE" <> parenthesized (commaSeparated
340 ((renderExpression <$> nullxs) <> [renderExpression notNullx]))
264 ((renderExpression <$> nullxs) <> [renderExpression notNullx]))
341
265
342−-- | analagous to `fromMaybe` using @COALESCE@
266+-- | analagous to `Data.Maybe.fromMaybe` using @COALESCE@
343--
267--
344-- >>> renderExpression $ fromNull true null_
268-- >>> renderExpression $ fromNull true null_
345-- "COALESCE(NULL, TRUE)"
269-- "COALESCE(NULL, TRUE)"
346fromNull
270fromNull
347− :: Expression tables grouping params ('Required ('NotNull ty))
271+ :: Expression relations grouping params ('NotNull ty)
348 -- ^ what to convert @NULL@ to
272 -- ^ what to convert @NULL@ to
349− -> Expression tables grouping params ('Required ('Null ty))
273+ -> Expression relations grouping params ('Null ty)
350− -> Expression tables grouping params ('Required ('NotNull ty))
274+ -> Expression relations grouping params ('NotNull ty)
351fromNull notNullx nullx = coalesce [nullx] notNullx
275fromNull notNullx nullx = coalesce [nullx] notNullx
352
276
353-- | >>> renderExpression $ null_ & isNull
277-- | >>> renderExpression $ null_ & isNull
354-- "NULL IS NULL"
278-- "NULL IS NULL"
355isNull
279isNull
356− :: Expression tables grouping params ('Required ('Null ty))
280+ :: Expression relations grouping params ('Null ty)
357 -- ^ possibly @NULL@
281 -- ^ possibly @NULL@
358− -> Condition tables grouping params
282+ -> Condition relations grouping params
359isNull x = UnsafeExpression $ renderExpression x <+> "IS NULL"
283isNull x = UnsafeExpression $ renderExpression x <+> "IS NULL"
360
284
361-- | >>> renderExpression $ null_ & isn'tNull
285-- | >>> renderExpression $ null_ & isn'tNull
362-- "NULL IS NOT NULL"
286-- "NULL IS NOT NULL"
363isn'tNull
287isn'tNull
364− :: Expression tables grouping params ('Required ('Null ty))
288+ :: Expression relations grouping params ('Null ty)
365 -- ^ possibly @NULL@
289 -- ^ possibly @NULL@
366− -> Condition tables grouping params
290+ -> Condition relations grouping params
367isn'tNull x = UnsafeExpression $ renderExpression x <+> "IS NOT NULL"
291isn'tNull x = UnsafeExpression $ renderExpression x <+> "IS NOT NULL"
368
292
369-- | analagous to `maybe` using @IS NULL@
293-- | analagous to `maybe` using @IS NULL@
@@ -371,13 +295,13 @@ isn'tNull x = UnsafeExpression $ renderExpression x <+> "IS NOT NULL"
371-- >>> renderExpression $ matchNull true not_ null_
295-- >>> renderExpression $ matchNull true not_ null_
372-- "CASE WHEN NULL IS NULL THEN TRUE ELSE (NOT NULL) END"
296-- "CASE WHEN NULL IS NULL THEN TRUE ELSE (NOT NULL) END"
373matchNull
297matchNull
374− :: Expression tables grouping params ('Required nullty)
298+ :: Expression relations grouping params (nullty)
375 -- ^ what to convert @NULL@ to
299 -- ^ what to convert @NULL@ to
376− -> ( Expression tables grouping params ('Required ('NotNull ty))
300+ -> ( Expression relations grouping params ('NotNull ty)
377− -> Expression tables grouping params ('Required nullty) )
301+ -> Expression relations grouping params (nullty) )
378 -- ^ function to perform when @NULL@ is absent
302 -- ^ function to perform when @NULL@ is absent
379− -> Expression tables grouping params ('Required ('Null ty))
303+ -> Expression relations grouping params ('Null ty)
380− -> Expression tables grouping params ('Required nullty)
304+ -> Expression relations grouping params (nullty)
381matchNull y f x = ifThenElse (isNull x) y
305matchNull y f x = ifThenElse (isNull x) y
382 (f (UnsafeExpression (renderExpression x)))
306 (f (UnsafeExpression (renderExpression x)))
383
307
@@ -388,33 +312,47 @@ matchNull y f x = ifThenElse (isNull x) y
388-- >>> renderExpression @_ @_ @'[_] $ fromNull false (nullIf false (param @1))
312-- >>> renderExpression @_ @_ @'[_] $ fromNull false (nullIf false (param @1))
389-- "COALESCE(NULL IF (FALSE, ($1 :: bool)), FALSE)"
313-- "COALESCE(NULL IF (FALSE, ($1 :: bool)), FALSE)"
390nullIf
314nullIf
391− :: Expression tables grouping params ('Required ('NotNull ty))
315+ :: Expression relations grouping params ('NotNull ty)
392 -- ^ @NULL@ is absent
316 -- ^ @NULL@ is absent
393− -> Expression tables grouping params ('Required ('NotNull ty))
317+ -> Expression relations grouping params ('NotNull ty)
394 -- ^ @NULL@ is absent
318 -- ^ @NULL@ is absent
395− -> Expression tables grouping params ('Required ('Null ty))
319+ -> Expression relations grouping params ('Null ty)
396nullIf x y = UnsafeExpression $ "NULL IF" <+> parenthesized
320nullIf x y = UnsafeExpression $ "NULL IF" <+> parenthesized
397 (renderExpression x <> ", " <> renderExpression y)
321 (renderExpression x <> ", " <> renderExpression y)
398
322
323+-- | >>> renderExpression $ array [null_, unNull false, unNull true]
324+-- "ARRAY[NULL, FALSE, TRUE]"
325+array
326+ :: [Expression relations grouping params ('Null ty)]
327+ -- ^ array elements
328+ -> Expression relations grouping params (nullity ('PGvararray ty))
329+array xs = UnsafeExpression $
330+ "ARRAY[" <> commaSeparated (renderExpression <$> xs) <> "]"
331+
332+instance Monoid
333+ (Expression relations grouping params (nullity ('PGvararray ty))) where
334+ mempty = array []
335+ mappend = unsafeBinaryOp "||"
336+
399-- | >>> renderExpression @_ @_ @'[_] $ greatest currentTimestamp [param @1]
337-- | >>> renderExpression @_ @_ @'[_] $ greatest currentTimestamp [param @1]
400-- "GREATEST(CURRENT_TIMESTAMP, ($1 :: timestamp with time zone))"
338-- "GREATEST(CURRENT_TIMESTAMP, ($1 :: timestamp with time zone))"
401greatest
339greatest
402− :: Expression tables grouping params ('Required nullty)
340+ :: Expression relations grouping params (nullty)
403 -- ^ needs at least 1 argument
341 -- ^ needs at least 1 argument
404− -> [Expression tables grouping params ('Required nullty)]
342+ -> [Expression relations grouping params (nullty)]
405 -- ^ or more
343 -- ^ or more
406− -> Expression tables grouping params ('Required nullty)
344+ -> Expression relations grouping params (nullty)
407greatest x xs = UnsafeExpression $ "GREATEST("
345greatest x xs = UnsafeExpression $ "GREATEST("
408 <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
346 <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
409
347
410-- | >>> renderExpression $ least currentTimestamp [null_]
348-- | >>> renderExpression $ least currentTimestamp [null_]
411-- "LEAST(CURRENT_TIMESTAMP, NULL)"
349-- "LEAST(CURRENT_TIMESTAMP, NULL)"
412least
350least
413− :: Expression tables grouping params ('Required nullty)
351+ :: Expression relations grouping params (nullty)
414 -- ^ needs at least 1 argument
352 -- ^ needs at least 1 argument
415− -> [Expression tables grouping params ('Required nullty)]
353+ -> [Expression relations grouping params (nullty)]
416 -- ^ or more
354 -- ^ or more
417− -> Expression tables grouping params ('Required nullty)
355+ -> Expression relations grouping params (nullty)
418least x xs = UnsafeExpression $ "LEAST("
356least x xs = UnsafeExpression $ "LEAST("
419 <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
357 <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
420
358
@@ -423,9 +361,9 @@ least x xs = UnsafeExpression $ "LEAST("
423unsafeBinaryOp
361unsafeBinaryOp
424 :: ByteString
362 :: ByteString
425 -- ^ operator
363 -- ^ operator
426− -> Expression tables grouping params ('Required ty0)
364+ -> Expression relations grouping params (ty0)
427− -> Expression tables grouping params ('Required ty1)
365+ -> Expression relations grouping params (ty1)
428− -> Expression tables grouping params ('Required ty2)
366+ -> Expression relations grouping params (ty2)
429unsafeBinaryOp op x y = UnsafeExpression $ parenthesized $
367unsafeBinaryOp op x y = UnsafeExpression $ parenthesized $
430 renderExpression x <+> op <+> renderExpression y
368 renderExpression x <+> op <+> renderExpression y
431
369
@@ -434,8 +372,8 @@ unsafeBinaryOp op x y = UnsafeExpression $ parenthesized $
434unsafeUnaryOp
372unsafeUnaryOp
435 :: ByteString
373 :: ByteString
436 -- ^ operator
374 -- ^ operator
437− -> Expression tables grouping params ('Required ty0)
375+ -> Expression relations grouping params (ty0)
438− -> Expression tables grouping params ('Required ty1)
376+ -> Expression relations grouping params (ty1)
439unsafeUnaryOp op x = UnsafeExpression $ parenthesized $
377unsafeUnaryOp op x = UnsafeExpression $ parenthesized $
440 op <+> renderExpression x
378 op <+> renderExpression x
441
379
@@ -444,13 +382,13 @@ unsafeUnaryOp op x = UnsafeExpression $ parenthesized $
444unsafeFunction
382unsafeFunction
445 :: ByteString
383 :: ByteString
446 -- ^ function
384 -- ^ function
447− -> Expression tables grouping params ('Required xty)
385+ -> Expression relations grouping params (xty)
448− -> Expression tables grouping params ('Required yty)
386+ -> Expression relations grouping params (yty)
449unsafeFunction fun x = UnsafeExpression $
387unsafeFunction fun x = UnsafeExpression $
450 fun <> parenthesized (renderExpression x)
388 fun <> parenthesized (renderExpression x)
451
389
452instance PGNum ty
390instance PGNum ty
453− => Num (Expression tables grouping params ('Required (nullity ty))) where
391+ => Num (Expression relations grouping params (nullity ty)) where
454 (+) = unsafeBinaryOp "+"
392 (+) = unsafeBinaryOp "+"
455 (-) = unsafeBinaryOp "-"
393 (-) = unsafeBinaryOp "-"
456 (*) = unsafeBinaryOp "*"
394 (*) = unsafeBinaryOp "*"
@@ -462,12 +400,12 @@ instance PGNum ty
462 . show
400 . show
463
401
464instance (PGNum ty, PGFloating ty) => Fractional
402instance (PGNum ty, PGFloating ty) => Fractional
465− (Expression tables grouping params ('Required (nullity ty))) where
403+ (Expression relations grouping params (nullity ty)) where
466 (/) = unsafeBinaryOp "/"
404 (/) = unsafeBinaryOp "/"
467 fromRational x = fromInteger (numerator x) / fromInteger (denominator x)
405 fromRational x = fromInteger (numerator x) / fromInteger (denominator x)
468
406
469instance (PGNum ty, PGFloating ty) => Floating
407instance (PGNum ty, PGFloating ty) => Floating
470− (Expression tables grouping params ('Required (nullity ty))) where
408+ (Expression relations grouping params (nullity ty)) where
471 pi = UnsafeExpression "pi()"
409 pi = UnsafeExpression "pi()"
472 exp = unsafeFunction "exp"
410 exp = unsafeFunction "exp"
473 log = unsafeFunction "ln"
411 log = unsafeFunction "ln"
@@ -488,15 +426,20 @@ instance (PGNum ty, PGFloating ty) => Floating
488 acosh x = log (x + sqrt (x*x - 1))
426 acosh x = log (x + sqrt (x*x - 1))
489 atanh x = log ((1 + x) / (1 - x)) / 2
427 atanh x = log ((1 + x) / (1 - x)) / 2
490
428
491−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ atan2_ pi 2
429+-- | >>> :{
430+-- let
431+-- expression :: Expression relations grouping params (nullity 'PGfloat4)
432+-- expression = atan2_ pi 2
433+-- in renderExpression expression
434+-- :}
492-- "atan2(pi(), 2)"
435-- "atan2(pi(), 2)"
493atan2_
436atan2_
494 :: PGFloating float
437 :: PGFloating float
495− => Expression tables grouping params ('Required (nullity float))
438+ => Expression relations grouping params (nullity float)
496 -- ^ numerator
439 -- ^ numerator
497− -> Expression tables grouping params ('Required (nullity float))
440+ -> Expression relations grouping params (nullity float)
498 -- ^ denominator
441 -- ^ denominator
499− -> Expression tables grouping params ('Required (nullity float))
442+ -> Expression relations grouping params (nullity float)
500atan2_ y x = UnsafeExpression $
443atan2_ y x = UnsafeExpression $
501 "atan2(" <> renderExpression y <> ", " <> renderExpression x <> ")"
444 "atan2(" <> renderExpression y <> ", " <> renderExpression x <> ")"
502
445
@@ -507,115 +450,147 @@ atan2_ y x = UnsafeExpression $
507-- | >>> renderExpression $ true & cast int4
450-- | >>> renderExpression $ true & cast int4
508-- "(TRUE :: int4)"
451-- "(TRUE :: int4)"
509cast
452cast
510− :: TypeExpression ('Required ('Null ty1))
453+ :: TypeExpression ('NoDef :=> 'Null ty1)
511 -- ^ type to cast as
454 -- ^ type to cast as
512− -> Expression tables grouping params ('Required (nullity ty0))
455+ -> Expression relations grouping params (nullity ty0)
513 -- ^ value to convert
456 -- ^ value to convert
514− -> Expression tables grouping params ('Required (nullity ty1))
457+ -> Expression relations grouping params (nullity ty1)
515cast ty x = UnsafeExpression $ parenthesized $
458cast ty x = UnsafeExpression $ parenthesized $
516 renderExpression x <+> "::" <+> renderTypeExpression ty
459 renderExpression x <+> "::" <+> renderTypeExpression ty
517
460
518-- | integer division, truncates the result
461-- | integer division, truncates the result
519--
462--
520−-- >>> renderExpression @_ @_ @_ @(_(_ 'PGint2)) $ 5 `quot_` 2
463+-- >>> :{
464+-- let
465+-- expression :: Expression relations grouping params (nullity 'PGint2)
466+-- expression = 5 `quot_` 2
467+-- in renderExpression expression
468+-- :}
521-- "(5 / 2)"
469-- "(5 / 2)"
522quot_
470quot_
523 :: PGIntegral int
471 :: PGIntegral int
524− => Expression tables grouping params ('Required (nullity int))
472+ => Expression relations grouping params (nullity int)
525 -- ^ numerator
473 -- ^ numerator
526− -> Expression tables grouping params ('Required (nullity int))
474+ -> Expression relations grouping params (nullity int)
527 -- ^ denominator
475 -- ^ denominator
528− -> Expression tables grouping params ('Required (nullity int))
476+ -> Expression relations grouping params (nullity int)
529quot_ = unsafeBinaryOp "/"
477quot_ = unsafeBinaryOp "/"
530
478
531-- | remainder upon integer division
479-- | remainder upon integer division
532--
480--
533−-- >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ 5 `rem_` 2
481+-- >>> :{
482+-- let
483+-- expression :: Expression relations grouping params (nullity 'PGint2)
484+-- expression = 5 `rem_` 2
485+-- in renderExpression expression
486+-- :}
534-- "(5 % 2)"
487-- "(5 % 2)"
535rem_
488rem_
536 :: PGIntegral int
489 :: PGIntegral int
537− => Expression tables grouping params ('Required (nullity int))
490+ => Expression relations grouping params (nullity int)
538 -- ^ numerator
491 -- ^ numerator
539− -> Expression tables grouping params ('Required (nullity int))
492+ -> Expression relations grouping params (nullity int)
540 -- ^ denominator
493 -- ^ denominator
541− -> Expression tables grouping params ('Required (nullity int))
494+ -> Expression relations grouping params (nullity int)
542rem_ = unsafeBinaryOp "%"
495rem_ = unsafeBinaryOp "%"
543
496
544−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ trunc pi
497+-- | >>> :{
498+-- let
499+-- expression :: Expression relations grouping params (nullity 'PGfloat4)
500+-- expression = trunc pi
501+-- in renderExpression expression
502+-- :}
545-- "trunc(pi())"
503-- "trunc(pi())"
546trunc
504trunc
547 :: PGFloating frac
505 :: PGFloating frac
548− => Expression tables grouping params ('Required (nullity frac))
506+ => Expression relations grouping params (nullity frac)
549 -- ^ fractional number
507 -- ^ fractional number
550− -> Expression tables grouping params ('Required (nullity frac))
508+ -> Expression relations grouping params (nullity frac)
551trunc = unsafeFunction "trunc"
509trunc = unsafeFunction "trunc"
552
510
553−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ round_ pi
511+-- | >>> :{
512+-- let
513+-- expression :: Expression relations grouping params (nullity 'PGfloat4)
514+-- expression = round_ pi
515+-- in renderExpression expression
516+-- :}
554-- "round(pi())"
517-- "round(pi())"
555round_
518round_
556 :: PGFloating frac
519 :: PGFloating frac
557− => Expression tables grouping params ('Required (nullity frac))
520+ => Expression relations grouping params (nullity frac)
558 -- ^ fractional number
521 -- ^ fractional number
559− -> Expression tables grouping params ('Required (nullity frac))
522+ -> Expression relations grouping params (nullity frac)
560round_ = unsafeFunction "round"
523round_ = unsafeFunction "round"
561
524
562−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ ceiling_ pi
525+-- | >>> :{
526+-- let
527+-- expression :: Expression relations grouping params (nullity 'PGfloat4)
528+-- expression = ceiling_ pi
529+-- in renderExpression expression
530+-- :}
563-- "ceiling(pi())"
531-- "ceiling(pi())"
564ceiling_
532ceiling_
565 :: PGFloating frac
533 :: PGFloating frac
566− => Expression tables grouping params ('Required (nullity frac))
534+ => Expression relations grouping params (nullity frac)
567 -- ^ fractional number
535 -- ^ fractional number
568− -> Expression tables grouping params ('Required (nullity frac))
536+ -> Expression relations grouping params (nullity frac)
569ceiling_ = unsafeFunction "ceiling"
537ceiling_ = unsafeFunction "ceiling"
570
538
571-- | A `Condition` is a boolean valued `Expression`. While SQL allows
539-- | A `Condition` is a boolean valued `Expression`. While SQL allows
572−-- conditions to have @NULL@, squeal instead chooses to disallow @NULL@,
540+-- conditions to have @NULL@, Squeal instead chooses to disallow @NULL@,
573-- forcing one to handle the case of @NULL@ explicitly to produce
541-- forcing one to handle the case of @NULL@ explicitly to produce
574-- a `Condition`.
542-- a `Condition`.
575−type Condition tables grouping params =
543+type Condition relations grouping params =
576− Expression tables grouping params ('Required ('NotNull 'PGbool))
544+ Expression relations grouping params ('NotNull 'PGbool)
577
545
578-- | >>> renderExpression true
546-- | >>> renderExpression true
579-- "TRUE"
547-- "TRUE"
580−true :: Condition tables grouping params
548+true :: Condition relations grouping params
581true = UnsafeExpression "TRUE"
549true = UnsafeExpression "TRUE"
582
550
583-- | >>> renderExpression false
551-- | >>> renderExpression false
584-- "FALSE"
552-- "FALSE"
585−false :: Condition tables grouping params
553+false :: Condition relations grouping params
586false = UnsafeExpression "FALSE"
554false = UnsafeExpression "FALSE"
587
555
588-- | >>> renderExpression $ not_ true
556-- | >>> renderExpression $ not_ true
589-- "(NOT TRUE)"
557-- "(NOT TRUE)"
590not_
558not_
591− :: Condition tables grouping params
559+ :: Condition relations grouping params
592− -> Condition tables grouping params
560+ -> Condition relations grouping params
593not_ = unsafeUnaryOp "NOT"
561not_ = unsafeUnaryOp "NOT"
594
562
595-- | >>> renderExpression $ true .&& false
563-- | >>> renderExpression $ true .&& false
596-- "(TRUE AND FALSE)"
564-- "(TRUE AND FALSE)"
597(.&&)
565(.&&)
598− :: Condition tables grouping params
566+ :: Condition relations grouping params
599− -> Condition tables grouping params
567+ -> Condition relations grouping params
600− -> Condition tables grouping params
568+ -> Condition relations grouping params
601(.&&) = unsafeBinaryOp "AND"
569(.&&) = unsafeBinaryOp "AND"
602
570
603-- | >>> renderExpression $ true .|| false
571-- | >>> renderExpression $ true .|| false
604-- "(TRUE OR FALSE)"
572-- "(TRUE OR FALSE)"
605(.||)
573(.||)
606− :: Condition tables grouping params
574+ :: Condition relations grouping params
607− -> Condition tables grouping params
575+ -> Condition relations grouping params
608− -> Condition tables grouping params
576+ -> Condition relations grouping params
609(.||) = unsafeBinaryOp "OR"
577(.||) = unsafeBinaryOp "OR"
610
578
579+-- | >>> :{
580+-- let
581+-- expression :: Expression relations grouping params (nullity 'PGint2)
611−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ caseWhenThenElse [(true, 1), (false, 2)] 3
582+-- expression = caseWhenThenElse [(true, 1), (false, 2)] 3
583+-- in renderExpression expression
584+-- :}
612-- "CASE WHEN TRUE THEN 1 WHEN FALSE THEN 2 ELSE 3 END"
585-- "CASE WHEN TRUE THEN 1 WHEN FALSE THEN 2 ELSE 3 END"
613caseWhenThenElse
586caseWhenThenElse
614− :: [ ( Condition tables grouping params
587+ :: [ ( Condition relations grouping params
615− , Expression tables grouping params ('Required ty)
588+ , Expression relations grouping params (ty)
616 ) ]
589 ) ]
590+ -- ^ whens and thens
617− -> Expression tables grouping params ('Required ty)
591+ -> Expression relations grouping params (ty)
592+ -- ^ else
618− -> Expression tables grouping params ('Required ty)
593+ -> Expression relations grouping params (ty)
619caseWhenThenElse whenThens else_ = UnsafeExpression $ mconcat
594caseWhenThenElse whenThens else_ = UnsafeExpression $ mconcat
620 [ "CASE"
595 [ "CASE"
621 , mconcat
596 , mconcat
@@ -629,13 +604,18 @@ caseWhenThenElse whenThens else_ = UnsafeExpression $ mconcat
629 , " END"
604 , " END"
630 ]
605 ]
631
606
607+-- | >>> :{
608+-- let
609+-- expression :: Expression relations grouping params (nullity 'PGint2)
632−-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ ifThenElse true 1 0
610+-- expression = ifThenElse true 1 0
611+-- in renderExpression expression
612+-- :}
633-- "CASE WHEN TRUE THEN 1 ELSE 0 END"
613-- "CASE WHEN TRUE THEN 1 ELSE 0 END"
634ifThenElse
614ifThenElse
635− :: Condition tables grouping params
615+ :: Condition relations grouping params
636− -> Expression tables grouping params ('Required ty)
616+ -> Expression relations grouping params (ty) -- ^ then
637− -> Expression tables grouping params ('Required ty)
617+ -> Expression relations grouping params (ty) -- ^ else
638− -> Expression tables grouping params ('Required ty)
618+ -> Expression relations grouping params (ty)
639ifThenElse if_ then_ else_ = caseWhenThenElse [(if_,then_)] else_
619ifThenElse if_ then_ else_ = caseWhenThenElse [(if_,then_)] else_
640
620
641-- | Comparison operations like `.==`, `./=`, `.>`, `.>=`, `.<` and `.<=`
621-- | Comparison operations like `.==`, `./=`, `.>`, `.>=`, `.<` and `.<=`
@@ -644,85 +624,85 @@ ifThenElse if_ then_ else_ = caseWhenThenElse [(if_,then_)] else_
644-- >>> renderExpression $ unNull true .== null_
624-- >>> renderExpression $ unNull true .== null_
645-- "(TRUE = NULL)"
625-- "(TRUE = NULL)"
646(.==)
626(.==)
647− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
627+ :: Expression relations grouping params (nullity ty) -- ^ lhs
648− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
628+ -> Expression relations grouping params (nullity ty) -- ^ rhs
649− -> Expression tables grouping params ('Required (nullity 'PGbool))
629+ -> Expression relations grouping params (nullity 'PGbool)
650(.==) = unsafeBinaryOp "="
630(.==) = unsafeBinaryOp "="
651infix 4 .==
631infix 4 .==
652
632
653-- | >>> renderExpression $ unNull true ./= null_
633-- | >>> renderExpression $ unNull true ./= null_
654-- "(TRUE <> NULL)"
634-- "(TRUE <> NULL)"
655(./=)
635(./=)
656− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
636+ :: Expression relations grouping params (nullity ty) -- ^ lhs
657− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
637+ -> Expression relations grouping params (nullity ty) -- ^ rhs
658− -> Expression tables grouping params ('Required (nullity 'PGbool))
638+ -> Expression relations grouping params (nullity 'PGbool)
659(./=) = unsafeBinaryOp "<>"
639(./=) = unsafeBinaryOp "<>"
660infix 4 ./=
640infix 4 ./=
661
641
662-- | >>> renderExpression $ unNull true .>= null_
642-- | >>> renderExpression $ unNull true .>= null_
663-- "(TRUE >= NULL)"
643-- "(TRUE >= NULL)"
664(.>=)
644(.>=)
665− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
645+ :: Expression relations grouping params (nullity ty) -- ^ lhs
666− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
646+ -> Expression relations grouping params (nullity ty) -- ^ rhs
667− -> Expression tables grouping params ('Required (nullity 'PGbool))
647+ -> Expression relations grouping params (nullity 'PGbool)
668(.>=) = unsafeBinaryOp ">="
648(.>=) = unsafeBinaryOp ">="
669infix 4 .>=
649infix 4 .>=
670
650
671-- | >>> renderExpression $ unNull true .< null_
651-- | >>> renderExpression $ unNull true .< null_
672-- "(TRUE < NULL)"
652-- "(TRUE < NULL)"
673(.<)
653(.<)
674− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
654+ :: Expression relations grouping params (nullity ty) -- ^ lhs
675− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
655+ -> Expression relations grouping params (nullity ty) -- ^ rhs
676− -> Expression tables grouping params ('Required (nullity 'PGbool))
656+ -> Expression relations grouping params (nullity 'PGbool)
677(.<) = unsafeBinaryOp "<"
657(.<) = unsafeBinaryOp "<"
678infix 4 .<
658infix 4 .<
679
659
680-- | >>> renderExpression $ unNull true .<= null_
660-- | >>> renderExpression $ unNull true .<= null_
681-- "(TRUE <= NULL)"
661-- "(TRUE <= NULL)"
682(.<=)
662(.<=)
683− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
663+ :: Expression relations grouping params (nullity ty) -- ^ lhs
684− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
664+ -> Expression relations grouping params (nullity ty) -- ^ rhs
685− -> Expression tables grouping params ('Required (nullity 'PGbool))
665+ -> Expression relations grouping params (nullity 'PGbool)
686(.<=) = unsafeBinaryOp "<="
666(.<=) = unsafeBinaryOp "<="
687infix 4 .<=
667infix 4 .<=
688
668
689-- | >>> renderExpression $ unNull true .> null_
669-- | >>> renderExpression $ unNull true .> null_
690-- "(TRUE > NULL)"
670-- "(TRUE > NULL)"
691(.>)
671(.>)
692− :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
672+ :: Expression relations grouping params (nullity ty) -- ^ lhs
693− -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
673+ -> Expression relations grouping params (nullity ty) -- ^ rhs
694− -> Expression tables grouping params ('Required (nullity 'PGbool))
674+ -> Expression relations grouping params (nullity 'PGbool)
695(.>) = unsafeBinaryOp ">"
675(.>) = unsafeBinaryOp ">"
696infix 4 .>
676infix 4 .>
697
677
698−-- | >>> renderExpression $ currentDate
678+-- | >>> renderExpression currentDate
699-- "CURRENT_DATE"
679-- "CURRENT_DATE"
700currentDate
680currentDate
701− :: Expression tables grouping params ('Required (nullity 'PGdate))
681+ :: Expression relations grouping params (nullity 'PGdate)
702currentDate = UnsafeExpression "CURRENT_DATE"
682currentDate = UnsafeExpression "CURRENT_DATE"
703
683
704−-- | >>> renderExpression $ currentTime
684+-- | >>> renderExpression currentTime
705-- "CURRENT_TIME"
685-- "CURRENT_TIME"
706currentTime
686currentTime
707− :: Expression tables grouping params ('Required (nullity 'PGtimetz))
687+ :: Expression relations grouping params (nullity 'PGtimetz)
708currentTime = UnsafeExpression "CURRENT_TIME"
688currentTime = UnsafeExpression "CURRENT_TIME"
709
689
710−-- | >>> renderExpression $ currentTimestamp
690+-- | >>> renderExpression currentTimestamp
711-- "CURRENT_TIMESTAMP"
691-- "CURRENT_TIMESTAMP"
712currentTimestamp
692currentTimestamp
713− :: Expression tables grouping params ('Required (nullity 'PGtimestamptz))
693+ :: Expression relations grouping params (nullity 'PGtimestamptz)
714currentTimestamp = UnsafeExpression "CURRENT_TIMESTAMP"
694currentTimestamp = UnsafeExpression "CURRENT_TIMESTAMP"
715
695
716−-- | >>> renderExpression $ localTime
696+-- | >>> renderExpression localTime
717-- "LOCALTIME"
697-- "LOCALTIME"
718localTime
698localTime
719− :: Expression tables grouping params ('Required (nullity 'PGtime))
699+ :: Expression relations grouping params (nullity 'PGtime)
720localTime = UnsafeExpression "LOCALTIME"
700localTime = UnsafeExpression "LOCALTIME"
721
701
722−-- | >>> renderExpression $ localTimestamp
702+-- | >>> renderExpression localTimestamp
723-- "LOCALTIMESTAMP"
703-- "LOCALTIMESTAMP"
724localTimestamp
704localTimestamp
725− :: Expression tables grouping params ('Required (nullity 'PGtimestamp))
705+ :: Expression relations grouping params (nullity 'PGtimestamp)
726localTimestamp = UnsafeExpression "LOCALTIMESTAMP"
706localTimestamp = UnsafeExpression "LOCALTIMESTAMP"
727
707
728{-----------------------------------------
708{-----------------------------------------
@@ -730,7 +710,7 @@ text
730-----------------------------------------}
710-----------------------------------------}
731
711
732instance IsString
712instance IsString
733− (Expression tables grouping params ('Required (nullity 'PGtext))) where
713+ (Expression relations grouping params (nullity 'PGtext)) where
734 fromString str = UnsafeExpression $
714 fromString str = UnsafeExpression $
735 "E\'" <> fromString (escape =<< str) <> "\'"
715 "E\'" <> fromString (escape =<< str) <> "\'"
736 where
716 where
@@ -746,32 +726,32 @@ instance IsString
746 c -> [c]
726 c -> [c]
747
727
748instance Monoid
728instance Monoid
749− (Expression tables grouping params ('Required (nullity 'PGtext))) where
729+ (Expression relations grouping params (nullity 'PGtext)) where
750 mempty = fromString ""
730 mempty = fromString ""
751 mappend = unsafeBinaryOp "||"
731 mappend = unsafeBinaryOp "||"
752
732
753-- | >>> renderExpression $ lower "ARRRGGG"
733-- | >>> renderExpression $ lower "ARRRGGG"
754-- "lower(E'ARRRGGG')"
734-- "lower(E'ARRRGGG')"
755lower
735lower
756− :: Expression tables grouping params ('Required (nullity 'PGtext))
736+ :: Expression relations grouping params (nullity 'PGtext)
757 -- ^ string to lower case
737 -- ^ string to lower case
758− -> Expression tables grouping params ('Required (nullity 'PGtext))
738+ -> Expression relations grouping params (nullity 'PGtext)
759lower = unsafeFunction "lower"
739lower = unsafeFunction "lower"
760
740
761-- | >>> renderExpression $ upper "eeee"
741-- | >>> renderExpression $ upper "eeee"
762-- "upper(E'eeee')"
742-- "upper(E'eeee')"
763upper
743upper
764− :: Expression tables grouping params ('Required (nullity 'PGtext))
744+ :: Expression relations grouping params (nullity 'PGtext)
765 -- ^ string to upper case
745 -- ^ string to upper case
766− -> Expression tables grouping params ('Required (nullity 'PGtext))
746+ -> Expression relations grouping params (nullity 'PGtext)
767upper = unsafeFunction "upper"
747upper = unsafeFunction "upper"
768
748
769-- | >>> renderExpression $ charLength "four"
749-- | >>> renderExpression $ charLength "four"
770-- "char_length(E'four')"
750-- "char_length(E'four')"
771charLength
751charLength
772− :: Expression tables grouping params ('Required (nullity 'PGtext))
752+ :: Expression relations grouping params (nullity 'PGtext)
773 -- ^ string to measure
753 -- ^ string to measure
774− -> Expression tables grouping params ('Required (nullity 'PGint4))
754+ -> Expression relations grouping params (nullity 'PGint4)
775charLength = unsafeFunction "char_length"
755charLength = unsafeFunction "char_length"
776
756
777-- | The `like` expression returns true if the @string@ matches
757-- | The `like` expression returns true if the @string@ matches
@@ -784,11 +764,11 @@ charLength = unsafeFunction "char_length"
784-- >>> renderExpression $ "abc" `like` "a%"
764-- >>> renderExpression $ "abc" `like` "a%"
785-- "(E'abc' LIKE E'a%')"
765-- "(E'abc' LIKE E'a%')"
786like
766like
787− :: Expression tables grouping params ('Required (nullity 'PGtext))
767+ :: Expression relations grouping params (nullity 'PGtext)
788 -- ^ string
768 -- ^ string
789− -> Expression tables grouping params ('Required (nullity 'PGtext))
769+ -> Expression relations grouping params (nullity 'PGtext)
790 -- ^ pattern
770 -- ^ pattern
791− -> Expression tables grouping params ('Required (nullity 'PGbool))
771+ -> Expression relations grouping params (nullity 'PGbool)
792like = unsafeBinaryOp "LIKE"
772like = unsafeBinaryOp "LIKE"
793
773
794{-----------------------------------------
774{-----------------------------------------
@@ -798,44 +778,54 @@ aggregation
798-- | escape hatch to define aggregate functions
778-- | escape hatch to define aggregate functions
799unsafeAggregate
779unsafeAggregate
800 :: ByteString -- ^ aggregate function
780 :: ByteString -- ^ aggregate function
801− -> Expression tables 'Ungrouped params ('Required xty)
781+ -> Expression relations 'Ungrouped params (xty)
802− -> Expression tables ('Grouped bys) params ('Required yty)
782+ -> Expression relations ('Grouped bys) params (yty)
803unsafeAggregate fun x = UnsafeExpression $ mconcat
783unsafeAggregate fun x = UnsafeExpression $ mconcat
804 [fun, "(", renderExpression x, ")"]
784 [fun, "(", renderExpression x, ")"]
805
785
806-- | escape hatch to define aggregate functions over distinct values
786-- | escape hatch to define aggregate functions over distinct values
807unsafeAggregateDistinct
787unsafeAggregateDistinct
808 :: ByteString -- ^ aggregate function
788 :: ByteString -- ^ aggregate function
809− -> Expression tables 'Ungrouped params ('Required xty)
789+ -> Expression relations 'Ungrouped params (xty)
810− -> Expression tables ('Grouped bys) params ('Required yty)
790+ -> Expression relations ('Grouped bys) params (yty)
811unsafeAggregateDistinct fun x = UnsafeExpression $ mconcat
791unsafeAggregateDistinct fun x = UnsafeExpression $ mconcat
812 [fun, "(DISTINCT ", renderExpression x, ")"]
792 [fun, "(DISTINCT ", renderExpression x, ")"]
813
793
814−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGnumeric)]] $ sum_ #col
794+-- | >>> :{
795+-- let
796+-- expression :: Expression '[tab ::: '["col" ::: 'Null 'PGnumeric]] ('Grouped bys) params ('Null 'PGnumeric)
797+-- expression = sum_ #col
798+-- in renderExpression expression
799+-- :}
815-- "sum(col)"
800-- "sum(col)"
816sum_
801sum_
817 :: PGNum ty
802 :: PGNum ty
818− => Expression tables 'Ungrouped params ('Required (nullity ty))
803+ => Expression relations 'Ungrouped params (nullity ty)
819 -- ^ what to sum
804 -- ^ what to sum
820− -> Expression tables ('Grouped bys) params ('Required (nullity ty))
805+ -> Expression relations ('Grouped bys) params (nullity ty)
821sum_ = unsafeAggregate "sum"
806sum_ = unsafeAggregate "sum"
822
807
823−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGnumeric)]] $ sumDistinct #col
808+-- | >>> :{
809+-- let
810+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGnumeric]] ('Grouped bys) params (nullity 'PGnumeric)
811+-- expression = sumDistinct #col
812+-- in renderExpression expression
813+-- :}
824-- "sum(DISTINCT col)"
814-- "sum(DISTINCT col)"
825sumDistinct
815sumDistinct
826 :: PGNum ty
816 :: PGNum ty
827− => Expression tables 'Ungrouped params ('Required (nullity ty))
817+ => Expression relations 'Ungrouped params (nullity ty)
828 -- ^ what to sum
818 -- ^ what to sum
829− -> Expression tables ('Grouped bys) params ('Required (nullity ty))
819+ -> Expression relations ('Grouped bys) params (nullity ty)
830sumDistinct = unsafeAggregateDistinct "sum"
820sumDistinct = unsafeAggregateDistinct "sum"
831
821
832-- | A constraint for `PGType`s that you can take averages of and the resulting
822-- | A constraint for `PGType`s that you can take averages of and the resulting
833-- `PGType`.
823-- `PGType`.
834class PGAvg ty avg | ty -> avg where
824class PGAvg ty avg | ty -> avg where
835 avg, avgDistinct
825 avg, avgDistinct
836− :: Expression tables 'Ungrouped params ('Required (nullity ty))
826+ :: Expression relations 'Ungrouped params (nullity ty)
837 -- ^ what to average
827 -- ^ what to average
838− -> Expression tables ('Grouped bys) params ('Required (nullity avg))
828+ -> Expression relations ('Grouped bys) params (nullity avg)
839 avg = unsafeAggregate "avg"
829 avg = unsafeAggregate "avg"
840 avgDistinct = unsafeAggregateDistinct "avg"
830 avgDistinct = unsafeAggregateDistinct "avg"
841instance PGAvg 'PGint2 'PGnumeric
831instance PGAvg 'PGint2 'PGnumeric
@@ -846,72 +836,112 @@ instance PGAvg 'PGfloat4 'PGfloat8
846instance PGAvg 'PGfloat8 'PGfloat8
836instance PGAvg 'PGfloat8 'PGfloat8
847instance PGAvg 'PGinterval 'PGinterval
837instance PGAvg 'PGinterval 'PGinterval
848
838
849−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitAnd #col
839+-- | >>> :{
840+-- let
841+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGint4]] (Grouped bys) params (nullity 'PGint4)
842+-- expression = bitAnd #col
843+-- in renderExpression expression
844+-- :}
850-- "bit_and(col)"
845-- "bit_and(col)"
851bitAnd
846bitAnd
852 :: PGIntegral int
847 :: PGIntegral int
853− => Expression tables 'Ungrouped params ('Required (nullity int))
848+ => Expression relations 'Ungrouped params (nullity int)
854 -- ^ what to aggregate
849 -- ^ what to aggregate
855− -> Expression tables ('Grouped bys) params ('Required (nullity int))
850+ -> Expression relations ('Grouped bys) params (nullity int)
856bitAnd = unsafeAggregate "bit_and"
851bitAnd = unsafeAggregate "bit_and"
857
852
858−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitOr #col
853+-- | >>> :{
854+-- let
855+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGint4]] (Grouped bys) params (nullity 'PGint4)
856+-- expression = bitOr #col
857+-- in renderExpression expression
858+-- :}
859-- "bit_or(col)"
859-- "bit_or(col)"
860bitOr
860bitOr
861 :: PGIntegral int
861 :: PGIntegral int
862− => Expression tables 'Ungrouped params ('Required (nullity int))
862+ => Expression relations 'Ungrouped params (nullity int)
863 -- ^ what to aggregate
863 -- ^ what to aggregate
864− -> Expression tables ('Grouped bys) params ('Required (nullity int))
864+ -> Expression relations ('Grouped bys) params (nullity int)
865bitOr = unsafeAggregate "bit_or"
865bitOr = unsafeAggregate "bit_or"
866
866
867+-- | >>> :{
868+-- let
869+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGint4]] (Grouped bys) params (nullity 'PGint4)
867−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitAndDistinct #col
870+-- expression = bitAndDistinct #col
871+-- in renderExpression expression
872+-- :}
868-- "bit_and(DISTINCT col)"
873-- "bit_and(DISTINCT col)"
869bitAndDistinct
874bitAndDistinct
870 :: PGIntegral int
875 :: PGIntegral int
871− => Expression tables 'Ungrouped params ('Required (nullity int))
876+ => Expression relations 'Ungrouped params (nullity int)
872 -- ^ what to aggregate
877 -- ^ what to aggregate
873− -> Expression tables ('Grouped bys) params ('Required (nullity int))
878+ -> Expression relations ('Grouped bys) params (nullity int)
874bitAndDistinct = unsafeAggregateDistinct "bit_and"
879bitAndDistinct = unsafeAggregateDistinct "bit_and"
875
880
881+-- | >>> :{
882+-- let
883+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGint4]] (Grouped bys) params (nullity 'PGint4)
876−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitOrDistinct #col
884+-- expression = bitOrDistinct #col
885+-- in renderExpression expression
886+-- :}
877-- "bit_or(DISTINCT col)"
887-- "bit_or(DISTINCT col)"
878bitOrDistinct
888bitOrDistinct
879 :: PGIntegral int
889 :: PGIntegral int
880− => Expression tables 'Ungrouped params ('Required (nullity int))
890+ => Expression relations 'Ungrouped params (nullity int)
881 -- ^ what to aggregate
891 -- ^ what to aggregate
882− -> Expression tables ('Grouped bys) params ('Required (nullity int))
892+ -> Expression relations ('Grouped bys) params (nullity int)
883bitOrDistinct = unsafeAggregateDistinct "bit_or"
893bitOrDistinct = unsafeAggregateDistinct "bit_or"
884
894
885−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolAnd #col
895+-- | >>> :{
896+-- let
897+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
898+-- expression = boolAnd #col
899+-- in renderExpression expression
900+-- :}
886-- "bool_and(col)"
901-- "bool_and(col)"
887boolAnd
902boolAnd
888− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
903+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
889 -- ^ what to aggregate
904 -- ^ what to aggregate
890− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
905+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
891boolAnd = unsafeAggregate "bool_and"
906boolAnd = unsafeAggregate "bool_and"
892
907
893−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolOr #col
908+-- | >>> :{
909+-- let
910+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
911+-- expression = boolOr #col
912+-- in renderExpression expression
913+-- :}
894-- "bool_or(col)"
914-- "bool_or(col)"
895boolOr
915boolOr
896− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
916+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
897 -- ^ what to aggregate
917 -- ^ what to aggregate
898− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
918+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
899boolOr = unsafeAggregate "bool_or"
919boolOr = unsafeAggregate "bool_or"
900
920
921+-- | >>> :{
922+-- let
923+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
901−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolAndDistinct #col
924+-- expression = boolAndDistinct #col
925+-- in renderExpression expression
926+-- :}
902-- "bool_and(DISTINCT col)"
927-- "bool_and(DISTINCT col)"
903boolAndDistinct
928boolAndDistinct
904− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
929+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
905 -- ^ what to aggregate
930 -- ^ what to aggregate
906− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
931+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
907boolAndDistinct = unsafeAggregateDistinct "bool_and"
932boolAndDistinct = unsafeAggregateDistinct "bool_and"
908
933
934+-- | >>> :{
935+-- let
936+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
909−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolOrDistinct #col
937+-- expression = boolOrDistinct #col
938+-- in renderExpression expression
939+-- :}
910-- "bool_or(DISTINCT col)"
940-- "bool_or(DISTINCT col)"
911boolOrDistinct
941boolOrDistinct
912− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
942+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
913 -- ^ what to aggregate
943 -- ^ what to aggregate
914− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
944+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
915boolOrDistinct = unsafeAggregateDistinct "bool_or"
945boolOrDistinct = unsafeAggregateDistinct "bool_or"
916
946
917-- | A special aggregation that does not require an input
947-- | A special aggregation that does not require an input
@@ -919,50 +949,70 @@ boolOrDistinct = unsafeAggregateDistinct "bool_or"
919-- >>> renderExpression countStar
949-- >>> renderExpression countStar
920-- "count(*)"
950-- "count(*)"
921countStar
951countStar
922− :: Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
952+ :: Expression relations ('Grouped bys) params ('NotNull 'PGint8)
923countStar = UnsafeExpression $ "count(*)"
953countStar = UnsafeExpression $ "count(*)"
924
954
925−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Optional _]] $ count #col
955+-- | >>> :{
956+-- let
957+-- expression :: Expression '[tab ::: '["col" ::: nullity ty]] (Grouped bys) params ('NotNull 'PGint8)
958+-- expression = count #col
959+-- in renderExpression expression
960+-- :}
926-- "count(col)"
961-- "count(col)"
927count
962count
928− :: Expression tables 'Ungrouped params ('Required ty)
963+ :: Expression relations 'Ungrouped params ty
929 -- ^ what to count
964 -- ^ what to count
930− -> Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
965+ -> Expression relations ('Grouped bys) params ('NotNull 'PGint8)
931count = unsafeAggregate "count"
966count = unsafeAggregate "count"
932
967
968+-- | >>> :{
969+-- let
970+-- expression :: Expression '[tab ::: '["col" ::: nullity ty]] (Grouped bys) params ('NotNull 'PGint8)
933−-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required _]] $ countDistinct #col
971+-- expression = countDistinct #col
972+-- in renderExpression expression
973+-- :}
934-- "count(DISTINCT col)"
974-- "count(DISTINCT col)"
935countDistinct
975countDistinct
936− :: Expression tables 'Ungrouped params ('Required ty)
976+ :: Expression relations 'Ungrouped params ty
937 -- ^ what to count
977 -- ^ what to count
938− -> Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
978+ -> Expression relations ('Grouped bys) params ('NotNull 'PGint8)
939countDistinct = unsafeAggregateDistinct "count"
979countDistinct = unsafeAggregateDistinct "count"
940
980
941-- | synonym for `boolAnd`
981-- | synonym for `boolAnd`
942--
982--
943−-- >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ every #col
983+-- >>> :{
984+-- let
985+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
986+-- expression = every #col
987+-- in renderExpression expression
988+-- :}
944-- "every(col)"
989-- "every(col)"
945every
990every
946− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
991+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
947 -- ^ what to aggregate
992 -- ^ what to aggregate
948− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
993+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
949every = unsafeAggregate "every"
994every = unsafeAggregate "every"
950
995
951-- | synonym for `boolAndDistinct`
996-- | synonym for `boolAndDistinct`
952--
997--
998+-- >>> :{
999+-- let
1000+-- expression :: Expression '[tab ::: '["col" ::: nullity 'PGbool]] (Grouped bys) params (nullity 'PGbool)
953−-- >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ everyDistinct #col
1001+-- expression = everyDistinct #col
1002+-- in renderExpression expression
1003+-- :}
954-- "every(DISTINCT col)"
1004-- "every(DISTINCT col)"
955everyDistinct
1005everyDistinct
956− :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
1006+ :: Expression relations 'Ungrouped params (nullity 'PGbool)
957 -- ^ what to aggregate
1007 -- ^ what to aggregate
958− -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
1008+ -> Expression relations ('Grouped bys) params (nullity 'PGbool)
959everyDistinct = unsafeAggregateDistinct "every"
1009everyDistinct = unsafeAggregateDistinct "every"
960
1010
961-- | minimum and maximum aggregation
1011-- | minimum and maximum aggregation
962max_, min_, maxDistinct, minDistinct
1012max_, min_, maxDistinct, minDistinct
963− :: Expression tables 'Ungrouped params ('Required (nullity ty))
1013+ :: Expression relations 'Ungrouped params (nullity ty)
964 -- ^ what to aggregate
1014 -- ^ what to aggregate
965− -> Expression tables ('Grouped bys) params ('Required (nullity ty))
1015+ -> Expression relations ('Grouped bys) params (nullity ty)
966max_ = unsafeAggregate "max"
1016max_ = unsafeAggregate "max"
967min_ = unsafeAggregate "min"
1017min_ = unsafeAggregate "min"
968maxDistinct = unsafeAggregateDistinct "max"
1018maxDistinct = unsafeAggregateDistinct "max"
@@ -972,28 +1022,18 @@ minDistinct = unsafeAggregateDistinct "min"
972tables
1022tables
973-----------------------------------------}
1023-----------------------------------------}
974
1024
975−-- | A `Table` from a schema without its alias with an `IsLabel` instance
1025+-- | A `Table` from a table expression is a way
976-- to call a table reference by its alias.
1026-- to call a table reference by its alias.
977newtype Table
1027newtype Table
978 (schema :: TablesType)
1028 (schema :: TablesType)
979− (columns :: ColumnsType)
1029+ (columns :: RelationType)
980 = UnsafeTable { renderTable :: ByteString }
1030 = UnsafeTable { renderTable :: ByteString }
981 deriving (GHC.Generic,Show,Eq,Ord,NFData)
1031 deriving (GHC.Generic,Show,Eq,Ord,NFData)
982−
1032+instance
983−-- | A `HasTable` constraint indicates a table reference.
1033+ ( Has alias schema table
984−class KnownSymbol table => HasTable table tables columns
1034+ , relation ~ ColumnsToRelation (TableToColumns table)
985− | table tables -> columns where
986− getTable :: Alias table -> Table tables columns
987− getTable table = UnsafeTable $ renderAlias table
988−instance {-# OVERLAPPING #-} KnownSymbol table
989− => HasTable table ((table ::: columns) ': tables) columns
990−instance {-# OVERLAPPABLE #-}
991− (KnownSymbol table, HasTable table schema columns)
992− => HasTable table (table' ': schema) columns
993−
994−instance HasTable table schema columns
995− => IsLabel table (Table schema columns) where
1035+ ) => IsLabel alias (Table schema relation) where
996− fromLabel = getTable (Alias @table)
1036+ fromLabel = UnsafeTable $ renderAlias (Alias @alias)
997
1037
998{-----------------------------------------
1038{-----------------------------------------
999type expressions
1039type expressions
@@ -1005,117 +1045,136 @@ newtype TypeExpression (ty :: ColumnType)
1005 deriving (GHC.Generic,Show,Eq,Ord,NFData)
1045 deriving (GHC.Generic,Show,Eq,Ord,NFData)
1006
1046
1007-- | logical Boolean (true/false)
1047-- | logical Boolean (true/false)
1008−bool :: TypeExpression ('Required ('Null 'PGbool))
1048+bool :: TypeExpression ('NoDef :=> 'Null 'PGbool)
1009bool = UnsafeTypeExpression "bool"
1049bool = UnsafeTypeExpression "bool"
1010-- | signed two-byte integer
1050-- | signed two-byte integer
1011−int2, smallint :: TypeExpression ('Required ('Null 'PGint2))
1051+int2, smallint :: TypeExpression ('NoDef :=> 'Null 'PGint2)
1012int2 = UnsafeTypeExpression "int2"
1052int2 = UnsafeTypeExpression "int2"
1013smallint = UnsafeTypeExpression "smallint"
1053smallint = UnsafeTypeExpression "smallint"
1014-- | signed four-byte integer
1054-- | signed four-byte integer
1015−int4, int, integer :: TypeExpression ('Required ('Null 'PGint4))
1055+int4, int, integer :: TypeExpression ('NoDef :=> 'Null 'PGint4)
1016int4 = UnsafeTypeExpression "int4"
1056int4 = UnsafeTypeExpression "int4"
1017int = UnsafeTypeExpression "int"
1057int = UnsafeTypeExpression "int"
1018integer = UnsafeTypeExpression "integer"
1058integer = UnsafeTypeExpression "integer"
1019-- | signed eight-byte integer
1059-- | signed eight-byte integer
1020−int8, bigint :: TypeExpression ('Required ('Null 'PGint8))
1060+int8, bigint :: TypeExpression ('NoDef :=> 'Null 'PGint8)
1021int8 = UnsafeTypeExpression "int8"
1061int8 = UnsafeTypeExpression "int8"
1022bigint = UnsafeTypeExpression "bigint"
1062bigint = UnsafeTypeExpression "bigint"
1023-- | arbitrary precision numeric type
1063-- | arbitrary precision numeric type
1024−numeric :: TypeExpression ('Required ('Null 'PGnumeric))
1064+numeric :: TypeExpression ('NoDef :=> 'Null 'PGnumeric)
1025numeric = UnsafeTypeExpression "numeric"
1065numeric = UnsafeTypeExpression "numeric"
1026-- | single precision floating-point number (4 bytes)
1066-- | single precision floating-point number (4 bytes)
1027−float4, real :: TypeExpression ('Required ('Null 'PGfloat4))
1067+float4, real :: TypeExpression ('NoDef :=> 'Null 'PGfloat4)
1028float4 = UnsafeTypeExpression "float4"
1068float4 = UnsafeTypeExpression "float4"
1029real = UnsafeTypeExpression "real"
1069real = UnsafeTypeExpression "real"
1030-- | double precision floating-point number (8 bytes)
1070-- | double precision floating-point number (8 bytes)
1031−float8, doublePrecision :: TypeExpression ('Required ('Null 'PGfloat8))
1071+float8, doublePrecision :: TypeExpression ('NoDef :=> 'Null 'PGfloat8)
1032float8 = UnsafeTypeExpression "float8"
1072float8 = UnsafeTypeExpression "float8"
1033doublePrecision = UnsafeTypeExpression "double precision"
1073doublePrecision = UnsafeTypeExpression "double precision"
1034-- | not a true type, but merely a notational convenience for creating
1074-- | not a true type, but merely a notational convenience for creating
1035-- unique identifier columns with type `'PGint2`
1075-- unique identifier columns with type `'PGint2`
1076+serial2, smallserial
1036−serial2, smallserial :: TypeExpression ('Optional ('NotNull 'PGint2))
1077+ :: TypeExpression ('Def :=> 'NotNull 'PGint2)
1037serial2 = UnsafeTypeExpression "serial2"
1078serial2 = UnsafeTypeExpression "serial2"
1038smallserial = UnsafeTypeExpression "smallserial"
1079smallserial = UnsafeTypeExpression "smallserial"
1039-- | not a true type, but merely a notational convenience for creating
1080-- | not a true type, but merely a notational convenience for creating
1040-- unique identifier columns with type `'PGint4`
1081-- unique identifier columns with type `'PGint4`
1082+serial4, serial
1041−serial4, serial :: TypeExpression ('Optional ('NotNull 'PGint4))
1083+ :: TypeExpression ('Def :=> 'NotNull 'PGint4)
1042serial4 = UnsafeTypeExpression "serial4"
1084serial4 = UnsafeTypeExpression "serial4"
1043serial = UnsafeTypeExpression "serial"
1085serial = UnsafeTypeExpression "serial"
1044-- | not a true type, but merely a notational convenience for creating
1086-- | not a true type, but merely a notational convenience for creating
1045-- unique identifier columns with type `'PGint8`
1087-- unique identifier columns with type `'PGint8`
1088+serial8, bigserial
1046−serial8, bigserial :: TypeExpression ('Optional ('NotNull 'PGint8))
1089+ :: TypeExpression ('Def :=> 'NotNull 'PGint8)
1047serial8 = UnsafeTypeExpression "serial8"
1090serial8 = UnsafeTypeExpression "serial8"
1048bigserial = UnsafeTypeExpression "bigserial"
1091bigserial = UnsafeTypeExpression "bigserial"
1049-- | variable-length character string
1092-- | variable-length character string
1050−text :: TypeExpression ('Required ('Null 'PGtext))
1093+text :: TypeExpression ('NoDef :=> 'Null 'PGtext)
1051text = UnsafeTypeExpression "text"
1094text = UnsafeTypeExpression "text"
1052-- | fixed-length character string
1095-- | fixed-length character string
1053char, character
1096char, character
1054 :: (KnownNat n, 1 <= n)
1097 :: (KnownNat n, 1 <= n)
1055 => proxy n
1098 => proxy n
1056− -> TypeExpression ('Required ('Null ('PGchar n)))
1099+ -> TypeExpression ('NoDef :=> 'Null ('PGchar n))
1057char p = UnsafeTypeExpression $ "char(" <> renderNat p <> ")"
1100char p = UnsafeTypeExpression $ "char(" <> renderNat p <> ")"
1058character p = UnsafeTypeExpression $ "character(" <> renderNat p <> ")"
1101character p = UnsafeTypeExpression $ "character(" <> renderNat p <> ")"
1059-- | variable-length character string
1102-- | variable-length character string
1060varchar, characterVarying
1103varchar, characterVarying
1061 :: (KnownNat n, 1 <= n)
1104 :: (KnownNat n, 1 <= n)
1062 => proxy n
1105 => proxy n
1063− -> TypeExpression ('Required ('Null ('PGvarchar n)))
1106+ -> TypeExpression ('NoDef :=> 'Null ('PGvarchar n))
1064varchar p = UnsafeTypeExpression $ "varchar(" <> renderNat p <> ")"
1107varchar p = UnsafeTypeExpression $ "varchar(" <> renderNat p <> ")"
1065characterVarying p = UnsafeTypeExpression $
1108characterVarying p = UnsafeTypeExpression $
1066 "character varying(" <> renderNat p <> ")"
1109 "character varying(" <> renderNat p <> ")"
1067-- | binary data ("byte array")
1110-- | binary data ("byte array")
1068−bytea :: TypeExpression ('Required ('Null 'PGbytea))
1111+bytea :: TypeExpression ('NoDef :=> 'Null 'PGbytea)
1069bytea = UnsafeTypeExpression "bytea"
1112bytea = UnsafeTypeExpression "bytea"
1070-- | date and time (no time zone)
1113-- | date and time (no time zone)
1071−timestamp :: TypeExpression ('Required ('Null 'PGtimestamp))
1114+timestamp :: TypeExpression ('NoDef :=> 'Null 'PGtimestamp)
1072timestamp = UnsafeTypeExpression "timestamp"
1115timestamp = UnsafeTypeExpression "timestamp"
1073-- | date and time, including time zone
1116-- | date and time, including time zone
1074−timestampWithTimeZone :: TypeExpression ('Required ('Null 'PGtimestamptz))
1117+timestampWithTimeZone :: TypeExpression ('NoDef :=> 'Null 'PGtimestamptz)
1075timestampWithTimeZone = UnsafeTypeExpression "timestamp with time zone"
1118timestampWithTimeZone = UnsafeTypeExpression "timestamp with time zone"
1076-- | calendar date (year, month, day)
1119-- | calendar date (year, month, day)
1077−date :: TypeExpression ('Required ('Null 'PGdate))
1120+date :: TypeExpression ('NoDef :=> 'Null 'PGdate)
1078date = UnsafeTypeExpression "date"
1121date = UnsafeTypeExpression "date"
1079-- | time of day (no time zone)
1122-- | time of day (no time zone)
1080−time :: TypeExpression ('Required ('Null 'PGtime))
1123+time :: TypeExpression ('NoDef :=> 'Null 'PGtime)
1081time = UnsafeTypeExpression "time"
1124time = UnsafeTypeExpression "time"
1082-- | time of day, including time zone
1125-- | time of day, including time zone
1083−timeWithTimeZone :: TypeExpression ('Required ('Null 'PGtimetz))
1126+timeWithTimeZone :: TypeExpression ('NoDef :=> 'Null 'PGtimetz)
1084timeWithTimeZone = UnsafeTypeExpression "time with time zone"
1127timeWithTimeZone = UnsafeTypeExpression "time with time zone"
1085-- | time span
1128-- | time span
1086−interval :: TypeExpression ('Required ('Null 'PGinterval))
1129+interval :: TypeExpression ('NoDef :=> 'Null 'PGinterval)
1087interval = UnsafeTypeExpression "interval"
1130interval = UnsafeTypeExpression "interval"
1088-- | universally unique identifier
1131-- | universally unique identifier
1089−uuid :: TypeExpression ('Required ('Null 'PGuuid))
1132+uuid :: TypeExpression ('NoDef :=> 'Null 'PGuuid)
1090uuid = UnsafeTypeExpression "uuid"
1133uuid = UnsafeTypeExpression "uuid"
1091-- | IPv4 or IPv6 host address
1134-- | IPv4 or IPv6 host address
1092−inet :: TypeExpression ('Required ('Null 'PGinet))
1135+inet :: TypeExpression ('NoDef :=> 'Null 'PGinet)
1093inet = UnsafeTypeExpression "inet"
1136inet = UnsafeTypeExpression "inet"
1094-- | textual JSON data
1137-- | textual JSON data
1095−json :: TypeExpression ('Required ('Null 'PGjson))
1138+json :: TypeExpression ('NoDef :=> 'Null 'PGjson)
1096json = UnsafeTypeExpression "json"
1139json = UnsafeTypeExpression "json"
1097-- | binary JSON data, decomposed
1140-- | binary JSON data, decomposed
1098−jsonb :: TypeExpression ('Required ('Null 'PGjsonb))
1141+jsonb :: TypeExpression ('NoDef :=> 'Null 'PGjsonb)
1099jsonb = UnsafeTypeExpression "jsonb"
1142jsonb = UnsafeTypeExpression "jsonb"
1143+-- | variable length array
1144+vararray
1145+ :: TypeExpression ('NoDef :=> 'Null pg)
1146+ -> TypeExpression ('NoDef :=> 'Null ('PGvararray pg))
1147+vararray ty = UnsafeTypeExpression $ renderTypeExpression ty <> "[]"
1148+-- | fixed length array
1149+--
1150+-- >>> renderTypeExpression (fixarray (Proxy @2) json)
1151+-- "json[2]"
1152+fixarray
1153+ :: KnownNat n
1154+ => proxy n
1155+ -> TypeExpression ('NoDef :=> 'Null pg)
1156+ -> TypeExpression ('NoDef :=> 'Null ('PGfixarray n pg))
1157+fixarray p ty = UnsafeTypeExpression $
1158+ renderTypeExpression ty <> "[" <> renderNat p <> "]"
1100
1159
1101-- | used in `createTable` commands as a column constraint to ensure
1160-- | used in `createTable` commands as a column constraint to ensure
1102-- @NULL@ is not present
1161-- @NULL@ is not present
1103notNull
1162notNull
1104− :: TypeExpression (optionality ('Null ty))
1163+ :: TypeExpression (def :=> 'Null ty)
1105− -> TypeExpression (optionality ('NotNull ty))
1164+ -> TypeExpression (def :=> 'NotNull ty)
1106notNull ty = UnsafeTypeExpression $ renderTypeExpression ty <+> "NOT NULL"
1165notNull ty = UnsafeTypeExpression $ renderTypeExpression ty <+> "NOT NULL"
1107
1166
1108-- | used in `createTable` commands as a column constraint to give a default
1167-- | used in `createTable` commands as a column constraint to give a default
1109default_
1168default_
1110− :: Expression '[] 'Ungrouped '[] ('Required ty)
1169+ :: Expression '[] 'Ungrouped '[] ty
1111− -> TypeExpression ('Required ty)
1170+ -> TypeExpression ('NoDef :=> ty)
1112− -> TypeExpression ('Optional ty)
1171+ -> TypeExpression ('Def :=> ty)
1113default_ x ty = UnsafeTypeExpression $
1172default_ x ty = UnsafeTypeExpression $
1114 renderTypeExpression ty <+> "DEFAULT" <+> renderExpression x
1173 renderTypeExpression ty <+> "DEFAULT" <+> renderExpression x
1115
1174
1116-- | `pgtype` is a demoted version of a `PGType`
1175-- | `pgtype` is a demoted version of a `PGType`
1117class PGTyped (ty :: PGType) where
1176class PGTyped (ty :: PGType) where
1118− pgtype :: TypeExpression ('Required ('Null ty))
1177+ pgtype :: TypeExpression ('NoDef :=> 'Null ty)
1119instance PGTyped 'PGbool where pgtype = bool
1178instance PGTyped 'PGbool where pgtype = bool
1120instance PGTyped 'PGint2 where pgtype = int2
1179instance PGTyped 'PGint2 where pgtype = int2
1121instance PGTyped 'PGint4 where pgtype = int4
1180instance PGTyped 'PGint4 where pgtype = int4
@@ -1138,3 +1197,7 @@ instance PGTyped 'PGinterval where pgtype = interval
1138instance PGTyped 'PGuuid where pgtype = uuid
1197instance PGTyped 'PGuuid where pgtype = uuid
1139instance PGTyped 'PGjson where pgtype = json
1198instance PGTyped 'PGjson where pgtype = json
1140instance PGTyped 'PGjsonb where pgtype = jsonb
1199instance PGTyped 'PGjsonb where pgtype = jsonb
1200+instance PGTyped ty => PGTyped ('PGvararray ty) where
1201+ pgtype = vararray (pgtype @ty)
1202+instance (KnownNat n, PGTyped ty) => PGTyped ('PGfixarray n ty) where
1203+ pgtype = fixarray (Proxy @n) (pgtype @ty)
squeal-postgresql/src/Squeal/PostgreSQL/Manipulation.hs+332−188
@@ -12,6 +12,7 @@ Squeal data manipulation language.
12 DataKinds
12 DataKinds
13 , DeriveDataTypeable
13 , DeriveDataTypeable
14 , DeriveGeneric
14 , DeriveGeneric
15+ , FlexibleContexts
15 , GADTs
16 , GADTs
16 , GeneralizedNewtypeDeriving
17 , GeneralizedNewtypeDeriving
17 , KindSignatures
18 , KindSignatures
@@ -27,90 +28,72 @@ module Squeal.PostgreSQL.Manipulation
27 ( -- * Manipulation
28 ( -- * Manipulation
28 Manipulation (UnsafeManipulation, renderManipulation)
29 Manipulation (UnsafeManipulation, renderManipulation)
29 , queryStatement
30 , queryStatement
30− -- * Insert
31+ , ColumnValue (..)
31− , insertInto
32− , ValuesClause (Values, ValuesQuery)
33− , renderValuesClause
34 , ReturningClause (ReturningStar, Returning)
32 , ReturningClause (ReturningStar, Returning)
35− , renderReturningClause
36 , ConflictClause (OnConflictDoRaise, OnConflictDoNothing, OnConflictDoUpdate)
33 , ConflictClause (OnConflictDoRaise, OnConflictDoNothing, OnConflictDoUpdate)
34+ -- * Insert
35+ , insertRows
36+ , insertRow
37+ , insertRows_
38+ , insertRow_
39+ , insertQuery
40+ , insertQuery_
41+ , renderReturningClause
37 , renderConflictClause
42 , renderConflictClause
38 -- * Update
43 -- * Update
39 , update
44 , update
40− , UpdateExpression (Same, Set)
45+ , update_
41− , renderUpdateExpression
46+ -- * Delete
42 , deleteFrom
47 , deleteFrom
48+ , deleteFrom_
49+ -- * With
50+ , with
43 ) where
51 ) where
44
52
45import Control.DeepSeq
53import Control.DeepSeq
46−import Data.ByteString
54+import Data.ByteString hiding (foldr)
47import Data.Monoid
55import Data.Monoid
48
56
57+import qualified Data.ByteString as ByteString
49import qualified Generics.SOP as SOP
58import qualified Generics.SOP as SOP
50import qualified GHC.Generics as GHC
59import qualified GHC.Generics as GHC
51
60
52import Squeal.PostgreSQL.Expression
61import Squeal.PostgreSQL.Expression
53−import Squeal.PostgreSQL.Prettyprint
62+import Squeal.PostgreSQL.Render
54import Squeal.PostgreSQL.Query
63import Squeal.PostgreSQL.Query
55import Squeal.PostgreSQL.Schema
64import Squeal.PostgreSQL.Schema
56
65
57−-- | A `Manipulation` is a statement which may modify data in the database,
58−-- but does not alter the schema. Examples are `insertInto`, `update` and
59−-- `deleteFrom`. A `Query` is also considered a `Manipulation` even though
60−-- it does not modify data.
61−newtype Manipulation
62− (schema :: TablesType)
63− (params :: [ColumnType])
64− (columns :: ColumnsType)
65− = UnsafeManipulation { renderManipulation :: ByteString }
66− deriving (GHC.Generic,Show,Eq,Ord,NFData)
67−
68−-- | Convert a `Query` into a `Manipulation`.
69−queryStatement
70− :: Query schema params columns
71− -> Manipulation schema params columns
72−queryStatement q = UnsafeManipulation $ renderQuery q <> ";"
73−
74−{-----------------------------------------
75−INSERT statements
76−-----------------------------------------}
77−
78{- |
66{- |
79−When a table is created, it contains no data. The first thing to do
67+A `Manipulation` is a statement which may modify data in the database,
80−before a database can be of much use is to insert data. Data is
68+but does not alter the schema. Examples are inserts, updates and deletes.
81−conceptually inserted one row at a time. Of course you can also insert
69+A `Query` is also considered a `Manipulation` even though it does not modify data.
82−more than one row, but there is no way to insert less than one row.
83−Even if you know only some column values, a complete row must be created.
84
70
85simple insert:
71simple insert:
86
72
87>>> :{
73>>> :{
88let
74let
89 manipulation :: Manipulation
75 manipulation :: Manipulation
90− '[ "tab" :::
76+ '[ "tab" ::: '[] :=>
91− '[ "col1" ::: 'Required ('NotNull 'PGint4)
77+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
92− , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[] '[]
78+ , "col2" ::: 'Def :=> 'NotNull 'PGint4 ]] '[] '[]
93 manipulation =
79 manipulation =
94− insertInto #tab (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
80+ insertRow_ #tab (Set 2 `As` #col1 :* Default `As` #col2 :* Nil)
95− OnConflictDoRaise (Returning Nil)
96in renderManipulation manipulation
81in renderManipulation manipulation
97:}
82:}
98−"INSERT INTO tab (col1, col2) VALUES (2, 4);"
83+"INSERT INTO tab (col1, col2) VALUES (2, DEFAULT);"
99
84
100parameterized insert:
85parameterized insert:
101
86
102>>> :{
87>>> :{
103let
88let
104 manipulation :: Manipulation
89 manipulation :: Manipulation
105− '[ "tab" :::
90+ '[ "tab" ::: '[] :=>
106− '[ "col1" ::: 'Required ('NotNull 'PGint4)
91+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
107− , "col2" ::: 'Required ('NotNull 'PGint4) ]]
92+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]]
108− '[ 'Required ('NotNull 'PGint4)
93+ '[ 'NotNull 'PGint4, 'NotNull 'PGint4 ] '[]
109− , 'Required ('NotNull 'PGint4) ] '[]
110 manipulation =
94 manipulation =
111− insertInto #tab
95+ insertRow_ #tab
112− (Values (param @1 `As` #col1 :* param @2 `As` #col2 :* Nil) [])
96+ (Set (param @1) `As` #col1 :* Set (param @2) `As` #col2 :* Nil)
113− OnConflictDoRaise (Returning Nil)
114in renderManipulation manipulation
97in renderManipulation manipulation
115:}
98:}
116"INSERT INTO tab (col1, col2) VALUES (($1 :: int4), ($2 :: int4));"
99"INSERT INTO tab (col1, col2) VALUES (($1 :: int4), ($2 :: int4));"
@@ -120,105 +103,222 @@ returning insert:
120>>> :{
103>>> :{
121let
104let
122 manipulation :: Manipulation
105 manipulation :: Manipulation
123− '[ "tab" :::
106+ '[ "tab" ::: '[] :=>
124− '[ "col1" ::: 'Required ('NotNull 'PGint4)
107+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
125− , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[]
108+ , "col2" ::: 'Def :=> 'NotNull 'PGint4 ]] '[]
126− '["fromOnly" ::: 'Required ('NotNull 'PGint4)]
109+ '["fromOnly" ::: 'NotNull 'PGint4]
127 manipulation =
110 manipulation =
128− insertInto #tab (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
111+ insertRow #tab (Set 2 `As` #col1 :* Default `As` #col2 :* Nil)
129 OnConflictDoRaise (Returning (#col1 `As` #fromOnly :* Nil))
112 OnConflictDoRaise (Returning (#col1 `As` #fromOnly :* Nil))
130in renderManipulation manipulation
113in renderManipulation manipulation
131:}
114:}
132−"INSERT INTO tab (col1, col2) VALUES (2, 4) RETURNING col1 AS fromOnly;"
115+"INSERT INTO tab (col1, col2) VALUES (2, DEFAULT) RETURNING col1 AS fromOnly;"
116+
117+upsert:
118+
119+>>> :{
120+let
121+ manipulation :: Manipulation
122+ '[ "tab" ::: '[] :=>
123+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
124+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]]
125+ '[] '[ "sum" ::: 'NotNull 'PGint4]
126+ manipulation =
127+ insertRows #tab
128+ (Set 2 `As` #col1 :* Set 4 `As` #col2 :* Nil)
129+ [Set 6 `As` #col1 :* Set 8 `As` #col2 :* Nil]
130+ (OnConflictDoUpdate
131+ (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
132+ [#col1 .== #col2])
133+ (Returning $ (#col1 + #col2) `As` #sum :* Nil)
134+in renderManipulation manipulation
135+:}
136+"INSERT INTO tab (col1, col2) VALUES (2, 4), (6, 8) ON CONFLICT DO UPDATE SET col1 = 2 WHERE (col1 = col2) RETURNING (col1 + col2) AS sum;"
133
137
134query insert:
138query insert:
135
139
136>>> :{
140>>> :{
137let
141let
138 manipulation :: Manipulation
142 manipulation :: Manipulation
139− '[ "tab" :::
143+ '[ "tab" ::: '[] :=>
140− '[ "col1" ::: 'Required ('NotNull 'PGint4)
144+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
141− , "col2" ::: 'Required ('NotNull 'PGint4)
145+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4
142 ]
146 ]
143− , "other_tab" :::
147+ , "other_tab" ::: '[] :=>
144− '[ "col1" ::: 'Required ('NotNull 'PGint4)
148+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
145− , "col2" ::: 'Required ('NotNull 'PGint4)
149+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4
146 ]
150 ]
147 ] '[] '[]
151 ] '[] '[]
148 manipulation =
152 manipulation =
149− insertInto #tab
153+ insertQuery_ #tab
150− ( ValuesQuery $
151− selectStar (from (Table (#other_tab `As` #t))) )
154+ (selectStar (from (table (#other_tab `As` #t))))
152− OnConflictDoRaise (Returning Nil)
153in renderManipulation manipulation
155in renderManipulation manipulation
154:}
156:}
155"INSERT INTO tab SELECT * FROM other_tab AS t;"
157"INSERT INTO tab SELECT * FROM other_tab AS t;"
156
158
157−upsert:
159+update:
158
160
159>>> :{
161>>> :{
160let
162let
161 manipulation :: Manipulation
163 manipulation :: Manipulation
162− '[ "tab" :::
164+ '[ "tab" ::: '[] :=>
163− '[ "col1" ::: 'Required ('NotNull 'PGint4)
165+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
164− , "col2" ::: 'Required ('NotNull 'PGint4) ]]
166+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]] '[] '[]
165− '[] '[ "sum" ::: 'Required ('NotNull 'PGint4)]
166 manipulation =
167 manipulation =
167− insertInto #tab
168− (Values
169− (2 `As` #col1 :* 4 `As` #col2 :* Nil)
170− [6 `As` #col1 :* 8 `As` #col2 :* Nil])
171− (OnConflictDoUpdate
172− (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
168+ update_ #tab (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
173− (Just (#col1 .== #col2)))
169+ (#col1 ./= #col2)
174− (Returning $ (#col1 + #col2) `As` #sum :* Nil)
175in renderManipulation manipulation
170in renderManipulation manipulation
176:}
171:}
177−"INSERT INTO tab (col1, col2) VALUES (2, 4), (6, 8) ON CONFLICT DO UPDATE SET col1 = 2 WHERE (col1 = col2) RETURNING (col1 + col2) AS sum;"
172+"UPDATE tab SET col1 = 2 WHERE (col1 <> col2);"
173+
174+delete:
175+
176+>>> :{
177+let
178+ manipulation :: Manipulation
179+ '[ "tab" ::: '[] :=>
180+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
181+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]] '[]
182+ '[ "col1" ::: 'NotNull 'PGint4
183+ , "col2" ::: 'NotNull 'PGint4 ]
184+ manipulation = deleteFrom #tab (#col1 .== #col2) ReturningStar
185+in renderManipulation manipulation
186+:}
187+"DELETE FROM tab WHERE (col1 = col2) RETURNING *;"
178-}
188-}
179−insertInto
189+newtype Manipulation
190+ (schema :: TablesType)
191+ (params :: [NullityType])
192+ (columns :: RelationType)
193+ = UnsafeManipulation { renderManipulation :: ByteString }
194+ deriving (GHC.Generic,Show,Eq,Ord,NFData)
195+
196+-- | Convert a `Query` into a `Manipulation`.
197+queryStatement
198+ :: Query schema params columns
199+ -> Manipulation schema params columns
200+queryStatement q = UnsafeManipulation $ renderQuery q <> ";"
201+
202+{-----------------------------------------
203+INSERT statements
204+-----------------------------------------}
205+
206+-- | Insert multiple rows.
207+--
208+-- When a table is created, it contains no data. The first thing to do
209+-- before a database can be of much use is to insert data. Data is
210+-- conceptually inserted one row at a time. Of course you can also insert
211+-- more than one row, but there is no way to insert less than one row.
212+-- Even if you know only some column values, a complete row must be created.
213+insertRows
180− :: (SOP.SListI columns, SOP.SListI results, HasTable table schema columns)
214+ :: ( SOP.SListI columns
215+ , SOP.SListI results
216+ , Has tab schema table
217+ , columns ~ TableToColumns table )
181− => Alias table -- ^ table to insert into
218+ => Alias tab -- ^ table to insert into
182− -> ValuesClause schema params columns -- ^ values to insert
219+ -> NP (Aliased (ColumnValue '[] params)) columns -- ^ row to insert
220+ -> [NP (Aliased (ColumnValue '[] params)) columns] -- ^ more rows to insert
183 -> ConflictClause columns params
221 -> ConflictClause columns params
184 -- ^ what to do in case of constraint conflict
222 -- ^ what to do in case of constraint conflict
185 -> ReturningClause columns params results -- ^ results to return
223 -> ReturningClause columns params results -- ^ results to return
186 -> Manipulation schema params results
224 -> Manipulation schema params results
187−insertInto table insert conflict returning = UnsafeManipulation $
225+insertRows tab row rows conflict returning = UnsafeManipulation $
188− "INSERT" <+> "INTO" <+> renderAlias table
226+ "INSERT" <+> "INTO" <+> renderAlias tab
189− <+> renderValuesClause insert
190− <> renderConflictClause conflict
191− <> renderReturningClause returning
192−
193−-- | A `ValuesClause` lets you insert either values, free `Expression`s,
194−-- or the result of a `Query`.
195−data ValuesClause
196− (schema :: TablesType)
197− (params :: [ColumnType])
198− (columns :: ColumnsType)
199− = Values
200− (NP (Aliased (Expression '[] 'Ungrouped params)) columns)
201− [NP (Aliased (Expression '[] 'Ungrouped params)) columns]
202− -- ^ at least one row of values
203− | ValuesQuery (Query schema params columns)
204−
205−-- | Render a `ValuesClause`.
206−renderValuesClause
207− :: SOP.SListI columns
208− => ValuesClause schema params columns
209− -> ByteString
210−renderValuesClause = \case
211− Values row rows ->
212− parenthesized (renderCommaSeparated renderAliasPart row)
227+ <+> parenthesized (renderCommaSeparated renderAliasPart row)
213 <+> "VALUES"
228 <+> "VALUES"
214 <+> commaSeparated
229 <+> commaSeparated
230+ ( parenthesized
215− (parenthesized . renderCommaSeparated renderValuePart <$> row:rows)
231+ . renderCommaSeparated renderColumnValuePart <$> row:rows )
232+ <> renderConflictClause conflict
233+ <> renderReturningClause returning
216 where
234 where
217− renderAliasPart, renderValuePart
235+ renderAliasPart, renderColumnValuePart
218− :: Aliased (Expression '[] 'Ungrouped params) ty -> ByteString
236+ :: Aliased (ColumnValue '[] params) ty -> ByteString
219 renderAliasPart (_ `As` name) = renderAlias name
237 renderAliasPart (_ `As` name) = renderAlias name
220− renderValuePart (value `As` _) = renderExpression value
238+ renderColumnValuePart (value `As` _) = case value of
221− ValuesQuery q -> renderQuery q
239+ Default -> "DEFAULT"
240+ Set expression -> renderExpression expression
241+
242+-- | Insert a single row.
243+insertRow
244+ :: ( SOP.SListI columns
245+ , SOP.SListI results
246+ , Has tab schema table
247+ , columns ~ TableToColumns table )
248+ => Alias tab -- ^ table to insert into
249+ -> NP (Aliased (ColumnValue '[] params)) columns -- ^ row to insert
250+ -> ConflictClause columns params
251+ -- ^ what to do in case of constraint conflict
252+ -> ReturningClause columns params results -- ^ results to return
253+ -> Manipulation schema params results
254+insertRow tab row = insertRows tab row []
255+
256+-- | Insert multiple rows returning `Nil` and raising an error on conflicts.
257+insertRows_
258+ :: ( SOP.SListI columns
259+ , Has tab schema table
260+ , columns ~ TableToColumns table )
261+ => Alias tab -- ^ table to insert into
262+ -> NP (Aliased (ColumnValue '[] params)) columns -- ^ row to insert
263+ -> [NP (Aliased (ColumnValue '[] params)) columns] -- ^ more rows to insert
264+ -> Manipulation schema params '[]
265+insertRows_ tab row rows =
266+ insertRows tab row rows OnConflictDoRaise (Returning Nil)
267+
268+-- | Insert a single row returning `Nil` and raising an error on conflicts.
269+insertRow_
270+ :: ( SOP.SListI columns
271+ , Has tab schema table
272+ , columns ~ TableToColumns table )
273+ => Alias tab -- ^ table to insert into
274+ -> NP (Aliased (ColumnValue '[] params)) columns -- ^ row to insert
275+ -> Manipulation schema params '[]
276+insertRow_ tab row = insertRow tab row OnConflictDoRaise (Returning Nil)
277+
278+-- | Insert a `Query`.
279+insertQuery
280+ :: ( SOP.SListI columns
281+ , SOP.SListI results
282+ , Has tab schema table
283+ , columns ~ TableToColumns table )
284+ => Alias tab -- ^ table to insert into
285+ -> Query schema params (ColumnsToRelation columns)
286+ -> ConflictClause columns params
287+ -- ^ what to do in case of constraint conflict
288+ -> ReturningClause columns params results -- ^ results to return
289+ -> Manipulation schema params results
290+insertQuery tab query conflict returning = UnsafeManipulation $
291+ "INSERT" <+> "INTO" <+> renderAlias tab
292+ <+> renderQuery query
293+ <> renderConflictClause conflict
294+ <> renderReturningClause returning
295+
296+-- | Insert a `Query` returning `Nil` and raising an error on conflicts.
297+insertQuery_
298+ :: ( SOP.SListI columns
299+ , Has tab schema table
300+ , columns ~ TableToColumns table )
301+ => Alias tab -- ^ table to insert into
302+ -> Query schema params (ColumnsToRelation columns)
303+ -> Manipulation schema params '[]
304+insertQuery_ tab query =
305+ insertQuery tab query OnConflictDoRaise (Returning Nil)
306+
307+-- | `ColumnValue`s are values to insert or update in a row
308+-- `Same` updates with the same value.
309+-- `Default` inserts or updates with the @DEFAULT@ value
310+-- `Set` a value to be an `Expression`, relative to the given
311+-- row for an update, and closed for an insert.
312+data ColumnValue
313+ (columns :: RelationType)
314+ (params :: [NullityType])
315+ (ty :: ColumnType)
316+ where
317+ Same :: ColumnValue (column ': columns) params ty
318+ Default :: ColumnValue columns params ('Def :=> ty)
319+ Set
320+ :: (forall table. Expression '[table ::: columns] 'Ungrouped params ty)
321+ -> ColumnValue columns params (constraint :=> ty)
222
322
223-- | A `ReturningClause` computes and return value(s) based
323-- | A `ReturningClause` computes and return value(s) based
224-- on each row actually inserted, updated or deleted. This is primarily
324-- on each row actually inserted, updated or deleted. This is primarily
@@ -228,18 +328,19 @@ renderValuesClause = \case
228-- deleted will be returned. For example, if a row was locked
328-- deleted will be returned. For example, if a row was locked
229-- but not updated because an `OnConflictDoUpdate` condition was not satisfied,
329-- but not updated because an `OnConflictDoUpdate` condition was not satisfied,
230-- the row will not be returned. `ReturningStar` will return all columns
330-- the row will not be returned. `ReturningStar` will return all columns
231−-- in the row. Use `Returning Nil` in the common case where no return
331+-- in the row. Use @Returning Nil@ in the common case where no return
232-- values are desired.
332-- values are desired.
233data ReturningClause
333data ReturningClause
234 (columns :: ColumnsType)
334 (columns :: ColumnsType)
235− (params :: [ColumnType])
335+ (params :: [NullityType])
236− (results :: ColumnsType)
336+ (results :: RelationType)
237 where
337 where
238− ReturningStar :: ReturningClause columns params columns
338+ ReturningStar
339+ :: results ~ ColumnsToRelation columns
340+ => ReturningClause columns params results
239 Returning
341 Returning
240− :: NP
342+ :: rel ~ ColumnsToRelation columns
241− (Aliased (Expression '[table ::: columns] 'Ungrouped params))
343+ => NP (Aliased (Expression '[table ::: rel] 'Ungrouped params)) results
242− results
243 -> ReturningClause columns params results
344 -> ReturningClause columns params results
244
345
245-- | Render a `ReturningClause`.
346-- | Render a `ReturningClause`.
@@ -251,19 +352,19 @@ renderReturningClause = \case
251 ReturningStar -> " RETURNING *;"
352 ReturningStar -> " RETURNING *;"
252 Returning Nil -> ";"
353 Returning Nil -> ";"
253 Returning results -> " RETURNING"
354 Returning results -> " RETURNING"
254− <+> renderCommaSeparated (renderAliased renderExpression) results <> ";"
355+ <+> renderCommaSeparated (renderAliasedAs renderExpression) results <> ";"
255
356
256-- | A `ConflictClause` specifies an action to perform upon a constraint
357-- | A `ConflictClause` specifies an action to perform upon a constraint
257-- violation. `OnConflictDoRaise` will raise an error.
358-- violation. `OnConflictDoRaise` will raise an error.
258-- `OnConflictDoNothing` simply avoids inserting a row.
359-- `OnConflictDoNothing` simply avoids inserting a row.
259-- `OnConflictDoUpdate` updates the existing row that conflicts with the row
360-- `OnConflictDoUpdate` updates the existing row that conflicts with the row
260-- proposed for insertion.
361-- proposed for insertion.
261−data ConflictClause columns params where
362+data ConflictClause (columns :: ColumnsType) params where
262 OnConflictDoRaise :: ConflictClause columns params
363 OnConflictDoRaise :: ConflictClause columns params
263 OnConflictDoNothing :: ConflictClause columns params
364 OnConflictDoNothing :: ConflictClause columns params
264 OnConflictDoUpdate
365 OnConflictDoUpdate
265− :: NP (Aliased (UpdateExpression columns params)) columns
366+ :: NP (Aliased (ColumnValue (ColumnsToRelation columns) params)) columns
266− -> Maybe (Condition '[table ::: columns] 'Ungrouped params)
367+ -> [Condition '[table ::: ColumnsToRelation columns] 'Ungrouped params]
267 -> ConflictClause columns params
368 -> ConflictClause columns params
268
369
269-- | Render a `ConflictClause`.
370-- | Render a `ConflictClause`.
@@ -274,12 +375,22 @@ renderConflictClause
274renderConflictClause = \case
375renderConflictClause = \case
275 OnConflictDoRaise -> ""
376 OnConflictDoRaise -> ""
276 OnConflictDoNothing -> " ON CONFLICT DO NOTHING"
377 OnConflictDoNothing -> " ON CONFLICT DO NOTHING"
277− OnConflictDoUpdate updates whMaybe
378+ OnConflictDoUpdate updates whs'
278 -> " ON CONFLICT DO UPDATE SET"
379 -> " ON CONFLICT DO UPDATE SET"
279− <+> renderCommaSeparatedMaybe renderUpdateExpression updates
380+ <+> renderCommaSeparatedMaybe renderUpdate updates
280− <> case whMaybe of
381+ <> case whs' of
281− Nothing -> ""
382+ [] -> ""
282− Just wh -> " WHERE" <+> renderExpression wh
383+ wh:whs -> " WHERE" <+> renderExpression (foldr (.&&) wh whs)
384+ where
385+ renderUpdate
386+ :: Aliased (ColumnValue columns params) column
387+ -> Maybe ByteString
388+ renderUpdate = \case
389+ Same `As` _ -> Nothing
390+ Default `As` column -> Just $
391+ renderAlias column <+> "=" <+> "DEFAULT"
392+ Set expression `As` column -> Just $
393+ renderAlias column <+> "=" <+> renderExpression expression
283
394
284{-----------------------------------------
395{-----------------------------------------
285UPDATE statements
396UPDATE statements
@@ -287,82 +398,115 @@ UPDATE statements
287
398
288-- | An `update` command changes the values of the specified columns
399-- | An `update` command changes the values of the specified columns
289-- in all rows that satisfy the condition.
400-- in all rows that satisfy the condition.
290−--
291−-- >>> :{
292−-- let
293−-- manipulation :: Manipulation
294−-- '[ "tab" :::
295−-- '[ "col1" ::: 'Required ('NotNull 'PGint4)
296−-- , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[] '[]
297−-- manipulation =
298−-- update #tab (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
299−-- (#col1 ./= #col2) (Returning Nil)
300−-- in renderManipulation manipulation
301−-- :}
302−-- "UPDATE tab SET col1 = 2 WHERE (col1 <> col2);"
303update
401update
304− :: (HasTable table schema columns, SOP.SListI columns, SOP.SListI results)
402+ :: ( SOP.SListI columns
305− => Alias table -- ^ table to update
403+ , SOP.SListI results
306− -> NP (Aliased (UpdateExpression columns params)) columns
404+ , Has tab schema table
405+ , columns ~ TableToColumns table )
406+ => Alias tab -- ^ table to update
407+ -> NP (Aliased (ColumnValue (ColumnsToRelation columns) params)) columns
307 -- ^ modified values to replace old values
408 -- ^ modified values to replace old values
308− -> Condition '[tab ::: columns] 'Ungrouped params
409+ -> Condition '[tab ::: ColumnsToRelation columns] 'Ungrouped params
309 -- ^ condition under which to perform update on a row
410 -- ^ condition under which to perform update on a row
310 -> ReturningClause columns params results -- ^ results to return
411 -> ReturningClause columns params results -- ^ results to return
311 -> Manipulation schema params results
412 -> Manipulation schema params results
312−update table columns wh returning = UnsafeManipulation $
413+update tab columns wh returning = UnsafeManipulation $
313 "UPDATE"
414 "UPDATE"
314− <+> renderAlias table
415+ <+> renderAlias tab
315 <+> "SET"
416 <+> "SET"
316− <+> renderCommaSeparatedMaybe renderUpdateExpression columns
417+ <+> renderCommaSeparatedMaybe renderUpdate columns
317 <+> "WHERE" <+> renderExpression wh
418 <+> "WHERE" <+> renderExpression wh
318 <> renderReturningClause returning
419 <> renderReturningClause returning
319−
420+ where
320−-- | Columns to be updated are mentioned with `Set`; columns which are to
421+ renderUpdate
321−-- remain the same are mentioned with `Same`.
422+ :: Aliased (ColumnValue columns params) column
322−data UpdateExpression columns params ty
423+ -> Maybe ByteString
323− = Same
424+ renderUpdate = \case
324− -- ^ column to remain the same upon update
425+ Same `As` _ -> Nothing
325− | Set (forall table. Expression '[table ::: columns] 'Ungrouped params ty)
426+ Default `As` column -> Just $
326− -- ^ column to be updated
427+ renderAlias column <+> "=" <+> "DEFAULT"
327−deriving instance Show (UpdateExpression columns params ty)
428+ Set expression `As` column -> Just $
328−deriving instance Eq (UpdateExpression columns params ty)
429+ renderAlias column <+> "=" <+> renderExpression expression
329−deriving instance Ord (UpdateExpression columns params ty)
430+
330−
431+-- | Update a row returning `Nil`.
331−-- | Render an `UpdateExpression`.
432+update_
332−renderUpdateExpression
433+ :: ( SOP.SListI columns
333− :: Aliased (UpdateExpression params columns) column
434+ , Has tab schema table
334− -> Maybe ByteString
435+ , columns ~ TableToColumns table )
335−renderUpdateExpression = \case
436+ => Alias tab -- ^ table to update
336− Same `As` _ -> Nothing
437+ -> NP (Aliased (ColumnValue (ColumnsToRelation columns) params)) columns
337− Set expression `As` column -> Just $
438+ -- ^ modified values to replace old values
338− renderAlias column <+> "=" <+> renderExpression expression
439+ -> Condition '[tab ::: ColumnsToRelation columns] 'Ungrouped params
440+ -- ^ condition under which to perform update on a row
441+ -> Manipulation schema params '[]
442+update_ tab columns wh = update tab columns wh (Returning Nil)
339
443
340{-----------------------------------------
444{-----------------------------------------
341DELETE statements
445DELETE statements
342-----------------------------------------}
446-----------------------------------------}
343
447
344-- | Delete rows of a table.
448-- | Delete rows of a table.
345−--
346−-- >>> :{
347−-- let
348−-- manipulation :: Manipulation
349−-- '[ "tab" :::
350−-- '[ "col1" ::: 'Required ('NotNull 'PGint4)
351−-- , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[]
352−-- '[ "col1" ::: 'Required ('NotNull 'PGint4)
353−-- , "col2" ::: 'Required ('NotNull 'PGint4) ]
354−-- manipulation = deleteFrom #tab (#col1 .== #col2) ReturningStar
355−-- in renderManipulation manipulation
356−-- :}
357−-- "DELETE FROM tab WHERE (col1 = col2) RETURNING *;"
358deleteFrom
449deleteFrom
359− :: (SOP.SListI results, HasTable table schema columns)
450+ :: ( SOP.SListI results
360− => Alias table -- ^ table to delete from
451+ , Has tab schema table
361− -> Condition '[table ::: columns] 'Ungrouped params
452+ , columns ~ TableToColumns table )
453+ => Alias tab -- ^ table to delete from
454+ -> Condition '[tab ::: ColumnsToRelation columns] 'Ungrouped params
362 -- ^ condition under which to delete a row
455 -- ^ condition under which to delete a row
363 -> ReturningClause columns params results -- ^ results to return
456 -> ReturningClause columns params results -- ^ results to return
364 -> Manipulation schema params results
457 -> Manipulation schema params results
365−deleteFrom table wh returning = UnsafeManipulation $
458+deleteFrom tab wh returning = UnsafeManipulation $
366− "DELETE FROM" <+> renderAlias table
459+ "DELETE FROM" <+> renderAlias tab
367 <+> "WHERE" <+> renderExpression wh
460 <+> "WHERE" <+> renderExpression wh
368 <> renderReturningClause returning
461 <> renderReturningClause returning
462+
463+-- | Delete rows returning `Nil`.
464+deleteFrom_
465+ :: ( Has tab schema table
466+ , columns ~ TableToColumns table )
467+ => Alias tab -- ^ table to delete from
468+ -> Condition '[tab ::: ColumnsToRelation columns] 'Ungrouped params
469+ -- ^ condition under which to delete a row
470+ -> Manipulation schema params '[]
471+deleteFrom_ tab wh = deleteFrom tab wh (Returning Nil)
472+
473+{-----------------------------------------
474+WITH statements
475+-----------------------------------------}
476+
477+-- | `with` provides a way to write auxiliary statements for use in a larger statement.
478+-- These statements, which are often referred to as Common Table Expressions or CTEs,
479+-- can be thought of as defining temporary tables that exist just for one statement.
480+--
481+-- >>> type ProductsTable = '[] :=> '["product" ::: 'NoDef :=> 'NotNull 'PGtext, "date" ::: 'Def :=> 'NotNull 'PGdate]
482+--
483+-- >>> :{
484+-- let
485+-- manipulation :: Manipulation '["products" ::: ProductsTable, "products_deleted" ::: ProductsTable] '[ 'NotNull 'PGdate] '[]
486+-- manipulation = with
487+-- (deleteFrom #products (#date .< param @1) ReturningStar `As` #deleted_rows :* Nil)
488+-- (insertQuery_ #products_deleted (selectStar (from (table (#deleted_rows `As` #t)))))
489+-- in renderManipulation manipulation
490+-- :}
491+-- "WITH deleted_rows AS (DELETE FROM products WHERE (date < ($1 :: date)) RETURNING *) INSERT INTO products_deleted SELECT * FROM deleted_rows AS t;"
492+with
493+ :: SOP.SListI commons
494+ => NP (Aliased (Manipulation schema params)) commons
495+ -- ^ common table expressions
496+ -> Manipulation (Join (RelationsToTables commons) schema) params results
497+ -> Manipulation schema params results
498+with commons manipulation = UnsafeManipulation $
499+ "WITH" <+> renderCommaSeparated renderCommon commons
500+ <+> renderManipulation manipulation
501+ where
502+ renderCommon
503+ :: Aliased (Manipulation schema params) common
504+ -> ByteString
505+ renderCommon (common `As` alias) =
506+ renderAlias alias <+> "AS" <+>
507+ let
508+ str = renderManipulation common
509+ len = ByteString.length str
510+ str' = ByteString.take (len - 1) str -- remove ';'
511+ in
512+ parenthesized str'
squeal-postgresql/src/Squeal/PostgreSQL/Migration.hsnew file+318
@@ -0,0 +1,318 @@
1+{-|
2+Module: Squeal.PostgreSQL.Migration
3+Description: Squeal migrations
4+Copyright: (c) Eitan Chatav, 2017
5+Maintainer: eitan@morphism.tech
6+Stability: experimental
7+
8+This module defines a `Migration` type to safely
9+change the schema of your database over time. Let's see an example!
10+
11+>>> :set -XDataKinds -XOverloadedLabels
12+>>> :set -XOverloadedStrings -XFlexibleContexts -XTypeOperators
13+>>> :{
14+type UsersTable =
15+ '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
16+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
17+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
18+ ]
19+:}
20+
21+>>> :{
22+type EmailsTable =
23+ '[ "pk_emails" ::: 'PrimaryKey '["id"]
24+ , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
25+ ] :=>
26+ '[ "id" ::: 'Def :=> 'NotNull 'PGint4
27+ , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
28+ , "email" ::: 'NoDef :=> 'Null 'PGtext
29+ ]
30+:}
31+
32+>>> :{
33+let
34+ makeUsers :: Migration IO '[] '["users" ::: UsersTable]
35+ makeUsers = Migration
36+ { name = "make users table"
37+ , up = void . define $
38+ createTable #users
39+ ( serial `As` #id :*
40+ (text & notNull) `As` #name :* Nil )
41+ ( primaryKey (Column #id :* Nil) `As` #pk_users :* Nil )
42+ , down = void . define $ dropTable #users
43+ }
44+:}
45+
46+>>> :{
47+let
48+ makeEmails :: Migration IO '["users" ::: UsersTable]
49+ '["users" ::: UsersTable, "emails" ::: EmailsTable]
50+ makeEmails = Migration
51+ { name = "make emails table"
52+ , up = void . define $
53+ createTable #emails
54+ ( serial `As` #id :*
55+ (int & notNull) `As` #user_id :*
56+ text `As` #email :* Nil )
57+ ( primaryKey (Column #id :* Nil) `As` #pk_emails :*
58+ foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
59+ OnDeleteCascade OnUpdateCascade `As` #fk_user_id :* Nil )
60+ , down = void . define $ dropTable #emails
61+ }
62+:}
63+
64+Now that we have a couple migrations we can chain them together.
65+
66+>>> let migrations = makeUsers :>> makeEmails :>> Done
67+
68+>>> :{
69+let
70+ numMigrations
71+ :: Has "schema_migrations" schema MigrationsTable
72+ => PQ schema schema IO ()
73+ numMigrations = do
74+ result <- runQuery (selectStar (from (table (#schema_migrations `As` #m))))
75+ num <- ntuples result
76+ liftBase $ print num
77+:}
78+
79+>>> :{
80+withConnection "host=localhost port=5432 dbname=exampledb" $
81+ manipulate (UnsafeManipulation "SET client_min_messages TO WARNING;")
82+ -- suppress notices
83+ & pqThen (migrateUp migrations)
84+ & pqThen numMigrations
85+ & pqThen (migrateDown migrations)
86+ & pqThen numMigrations
87+:}
88+Row 2
89+Row 0
90+-}
91+
92+{-# LANGUAGE
93+ ScopedTypeVariables
94+ , OverloadedStrings
95+ , DataKinds
96+ , GADTs
97+ , LambdaCase
98+ , PolyKinds
99+ , OverloadedLabels
100+ , TypeApplications
101+ , FlexibleContexts
102+ , TypeOperators
103+#-}
104+
105+module Squeal.PostgreSQL.Migration
106+ ( -- * Migration
107+ Migration (..)
108+ , migrateUp
109+ , migrateDown
110+ -- * Aligned lists
111+ , AlignedList (..)
112+ , single
113+ -- * Migration table
114+ , MigrationsTable
115+ , createMigrations
116+ , insertMigration
117+ , deleteMigration
118+ , selectMigration
119+ ) where
120+
121+import Control.Category
122+import Control.Monad
123+import Control.Monad.Base
124+import Control.Monad.Trans.Control
125+import Data.Monoid
126+import Generics.SOP (K(..))
127+import Data.Function ((&))
128+import Data.Text (Text)
129+import Prelude hiding (id, (.))
130+
131+import Squeal.PostgreSQL
132+
133+-- | A `Migration` should contain an inverse pair of
134+-- `up` and `down` instructions and a unique `name`.
135+data Migration io schema0 schema1 = Migration
136+ { name :: Text -- ^ The `name` of a `Migration`.
137+ -- Each `name` in a `Migration` should be unique.
138+ , up :: PQ schema0 schema1 io () -- ^ The `up` instruction of a `Migration`.
139+ , down :: PQ schema1 schema0 io () -- ^ The `down` instruction of a `Migration`.
140+ }
141+
142+-- | An `AlignedList` is a type-aligned list or free category.
143+data AlignedList p x0 x1 where
144+ Done :: AlignedList p x x
145+ (:>>) :: p x0 x1 -> AlignedList p x1 x2 -> AlignedList p x0 x2
146+infixr 7 :>>
147+instance Category (AlignedList p) where
148+ id = Done
149+ (.) list = \case
150+ Done -> list
151+ step :>> steps -> step :>> (steps >>> list)
152+
153+-- | A `single` step.
154+single :: p x0 x1 -> AlignedList p x0 x1
155+single step = step :>> Done
156+
157+-- | Run `Migration`s by creating the `MigrationsTable`
158+-- if it does not exist and then in a transaction, for each each `Migration`
159+-- query to see if the `Migration` is executed. If not, then
160+-- execute the `Migration` and insert its row in the `MigrationsTable`.
161+migrateUp
162+ :: MonadBaseControl IO io
163+ => AlignedList (Migration io) schema0 schema1 -- ^ migrations to run
164+ -> PQ
165+ ("schema_migrations" ::: MigrationsTable ': schema0)
166+ ("schema_migrations" ::: MigrationsTable ': schema1)
167+ io ()
168+migrateUp migration =
169+ define createMigrations
170+ & pqBind okResult
171+ & pqThen (transactionallySchema_ (upMigrations migration))
172+ where
173+
174+ upMigrations
175+ :: MonadBaseControl IO io
176+ => AlignedList (Migration io) schema0 schema1
177+ -> PQ
178+ ("schema_migrations" ::: MigrationsTable ': schema0)
179+ ("schema_migrations" ::: MigrationsTable ': schema1)
180+ io ()
181+ upMigrations = \case
182+ Done -> return ()
183+ step :>> steps -> upMigration step & pqThen (upMigrations steps)
184+
185+ upMigration
186+ :: MonadBase IO io
187+ => Migration io schema0 schema1 -> PQ
188+ ("schema_migrations" ::: MigrationsTable ': schema0)
189+ ("schema_migrations" ::: MigrationsTable ': schema1)
190+ io ()
191+ upMigration step =
192+ queryExecuted step
193+ & pqBind (\ executed ->
194+ if executed == 1 -- up step has already been executed
195+ then PQ (\ _ -> return (K ())) -- unsafely switch schemas
196+ else
197+ pqEmbed (up step) -- safely switch schemas
198+ & pqThen (manipulateParams insertMigration (Only (name step)))
199+ -- insert execution record
200+ & pqBind okResult)
201+
202+ queryExecuted
203+ :: MonadBase IO io
204+ => Migration io schema0 schema1 -> PQ
205+ ("schema_migrations" ::: MigrationsTable ': schema0)
206+ ("schema_migrations" ::: MigrationsTable ': schema0)
207+ io Row
208+ queryExecuted step = do
209+ result <- runQueryParams selectMigration (Only (name step))
210+ okResult result
211+ ntuples result
212+
213+-- | Rewind `Migration`s by creating the `MigrationsTable`
214+-- if it does not exist and then in a transaction, for each each `Migration`
215+-- query to see if the `Migration` is executed. If it is, then
216+-- rewind the `Migration` and delete its row in the `MigrationsTable`.
217+migrateDown
218+ :: MonadBaseControl IO io
219+ => AlignedList (Migration io) schema0 schema1 -- ^ migrations to rewind
220+ -> PQ
221+ ("schema_migrations" ::: MigrationsTable ': schema1)
222+ ("schema_migrations" ::: MigrationsTable ': schema0)
223+ io ()
224+migrateDown migrations =
225+ define createMigrations
226+ & pqBind okResult
227+ & pqThen (transactionallySchema_ (downMigrations migrations))
228+ where
229+
230+ downMigrations
231+ :: MonadBaseControl IO io
232+ => AlignedList (Migration io) schema0 schema1 -> PQ
233+ ("schema_migrations" ::: MigrationsTable ': schema1)
234+ ("schema_migrations" ::: MigrationsTable ': schema0)
235+ io ()
236+ downMigrations = \case
237+ Done -> return ()
238+ step :>> steps -> downMigrations steps & pqThen (downMigration step)
239+
240+ downMigration
241+ :: MonadBase IO io
242+ => Migration io schema0 schema1 -> PQ
243+ ("schema_migrations" ::: MigrationsTable ': schema1)
244+ ("schema_migrations" ::: MigrationsTable ': schema0)
245+ io ()
246+ downMigration step =
247+ queryExecuted step
248+ & pqBind (\ executed ->
249+ if executed == 0 -- up step has not been executed
250+ then PQ (\ _ -> return (K ())) -- unsafely switch schemas
251+ else
252+ pqEmbed (down step) -- safely switch schemas
253+ & pqThen (manipulateParams deleteMigration (Only (name step)))
254+ -- delete execution record
255+ & pqBind okResult)
256+
257+ queryExecuted
258+ :: MonadBase IO io
259+ => Migration io schema0 schema1 -> PQ
260+ ("schema_migrations" ::: MigrationsTable ': schema1)
261+ ("schema_migrations" ::: MigrationsTable ': schema1)
262+ io Row
263+ queryExecuted step = do
264+ result <- runQueryParams selectMigration (Only (name step))
265+ okResult result
266+ ntuples result
267+
268+okResult :: MonadBase IO io => K Result results -> PQ schema schema io ()
269+okResult result = do
270+ status <- resultStatus result
271+ when (not (status `elem` [CommandOk, TuplesOk])) $ do
272+ errorMessageMaybe <- resultErrorMessage result
273+ case errorMessageMaybe of
274+ Nothing -> error "migrateDown: unknown error"
275+ Just msg -> error ("migrationDown: " <> show msg)
276+
277+-- | The `TableType` for a Squeal migration.
278+type MigrationsTable =
279+ '[ "migrations_unique_name" ::: 'Unique '["name"]] :=>
280+ '[ "name" ::: 'NoDef :=> 'NotNull 'PGtext
281+ , "executed_at" ::: 'Def :=> 'NotNull 'PGtimestamptz
282+ ]
283+
284+-- | Creates a `MigrationsTable` if it does not already exist.
285+createMigrations
286+ :: Has "schema_migrations" schema MigrationsTable
287+ => Definition schema schema
288+createMigrations =
289+ createTableIfNotExists #schema_migrations
290+ ( (text & notNull) `As` #name :*
291+ (timestampWithTimeZone & notNull & default_ currentTimestamp)
292+ `As` #executed_at :* Nil )
293+ ( unique (Column #name :* Nil) `As` #migrations_unique_name :* Nil )
294+
295+-- | Inserts a `Migration` into the `MigrationsTable`
296+insertMigration
297+ :: Has "schema_migrations" schema MigrationsTable
298+ => Manipulation schema '[ 'NotNull 'PGtext] '[]
299+insertMigration = insertRow_ #schema_migrations
300+ ( Set (param @1) `As` #name :*
301+ Default `As` #executed_at :* Nil )
302+
303+-- | Deletes a `Migration` from the `MigrationsTable`
304+deleteMigration
305+ :: Has "schema_migrations" schema MigrationsTable
306+ => Manipulation schema '[ 'NotNull 'PGtext ] '[]
307+deleteMigration = deleteFrom_ #schema_migrations (#name .== param @1)
308+
309+-- | Selects a `Migration` from the `MigrationsTable`, returning
310+-- the time at which it was executed.
311+selectMigration
312+ :: Has "schema_migrations" schema MigrationsTable
313+ => Query schema '[ 'NotNull 'PGtext ]
314+ '[ "executed_at" ::: 'NotNull 'PGtimestamptz ]
315+selectMigration = select
316+ (#executed_at `As` #executed_at :* Nil)
317+ ( from (table (#schema_migrations `As` #m))
318+ & where_ (#name .== param @1))
squeal-postgresql/src/Squeal/PostgreSQL/PQ.hs+227−194
@@ -5,9 +5,17 @@ Copyright: (c) Eitan Chatav, 2017
5Maintainer: eitan@morphism.tech
5Maintainer: eitan@morphism.tech
6Stability: experimental
6Stability: experimental
7
7
8−`Squeal.PostgreSQL.PQ` is where Squeal statements come to actually get run by
8+This module is where Squeal commands actually get executed by
9−`LibPQ`. It contains a `PQ` indexed monad transformer to run `Definition`s and
9+`LibPQ`. It containts two typeclasses, `IndexedMonadTransPQ` for executing
10−a `MonadPQ` constraint for running a `Manipulation` or `Query`.
10+a `Definition` and `MonadPQ` for executing a `Manipulation` or `Query`,
11+and a `PQ` type with instances for them.
12+
13+Using Squeal in your application will come down to defining
14+the @schema@ of your database and including @PQ schema schema@ in your
15+application's monad transformer stack, giving it an instance of `MonadPQ`.
16+
17+This module also provides functions for retrieving rows from the `LibPQ.Result`
18+of executing Squeal commands.
11-}
19-}
12
20
13{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
21{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
@@ -33,39 +41,38 @@ a `MonadPQ` constraint for running a `Manipulation` or `Query`.
33
41
34module Squeal.PostgreSQL.PQ
42module Squeal.PostgreSQL.PQ
35 ( -- * Connection
43 ( -- * Connection
36− Connection (Connection, unConnection)
44+ LibPQ.Connection
37 , connectdb
45 , connectdb
38 , finish
46 , finish
39 , withConnection
47 , withConnection
48+ , lowerConnection
40 -- * PQ
49 -- * PQ
41− , PQ (PQ, runPQ)
50+ , PQ (PQ, unPQ)
51+ , runPQ
42 , execPQ
52 , execPQ
43− , pqAp
53+ , evalPQ
44− , pqBind
54+ , IndexedMonadTransPQ (..)
45− , pqThen
46− , define
47− , thenDefine
48− -- * MonadPQ
49 , MonadPQ (..)
55 , MonadPQ (..)
50 , PQRun
56 , PQRun
51 , pqliftWith
57 , pqliftWith
52 -- * Result
58 -- * Result
53− , Result (Result, unResult)
59+ , LibPQ.Result
54− , RowNumber (RowNumber, unRowNumber)
60+ , LibPQ.Row
55− , ColumnNumber (UnsafeColumnNumber, getColumnNumber)
61+ , ntuples
56− , HasColumnNumber (columnNumber)
57− , getValue
58 , getRow
62 , getRow
59 , getRows
63 , getRows
60− , ntuples
61 , nextRow
64 , nextRow
62 , firstRow
65 , firstRow
63 , liftResult
66 , liftResult
67+ , LibPQ.ExecStatus (..)
68+ , resultStatus
69+ , resultErrorMessage
64 ) where
70 ) where
65
71
66import Control.Exception.Lifted
72import Control.Exception.Lifted
67import Control.Monad.Base
73import Control.Monad.Base
68import Control.Monad.Except
74import Control.Monad.Except
75+import Control.Monad.Morph
69import Control.Monad.Trans.Control
76import Control.Monad.Trans.Control
70import Data.ByteString (ByteString)
77import Data.ByteString (ByteString)
71import Data.Foldable
78import Data.Foldable
@@ -74,8 +81,6 @@ import Data.Kind
74import Data.Monoid
81import Data.Monoid
75import Data.Traversable
82import Data.Traversable
76import Generics.SOP
83import Generics.SOP
77−import GHC.Exts hiding (fromList)
78−import GHC.TypeLits
79
84
80import qualified Database.PostgreSQL.LibPQ as LibPQ
85import qualified Database.PostgreSQL.LibPQ as LibPQ
81
86
@@ -99,11 +104,6 @@ import qualified Control.Monad.Trans.Writer.Strict as Strict
99import qualified Control.Monad.Trans.RWS.Lazy as Lazy
104import qualified Control.Monad.Trans.RWS.Lazy as Lazy
100import qualified Control.Monad.Trans.RWS.Strict as Strict
105import qualified Control.Monad.Trans.RWS.Strict as Strict
101
106
102−-- | A `Connection` consists of a `Database.PostgreSQL.LibPQ`
103−-- `Database.PastgreSQL.LibPQ.Connection` and a phantom `TablesType`
104−newtype Connection (schema :: TablesType) =
105− Connection { unConnection :: LibPQ.Connection }
106−
107{- | Makes a new connection to the database server.
107{- | Makes a new connection to the database server.
108
108
109This function opens a new database connection using the parameters taken
109This function opens a new database connection using the parameters taken
@@ -119,8 +119,9 @@ backslashes within the value must be escaped with a backslash, i.e., ' and \.
119To specify the schema you wish to connect with, use type application.
119To specify the schema you wish to connect with, use type application.
120
120
121>>> :set -XDataKinds
121>>> :set -XDataKinds
122+>>> :set -XPolyKinds
122>>> :set -XTypeOperators
123>>> :set -XTypeOperators
123−>>> type Schema = '["tab" ::: '["col" ::: 'Required ('Null 'PGint2)]]
124+>>> type Schema = '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint2]]
124>>> :set -XTypeApplications
125>>> :set -XTypeApplications
125>>> :set -XOverloadedStrings
126>>> :set -XOverloadedStrings
126>>> conn <- connectdb @Schema "host=localhost port=5432 dbname=exampledb"
127>>> conn <- connectdb @Schema "host=localhost port=5432 dbname=exampledb"
@@ -132,24 +133,30 @@ connectdb
132 :: forall schema io
133 :: forall schema io
133 . MonadBase IO io
134 . MonadBase IO io
134 => ByteString -- ^ conninfo
135 => ByteString -- ^ conninfo
135− -> io (Connection schema)
136+ -> io (K LibPQ.Connection schema)
136−connectdb = fmap Connection . liftBase . LibPQ.connectdb
137+connectdb = fmap K . liftBase . LibPQ.connectdb
137
138
138-- | Closes the connection to the server.
139-- | Closes the connection to the server.
139−finish :: MonadBase IO io => Connection schema -> io ()
140+finish :: MonadBase IO io => K LibPQ.Connection schema -> io ()
140−finish = liftBase . LibPQ.finish . unConnection
141+finish = liftBase . LibPQ.finish . unK
141
142
142-- | Do `connectdb` and `finish` before and after a computation.
143-- | Do `connectdb` and `finish` before and after a computation.
143withConnection
144withConnection
144 :: forall schema0 schema1 io x
145 :: forall schema0 schema1 io x
145 . MonadBaseControl IO io
146 . MonadBaseControl IO io
146 => ByteString
147 => ByteString
147− -> (Connection schema0 -> io (x, Connection schema1))
148+ -> PQ schema0 schema1 io x
148 -> io x
149 -> io x
149withConnection connString action = do
150withConnection connString action = do
150− (x, _conn) <- bracket (connectdb connString) finish action
151+ K x <- bracket (connectdb connString) finish (unPQ action)
151 return x
152 return x
152
153
154+-- | Safely `lowerConnection` to a smaller schema.
155+lowerConnection
156+ :: K LibPQ.Connection (table ': schema)
157+ -> K LibPQ.Connection schema
158+lowerConnection (K conn) = K conn
159+
153-- | We keep track of the schema via an Atkey indexed state monad transformer,
160-- | We keep track of the schema via an Atkey indexed state monad transformer,
154-- `PQ`.
161-- `PQ`.
155newtype PQ
162newtype PQ
@@ -157,67 +164,113 @@ newtype PQ
157 (schema1 :: TablesType)
164 (schema1 :: TablesType)
158 (m :: Type -> Type)
165 (m :: Type -> Type)
159 (x :: Type) =
166 (x :: Type) =
160− PQ { runPQ :: Connection schema0 -> m (x, Connection schema1) }
167+ PQ { unPQ :: K LibPQ.Connection schema0 -> m (K x schema1) }
161− deriving Functor
162
168
163−-- | Run a `PQ` and discard the result but keep the `Connection`.
169+instance Monad m => Functor (PQ schema0 schema1 m) where
170+ fmap f (PQ pq) = PQ $ \ conn -> do
171+ K x <- pq conn
172+ return $ K (f x)
173+
174+-- | Run a `PQ` and keep the result and the `Connection`.
175+runPQ
176+ :: Functor m
177+ => PQ schema0 schema1 m x
178+ -> K LibPQ.Connection schema0
179+ -> m (x, K LibPQ.Connection schema1)
180+runPQ (PQ pq) conn = (\ x -> (unK x, K (unK conn))) <$> pq conn
181+ -- K x <- pq conn
182+ -- return (x, K (unK conn))
183+
184+-- | Execute a `PQ` and discard the result but keep the `Connection`.
164execPQ
185execPQ
165 :: Functor m
186 :: Functor m
166 => PQ schema0 schema1 m x
187 => PQ schema0 schema1 m x
167− -> Connection schema0
188+ -> K LibPQ.Connection schema0
168− -> m (Connection schema1)
189+ -> m (K LibPQ.Connection schema1)
169−execPQ (PQ pq) = fmap snd . pq
190+execPQ (PQ pq) conn = mapKK (\ _ -> unK conn) <$> pq conn
170−
191+
171−-- | indexed analog of `<*>`
192+-- | Evaluate a `PQ` and discard the `Connection` but keep the result.
172−pqAp
193+evalPQ
173− :: Monad m
194+ :: Functor m
174− => PQ schema0 schema1 m (x -> y)
195+ => PQ schema0 schema1 m x
175− -> PQ schema1 schema2 m x
196+ -> K LibPQ.Connection schema0
176− -> PQ schema0 schema2 m y
197+ -> m x
177−pqAp (PQ f) (PQ x) = PQ $ \ conn -> do
198+evalPQ (PQ pq) conn = unK <$> pq conn
178− (f', conn') <- f conn
199+
179− (x', conn'') <- x conn'
200+-- | An [Atkey indexed monad](https://bentnib.org/paramnotions-jfp.pdf) is a `Functor`
180− return (f' x', conn'')
201+-- [enriched category](https://ncatlab.org/nlab/show/enriched+category).
181−
202+-- An indexed monad transformer transforms a `Monad` into an indexed monad.
182−-- | indexed analog of `=<<`
203+-- And, `IndexedMonadTransPQ` is a class for indexed monad transformers that
183−pqBind
204+-- support running `Definition`s using `define` and embedding a computation
184− :: Monad m
205+-- in a larger schema using `pqEmbed`.
185− => (x -> PQ schema1 schema2 m y)
206+class IndexedMonadTransPQ pq where
186− -> PQ schema0 schema1 m x
207+
187− -> PQ schema0 schema2 m y
208+ -- | indexed analog of `<*>`
188−pqBind f (PQ x) = PQ $ \ conn -> do
209+ pqAp
189− (x', conn') <- x conn
210+ :: Monad m
190− runPQ (f x') conn'
211+ => pq schema0 schema1 m (x -> y)
191−
212+ -> pq schema1 schema2 m x
192−-- | indexed analog of flipped `>>`
213+ -> pq schema0 schema2 m y
193−pqThen
214+
194− :: Monad m
215+ -- | indexed analog of `join`
195− => PQ schema1 schema2 m y
216+ pqJoin
196− -> PQ schema0 schema1 m x
217+ :: Monad m
197− -> PQ schema0 schema2 m y
218+ => pq schema0 schema1 m (pq schema1 schema2 m y)
198−pqThen pq2 pq1 = pq1 & pqBind (\ _ -> pq2)
219+ -> pq schema0 schema2 m y
199−
220+
200−-- | Run a `Definition` with `LibPQ.exec`, we expect that libpq obeys the law
221+ -- | indexed analog of `=<<`
201−--
222+ pqBind
202−-- @define statement1 & thenDefine statement2 = define (statement1 >>> statement2)@
223+ :: Monad m
203−define
224+ => (x -> pq schema1 schema2 m y)
204− :: MonadBase IO io
225+ -> pq schema0 schema1 m x
205− => Definition schema0 schema1
226+ -> pq schema0 schema2 m y
206− -> PQ schema0 schema1 io (Result '[])
227+
207−define (UnsafeDefinition q) = PQ $ \ (Connection conn) -> do
228+ -- | indexed analog of flipped `>>`
208− resultMaybe <- liftBase $ LibPQ.exec conn q
229+ pqThen
209− case resultMaybe of
230+ :: Monad m
210− Nothing -> error
231+ => pq schema1 schema2 m y
211− "define: LibPQ.exec returned no results"
232+ -> pq schema0 schema1 m x
212− Just result -> return (Result result, Connection conn)
233+ -> pq schema0 schema2 m y
213−
234+
214−-- | Chain together `define` actions.
235+ -- | Safely embed a computation in a larger schema.
215−thenDefine
236+ pqEmbed
216− :: MonadBase IO io
237+ :: Monad m
217− => Definition schema1 schema2
238+ => pq schema0 schema1 m x
218− -> PQ schema0 schema1 io x
239+ -> pq (table ': schema0) (table : schema1) m x
219− -> PQ schema0 schema2 io (Result '[])
240+
220−thenDefine = pqThen . define
241+ -- | Run a `Definition` with `LibPQ.exec`, we expect that libpq obeys the law
242+ --
243+ -- @define statement1 & pqThen (define statement2) = define (statement1 >>> statement2)@
244+ define
245+ :: MonadBase IO io
246+ => Definition schema0 schema1
247+ -> pq schema0 schema1 io (K LibPQ.Result '[])
248+
249+instance IndexedMonadTransPQ PQ where
250+
251+ pqAp (PQ f) (PQ x) = PQ $ \ conn -> do
252+ K f' <- f conn
253+ K x' <- x (K (unK conn))
254+ return $ K (f' x')
255+
256+ pqJoin pq = pq & pqBind id
257+
258+ pqBind f (PQ x) = PQ $ \ conn -> do
259+ K x' <- x conn
260+ unPQ (f x') (K (unK conn))
261+
262+ pqThen pq2 pq1 = pq1 & pqBind (\ _ -> pq2)
263+
264+ pqEmbed (PQ pq) = PQ $ \ (K conn) -> do
265+ K x <- pq (K conn)
266+ return $ K x
267+
268+ define (UnsafeDefinition q) = PQ $ \ (K conn) -> do
269+ resultMaybe <- liftBase $ LibPQ.exec conn q
270+ case resultMaybe of
271+ Nothing -> error
272+ "define: LibPQ.exec returned no results"
273+ Just result -> return $ K (K result)
221
274
222{- | `MonadPQ` is an `mtl` style constraint, similar to
275{- | `MonadPQ` is an `mtl` style constraint, similar to
223`Control.Monad.State.Class.MonadState`, for using `LibPQ` to
276`Control.Monad.State.Class.MonadState`, for using `LibPQ` to
@@ -230,6 +283,8 @@ thenDefine = pqThen . define
230
283
231* `runQueryParams` is like `manipulateParams` for query statements.
284* `runQueryParams` is like `manipulateParams` for query statements.
232
285
286+* `runQuery` is like `runQueryParams` for a parameter-free statement.
287+
233* `traversePrepared` has the same type signature as a composition of
288* `traversePrepared` has the same type signature as a composition of
234 `traverse` and `manipulateParams` but provides an optimization by
289 `traverse` and `manipulateParams` but provides an optimization by
235 preparing the statement with `LibPQ.prepare` and then traversing a
290 preparing the statement with `LibPQ.prepare` and then traversing a
@@ -255,42 +310,42 @@ class Monad pq => MonadPQ schema pq | pq -> schema where
255 manipulateParams
310 manipulateParams
256 :: ToParams x params
311 :: ToParams x params
257 => Manipulation schema params ys
312 => Manipulation schema params ys
258− -- ^ `insertInto`, `update` or `deleteFrom`
313+ -- ^ `insertRows`, `update` or `deleteFrom`
259− -> x -> pq (Result ys)
314+ -> x -> pq (K LibPQ.Result ys)
260 default manipulateParams
315 default manipulateParams
261 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
316 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
262 => ToParams x params
317 => ToParams x params
263 => Manipulation schema params ys
318 => Manipulation schema params ys
264− -- ^ `insertInto`, `update` or `deleteFrom`
319+ -- ^ `insertRows`, `update` or `deleteFrom`
265− -> x -> pq (Result ys)
320+ -> x -> pq (K LibPQ.Result ys)
266 manipulateParams manipulation params = lift $
321 manipulateParams manipulation params = lift $
267 manipulateParams manipulation params
322 manipulateParams manipulation params
268
323
269− manipulate :: Manipulation schema '[] ys -> pq (Result ys)
324+ manipulate :: Manipulation schema '[] ys -> pq (K LibPQ.Result ys)
270 manipulate statement = manipulateParams statement ()
325 manipulate statement = manipulateParams statement ()
271
326
272 runQueryParams
327 runQueryParams
273 :: ToParams x params
328 :: ToParams x params
274 => Query schema params ys
329 => Query schema params ys
275 -- ^ `select` and friends
330 -- ^ `select` and friends
276− -> x -> pq (Result ys)
331+ -> x -> pq (K LibPQ.Result ys)
277 runQueryParams = manipulateParams . queryStatement
332 runQueryParams = manipulateParams . queryStatement
278
333
279 runQuery
334 runQuery
280 :: Query schema '[] ys
335 :: Query schema '[] ys
281 -- ^ `select` and friends
336 -- ^ `select` and friends
282− -> pq (Result ys)
337+ -> pq (K LibPQ.Result ys)
283 runQuery q = runQueryParams q ()
338 runQuery q = runQueryParams q ()
284
339
285 traversePrepared
340 traversePrepared
286 :: (ToParams x params, Traversable list)
341 :: (ToParams x params, Traversable list)
287 => Manipulation schema params ys
342 => Manipulation schema params ys
288− -- ^ `insertInto`, `update` or `deleteFrom`
343+ -- ^ `insertRows`, `update`, or `deleteFrom`, and friends
289− -> list x -> pq (list (Result ys))
344+ -> list x -> pq (list (K LibPQ.Result ys))
290 default traversePrepared
345 default traversePrepared
291 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
346 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
292 => (ToParams x params, Traversable list)
347 => (ToParams x params, Traversable list)
293− => Manipulation schema params ys -> list x -> pq (list (Result ys))
348+ => Manipulation schema params ys -> list x -> pq (list (K LibPQ.Result ys))
294 traversePrepared manipulation params = lift $
349 traversePrepared manipulation params = lift $
295 traversePrepared manipulation params
350 traversePrepared manipulation params
296
351
@@ -298,20 +353,20 @@ class Monad pq => MonadPQ schema pq | pq -> schema where
298 :: (ToParams x params, Traversable list)
353 :: (ToParams x params, Traversable list)
299 => list x
354 => list x
300 -> Manipulation schema params ys
355 -> Manipulation schema params ys
301− -- ^ `insertInto`, `update` or `deleteFrom`
356+ -- ^ `insertRows`, `update` or `deleteFrom`
302− -> pq (list (Result ys))
357+ -> pq (list (K LibPQ.Result ys))
303 forPrepared = flip traversePrepared
358 forPrepared = flip traversePrepared
304
359
305 traversePrepared_
360 traversePrepared_
306 :: (ToParams x params, Foldable list)
361 :: (ToParams x params, Foldable list)
307 => Manipulation schema params '[]
362 => Manipulation schema params '[]
308− -- ^ `insertInto`, `update` or `deleteFrom`
363+ -- ^ `insertRows`, `update` or `deleteFrom`
309 -> list x -> pq ()
364 -> list x -> pq ()
310 default traversePrepared_
365 default traversePrepared_
311 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
366 :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
312 => (ToParams x params, Foldable list)
367 => (ToParams x params, Foldable list)
313 => Manipulation schema params '[]
368 => Manipulation schema params '[]
314− -- ^ `insertInto`, `update` or `deleteFrom`
369+ -- ^ `insertRows`, `update` or `deleteFrom`
315 -> list x -> pq ()
370 -> list x -> pq ()
316 traversePrepared_ manipulation params = lift $
371 traversePrepared_ manipulation params = lift $
317 traversePrepared_ manipulation params
372 traversePrepared_ manipulation params
@@ -320,7 +375,7 @@ class Monad pq => MonadPQ schema pq | pq -> schema where
320 :: (ToParams x params, Foldable list)
375 :: (ToParams x params, Foldable list)
321 => list x
376 => list x
322 -> Manipulation schema params '[]
377 -> Manipulation schema params '[]
323− -- ^ `insertInto`, `update` or `deleteFrom`
378+ -- ^ `insertRows`, `update` or `deleteFrom`
324 -> pq ()
379 -> pq ()
325 forPrepared_ = flip traversePrepared_
380 forPrepared_ = flip traversePrepared_
326
381
@@ -330,11 +385,12 @@ class Monad pq => MonadPQ schema pq | pq -> schema where
330 => (LibPQ.Connection -> IO a) -> pq a
385 => (LibPQ.Connection -> IO a) -> pq a
331 liftPQ = lift . liftPQ
386 liftPQ = lift . liftPQ
332
387
333−instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
388+instance (MonadBase IO io, schema0 ~ schema, schema1 ~ schema)
389+ => MonadPQ schema (PQ schema0 schema1 io) where
334
390
335 manipulateParams
391 manipulateParams
336 (UnsafeManipulation q :: Manipulation schema ps ys) (params :: x) =
392 (UnsafeManipulation q :: Manipulation schema ps ys) (params :: x) =
337− PQ $ \ (Connection conn) -> do
393+ PQ $ \ (K conn) -> do
338 let
394 let
339 toParam' bytes = (LibPQ.invalidOid,bytes,LibPQ.Binary)
395 toParam' bytes = (LibPQ.invalidOid,bytes,LibPQ.Binary)
340 params' = fmap (fmap toParam') (hcollapse (toParams @x @ps params))
396 params' = fmap (fmap toParam') (hcollapse (toParams @x @ps params))
@@ -342,11 +398,11 @@ instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
342 case resultMaybe of
398 case resultMaybe of
343 Nothing -> error
399 Nothing -> error
344 "manipulateParams: LibPQ.execParams returned no results"
400 "manipulateParams: LibPQ.execParams returned no results"
345− Just result -> return (Result result, Connection conn)
401+ Just result -> return $ K (K result)
346
402
347 traversePrepared
403 traversePrepared
348 (UnsafeManipulation q :: Manipulation schema xs ys) (list :: list x) =
404 (UnsafeManipulation q :: Manipulation schema xs ys) (list :: list x) =
349− PQ $ \ (Connection conn) -> liftBase $ do
405+ PQ $ \ (K conn) -> liftBase $ do
350 let temp = "temporary_statement"
406 let temp = "temporary_statement"
351 prepResultMaybe <- LibPQ.prepare conn temp q Nothing
407 prepResultMaybe <- LibPQ.prepare conn temp q Nothing
352 case prepResultMaybe of
408 case prepResultMaybe of
@@ -364,7 +420,7 @@ instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
364 case resultMaybe of
420 case resultMaybe of
365 Nothing -> error
421 Nothing -> error
366 "traversePrepared: LibPQ.execParams returned no results"
422 "traversePrepared: LibPQ.execParams returned no results"
367− Just result -> return $ Result result
423+ Just result -> return $ K result
368 deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";")
424 deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";")
369 case deallocResultMaybe of
425 case deallocResultMaybe of
370 Nothing -> error
426 Nothing -> error
@@ -373,11 +429,11 @@ instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
373 status <- LibPQ.resultStatus deallocResult
429 status <- LibPQ.resultStatus deallocResult
374 unless (status == LibPQ.CommandOk) . error $
430 unless (status == LibPQ.CommandOk) . error $
375 "traversePrepared: DEALLOCATE status " <> show status
431 "traversePrepared: DEALLOCATE status " <> show status
376− return (results, Connection conn)
432+ return (K results)
377
433
378 traversePrepared_
434 traversePrepared_
379 (UnsafeManipulation q :: Manipulation schema xs '[]) (list :: list x) =
435 (UnsafeManipulation q :: Manipulation schema xs '[]) (list :: list x) =
380− PQ $ \ (Connection conn) -> liftBase $ do
436+ PQ $ \ (K conn) -> liftBase $ do
381 let temp = "temporary_statement"
437 let temp = "temporary_statement"
382 prepResultMaybe <- LibPQ.prepare conn temp q Nothing
438 prepResultMaybe <- LibPQ.prepare conn temp q Nothing
383 case prepResultMaybe of
439 case prepResultMaybe of
@@ -404,11 +460,11 @@ instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
404 status <- LibPQ.resultStatus deallocResult
460 status <- LibPQ.resultStatus deallocResult
405 unless (status == LibPQ.CommandOk) . error $
461 unless (status == LibPQ.CommandOk) . error $
406 "traversePrepared: DEALLOCATE status " <> show status
462 "traversePrepared: DEALLOCATE status " <> show status
407− return ((), Connection conn)
463+ return (K ())
408
464
409− liftPQ pq = PQ $ \ (Connection conn) -> do
465+ liftPQ pq = PQ $ \ (K conn) -> do
410 y <- liftBase $ pq conn
466 y <- liftBase $ pq conn
411− return (y, Connection conn)
467+ return (K y)
412
468
413instance MonadPQ schema m => MonadPQ schema (IdentityT m)
469instance MonadPQ schema m => MonadPQ schema (IdentityT m)
414instance MonadPQ schema m => MonadPQ schema (ReaderT r m)
470instance MonadPQ schema m => MonadPQ schema (ReaderT r m)
@@ -423,91 +479,58 @@ instance (Monoid w, MonadPQ schema m) => MonadPQ schema (Lazy.RWST r w s m)
423instance MonadPQ schema m => MonadPQ schema (ContT r m)
479instance MonadPQ schema m => MonadPQ schema (ContT r m)
424instance MonadPQ schema m => MonadPQ schema (ListT m)
480instance MonadPQ schema m => MonadPQ schema (ListT m)
425
481
426−instance Monad m => Applicative (PQ schema schema m) where
482+instance (Monad m, schema0 ~ schema1)
427− pure x = PQ $ \ conn -> pure (x, conn)
483+ => Applicative (PQ schema0 schema1 m) where
484+ pure x = PQ $ \ _conn -> pure (K x)
428 (<*>) = pqAp
485 (<*>) = pqAp
429
486
430−instance Monad m => Monad (PQ schema schema m) where
487+instance (Monad m, schema0 ~ schema1)
488+ => Monad (PQ schema0 schema1 m) where
431 return = pure
489 return = pure
432 (>>=) = flip pqBind
490 (>>=) = flip pqBind
433
491
434−instance MonadTrans (PQ schema schema) where
492+instance schema0 ~ schema1 => MFunctor (PQ schema0 schema1) where
435− lift m = PQ $ \ conn -> do
493+ hoist f (PQ pq) = PQ (f . pq)
494+
495+instance schema0 ~ schema1 => MonadTrans (PQ schema0 schema1) where
496+ lift m = PQ $ \ _conn -> do
436 x <- m
497 x <- m
437− return (x, conn)
498+ return (K x)
438
499
439−instance MonadBase b m => MonadBase b (PQ schema schema m) where
500+instance schema0 ~ schema1 => MMonad (PQ schema0 schema1) where
501+ embed f (PQ pq) = PQ $ \ conn -> do
502+ evalPQ (f (pq conn)) conn
503+
504+instance (MonadBase b m, schema0 ~ schema1)
505+ => MonadBase b (PQ schema0 schema1 m) where
440 liftBase = lift . liftBase
506 liftBase = lift . liftBase
441
507
442-- | A snapshot of the state of a `PQ` computation.
508-- | A snapshot of the state of a `PQ` computation.
443type PQRun schema =
509type PQRun schema =
444− forall m x. Monad m => PQ schema schema m x -> m (x, Connection schema)
510+ forall m x. Monad m => PQ schema schema m x -> m (K x schema)
445
511
446-- | Helper function in defining `MonadBaseControl` instance for `PQ`.
512-- | Helper function in defining `MonadBaseControl` instance for `PQ`.
447pqliftWith :: Functor m => (PQRun schema -> m a) -> PQ schema schema m a
513pqliftWith :: Functor m => (PQRun schema -> m a) -> PQ schema schema m a
448pqliftWith f = PQ $ \ conn ->
514pqliftWith f = PQ $ \ conn ->
449− fmap (\ x -> (x, conn)) (f $ \ pq -> runPQ pq conn)
515+ fmap K (f $ \ pq -> unPQ pq conn)
450
516
451−instance MonadBaseControl b m => MonadBaseControl b (PQ schema schema m) where
517+instance (MonadBaseControl b m, schema0 ~ schema1)
452− type StM (PQ schema schema m) x = StM m (x, Connection schema)
518+ => MonadBaseControl b (PQ schema0 schema1 m) where
519+ type StM (PQ schema0 schema1 m) x = StM m (K x schema0)
453 liftBaseWith f =
520 liftBaseWith f =
454 pqliftWith $ \ run -> liftBaseWith $ \ runInBase -> f $ runInBase . run
521 pqliftWith $ \ run -> liftBaseWith $ \ runInBase -> f $ runInBase . run
455 restoreM = PQ . const . restoreM
522 restoreM = PQ . const . restoreM
456
523
457−-- | Encapsulates the result of a squeal command run by @LibPQ@.
524+-- | Get a row corresponding to a given row number from a `LibPQ.Result`,
458−-- `Result`s are parameterized by a `ColumnsType` describing the column names
525+-- throwing an exception if the row number is out of bounds.
459−-- and their types.
460−newtype Result (columns :: ColumnsType)
461− = Result { unResult :: LibPQ.Result }
462−
463−-- | Just newtypes around a `CInt`
464−newtype RowNumber = RowNumber { unRowNumber :: LibPQ.Row }
465−
466−-- | In addition to being newtypes around a `CInt`, a `ColumnNumber` is
467−-- parameterized by a `Nat`ural number and acts as an index into a row.
468−newtype ColumnNumber (n :: Nat) (cs :: [k]) (c :: k) =
469− UnsafeColumnNumber { getColumnNumber :: LibPQ.Column }
470−
471−-- | >>> getColumnNumber (columnNumber @5 @'[_,_,_,_,_,_])
472−-- Col 5
473−class KnownNat n => HasColumnNumber n columns column
474− | n columns -> column where
475− columnNumber :: ColumnNumber n columns column
476− columnNumber =
477− UnsafeColumnNumber . fromIntegral $ natVal' (proxy# :: Proxy# n)
478−instance {-# OVERLAPPING #-} HasColumnNumber 0 (column1:columns) column1
479−instance {-# OVERLAPPABLE #-}
480− (KnownNat n, HasColumnNumber (n-1) columns column)
481− => HasColumnNumber n (column' : columns) column
482−
483−-- | Get a single value corresponding to a given row and column number
484−-- from a `Result`.
485−getValue
486− :: (FromColumnValue colty y, MonadBase IO io)
487− => RowNumber -- ^ row
488− -> ColumnNumber n columns colty -- ^ col
489− -> Result columns -- ^ result
490− -> io y
491−getValue
492− (RowNumber r)
493− (UnsafeColumnNumber c :: ColumnNumber n columns colty)
494− (Result result)
495− = fmap (fromColumnValue @colty . K) $ liftBase $ do
496− numRows <- LibPQ.ntuples result
497− when (numRows < r) $ error $
498− "getValue: expected at least " <> show r <> "rows but only saw "
499− <> show numRows
500− LibPQ.getvalue result r c
501−
502−-- | Get a row corresponding to a given row number from a `Result`.
503getRow
526getRow
504 :: (FromRow columns y, MonadBase IO io)
527 :: (FromRow columns y, MonadBase IO io)
505− => RowNumber
528+ => LibPQ.Row
506− -- ^ row
529+ -- ^ row number
507− -> Result columns
530+ -> K LibPQ.Result columns
508 -- ^ result
531 -- ^ result
509 -> io y
532 -> io y
510−getRow (RowNumber r) (Result result :: Result columns) = liftBase $ do
533+getRow r (K result :: K LibPQ.Result columns) = liftBase $ do
511 numRows <- LibPQ.ntuples result
534 numRows <- LibPQ.ntuples result
512 when (numRows < r) $ error $
535 when (numRows < r) $ error $
513 "getRow: expected at least " <> show r <> "rows but only saw "
536 "getRow: expected at least " <> show r <> "rows but only saw "
@@ -518,34 +541,30 @@ getRow (RowNumber r) (Result result :: Result columns) = liftBase $ do
518 Nothing -> error "getRow: found unexpected length"
541 Nothing -> error "getRow: found unexpected length"
519 Just row -> return $ fromRow @columns row
542 Just row -> return $ fromRow @columns row
520
543
521−-- | Returns the number of rows (tuples) in the query result.
522−ntuples :: MonadBase IO io => Result columns -> io RowNumber
523−ntuples (Result result) = liftBase $ RowNumber <$> LibPQ.ntuples result
524−
525-- | Intended to be used for unfolding in streaming libraries, `nextRow`
544-- | Intended to be used for unfolding in streaming libraries, `nextRow`
526-- takes a total number of rows (which can be found with `ntuples`)
545-- takes a total number of rows (which can be found with `ntuples`)
527−-- and a `Result` and given a row number if it's too large returns `Nothing`,
546+-- and a `LibPQ.Result` and given a row number if it's too large returns `Nothing`,
528-- otherwise returning the row along with the next row number.
547-- otherwise returning the row along with the next row number.
529nextRow
548nextRow
530 :: (FromRow columns y, MonadBase IO io)
549 :: (FromRow columns y, MonadBase IO io)
531− => RowNumber -- ^ total number of rows
550+ => LibPQ.Row -- ^ total number of rows
532− -> Result columns -- ^ result
551+ -> K LibPQ.Result columns -- ^ result
533− -> RowNumber -- ^ row number
552+ -> LibPQ.Row -- ^ row number
534− -> io (Maybe (RowNumber,y))
553+ -> io (Maybe (LibPQ.Row,y))
535−nextRow (RowNumber total) (Result result :: Result columns) (RowNumber r)
554+nextRow total (K result :: K LibPQ.Result columns) r
536 = liftBase $ if r >= total then return Nothing else do
555 = liftBase $ if r >= total then return Nothing else do
537 let len = fromIntegral (lengthSList (Proxy @columns))
556 let len = fromIntegral (lengthSList (Proxy @columns))
538 row' <- traverse (LibPQ.getvalue result r) [0 .. len - 1]
557 row' <- traverse (LibPQ.getvalue result r) [0 .. len - 1]
539 case fromList row' of
558 case fromList row' of
540 Nothing -> error "nextRow: found unexpected length"
559 Nothing -> error "nextRow: found unexpected length"
541− Just row -> return $ Just (RowNumber (r+1), fromRow @columns row)
560+ Just row -> return $ Just (r+1, fromRow @columns row)
542
561
543−-- | Get all rows from a `Result`.
562+-- | Get all rows from a `LibPQ.Result`.
544getRows
563getRows
545 :: (FromRow columns y, MonadBase IO io)
564 :: (FromRow columns y, MonadBase IO io)
546− => Result columns -- ^ result
565+ => K LibPQ.Result columns -- ^ result
547 -> io [y]
566 -> io [y]
548−getRows (Result result :: Result columns) = liftBase $ do
567+getRows (K result :: K LibPQ.Result columns) = liftBase $ do
549 let len = fromIntegral (lengthSList (Proxy @columns))
568 let len = fromIntegral (lengthSList (Proxy @columns))
550 numRows <- LibPQ.ntuples result
569 numRows <- LibPQ.ntuples result
551 for [0 .. numRows - 1] $ \ r -> do
570 for [0 .. numRows - 1] $ \ r -> do
@@ -554,12 +573,12 @@ getRows (Result result :: Result columns) = liftBase $ do
554 Nothing -> error "getRows: found unexpected length"
573 Nothing -> error "getRows: found unexpected length"
555 Just row -> return $ fromRow @columns row
574 Just row -> return $ fromRow @columns row
556
575
557−-- | Get the first row if possible from a `Result`.
576+-- | Get the first row if possible from a `LibPQ.Result`.
558firstRow
577firstRow
559 :: (FromRow columns y, MonadBase IO io)
578 :: (FromRow columns y, MonadBase IO io)
560− => Result columns -- ^ result
579+ => K LibPQ.Result columns -- ^ result
561 -> io (Maybe y)
580 -> io (Maybe y)
562−firstRow (Result result :: Result columns) = liftBase $ do
581+firstRow (K result :: K LibPQ.Result columns) = liftBase $ do
563 numRows <- LibPQ.ntuples result
582 numRows <- LibPQ.ntuples result
564 if numRows <= 0 then return Nothing else do
583 if numRows <= 0 then return Nothing else do
565 let len = fromIntegral (lengthSList (Proxy @columns))
584 let len = fromIntegral (lengthSList (Proxy @columns))
@@ -568,9 +587,23 @@ firstRow (Result result :: Result columns) = liftBase $ do
568 Nothing -> error "firstRow: found unexpected length"
587 Nothing -> error "firstRow: found unexpected length"
569 Just row -> return . Just $ fromRow @columns row
588 Just row -> return . Just $ fromRow @columns row
570
589
571−-- | Lifts actions on results from `LibPQ`.
590+-- | Lifts actions on results from @LibPQ@.
572liftResult
591liftResult
573 :: MonadBase IO io
592 :: MonadBase IO io
574 => (LibPQ.Result -> IO x)
593 => (LibPQ.Result -> IO x)
575− -> Result results -> io x
594+ -> K LibPQ.Result results -> io x
576−liftResult f (Result result) = liftBase $ f result
595+liftResult f (K result) = liftBase $ f result
596+
597+-- | Returns the number of rows (tuples) in the query result.
598+ntuples :: MonadBase IO io => K LibPQ.Result columns -> io LibPQ.Row
599+ntuples = liftResult LibPQ.ntuples
600+
601+-- | Returns the result status of the command.
602+resultStatus :: MonadBase IO io => K LibPQ.Result results -> io LibPQ.ExecStatus
603+resultStatus = liftResult LibPQ.resultStatus
604+
605+-- | Returns the error message most recently generated by an operation
606+-- on the connection.
607+resultErrorMessage
608+ :: MonadBase IO io => K LibPQ.Result results -> io (Maybe ByteString)
609+resultErrorMessage = liftResult LibPQ.resultErrorMessage
squeal-postgresql/src/Squeal/PostgreSQL/Pool.hsnew file+133
@@ -0,0 +1,133 @@
1+{-|
2+Module: Squeal.PostgreSQL.Definition
3+Description: Pooled connections
4+Copyright: (c) Eitan Chatav, 2017
5+Maintainer: eitan@morphism.tech
6+Stability: experimental
7+
8+A `MonadPQ` for pooled connections.
9+-}
10+
11+{-# LANGUAGE
12+ DataKinds
13+ , DeriveFunctor
14+ , FlexibleContexts
15+ , FlexibleInstances
16+ , MultiParamTypeClasses
17+ , RankNTypes
18+ , ScopedTypeVariables
19+ , TypeFamilies
20+ , TypeInType
21+ , UndecidableInstances
22+#-}
23+
24+module Squeal.PostgreSQL.Pool
25+ ( -- * Pools
26+ PoolPQ (..)
27+ , createConnectionPool
28+ , Pool
29+ , destroyAllResources
30+ , PoolPQRun
31+ , poolpqliftWith
32+ ) where
33+
34+import Control.Monad.Base
35+import Control.Monad.Trans
36+import Control.Monad.Trans.Control
37+import Data.ByteString
38+import Data.Pool
39+import Data.Time
40+import Generics.SOP (K(..))
41+
42+import Squeal.PostgreSQL.PQ
43+import Squeal.PostgreSQL.Schema
44+
45+-- | `PoolPQ` @schema@ should be a drop-in replacement for `PQ` @schema schema@.
46+newtype PoolPQ (schema :: TablesType) m x =
47+ PoolPQ { runPoolPQ :: Pool (K Connection schema) -> m x }
48+ deriving Functor
49+
50+-- | Create a striped pool of connections.
51+-- Although the garbage collector will destroy all idle connections when the pool is garbage collected it's recommended to manually `destroyAllResources` when you're done with the pool so that the connections are freed up as soon as possible.
52+createConnectionPool
53+ :: MonadBase IO io
54+ => ByteString
55+ -- ^ The passed string can be empty to use all default parameters, or it can
56+ -- contain one or more parameter settings separated by whitespace.
57+ -- Each parameter setting is in the form keyword = value. Spaces around the equal
58+ -- sign are optional. To write an empty value or a value containing spaces,
59+ -- surround it with single quotes, e.g., keyword = 'a value'. Single quotes and
60+ -- backslashes within the value must be escaped with a backslash, i.e., ' and \.
61+ -> Int
62+ -- ^ The number of stripes (distinct sub-pools) to maintain. The smallest acceptable value is 1.
63+ -> NominalDiffTime
64+ -- ^ Amount of time for which an unused connection is kept open. The smallest acceptable value is 0.5 seconds.
65+ -- The elapsed time before destroying a connection may be a little longer than requested, as the reaper thread wakes at 1-second intervals.
66+ -> Int
67+ -- ^ Maximum number of connections to keep open per stripe. The smallest acceptable value is 1.
68+ -- Requests for connections will block if this limit is reached on a single stripe, even if other stripes have idle connections available.
69+ -> io (Pool (K Connection schema))
70+createConnectionPool conninfo stripes idle maxResrc = liftBase $
71+ createPool (connectdb conninfo) finish stripes idle maxResrc
72+
73+-- | `Applicative` instance for `PoolPQ`.
74+instance Monad m => Applicative (PoolPQ schema m) where
75+ pure x = PoolPQ $ \ _ -> pure x
76+ PoolPQ f <*> PoolPQ x = PoolPQ $ \ pool -> do
77+ f' <- f pool
78+ x' <- x pool
79+ return $ f' x'
80+
81+-- | `Monad` instance for `PoolPQ`.
82+instance Monad m => Monad (PoolPQ schema m) where
83+ return = pure
84+ PoolPQ x >>= f = PoolPQ $ \ pool -> do
85+ x' <- x pool
86+ runPoolPQ (f x') pool
87+
88+-- | `MonadTrans` instance for `PoolPQ`.
89+instance MonadTrans (PoolPQ schema) where
90+ lift m = PoolPQ $ \ _pool -> m
91+
92+-- | `MonadBase` instance for `PoolPQ`.
93+instance MonadBase b m => MonadBase b (PoolPQ schema m) where
94+ liftBase = lift . liftBase
95+
96+-- | `MonadPQ` instance for `PoolPQ`.
97+instance MonadBaseControl IO io => MonadPQ schema (PoolPQ schema io) where
98+ manipulateParams manipulation params = PoolPQ $ \ pool -> do
99+ withResource pool $ \ conn -> do
100+ (K result :: K (K Result ys) schema) <- flip unPQ conn $
101+ manipulateParams manipulation params
102+ return result
103+ traversePrepared manipulation params = PoolPQ $ \ pool ->
104+ withResource pool $ \ conn -> do
105+ (K result :: K (list (K Result ys)) schema) <- flip unPQ conn $
106+ traversePrepared manipulation params
107+ return result
108+ traversePrepared_ manipulation params = PoolPQ $ \ pool -> do
109+ withResource pool $ \ conn -> do
110+ (_ :: K () schema) <- flip unPQ conn $
111+ traversePrepared_ manipulation params
112+ return ()
113+ liftPQ m = PoolPQ $ \ pool ->
114+ withResource pool $ \ conn -> do
115+ (K result :: K result schema) <- flip unPQ conn $
116+ liftPQ m
117+ return result
118+
119+-- | A snapshot of the state of a `PoolPQ` computation.
120+type PoolPQRun schema =
121+ forall m x. Monad m => PoolPQ schema m x -> m x
122+
123+-- | Helper function in defining `MonadBaseControl` instance for `PoolPQ`.
124+poolpqliftWith :: Functor m => (PoolPQRun schema -> m a) -> PoolPQ schema m a
125+poolpqliftWith f = PoolPQ $ \ pool ->
126+ (f $ \ pq -> runPoolPQ pq pool)
127+
128+-- | `MonadBaseControl` instance for `PoolPQ`.
129+instance MonadBaseControl b m => MonadBaseControl b (PoolPQ schema m) where
130+ type StM (PoolPQ schema m) x = StM m x
131+ liftBaseWith f =
132+ poolpqliftWith $ \ run -> liftBaseWith $ \ runInBase -> f $ runInBase . run
133+ restoreM = PoolPQ . const . restoreM
squeal-postgresql/src/Squeal/PostgreSQL/Query.hs+287−248
@@ -12,6 +12,7 @@ Squeal queries.
12 DataKinds
12 DataKinds
13 , DeriveDataTypeable
13 , DeriveDataTypeable
14 , DeriveGeneric
14 , DeriveGeneric
15+ , FlexibleContexts
15 , FlexibleInstances
16 , FlexibleInstances
16 , GADTs
17 , GADTs
17 , GeneralizedNewtypeDeriving
18 , GeneralizedNewtypeDeriving
@@ -55,7 +56,13 @@ module Squeal.PostgreSQL.Query
55 , offset
56 , offset
56 -- * From
57 -- * From
57 , FromClause (..)
58 , FromClause (..)
58− , renderFromClause
59+ , table
60+ , subquery
61+ , crossJoin
62+ , innerJoin
63+ , leftOuterJoin
64+ , rightOuterJoin
65+ , fullOuterJoin
59 -- * Grouping
66 -- * Grouping
60 , By (By, By2)
67 , By (By, By2)
61 , renderBy
68 , renderBy
@@ -70,7 +77,7 @@ module Squeal.PostgreSQL.Query
70
77
71import Control.DeepSeq
78import Control.DeepSeq
72import Data.ByteString (ByteString)
79import Data.ByteString (ByteString)
73−import Data.Monoid
80+import Data.Monoid hiding (All)
74import Data.String
81import Data.String
75import Data.Word
82import Data.Word
76import Generics.SOP hiding (from)
83import Generics.SOP hiding (from)
@@ -79,7 +86,7 @@ import GHC.TypeLits
79import qualified GHC.Generics as GHC
86import qualified GHC.Generics as GHC
80
87
81import Squeal.PostgreSQL.Expression
88import Squeal.PostgreSQL.Expression
82−import Squeal.PostgreSQL.Prettyprint
89+import Squeal.PostgreSQL.Render
83import Squeal.PostgreSQL.Schema
90import Squeal.PostgreSQL.Schema
84
91
85{- |
92{- |
@@ -92,9 +99,11 @@ simple query:
92
99
93>>> :{
100>>> :{
94let
101let
95− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
102+ query :: Query
96− '["col" ::: 'Required ('Null 'PGint4)]
103+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
97− query = selectStar (from (Table (#tab `As` #t)))
104+ '[]
105+ '["col" ::: 'Null 'PGint4]
106+ query = selectStar (from (table (#tab `As` #t)))
98in renderQuery query
107in renderQuery query
99:}
108:}
100"SELECT * FROM tab AS t"
109"SELECT * FROM tab AS t"
@@ -104,16 +113,16 @@ restricted query:
104>>> :{
113>>> :{
105let
114let
106 query :: Query
115 query :: Query
107− '[ "tab" :::
116+ '[ "tab" ::: '[] :=>
108− '[ "col1" ::: 'Required ('NotNull 'PGint4)
117+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
109− , "col2" ::: 'Required ('NotNull 'PGint4) ]]
118+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]]
110 '[]
119 '[]
111− '[ "sum" ::: 'Required ('NotNull 'PGint4)
120+ '[ "sum" ::: 'NotNull 'PGint4
112− , "col1" ::: 'Required ('NotNull 'PGint4) ]
121+ , "col1" ::: 'NotNull 'PGint4 ]
113 query =
122 query =
114 select
123 select
115 ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
124 ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
116− ( from (Table (#tab `As` #t))
125+ ( from (table (#tab `As` #t))
117 & where_ (#col1 .> #col2)
126 & where_ (#col1 .> #col2)
118 & where_ (#col2 .> 0) )
127 & where_ (#col2 .> 0) )
119in renderQuery query
128in renderQuery query
@@ -124,11 +133,13 @@ subquery:
124
133
125>>> :{
134>>> :{
126let
135let
127− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
136+ query :: Query
128− '["col" ::: 'Required ('Null 'PGint4)]
137+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
138+ '[]
139+ '["col" ::: 'Null 'PGint4]
129 query =
140 query =
130 selectStar
141 selectStar
131− (from (Subquery (selectStar (from (Table (#tab `As` #t))) `As` #sub)))
142+ (from (subquery (selectStar (from (table (#tab `As` #t))) `As` #sub)))
132in renderQuery query
143in renderQuery query
133:}
144:}
134"SELECT * FROM (SELECT * FROM tab AS t) AS sub"
145"SELECT * FROM (SELECT * FROM tab AS t) AS sub"
@@ -137,10 +148,12 @@ limits and offsets:
137
148
138>>> :{
149>>> :{
139let
150let
140− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
151+ query :: Query
141− '["col" ::: 'Required ('Null 'PGint4)]
152+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
153+ '[]
154+ '["col" ::: 'Null 'PGint4]
142 query = selectStar
155 query = selectStar
143− (from (Table (#tab `As` #t)) & limit 100 & offset 2 & limit 50 & offset 2)
156+ (from (table (#tab `As` #t)) & limit 100 & offset 2 & limit 50 & offset 2)
144in renderQuery query
157in renderQuery query
145:}
158:}
146"SELECT * FROM tab AS t LIMIT 50 OFFSET 4"
159"SELECT * FROM tab AS t LIMIT 50 OFFSET 4"
@@ -149,11 +162,12 @@ parameterized query:
149
162
150>>> :{
163>>> :{
151let
164let
152− query :: Query '["tab" ::: '["col" ::: 'Required ('NotNull 'PGfloat8)]]
165+ query :: Query
153− '[ 'Required ('NotNull 'PGfloat8)]
166+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'NotNull 'PGfloat8]]
154− '["col" ::: 'Required ('NotNull 'PGfloat8)]
167+ '[ 'NotNull 'PGfloat8]
168+ '["col" ::: 'NotNull 'PGfloat8]
155 query = selectStar
169 query = selectStar
156− (from (Table (#tab `As` #t)) & where_ (#col .> param @1))
170+ (from (table (#tab `As` #t)) & where_ (#col .> param @1))
157in renderQuery query
171in renderQuery query
158:}
172:}
159"SELECT * FROM tab AS t WHERE (col > ($1 :: float8))"
173"SELECT * FROM tab AS t WHERE (col > ($1 :: float8))"
@@ -163,15 +177,15 @@ aggregation query:
163>>> :{
177>>> :{
164let
178let
165 query :: Query
179 query :: Query
166− '[ "tab" :::
180+ '[ "tab" ::: '[] :=>
167− '[ "col1" ::: 'Required ('NotNull 'PGint4)
181+ '[ "col1" ::: 'NoDef :=> 'NotNull 'PGint4
168− , "col2" ::: 'Required ('NotNull 'PGint4) ]]
182+ , "col2" ::: 'NoDef :=> 'NotNull 'PGint4 ]]
169 '[]
183 '[]
170− '[ "sum" ::: 'Required ('NotNull 'PGint4)
184+ '[ "sum" ::: 'NotNull 'PGint4
171− , "col1" ::: 'Required ('NotNull 'PGint4) ]
185+ , "col1" ::: 'NotNull 'PGint4 ]
172 query =
186 query =
173 select (sum_ #col2 `As` #sum :* #col1 `As` #col1 :* Nil)
187 select (sum_ #col2 `As` #sum :* #col1 `As` #col1 :* Nil)
174− ( from (Table (#tab `As` #table1))
188+ ( from (table (#tab `As` #table1))
175 & group (By #col1 :* Nil)
189 & group (By #col1 :* Nil)
176 & having (#col1 + sum_ #col2 .> 1) )
190 & having (#col1 + sum_ #col2 .> 1) )
177in renderQuery query
191in renderQuery query
@@ -182,10 +196,12 @@ sorted query:
182
196
183>>> :{
197>>> :{
184let
198let
185− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
199+ query :: Query
186− '["col" ::: 'Required ('Null 'PGint4)]
200+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
201+ '[]
202+ '["col" ::: 'Null 'PGint4]
187 query = selectStar
203 query = selectStar
188− (from (Table (#tab `As` #t)) & orderBy [#col & AscNullsFirst])
204+ (from (table (#tab `As` #t)) & orderBy [#col & AscNullsFirst])
189in renderQuery query
205in renderQuery query
190:}
206:}
191"SELECT * FROM tab AS t ORDER BY col ASC NULLS FIRST"
207"SELECT * FROM tab AS t ORDER BY col ASC NULLS FIRST"
@@ -197,33 +213,38 @@ joins:
197let
213let
198 query :: Query
214 query :: Query
199 '[ "orders" :::
215 '[ "orders" :::
200− '[ "id" ::: 'Required ('NotNull 'PGint4)
216+ '["pk_orders" ::: PrimaryKey '["id"]
201− , "price" ::: 'Required ('NotNull 'PGfloat4)
217+ ,"fk_customers" ::: ForeignKey '["customer_id"] "customers" '["id"]
202− , "customer_id" ::: 'Required ('NotNull 'PGint4)
218+ ,"fk_shippers" ::: ForeignKey '["shipper_id"] "shippers" '["id"]] :=>
203− , "shipper_id" ::: 'Required ('NotNull 'PGint4)
219+ '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4
220+ , "price" ::: 'NoDef :=> 'NotNull 'PGfloat4
221+ , "customer_id" ::: 'NoDef :=> 'NotNull 'PGint4
222+ , "shipper_id" ::: 'NoDef :=> 'NotNull 'PGint4
204 ]
223 ]
205 , "customers" :::
224 , "customers" :::
206− '[ "id" ::: 'Required ('NotNull 'PGint4)
225+ '["pk_customers" ::: PrimaryKey '["id"]] :=>
207− , "name" ::: 'Required ('NotNull 'PGtext)
226+ '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4
227+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
208 ]
228 ]
209 , "shippers" :::
229 , "shippers" :::
210− '[ "id" ::: 'Required ('NotNull 'PGint4)
230+ '["pk_shippers" ::: PrimaryKey '["id"]] :=>
211− , "name" ::: 'Required ('NotNull 'PGtext)
231+ '[ "id" ::: 'NoDef :=> 'NotNull 'PGint4
232+ , "name" ::: 'NoDef :=> 'NotNull 'PGtext
212 ]
233 ]
213 ]
234 ]
214 '[]
235 '[]
215− '[ "order_price" ::: 'Required ('NotNull 'PGfloat4)
236+ '[ "order_price" ::: 'NotNull 'PGfloat4
216− , "customer_name" ::: 'Required ('NotNull 'PGtext)
237+ , "customer_name" ::: 'NotNull 'PGtext
217− , "shipper_name" ::: 'Required ('NotNull 'PGtext)
238+ , "shipper_name" ::: 'NotNull 'PGtext
218 ]
239 ]
219 query = select
240 query = select
220 ( #o ! #price `As` #order_price :*
241 ( #o ! #price `As` #order_price :*
221 #c ! #name `As` #customer_name :*
242 #c ! #name `As` #customer_name :*
222 #s ! #name `As` #shipper_name :* Nil )
243 #s ! #name `As` #shipper_name :* Nil )
223− ( from (Table (#orders `As` #o)
244+ ( from (table (#orders `As` #o)
224− & InnerJoin (Table (#customers `As` #c))
245+ & innerJoin (table (#customers `As` #c))
225 (#o ! #customer_id .== #c ! #id)
246 (#o ! #customer_id .== #c ! #id)
226− & InnerJoin (Table (#shippers `As` #s))
247+ & innerJoin (table (#shippers `As` #s))
227 (#o ! #shipper_id .== #s ! #id)) )
248 (#o ! #shipper_id .== #s ! #id)) )
228in renderQuery query
249in renderQuery query
229:}
250:}
@@ -233,10 +254,12 @@ self-join:
233
254
234>>> :{
255>>> :{
235let
256let
236− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
257+ query :: Query
237− '["col" ::: 'Required ('Null 'PGint4)]
258+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
259+ '[]
260+ '["col" ::: 'Null 'PGint4]
238 query = selectDotStar #t1
261 query = selectDotStar #t1
239− (from (Table (#tab `As` #t1) & CrossJoin (Table (#tab `As` #t2))))
262+ (from (table (#tab `As` #t1) & crossJoin (table (#tab `As` #t2))))
240in renderQuery query
263in renderQuery query
241:}
264:}
242"SELECT t1.* FROM tab AS t1 CROSS JOIN tab AS t2"
265"SELECT t1.* FROM tab AS t1 CROSS JOIN tab AS t2"
@@ -245,21 +268,22 @@ set operations:
245
268
246>>> :{
269>>> :{
247let
270let
248− query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
271+ query :: Query
249− '["col" ::: 'Required ('Null 'PGint4)]
272+ '["tab" ::: '[] :=> '["col" ::: 'NoDef :=> 'Null 'PGint4]]
273+ '[]
274+ '["col" ::: 'Null 'PGint4]
250 query =
275 query =
251− selectStar (from (Table (#tab `As` #t)))
276+ selectStar (from (table (#tab `As` #t)))
252 `unionAll`
277 `unionAll`
253− selectStar (from (Table (#tab `As` #t)))
278+ selectStar (from (table (#tab `As` #t)))
254in renderQuery query
279in renderQuery query
255:}
280:}
256"(SELECT * FROM tab AS t) UNION ALL (SELECT * FROM tab AS t)"
281"(SELECT * FROM tab AS t) UNION ALL (SELECT * FROM tab AS t)"
257-}
282-}
258−
259newtype Query
283newtype Query
260 (schema :: TablesType)
284 (schema :: TablesType)
261− (params :: [ColumnType])
285+ (params :: [NullityType])
262− (columns :: ColumnsType)
286+ (columns :: RelationType)
263 = UnsafeQuery { renderQuery :: ByteString }
287 = UnsafeQuery { renderQuery :: ByteString }
264 deriving (GHC.Generic,Show,Eq,Ord,NFData)
288 deriving (GHC.Generic,Show,Eq,Ord,NFData)
265
289
@@ -340,73 +364,73 @@ SELECT queries
340-- the intermediate table are actually output.
364-- the intermediate table are actually output.
341select
365select
342 :: SListI columns
366 :: SListI columns
343− => NP (Aliased (Expression tables grouping params)) (column ': columns)
367+ => NP (Aliased (Expression relations grouping params)) (column ': columns)
344 -- ^ select list
368 -- ^ select list
345− -> TableExpression schema params tables grouping
369+ -> TableExpression schema params relations grouping
346 -- ^ intermediate virtual table
370 -- ^ intermediate virtual table
347 -> Query schema params (column ': columns)
371 -> Query schema params (column ': columns)
348−select list tabs = UnsafeQuery $
372+select list rels = UnsafeQuery $
349 "SELECT"
373 "SELECT"
350− <+> renderCommaSeparated (renderAliased renderExpression) list
374+ <+> renderCommaSeparated (renderAliasedAs renderExpression) list
351− <+> renderTableExpression tabs
375+ <+> renderTableExpression rels
352
376
353-- | After the select list has been processed, the result table can
377-- | After the select list has been processed, the result table can
354-- be subject to the elimination of duplicate rows using `selectDistinct`.
378-- be subject to the elimination of duplicate rows using `selectDistinct`.
355selectDistinct
379selectDistinct
356 :: SListI columns
380 :: SListI columns
357− => NP (Aliased (Expression tables 'Ungrouped params)) (column ': columns)
381+ => NP (Aliased (Expression relations 'Ungrouped params)) (column ': columns)
358 -- ^ select list
382 -- ^ select list
359− -> TableExpression schema params tables 'Ungrouped
383+ -> TableExpression schema params relations 'Ungrouped
360 -- ^ intermediate virtual table
384 -- ^ intermediate virtual table
361 -> Query schema params (column ': columns)
385 -> Query schema params (column ': columns)
362−selectDistinct list tabs = UnsafeQuery $
386+selectDistinct list rels = UnsafeQuery $
363 "SELECT DISTINCT"
387 "SELECT DISTINCT"
364− <+> renderCommaSeparated (renderAliased renderExpression) list
388+ <+> renderCommaSeparated (renderAliasedAs renderExpression) list
365− <+> renderTableExpression tabs
389+ <+> renderTableExpression rels
366
390
367-- | The simplest kind of query is `selectStar` which emits all columns
391-- | The simplest kind of query is `selectStar` which emits all columns
368-- that the table expression produces.
392-- that the table expression produces.
369selectStar
393selectStar
370− :: HasUnique table tables columns
394+ :: HasUnique relation relations columns
371− => TableExpression schema params tables 'Ungrouped
395+ => TableExpression schema params relations 'Ungrouped
372 -- ^ intermediate virtual table
396 -- ^ intermediate virtual table
373 -> Query schema params columns
397 -> Query schema params columns
374−selectStar tabs = UnsafeQuery $ "SELECT" <+> "*" <+> renderTableExpression tabs
398+selectStar rels = UnsafeQuery $ "SELECT" <+> "*" <+> renderTableExpression rels
375
399
376-- | A `selectDistinctStar` emits all columns that the table expression
400-- | A `selectDistinctStar` emits all columns that the table expression
377-- produces and eliminates duplicate rows.
401-- produces and eliminates duplicate rows.
378selectDistinctStar
402selectDistinctStar
379− :: HasUnique table tables columns
403+ :: HasUnique relation relations columns
380− => TableExpression schema params tables 'Ungrouped
404+ => TableExpression schema params relations 'Ungrouped
381 -- ^ intermediate virtual table
405 -- ^ intermediate virtual table
382 -> Query schema params columns
406 -> Query schema params columns
383−selectDistinctStar tabs = UnsafeQuery $
407+selectDistinctStar rels = UnsafeQuery $
384− "SELECT DISTINCT" <+> "*" <+> renderTableExpression tabs
408+ "SELECT DISTINCT" <+> "*" <+> renderTableExpression rels
385
409
386-- | When working with multiple tables, it can also be useful to ask
410-- | When working with multiple tables, it can also be useful to ask
387-- for all the columns of a particular table, using `selectDotStar`.
411-- for all the columns of a particular table, using `selectDotStar`.
388selectDotStar
412selectDotStar
389− :: HasTable table tables columns
413+ :: Has relation relations columns
390− => Alias table
414+ => Alias relation
391 -- ^ particular virtual subtable
415 -- ^ particular virtual subtable
392− -> TableExpression schema params tables 'Ungrouped
416+ -> TableExpression schema params relations 'Ungrouped
393 -- ^ intermediate virtual table
417 -- ^ intermediate virtual table
394 -> Query schema params columns
418 -> Query schema params columns
395−selectDotStar table tables = UnsafeQuery $
419+selectDotStar rel relations = UnsafeQuery $
396− "SELECT" <+> renderAlias table <> ".*" <+> renderTableExpression tables
420+ "SELECT" <+> renderAlias rel <> ".*" <+> renderTableExpression relations
397
421
398-- | A `selectDistinctDotStar` asks for all the columns of a particular table,
422-- | A `selectDistinctDotStar` asks for all the columns of a particular table,
399-- and eliminates duplicate rows.
423-- and eliminates duplicate rows.
400selectDistinctDotStar
424selectDistinctDotStar
401− :: HasTable table tables columns
425+ :: Has relation relations columns
402− => Alias table
426+ => Alias relation
403− -- ^ particular virtual subtable
427+ -- ^ particular virtual table
404− -> TableExpression schema params tables 'Ungrouped
428+ -> TableExpression schema params relations 'Ungrouped
405 -- ^ intermediate virtual table
429 -- ^ intermediate virtual table
406 -> Query schema params columns
430 -> Query schema params columns
407−selectDistinctDotStar table tables = UnsafeQuery $
431+selectDistinctDotStar rel relations = UnsafeQuery $
408− "SELECT DISTINCT" <+> renderAlias table <> ".*"
432+ "SELECT DISTINCT" <+> renderAlias rel <> ".*"
409− <+> renderTableExpression tables
433+ <+> renderTableExpression relations
410
434
411{-----------------------------------------
435{-----------------------------------------
412Table Expressions
436Table Expressions
@@ -420,14 +444,14 @@ Table Expressions
420-- can be used to modify or combine base tables in various ways.
444-- can be used to modify or combine base tables in various ways.
421data TableExpression
445data TableExpression
422 (schema :: TablesType)
446 (schema :: TablesType)
423− (params :: [ColumnType])
447+ (params :: [NullityType])
424− (tables :: TablesType)
448+ (relations :: RelationsType)
425 (grouping :: Grouping)
449 (grouping :: Grouping)
426 = TableExpression
450 = TableExpression
427− { fromClause :: FromClause schema params tables
451+ { fromClause :: FromClause schema params relations
428 -- ^ A table reference that can be a table name, or a derived table such
452 -- ^ A table reference that can be a table name, or a derived table such
429 -- as a subquery, a @JOIN@ construct, or complex combinations of these.
453 -- as a subquery, a @JOIN@ construct, or complex combinations of these.
430− , whereClause :: [Condition tables 'Ungrouped params]
454+ , whereClause :: [Condition relations 'Ungrouped params]
431 -- ^ optional search coditions, combined with `.&&`. After the processing
455 -- ^ optional search coditions, combined with `.&&`. After the processing
432 -- of the `fromClause` is done, each row of the derived virtual table
456 -- of the `fromClause` is done, each row of the derived virtual table
433 -- is checked against the search condition. If the result of the
457 -- is checked against the search condition. If the result of the
@@ -436,20 +460,20 @@ data TableExpression
436 -- at least one column of the table generated in the `fromClause`;
460 -- at least one column of the table generated in the `fromClause`;
437 -- this is not required, but otherwise the WHERE clause will
461 -- this is not required, but otherwise the WHERE clause will
438 -- be fairly useless.
462 -- be fairly useless.
439− , groupByClause :: GroupByClause tables grouping
463+ , groupByClause :: GroupByClause relations grouping
440 -- ^ The `groupByClause` is used to group together those rows in a table
464 -- ^ The `groupByClause` is used to group together those rows in a table
441 -- that have the same values in all the columns listed. The order in which
465 -- that have the same values in all the columns listed. The order in which
442 -- the columns are listed does not matter. The effect is to combine each
466 -- the columns are listed does not matter. The effect is to combine each
443 -- set of rows having common values into one group row that represents all
467 -- set of rows having common values into one group row that represents all
444 -- rows in the group. This is done to eliminate redundancy in the output
468 -- rows in the group. This is done to eliminate redundancy in the output
445 -- and/or compute aggregates that apply to these groups.
469 -- and/or compute aggregates that apply to these groups.
446− , havingClause :: HavingClause tables grouping params
470+ , havingClause :: HavingClause relations grouping params
447 -- ^ If a table has been grouped using `groupBy`, but only certain groups
471 -- ^ If a table has been grouped using `groupBy`, but only certain groups
448 -- are of interest, the `havingClause` can be used, much like a
472 -- are of interest, the `havingClause` can be used, much like a
449 -- `whereClause`, to eliminate groups from the result. Expressions in the
473 -- `whereClause`, to eliminate groups from the result. Expressions in the
450 -- `havingClause` can refer both to grouped expressions and to ungrouped
474 -- `havingClause` can refer both to grouped expressions and to ungrouped
451 -- expressions (which necessarily involve an aggregate function).
475 -- expressions (which necessarily involve an aggregate function).
452− , orderByClause :: [SortExpression tables grouping params]
476+ , orderByClause :: [SortExpression relations grouping params]
453 -- ^ The `orderByClause` is for optional sorting. When more than one
477 -- ^ The `orderByClause` is for optional sorting. When more than one
454 -- `SortExpression` is specified, the later (right) values are used to sort
478 -- `SortExpression` is specified, the later (right) values are used to sort
455 -- rows that are equal according to the earlier (left) values.
479 -- rows that are equal according to the earlier (left) values.
@@ -467,11 +491,11 @@ data TableExpression
467
491
468-- | Render a `TableExpression`
492-- | Render a `TableExpression`
469renderTableExpression
493renderTableExpression
470− :: TableExpression schema params tables grouping
494+ :: TableExpression schema params relations grouping
471 -> ByteString
495 -> ByteString
472renderTableExpression
496renderTableExpression
473− (TableExpression tables whs' grps' hvs' srts' lims' offs') = mconcat
497+ (TableExpression frm' whs' grps' hvs' srts' lims' offs') = mconcat
474− [ "FROM ", renderFromClause tables
498+ [ "FROM ", renderFromClause frm'
475 , renderWheres whs'
499 , renderWheres whs'
476 , renderGroupByClause grps'
500 , renderGroupByClause grps'
477 , renderHavingClause hvs'
501 , renderHavingClause hvs'
@@ -501,68 +525,68 @@ renderTableExpression
501-- `group`, `having`, `orderBy`, `limit` and `offset`, using the `&` operator
525-- `group`, `having`, `orderBy`, `limit` and `offset`, using the `&` operator
502-- to match the left-to-right sequencing of their placement in SQL.
526-- to match the left-to-right sequencing of their placement in SQL.
503from
527from
504− :: FromClause schema params tables -- ^ table reference
528+ :: FromClause schema params relations -- ^ table reference
505− -> TableExpression schema params tables 'Ungrouped
529+ -> TableExpression schema params relations 'Ungrouped
506−from tables = TableExpression tables [] NoGroups NoHaving [] [] []
530+from rels = TableExpression rels [] NoGroups NoHaving [] [] []
507
531
508-- | A `where_` is an endomorphism of `TableExpression`s which adds a
532-- | A `where_` is an endomorphism of `TableExpression`s which adds a
509-- search condition to the `whereClause`.
533-- search condition to the `whereClause`.
510where_
534where_
511− :: Condition tables 'Ungrouped params
535+ :: Condition relations 'Ungrouped params -- ^ filtering condition
512− -> TableExpression schema params tables grouping
536+ -> TableExpression schema params relations grouping
513− -> TableExpression schema params tables grouping
537+ -> TableExpression schema params relations grouping
514−where_ wh tables = tables {whereClause = wh : whereClause tables}
538+where_ wh rels = rels {whereClause = wh : whereClause rels}
515
539
516-- | A `group` is a transformation of `TableExpression`s which switches
540-- | A `group` is a transformation of `TableExpression`s which switches
517-- its `Grouping` from `Ungrouped` to `Grouped`. Use @group Nil@ to perform
541-- its `Grouping` from `Ungrouped` to `Grouped`. Use @group Nil@ to perform
518-- a "grand total" aggregation query.
542-- a "grand total" aggregation query.
519group
543group
520 :: SListI bys
544 :: SListI bys
521− => NP (By tables) bys
545+ => NP (By relations) bys -- ^ grouped columns
522− -> TableExpression schema params tables 'Ungrouped
546+ -> TableExpression schema params relations 'Ungrouped
523− -> TableExpression schema params tables ('Grouped bys)
547+ -> TableExpression schema params relations ('Grouped bys)
524−group bys tables = TableExpression
548+group bys rels = TableExpression
525− { fromClause = fromClause tables
549+ { fromClause = fromClause rels
526− , whereClause = whereClause tables
550+ , whereClause = whereClause rels
527 , groupByClause = Group bys
551 , groupByClause = Group bys
528 , havingClause = Having []
552 , havingClause = Having []
529 , orderByClause = []
553 , orderByClause = []
530− , limitClause = limitClause tables
554+ , limitClause = limitClause rels
531− , offsetClause = offsetClause tables
555+ , offsetClause = offsetClause rels
532 }
556 }
533
557
534-- | A `having` is an endomorphism of `TableExpression`s which adds a
558-- | A `having` is an endomorphism of `TableExpression`s which adds a
535-- search condition to the `havingClause`.
559-- search condition to the `havingClause`.
536having
560having
537− :: Condition tables ('Grouped bys) params
561+ :: Condition relations ('Grouped bys) params -- ^ having condition
538− -> TableExpression schema params tables ('Grouped bys)
562+ -> TableExpression schema params relations ('Grouped bys)
539− -> TableExpression schema params tables ('Grouped bys)
563+ -> TableExpression schema params relations ('Grouped bys)
540−having hv tables = tables
564+having hv rels = rels
541− { havingClause = case havingClause tables of Having hvs -> Having (hv:hvs) }
565+ { havingClause = case havingClause rels of Having hvs -> Having (hv:hvs) }
542
566
543-- | An `orderBy` is an endomorphism of `TableExpression`s which appends an
567-- | An `orderBy` is an endomorphism of `TableExpression`s which appends an
544-- ordering to the right of the `orderByClause`.
568-- ordering to the right of the `orderByClause`.
545orderBy
569orderBy
546− :: [SortExpression tables grouping params]
570+ :: [SortExpression relations grouping params] -- ^ sort expressions
547− -> TableExpression schema params tables grouping
571+ -> TableExpression schema params relations grouping
548− -> TableExpression schema params tables grouping
572+ -> TableExpression schema params relations grouping
549−orderBy srts tables = tables {orderByClause = orderByClause tables ++ srts}
573+orderBy srts rels = rels {orderByClause = orderByClause rels ++ srts}
550
574
551-- | A `limit` is an endomorphism of `TableExpression`s which adds to the
575-- | A `limit` is an endomorphism of `TableExpression`s which adds to the
552-- `limitClause`.
576-- `limitClause`.
553limit
577limit
554− :: Word64
578+ :: Word64 -- ^ limit parameter
555− -> TableExpression schema params tables grouping
579+ -> TableExpression schema params relations grouping
556− -> TableExpression schema params tables grouping
580+ -> TableExpression schema params relations grouping
557−limit lim tables = tables {limitClause = lim : limitClause tables}
581+limit lim rels = rels {limitClause = lim : limitClause rels}
558
582
559-- | An `offset` is an endomorphism of `TableExpression`s which adds to the
583-- | An `offset` is an endomorphism of `TableExpression`s which adds to the
560-- `offsetClause`.
584-- `offsetClause`.
561offset
585offset
562− :: Word64
586+ :: Word64 -- ^ offset parameter
563− -> TableExpression schema params tables grouping
587+ -> TableExpression schema params relations grouping
564− -> TableExpression schema params tables grouping
588+ -> TableExpression schema params relations grouping
565−offset off tables = tables {offsetClause = off : offsetClause tables}
589+offset off rels = rels {offsetClause = off : offsetClause rels}
566
590
567{-----------------------------------------
591{-----------------------------------------
568FROM clauses
592FROM clauses
@@ -571,93 +595,107 @@ FROM clauses
571{- |
595{- |
572A `FromClause` can be a table name, or a derived table such
596A `FromClause` can be a table name, or a derived table such
573as a subquery, a @JOIN@ construct, or complex combinations of these.
597as a subquery, a @JOIN@ construct, or complex combinations of these.
574−
598+-}
575−* A real `Table` is a table from the schema.
599+newtype FromClause schema params relations
576−
600+ = UnsafeFromClause { renderFromClause :: ByteString }
577−* `Subquery` derives a table from a `Query`.
601+ deriving (GHC.Generic,Show,Eq,Ord,NFData)
578−
602+
579−* A joined table is a table derived from two other (real or derived) tables
603+-- | A real `table` is a table from the schema.
580−according to the rules of the particular join type. `CrossJoin`, `InnerJoin`,
604+table
581−`LeftOuterJoin`, `RightOuterJoin` and `FullOuterJoin` are available and can
605+ :: Aliased (Table schema) table
582−be nested using the `&` operator to match the left-to-right sequencing of
606+ -> FromClause schema params '[table]
583−their placement in SQL.
607+table = UnsafeFromClause . renderAliasedAs renderTable
584−
608+
585− * @t1 & CrossJoin t2@. For every possible combination of rows from
609+-- | `subquery` derives a table from a `Query`.
586− @t1@ and @t2@ (i.e., a Cartesian product), the joined table will contain
610+subquery
587− a row consisting of all columns in @t1@ followed by all columns in @t2@.
611+ :: Aliased (Query schema params) table
612+ -> FromClause schema params '[table]
613+subquery = UnsafeFromClause . renderAliasedAs (parenthesized . renderQuery)
614+
615+{- | @left & crossJoin right@. For every possible combination of rows from
616+ @left@ and @right@ (i.e., a Cartesian product), the joined table will contain
617+ a row consisting of all columns in @left@ followed by all columns in @right@.
588 If the tables have @n@ and @m@ rows respectively, the joined table will
618 If the tables have @n@ and @m@ rows respectively, the joined table will
589 have @n * m@ rows.
619 have @n * m@ rows.
590−
620+-}
591− * @t1 & InnerJoin t2 on@. For each row @r1@ of @t1@, the joined
621+crossJoin
592− table has a row for each row in @t2@ that satisfies the @on@ condition
622+ :: FromClause schema params right
593− with @r1@
623+ -- ^ right
594−
624+ -> FromClause schema params left
595− * @t1 & LeftOuterJoin t2 on@. First, an inner join is performed.
625+ -- ^ left
596− Then, for each row in @t1@ that does not satisfy the @on@ condition with
626+ -> FromClause schema params (Join left right)
597− any row in @t2@, a joined row is added with null values in columns of @t2@.
627+crossJoin right left = UnsafeFromClause $
598− Thus, the joined table always has at least one row for each row in @t1@.
628+ renderFromClause left <+> "CROSS JOIN" <+> renderFromClause right
599−
629+
600− * @t1 & RightOuterJoin t2 on@. First, an inner join is performed.
630+{- | @left & innerJoin right on@. The joined table is filtered by
601− Then, for each row in @t2@ that does not satisfy the @on@ condition with
631+the @on@ condition.
602− any row in @t1@, a joined row is added with null values in columns of @t1@.
632+-}
633+innerJoin
634+ :: FromClause schema params right
635+ -- ^ right
636+ -> Condition (Join left right) 'Ungrouped params
637+ -- ^ @on@ condition
638+ -> FromClause schema params left
639+ -- ^ left
640+ -> FromClause schema params (Join left right)
641+innerJoin right on left = UnsafeFromClause $
642+ renderFromClause left <+> "INNER JOIN" <+> renderFromClause right
643+ <+> "ON" <+> renderExpression on
644+
645+{- | @left & leftOuterJoin right on@. First, an inner join is performed.
646+ Then, for each row in @left@ that does not satisfy the @on@ condition with
647+ any row in @right@, a joined row is added with null values in columns of @right@.
648+ Thus, the joined table always has at least one row for each row in @left@.
649+-}
650+leftOuterJoin
651+ :: FromClause schema params right
652+ -- ^ right
653+ -> Condition (Join left right) 'Ungrouped params
654+ -- ^ @on@ condition
655+ -> FromClause schema params left
656+ -- ^ left
657+ -> FromClause schema params (Join left (NullifyRelations right))
658+leftOuterJoin right on left = UnsafeFromClause $
659+ renderFromClause left <+> "LEFT OUTER JOIN" <+> renderFromClause right
660+ <+> "ON" <+> renderExpression on
661+
662+{- | @left & rightOuterJoin right on@. First, an inner join is performed.
663+ Then, for each row in @right@ that does not satisfy the @on@ condition with
664+ any row in @left@, a joined row is added with null values in columns of @left@.
603 This is the converse of a left join: the result table will always
665 This is the converse of a left join: the result table will always
604− have a row for each row in @t2@.
666+ have a row for each row in @right@.
605−
667+-}
606− * @t1 & FullOuterJoin t2 on@. First, an inner join is performed.
668+rightOuterJoin
607− Then, for each row in @t1@ that does not satisfy the @on@ condition with
669+ :: FromClause schema params right
608− any row in @t2@, a joined row is added with null values in columns of @t2@.
670+ -- ^ right
609− Also, for each row of @t2@ that does not satisfy the join condition
671+ -> Condition (Join left right) 'Ungrouped params
610− with any row in @t1@, a joined row with null values in the columns of @t1@
672+ -- ^ @on@ condition
673+ -> FromClause schema params left
674+ -- ^ left
675+ -> FromClause schema params (Join (NullifyRelations left) right)
676+rightOuterJoin right on left = UnsafeFromClause $
677+ renderFromClause left <+> "RIGHT OUTER JOIN" <+> renderFromClause right
678+ <+> "ON" <+> renderExpression on
679+
680+{- | @left & fullOuterJoin right on@. First, an inner join is performed.
681+ Then, for each row in @left@ that does not satisfy the @on@ condition with
682+ any row in @right@, a joined row is added with null values in columns of @right@.
683+ Also, for each row of @right@ that does not satisfy the join condition
684+ with any row in @left@, a joined row with null values in the columns of @left@
611 is added.
685 is added.
612-}
686-}
613−data FromClause schema params tables where
687+fullOuterJoin
614− Table
688+ :: FromClause schema params right
615− :: Aliased (Table schema) table
689+ -- ^ right
616− -> FromClause schema params '[table]
690+ -> Condition (Join left right) 'Ungrouped params
617− Subquery
691+ -- ^ @on@ condition
618− :: Aliased (Query schema params) table
692+ -> FromClause schema params left
619− -> FromClause schema params '[table]
693+ -- ^ left
620− CrossJoin
694+ -> FromClause schema params
621− :: FromClause schema params right
695+ (Join (NullifyRelations left) (NullifyRelations right))
622− -> FromClause schema params left
696+fullOuterJoin right on left = UnsafeFromClause $
623− -> FromClause schema params (Join left right)
697+ renderFromClause left <+> "FULL OUTER JOIN" <+> renderFromClause right
624− InnerJoin
698+ <+> "ON" <+> renderExpression on
625− :: FromClause schema params right
626− -> Condition (Join left right) 'Ungrouped params
627− -> FromClause schema params left
628− -> FromClause schema params (Join left right)
629− LeftOuterJoin
630− :: FromClause schema params right
631− -> Condition (Join left right) 'Ungrouped params
632− -> FromClause schema params left
633− -> FromClause schema params (Join left (NullifyTables right))
634− RightOuterJoin
635− :: FromClause schema params right
636− -> Condition (Join left right) 'Ungrouped params
637− -> FromClause schema params left
638− -> FromClause schema params (Join (NullifyTables left) right)
639− FullOuterJoin
640− :: FromClause schema params right
641− -> Condition (Join left right) 'Ungrouped params
642− -> FromClause schema params left
643− -> FromClause schema params
644− (Join (NullifyTables left) (NullifyTables right))
645−
646−-- | Renders a `FromClause`.
647−renderFromClause :: FromClause schema params tables -> ByteString
648−renderFromClause = \case
649− Table table -> renderAliased renderTable table
650− Subquery selection -> renderAliased (parenthesized . renderQuery) selection
651− CrossJoin right left ->
652− renderFromClause left <+> "CROSS JOIN" <+> renderFromClause right
653− InnerJoin right on left -> renderJoin "INNER JOIN" right on left
654− LeftOuterJoin right on left -> renderJoin "LEFT OUTER JOIN" right on left
655− RightOuterJoin right on left -> renderJoin "RIGHT OUTER JOIN" right on left
656− FullOuterJoin right on left -> renderJoin "FULL OUTER JOIN" right on left
657− where
658− renderJoin op right on left =
659− renderFromClause left <+> op <+> renderFromClause right
660− <+> "ON" <+> renderExpression on
661
699
662{-----------------------------------------
700{-----------------------------------------
663Grouping
701Grouping
@@ -669,40 +707,41 @@ Grouping
669-- column @col@; otherwise @By2 (\#tab \! \#col)@ will reference a table
707-- column @col@; otherwise @By2 (\#tab \! \#col)@ will reference a table
670-- qualified column @tab.col@.
708-- qualified column @tab.col@.
671data By
709data By
672− (tables :: TablesType)
710+ (relations :: RelationsType)
673 (by :: (Symbol,Symbol)) where
711 (by :: (Symbol,Symbol)) where
674 By
712 By
675− :: (HasUnique table tables columns, HasColumn column columns ty)
713+ :: (HasUnique relation relations columns, Has column columns ty)
676 => Alias column
714 => Alias column
677− -> By tables '(table, column)
715+ -> By relations '(relation, column)
678 By2
716 By2
679− :: (HasTable table tables columns, HasColumn column columns ty)
717+ :: (Has relation relations columns, Has column columns ty)
680− => (Alias table, Alias column)
718+ => (Alias relation, Alias column)
681− -> By tables '(table, column)
719+ -> By relations '(relation, column)
682−deriving instance Show (By tables by)
720+deriving instance Show (By relations by)
683−deriving instance Eq (By tables by)
721+deriving instance Eq (By relations by)
684−deriving instance Ord (By tables by)
722+deriving instance Ord (By relations by)
685
723
686-- | Renders a `By`.
724-- | Renders a `By`.
687−renderBy :: By tables tabcolty -> ByteString
725+renderBy :: By relations by -> ByteString
688renderBy = \case
726renderBy = \case
689 By column -> renderAlias column
727 By column -> renderAlias column
690− By2 (table, column) -> renderAlias table <> "." <> renderAlias column
728+ By2 (rel, column) -> renderAlias rel <> "." <> renderAlias column
691
729
692-- | A `GroupByClause` indicates the `Grouping` of a `TableExpression`.
730-- | A `GroupByClause` indicates the `Grouping` of a `TableExpression`.
693-- A `NoGroups` indicates `Ungrouped` while a `Group` indicates `Grouped`.
731-- A `NoGroups` indicates `Ungrouped` while a `Group` indicates `Grouped`.
694-- @NoGroups@ is distinguised from @Group Nil@ since no aggregation can be
732-- @NoGroups@ is distinguised from @Group Nil@ since no aggregation can be
695-- done on @NoGroups@ while all output `Expression`s must be aggregated
733-- done on @NoGroups@ while all output `Expression`s must be aggregated
696−-- in @Group Nil@.
734+-- in @Group Nil@. In general, all output `Expression`s in the
697−data GroupByClause tables grouping where
735+-- complement of @bys@ must be aggregated in @Group bys@.
698− NoGroups :: GroupByClause tables 'Ungrouped
736+data GroupByClause relations grouping where
737+ NoGroups :: GroupByClause relations 'Ungrouped
699 Group
738 Group
700 :: SListI bys
739 :: SListI bys
701− => NP (By tables) bys
740+ => NP (By relations) bys
702− -> GroupByClause tables ('Grouped bys)
741+ -> GroupByClause relations ('Grouped bys)
703
742
704-- | Renders a `GroupByClause`.
743-- | Renders a `GroupByClause`.
705−renderGroupByClause :: GroupByClause tables grouping -> ByteString
744+renderGroupByClause :: GroupByClause relations grouping -> ByteString
706renderGroupByClause = \case
745renderGroupByClause = \case
707 NoGroups -> ""
746 NoGroups -> ""
708 Group Nil -> ""
747 Group Nil -> ""
@@ -712,17 +751,17 @@ renderGroupByClause = \case
712-- An `Ungrouped` `TableExpression` may only use `NoHaving` while a `Grouped`
751-- An `Ungrouped` `TableExpression` may only use `NoHaving` while a `Grouped`
713-- `TableExpression` must use `Having` whose conditions are combined with
752-- `TableExpression` must use `Having` whose conditions are combined with
714-- `.&&`.
753-- `.&&`.
715−data HavingClause tables grouping params where
754+data HavingClause relations grouping params where
716− NoHaving :: HavingClause tables 'Ungrouped params
755+ NoHaving :: HavingClause relations 'Ungrouped params
717 Having
756 Having
718− :: [Condition tables ('Grouped bys) params]
757+ :: [Condition relations ('Grouped bys) params]
719− -> HavingClause tables ('Grouped bys) params
758+ -> HavingClause relations ('Grouped bys) params
720−deriving instance Show (HavingClause tables grouping params)
759+deriving instance Show (HavingClause relations grouping params)
721−deriving instance Eq (HavingClause tables grouping params)
760+deriving instance Eq (HavingClause relations grouping params)
722−deriving instance Ord (HavingClause tables grouping params)
761+deriving instance Ord (HavingClause relations grouping params)
723
762
724-- | Render a `HavingClause`.
763-- | Render a `HavingClause`.
725−renderHavingClause :: HavingClause tables grouping params -> ByteString
764+renderHavingClause :: HavingClause relations grouping params -> ByteString
726renderHavingClause = \case
765renderHavingClause = \case
727 NoHaving -> ""
766 NoHaving -> ""
728 Having [] -> ""
767 Having [] -> ""
@@ -741,29 +780,29 @@ Sorting
741-- `AscNullsLast`, `DescNullsFirst` and `DescNullsLast` options are used to
780-- `AscNullsLast`, `DescNullsFirst` and `DescNullsLast` options are used to
742-- determine whether nulls appear before or after non-null values in the sort
781-- determine whether nulls appear before or after non-null values in the sort
743-- ordering of a `Null` result column.
782-- ordering of a `Null` result column.
744−data SortExpression tables grouping params where
783+data SortExpression relations grouping params where
745 Asc
784 Asc
746− :: Expression tables grouping params ('Required ('NotNull ty))
785+ :: Expression relations grouping params ('NotNull ty)
747− -> SortExpression tables grouping params
786+ -> SortExpression relations grouping params
748 Desc
787 Desc
749− :: Expression tables grouping params ('Required ('NotNull ty))
788+ :: Expression relations grouping params ('NotNull ty)
750− -> SortExpression tables grouping params
789+ -> SortExpression relations grouping params
751 AscNullsFirst
790 AscNullsFirst
752− :: Expression tables grouping params ('Required ('Null ty))
791+ :: Expression relations grouping params ('Null ty)
753− -> SortExpression tables grouping params
792+ -> SortExpression relations grouping params
754 AscNullsLast
793 AscNullsLast
755− :: Expression tables grouping params ('Required ('Null ty))
794+ :: Expression relations grouping params ('Null ty)
756− -> SortExpression tables grouping params
795+ -> SortExpression relations grouping params
757 DescNullsFirst
796 DescNullsFirst
758− :: Expression tables grouping params ('Required ('Null ty))
797+ :: Expression relations grouping params ('Null ty)
759− -> SortExpression tables grouping params
798+ -> SortExpression relations grouping params
760 DescNullsLast
799 DescNullsLast
761− :: Expression tables grouping params ('Required ('Null ty))
800+ :: Expression relations grouping params ('Null ty)
762− -> SortExpression tables grouping params
801+ -> SortExpression relations grouping params
763−deriving instance Show (SortExpression tables grouping params)
802+deriving instance Show (SortExpression relations grouping params)
764
803
765-- | Render a `SortExpression`.
804-- | Render a `SortExpression`.
766−renderSortExpression :: SortExpression tables grouping params -> ByteString
805+renderSortExpression :: SortExpression relations grouping params -> ByteString
767renderSortExpression = \case
806renderSortExpression = \case
768 Asc expression -> renderExpression expression <+> "ASC"
807 Asc expression -> renderExpression expression <+> "ASC"
769 Desc expression -> renderExpression expression <+> "DESC"
808 Desc expression -> renderExpression expression <+> "DESC"
squeal-postgresql/src/Squeal/PostgreSQL/Prettyprint.hs → squeal-postgresql/src/Squeal/PostgreSQL/Render.hsrenamed+12−4
@@ -1,11 +1,11 @@
1{-|
1{-|
2−Module: Squeal.PostgreSQL.PrettyPrint
2+Module: Squeal.PostgreSQL.Render
3−Description: Pretty print helper functions
3+Description: Rendering helper functions
4Copyright: (c) Eitan Chatav, 2017
4Copyright: (c) Eitan Chatav, 2017
5Maintainer: eitan@morphism.tech
5Maintainer: eitan@morphism.tech
6Stability: experimental
6Stability: experimental
7
7
8−Pretty print helper functions.
8+Rendering helper functions.
9-}
9-}
10
10
11{-# LANGUAGE
11{-# LANGUAGE
@@ -17,7 +17,15 @@ Pretty print helper functions.
17 , TypeApplications
17 , TypeApplications
18#-}
18#-}
19
19
20−module Squeal.PostgreSQL.Prettyprint where
20+module Squeal.PostgreSQL.Render
21+ ( -- * Render
22+ parenthesized
23+ , (<+>)
24+ , commaSeparated
25+ , renderCommaSeparated
26+ , renderCommaSeparatedMaybe
27+ , renderNat
28+ ) where
21
29
22import Data.ByteString (ByteString)
30import Data.ByteString (ByteString)
23import Data.Maybe
31import Data.Maybe
squeal-postgresql/src/Squeal/PostgreSQL/Schema.hs+347−102
@@ -5,17 +5,19 @@ Copyright: (c) Eitan Chatav, 2017
5Maintainer: eitan@morphism.tech
5Maintainer: eitan@morphism.tech
6Stability: experimental
6Stability: experimental
7
7
8−Embedding of PostgreSQL type and alias system
8+A type-level DSL for kinds of PostgreSQL types, constraints, and aliases.
9-}
9-}
10
10
11{-# LANGUAGE
11{-# LANGUAGE
12− ConstraintKinds
12+ AllowAmbiguousTypes
13+ , ConstraintKinds
13 , DataKinds
14 , DataKinds
14 , DeriveAnyClass
15 , DeriveAnyClass
15 , DeriveDataTypeable
16 , DeriveDataTypeable
16 , DeriveGeneric
17 , DeriveGeneric
17 , FlexibleContexts
18 , FlexibleContexts
18 , FlexibleInstances
19 , FlexibleInstances
20+ , FunctionalDependencies
19 , GADTs
21 , GADTs
20 , MagicHash
22 , MagicHash
21 , MultiParamTypeClasses
23 , MultiParamTypeClasses
@@ -28,43 +30,67 @@ Embedding of PostgreSQL type and alias system
28 , TypeFamilies
30 , TypeFamilies
29 , TypeInType
31 , TypeInType
30 , TypeOperators
32 , TypeOperators
33+ , UndecidableInstances
31#-}
34#-}
32
35
33module Squeal.PostgreSQL.Schema
36module Squeal.PostgreSQL.Schema
34− ( -- * Kinds
37+ ( -- * Types
35 PGType (..)
38 PGType (..)
39+ , HasOid (..)
36 , NullityType (..)
40 , NullityType (..)
37− , ColumnType (..)
41+ , ColumnType
38 , ColumnsType
42 , ColumnsType
43+ , RelationType
44+ , NilRelation
45+ , RelationsType
46+ , TableType
39 , TablesType
47 , TablesType
48+ , NilTables
49+ -- * Grouping
40 , Grouping (..)
50 , Grouping (..)
51+ , GroupedBy
41 -- * Constraints
52 -- * Constraints
42− , PGNum
53+ , (:=>)
43− , PGIntegral
54+ , ColumnConstraint (..)
44− , PGFloating
55+ , TableConstraint (..)
56+ , TableConstraints
57+ , NilTableConstraints
45 -- * Aliases
58 -- * Aliases
46 , (:::)
59 , (:::)
47 , Alias (Alias)
60 , Alias (Alias)
48 , renderAlias
61 , renderAlias
49 , Aliased (As)
62 , Aliased (As)
50− , renderAliased
63+ , renderAliasedAs
64+ , AliasesOf
65+ , Has
66+ , HasUnique
51 , IsLabel (..)
67 , IsLabel (..)
52− , IsTableColumn (..)
68+ , IsQualified (..)
53 -- * Type Families
69 -- * Type Families
54− , In
55− , HasUnique
56− , BaseType
57− , SameTypes
58− , AllNotNull
59− , NotAllNull
60− , NullifyType
61− , NullifyColumns
62− , NullifyTables
63 , Join
70 , Join
64 , Create
71 , Create
65 , Drop
72 , Drop
66 , Alter
73 , Alter
67 , Rename
74 , Rename
75+ , Elem
76+ , In
77+ , PGNum
78+ , PGIntegral
79+ , PGFloating
80+ , PGTypeOf
81+ , SameTypes
82+ , AllNotNull
83+ , NotAllNull
84+ , NullifyType
85+ , NullifyRelation
86+ , NullifyRelations
87+ , ColumnsToRelation
88+ , RelationToColumns
89+ , TableToColumns
90+ , TablesToRelations
91+ , RelationsToTables
92+ , ConstraintInvolves
93+ , DropIfConstraintsInvolve
68 -- * Generics
94 -- * Generics
69 , SameField
95 , SameField
70 , SameFields
96 , SameFields
@@ -74,6 +100,8 @@ import Control.DeepSeq
74import Data.ByteString
100import Data.ByteString
75import Data.Monoid
101import Data.Monoid
76import Data.String
102import Data.String
103+import Data.Word
104+import Data.Type.Bool
77import Generics.SOP (AllZip)
105import Generics.SOP (AllZip)
78import GHC.Generics (Generic)
106import GHC.Generics (Generic)
79import GHC.Exts
107import GHC.Exts
@@ -83,6 +111,10 @@ import GHC.TypeLits
83import qualified Generics.SOP.Type.Metadata as Type
111import qualified Generics.SOP.Type.Metadata as Type
84
112
85-- | `PGType` is the promoted datakind of PostgreSQL types.
113-- | `PGType` is the promoted datakind of PostgreSQL types.
114+--
115+-- >>> import Squeal.PostgreSQL.Schema
116+-- >>> :kind 'PGbool
117+-- 'PGbool :: PGType
86data PGType
118data PGType
87 = PGbool -- ^ logical Boolean (true/false)
119 = PGbool -- ^ logical Boolean (true/false)
88 | PGint2 -- ^ signed two-byte integer
120 | PGint2 -- ^ signed two-byte integer
@@ -105,61 +137,214 @@ data PGType
105 | PGinet -- ^ IPv4 or IPv6 host address
137 | PGinet -- ^ IPv4 or IPv6 host address
106 | PGjson -- ^ textual JSON data
138 | PGjson -- ^ textual JSON data
107 | PGjsonb -- ^ binary JSON data, decomposed
139 | PGjsonb -- ^ binary JSON data, decomposed
140+ | PGvararray PGType -- ^ variable length array
141+ | PGfixarray Nat PGType -- ^ fixed length array
108 | UnsafePGType Symbol -- ^ an escape hatch for unsupported PostgreSQL types
142 | UnsafePGType Symbol -- ^ an escape hatch for unsupported PostgreSQL types
109
143
144+-- | The object identifier of a `PGType`.
145+--
146+-- >>> :set -XTypeApplications
147+-- >>> oid @'PGbool
148+-- 16
149+class HasOid (ty :: PGType) where oid :: Word32
150+instance HasOid 'PGbool where oid = 16
151+instance HasOid 'PGint2 where oid = 21
152+instance HasOid 'PGint4 where oid = 23
153+instance HasOid 'PGint8 where oid = 20
154+instance HasOid 'PGnumeric where oid = 1700
155+instance HasOid 'PGfloat4 where oid = 700
156+instance HasOid 'PGfloat8 where oid = 701
157+instance HasOid ('PGchar n) where oid = 18
158+instance HasOid ('PGvarchar n) where oid = 1043
159+instance HasOid 'PGtext where oid = 25
160+instance HasOid 'PGbytea where oid = 17
161+instance HasOid 'PGtimestamp where oid = 1114
162+instance HasOid 'PGtimestamptz where oid = 1184
163+instance HasOid 'PGdate where oid = 1082
164+instance HasOid 'PGtime where oid = 1083
165+instance HasOid 'PGtimetz where oid = 1266
166+instance HasOid 'PGinterval where oid = 1186
167+instance HasOid 'PGuuid where oid = 2950
168+instance HasOid 'PGinet where oid = 869
169+instance HasOid 'PGjson where oid = 114
170+instance HasOid 'PGjsonb where oid = 3802
171+
110-- | `NullityType` encodes the potential presence or definite absence of a
172-- | `NullityType` encodes the potential presence or definite absence of a
111-- @NULL@ allowing operations which are sensitive to such to be well typed.
173-- @NULL@ allowing operations which are sensitive to such to be well typed.
174+--
175+-- >>> :kind 'Null 'PGint4
176+-- 'Null 'PGint4 :: NullityType
177+-- >>> :kind 'NotNull ('PGvarchar 50)
178+-- 'NotNull ('PGvarchar 50) :: NullityType
112data NullityType
179data NullityType
113 = Null PGType -- ^ @NULL@ may be present
180 = Null PGType -- ^ @NULL@ may be present
114 | NotNull PGType -- ^ @NULL@ is absent
181 | NotNull PGType -- ^ @NULL@ is absent
115
182
116−-- | `ColumnType` encodes the allowance of @DEFAULT@ and the only way
183+-- | The constraint operator, `:=>` is a type level pair
117−-- to generate an `Optional` `Squeal.PostgreSQL.Expression.Expression`
184+-- between a "constraint" and some type, for use in pairing
118−-- is to use `Squeal.PostgreSQL.Expression.def`,
185+-- a `ColumnConstraint` with a `NullityType` to produce a `ColumnType`
119−-- `Squeal.PostgreSQL.Expression.unDef` or
186+-- or a `TableConstraints` and a `ColumnsType` to produce a `TableType`.
120−-- `Squeal.PostgreSQL.Expression.param`.
187+type (:=>) constraint ty = '(constraint,ty)
121−data ColumnType
188+infixr 7 :=>
122− = Optional NullityType
189+
123− -- ^ @DEFAULT@ is allowed
190+-- | The alias operator `:::` is like a promoted version of `As`,
124− | Required NullityType
191+-- a type level pair between
125− -- ^ @DEFAULT@ is not allowed
192+-- an alias and some type, like a column alias and either a `ColumnType` or
126−
193+-- `NullityType` or a table alias and either a `TableType` or a `RelationType`
127−-- | `ColumnsType` is a kind synonym for a row of `ColumnType`s.
194+-- or a constraint alias and a `TableConstraint`.
195+type (:::) (alias :: Symbol) ty = '(alias,ty)
196+infixr 6 :::
197+
198+-- | `ColumnConstraint` encodes the availability of @DEFAULT@ for inserts and updates.
199+-- A column can be assigned a default value.
200+-- A data `Squeal.PostgreSQL.Manipulations.Manipulation` command can also
201+-- request explicitly that a column be set to its default value,
202+-- without having to know what that value is.
203+data ColumnConstraint
204+ = Def -- ^ @DEFAULT@ is available for inserts and updates
205+ | NoDef -- ^ @DEFAULT@ is unavailable for inserts and updates
206+
207+-- | `ColumnType` encodes the allowance of @DEFAULT@ and @NULL@ and the
208+-- base `PGType` for a column.
209+--
210+-- >>> :set -XTypeFamilies -XTypeInType
211+-- >>> import GHC.TypeLits
212+-- >>> type family IdColumn :: ColumnType where IdColumn = 'Def :=> 'NotNull 'PGint4
213+-- >>> type family EmailColumn :: ColumnType where EmailColumn = 'NoDef :=> 'Null 'PGtext
214+type ColumnType = (ColumnConstraint,NullityType)
215+
216+-- | `ColumnsType` is a row of `ColumnType`s.
217+--
218+-- >>> :{
219+-- type family UsersColumns :: ColumnsType where
220+-- UsersColumns =
221+-- '[ "name" ::: 'NoDef :=> 'NotNull 'PGtext
222+-- , "id" ::: 'Def :=> 'NotNull 'PGint4
223+-- ]
224+-- :}
128type ColumnsType = [(Symbol,ColumnType)]
225type ColumnsType = [(Symbol,ColumnType)]
129
226
130−-- | `TablesType` is a kind synonym for a row of `ColumnsType`s.
227+-- | `TableConstraint` encodes various forms of data constraints
131−-- It is used as a kind for both a schema, a disjoint union of tables,
228+-- of columns in a table.
132−-- and a joined table `Squeal.PostgreSQL.Query.FromClause`,
229+-- `TableConstraint`s give you as much control over the data in your tables
133−-- a product of tables.
230+-- as you wish. If a user attempts to store data in a column that would
134−type TablesType = [(Symbol,ColumnsType)]
231+-- violate a constraint, an error is raised. This applies
232+-- even if the value came from the default value definition.
233+data TableConstraint
234+ = Check [Symbol]
235+ | Unique [Symbol]
236+ | PrimaryKey [Symbol]
237+ | ForeignKey [Symbol] Symbol [Symbol]
238+
239+-- | A `TableConstraints` is a row of `TableConstraint`s.
240+type TableConstraints = [(Symbol,TableConstraint)]
241+-- | A monokinded empty `TableConstraints`.
242+type family NilTableConstraints :: TableConstraints where
243+ NilTableConstraints = '[]
244+
245+-- | `TableType` encodes a row of constraints on a table as well as the types
246+-- of its columns.
247+--
248+-- >>> :{
249+-- type family UsersTable :: TableType where
250+-- UsersTable =
251+-- '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
252+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
253+-- , "name" ::: 'NoDef :=> 'NotNull 'PGtext
254+-- ]
255+-- :}
256+type TableType = (TableConstraints,ColumnsType)
257+
258+-- | `TablesType` is a row of `TableType`s, thought of as a union.
259+--
260+-- >>> :{
261+-- type family Schema :: TablesType where
262+-- Schema =
263+-- '[ "users" :::
264+-- '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
265+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
266+-- , "name" ::: 'NoDef :=> 'NotNull 'PGtext
267+-- , "vec" ::: 'NoDef :=> 'NotNull ('PGvararray 'PGint2)
268+-- ]
269+-- , "emails" :::
270+-- '[ "pk_emails" ::: 'PrimaryKey '["id"]
271+-- , "fk_user_id" ::: 'ForeignKey '["user_id"] "users" '["id"]
272+-- ] :=>
273+-- '[ "id" ::: 'Def :=> 'NotNull 'PGint4
274+-- , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
275+-- , "email" ::: 'NoDef :=> 'Null 'PGtext
276+-- ]
277+-- ]
278+-- :}
279+type TablesType = [(Symbol,TableType)]
280+-- | A monokinded empty `TablesType`.
281+type family NilTables :: TablesType where NilTables = '[]
282+
283+-- | `RelationType` is a row of `NullityType`
284+--
285+-- >>> :{
286+-- type family PersonRelation :: RelationType where
287+-- PersonRelation =
288+-- '[ "name" ::: 'NotNull 'PGtext
289+-- , "age" ::: 'NotNull 'PGint4
290+-- , "dateOfBirth" ::: 'Null 'PGdate
291+-- ]
292+-- :}
293+type RelationType = [(Symbol,NullityType)]
294+-- | A monokinded empty `RelationType`.
295+type family NilRelation :: RelationType where NilRelation = '[]
296+
297+-- | `RelationsType` is a row of `RelationType`s, thought of as a product.
298+type RelationsType = [(Symbol,RelationType)]
299+
300+-- | `ColumnsToRelation` removes column constraints.
301+type family ColumnsToRelation (columns :: ColumnsType) :: RelationType where
302+ ColumnsToRelation '[] = '[]
303+ ColumnsToRelation (column ::: constraint :=> ty ': columns) =
304+ column ::: ty ': ColumnsToRelation columns
305+
306+-- | `RelationToColumns` adds `'NoDef` column constraints.
307+type family RelationToColumns (relation :: RelationType) :: ColumnsType where
308+ RelationToColumns '[] = '[]
309+ RelationToColumns (column ::: ty ': columns) =
310+ column ::: 'NoDef :=> ty ': RelationToColumns columns
311+
312+-- | `TableToColumns` removes table constraints.
313+type family TableToColumns (table :: TableType) :: ColumnsType where
314+ TableToColumns (constraints :=> columns) = columns
315+
316+-- | `TablesToRelations` removes both table and column constraints.
317+type family TablesToRelations (tables :: TablesType) :: RelationsType where
318+ TablesToRelations '[] = '[]
319+ TablesToRelations (alias ::: constraint :=> columns ': tables) =
320+ alias ::: ColumnsToRelation columns ': TablesToRelations tables
321+
322+-- | `RelationsToTables` adds both trivial table and column constraints.
323+type family RelationsToTables (tables :: RelationsType) :: TablesType where
324+ RelationsToTables '[] = '[]
325+ RelationsToTables (alias ::: columns ': relations) =
326+ alias ::: '[] :=> RelationToColumns columns ': RelationsToTables relations
135
327
136-- | `Grouping` is an auxiliary namespace, created by
328-- | `Grouping` is an auxiliary namespace, created by
137-- @GROUP BY@ clauses (`Squeal.PostgreSQL.Query.group`), and used
329-- @GROUP BY@ clauses (`Squeal.PostgreSQL.Query.group`), and used
138-- for typesafe aggregation
330-- for typesafe aggregation
139data Grouping
331data Grouping
140− = Ungrouped
332+ = Ungrouped -- ^ no aggregation permitted
141− | Grouped [(Symbol,Symbol)]
333+ | Grouped [(Symbol,Symbol)] -- ^ aggregation required for any column which is not grouped
142−
143−-- | `PGNum` is a constraint on `PGType` whose
144−-- `Squeal.PostgreSQL.Expression.Expression`s have a `Num` constraint.
145−type PGNum ty =
146− In ty '[ 'PGint2, 'PGint4, 'PGint8, 'PGnumeric, 'PGfloat4, 'PGfloat8]
147
334
148−-- | `PGFloating` is a constraint on `PGType` whose
335+{- | A `GroupedBy` constraint indicates that a table qualified column is
149−-- `Squeal.PostgreSQL.Expression.Expression`s
336+a member of the auxiliary namespace created by @GROUP BY@ clauses and thus,
150−-- have `Fractional` and `Floating` constraints.
337+may be called in an output `Squeal.PostgreSQL.Expression.Expression` without aggregating.
151−type PGFloating ty = In ty '[ 'PGfloat4, 'PGfloat8, 'PGnumeric]
338+-}
152−
339+class (KnownSymbol relation, KnownSymbol column)
153−-- | `PGIntegral` is a constraint on `PGType` whose
340+ => GroupedBy relation column bys where
154−-- `Squeal.PostgreSQL.Expression.Expression`s
341+instance {-# OVERLAPPING #-} (KnownSymbol relation, KnownSymbol column)
155−-- have `Squeal.PostgreSQL.Expression.div_` and
342+ => GroupedBy relation column ('(table,column) ': bys)
156−-- `Squeal.PostgreSQL.Expression.mod_` functions.
343+instance {-# OVERLAPPABLE #-}
157−type PGIntegral ty = In ty '[ 'PGint2, 'PGint4, 'PGint8]
344+ ( KnownSymbol relation
158−
345+ , KnownSymbol column
159−-- | `:::` is like a promoted version of `As`, a type level pair between
346+ , GroupedBy relation column bys
160−-- an alias and some type, usually a column alias and a `ColumnType` or
347+ ) => GroupedBy relation column (tabcol ': bys)
161−-- a table alias and a `ColumnsType`.
162−type (:::) (alias :: Symbol) (ty :: polykind) = '(alias,ty)
163
348
164-- | `Alias`es are proxies for a type level string or `Symbol`
349-- | `Alias`es are proxies for a type level string or `Symbol`
165-- and have an `IsLabel` instance so that with @-XOverloadedLabels@
350-- and have an `IsLabel` instance so that with @-XOverloadedLabels@
@@ -172,8 +357,8 @@ data Alias (alias :: Symbol) = Alias
172instance alias1 ~ alias2 => IsLabel alias1 (Alias alias2) where
357instance alias1 ~ alias2 => IsLabel alias1 (Alias alias2) where
173 fromLabel = Alias
358 fromLabel = Alias
174
359
175−-- | >>> renderAlias #alias
360+-- | >>> renderAlias #jimbob
176−-- "alias"
361+-- "jimbob"
177renderAlias :: KnownSymbol alias => Alias alias -> ByteString
362renderAlias :: KnownSymbol alias => Alias alias -> ByteString
178renderAlias = fromString . symbolVal
363renderAlias = fromString . symbolVal
179
364
@@ -196,76 +381,108 @@ deriving instance Ord (expression ty)
196 => Ord (Aliased expression (alias ::: ty))
381 => Ord (Aliased expression (alias ::: ty))
197
382
198-- | >>> let renderMaybe = fromString . maybe "Nothing" (const "Just")
383-- | >>> let renderMaybe = fromString . maybe "Nothing" (const "Just")
199−-- >>> renderAliased renderMaybe (Just (3::Int) `As` #an_int)
384+-- >>> renderAliasedAs renderMaybe (Just (3::Int) `As` #an_int)
200-- "Just AS an_int"
385-- "Just AS an_int"
201−renderAliased
386+renderAliasedAs
202 :: (forall ty. expression ty -> ByteString)
387 :: (forall ty. expression ty -> ByteString)
203 -> Aliased expression aliased
388 -> Aliased expression aliased
204 -> ByteString
389 -> ByteString
205−renderAliased render (expression `As` alias) =
390+renderAliasedAs render (expression `As` alias) =
206 render expression <> " AS " <> renderAlias alias
391 render expression <> " AS " <> renderAlias alias
207
392
393+-- | `AliasesOf` retains the AliasesOf in a row.
394+type family AliasesOf aliaseds where
395+ AliasesOf '[] = '[]
396+ AliasesOf (alias ::: ty ': tys) = alias ': AliasesOf tys
397+
398+-- | @HasUnique alias fields field@ is a constraint that proves that
399+-- @fields@ is a singleton of @alias ::: field@.
400+type HasUnique alias fields field = fields ~ '[alias ::: field]
401+
402+-- | @Has alias fields field@ is a constraint that proves that
403+-- @fields@ has a field of @alias ::: field@.
404+class KnownSymbol alias =>
405+ Has (alias :: Symbol) (fields :: [(Symbol,kind)]) (field :: kind)
406+ | alias fields -> field where
407+instance {-# OVERLAPPING #-} KnownSymbol alias
408+ => Has alias (alias ::: field ': fields) field
409+instance {-# OVERLAPPABLE #-} (KnownSymbol alias, Has alias fields field)
410+ => Has alias (field' ': fields) field
411+
208-- | Analagous to `IsLabel`, the constraint
412-- | Analagous to `IsLabel`, the constraint
209−-- `IsTableColumn` defines `!` for a column alias qualified
413+-- `IsQualified` defines `!` for a column alias qualified
210-- by a table alias.
414-- by a table alias.
211−class IsTableColumn table column expression where
415+class IsQualified table column expression where
212 (!) :: Alias table -> Alias column -> expression
416 (!) :: Alias table -> Alias column -> expression
213 infixl 9 !
417 infixl 9 !
214−instance IsTableColumn table column (Alias table, Alias column) where (!) = (,)
418+instance IsQualified table column (Alias table, Alias column) where (!) = (,)
419+
420+-- | @Elem@ is a promoted `Data.List.elem`.
421+type family Elem x xs where
422+ Elem x '[] = 'False
423+ Elem x (x ': xs) = 'True
424+ Elem x (_ ': xs) = Elem x xs
215
425
216-- | @In x xs@ is a constraint that proves that @x@ is in @xs@.
426-- | @In x xs@ is a constraint that proves that @x@ is in @xs@.
217−type family In x xs :: Constraint where
427+type family In x xs :: Constraint where In x xs = Elem x xs ~ 'True
218− In x (x ': xs) = ()
219− In x (y ': xs) = In x xs
220
428
221−-- | @HasUnique alias xs x@ is a constraint that proves that @xs@ is a singleton
429+-- | `PGNum` is a constraint on `PGType` whose
222−-- of @alias ::: x@.
430+-- `Squeal.PostgreSQL.Expression.Expression`s have a `Num` constraint.
223−type HasUnique alias xs x = xs ~ '[alias ::: x]
431+type PGNum ty =
432+ In ty '[ 'PGint2, 'PGint4, 'PGint8, 'PGnumeric, 'PGfloat4, 'PGfloat8]
224
433
225−-- | `BaseType` forgets about @NULL@ and @DEFAULT@
434+-- | `PGFloating` is a constraint on `PGType` whose
226−type family BaseType (ty :: ColumnType) :: PGType where
435+-- `Squeal.PostgreSQL.Expression.Expression`s
227− BaseType (optionality (nullity pg)) = pg
436+-- have `Fractional` and `Floating` constraints.
437+type PGFloating ty = In ty '[ 'PGfloat4, 'PGfloat8, 'PGnumeric]
438+
439+-- | `PGIntegral` is a constraint on `PGType` whose
440+-- `Squeal.PostgreSQL.Expression.Expression`s
441+-- have `Squeal.PostgreSQL.Expression.div_` and
442+-- `Squeal.PostgreSQL.Expression.mod_` functions.
443+type PGIntegral ty = In ty '[ 'PGint2, 'PGint4, 'PGint8]
444+
445+-- | `PGTypeOf` forgets about @NULL@ and any column constraints.
446+type family PGTypeOf (ty :: NullityType) :: PGType where
447+ PGTypeOf (nullity pg) = pg
228
448
229-- | `SameTypes` is a constraint that proves two `ColumnsType`s have the same
449-- | `SameTypes` is a constraint that proves two `ColumnsType`s have the same
230-- length and the same `ColumnType`s.
450-- length and the same `ColumnType`s.
231−type family SameTypes
451+type family SameTypes (columns0 :: ColumnsType) (columns1 :: ColumnsType)
232− (columns0 :: ColumnsType) (columns1 :: ColumnsType)
452+ :: Constraint where
233− :: Constraint where
234 SameTypes '[] '[] = ()
453 SameTypes '[] '[] = ()
235− SameTypes ((column0 ::: ty0) ': columns0) ((column1 ::: ty1) ': columns1)
454+ SameTypes (column0 ::: def0 :=> ty0 ': columns0) (column1 ::: def1 :=> ty1 ': columns1)
236 = (ty0 ~ ty1, SameTypes columns0 columns1)
455 = (ty0 ~ ty1, SameTypes columns0 columns1)
237
456
238-- | `AllNotNull` is a constraint that proves a `ColumnsType` has no @NULL@s.
457-- | `AllNotNull` is a constraint that proves a `ColumnsType` has no @NULL@s.
239type family AllNotNull (columns :: ColumnsType) :: Constraint where
458type family AllNotNull (columns :: ColumnsType) :: Constraint where
240 AllNotNull '[] = ()
459 AllNotNull '[] = ()
241− AllNotNull ((column ::: (optionality ('NotNull ty))) ': columns)
460+ AllNotNull (column ::: def :=> 'NotNull ty ': columns) = AllNotNull columns
242− = AllNotNull columns
243
461
244-- | `NotAllNull` is a constraint that proves a `ColumnsType` has some
462-- | `NotAllNull` is a constraint that proves a `ColumnsType` has some
245-- @NOT NULL@.
463-- @NOT NULL@.
246−type family NotAllNull columns :: Constraint where
464+type family NotAllNull (columns :: ColumnsType) :: Constraint where
247− NotAllNull ((column ::: (optionality ('NotNull ty))) ': columns) = ()
465+ NotAllNull (column ::: def :=> 'NotNull ty ': columns) = ()
248− NotAllNull ((column ::: (optionality ('Null ty))) ': columns)
466+ NotAllNull (column ::: def :=> 'Null ty ': columns) = NotAllNull columns
249− = NotAllNull columns
250
467
251-- | `NullifyType` is an idempotent that nullifies a `ColumnType`.
468-- | `NullifyType` is an idempotent that nullifies a `ColumnType`.
252−type family NullifyType (ty :: ColumnType) :: ColumnType where
469+type family NullifyType (ty :: NullityType) :: NullityType where
253− NullifyType (optionality ('Null ty)) = optionality ('Null ty)
470+ NullifyType ('Null ty) = 'Null ty
254− NullifyType (optionality ('NotNull ty)) = optionality ('Null ty)
471+ NullifyType ('NotNull ty) = 'Null ty
255
472
256−-- | `NullifyColumns` is an idempotent that nullifies a `ColumnsType`.
473+-- | `NullifyRelation` is an idempotent that nullifies a `ColumnsType`.
257−type family NullifyColumns (columns :: ColumnsType) :: ColumnsType where
474+type family NullifyRelation (columns :: RelationType) :: RelationType where
258− NullifyColumns '[] = '[]
475+ NullifyRelation '[] = '[]
259− NullifyColumns ((column ::: ty) ': columns) =
476+ NullifyRelation (column ::: ty ': columns) =
260− (column ::: NullifyType ty) ': NullifyColumns columns
477+ column ::: NullifyType ty ': NullifyRelation columns
261
478
262−-- | `NullifyTables` is an idempotent that nullifies a `TablesType`
479+-- | `NullifyRelations` is an idempotent that nullifies a `RelationsType`
263-- used to nullify the left or right hand side of an outer join
480-- used to nullify the left or right hand side of an outer join
264-- in a `Squeal.PostgreSQL.Query.FromClause`.
481-- in a `Squeal.PostgreSQL.Query.FromClause`.
265−type family NullifyTables (tables :: TablesType) :: TablesType where
482+type family NullifyRelations (tables :: RelationsType) :: RelationsType where
266− NullifyTables '[] = '[]
483+ NullifyRelations '[] = '[]
267− NullifyTables ((table ::: columns) ': tables) =
484+ NullifyRelations (table ::: columns ': tables) =
268− (table ::: NullifyColumns columns) ': NullifyTables tables
485+ table ::: NullifyRelation columns ': NullifyRelations tables
269
486
270-- | `Join` is simply promoted `++` and is used in @JOIN@s in
487-- | `Join` is simply promoted `++` and is used in @JOIN@s in
271-- `Squeal.PostgreSQL.Query.FromClause`s.
488-- `Squeal.PostgreSQL.Query.FromClause`s.
@@ -275,8 +492,7 @@ type family Join xs ys where
275
492
276-- | @Create alias x xs@ adds @alias ::: x@ to the end of @xs@ and is used in
493-- | @Create alias x xs@ adds @alias ::: x@ to the end of @xs@ and is used in
277-- `Squeal.PostgreSQL.Definition.createTable` statements and in @ALTER TABLE@
494-- `Squeal.PostgreSQL.Definition.createTable` statements and in @ALTER TABLE@
278−-- `Squeal.PostgreSQL.Definition.addColumnDefault` and
495+-- `Squeal.PostgreSQL.Definition.addColumn`.
279−-- `Squeal.PostgreSQL.Definition.addColumnNull` statements.
280type family Create alias x xs where
496type family Create alias x xs where
281 Create alias x '[] = '[alias ::: x]
497 Create alias x '[] = '[alias ::: x]
282 Create alias y (x ': xs) = x ': Create alias y xs
498 Create alias y (x ': xs) = x ': Create alias y xs
@@ -295,6 +511,19 @@ type family Alter alias xs x where
295 Alter alias ((alias ::: x0) ': xs) x1 = (alias ::: x1) ': xs
511 Alter alias ((alias ::: x0) ': xs) x1 = (alias ::: x1) ': xs
296 Alter alias (x0 ': xs) x1 = x0 ': Alter alias xs x1
512 Alter alias (x0 ': xs) x1 = x0 ': Alter alias xs x1
297
513
514+-- type family AddConstraint constraint ty where
515+-- AddConstraint constraint (constraints :=> ty)
516+-- = AsSet (constraint ': constraints) :=> ty
517+
518+-- type family DeleteFromList (e :: elem) (list :: [elem]) where
519+-- DeleteFromList elem '[] = '[]
520+-- DeleteFromList elem (x ': xs) =
521+-- If (Cmp elem x == 'EQ) xs (x ': DeleteFromList elem xs)
522+
523+-- type family DropConstraint constraint ty where
524+-- DropConstraint constraint (constraints :=> ty)
525+-- = (AsSet (DeleteFromList constraint constraints)) :=> ty
526+
298-- | @Rename alias0 alias1 xs@ replaces the alias @alias0@ by @alias1@ in @xs@
527-- | @Rename alias0 alias1 xs@ replaces the alias @alias0@ by @alias1@ in @xs@
299-- and is used in `Squeal.PostgreSQL.Definition.alterTableRename` and
528-- and is used in `Squeal.PostgreSQL.Definition.alterTableRename` and
300-- `Squeal.PostgreSQL.Definition.renameColumn`.
529-- `Squeal.PostgreSQL.Definition.renameColumn`.
@@ -305,14 +534,14 @@ type family Rename alias0 alias1 xs where
305-- | A `SameField` constraint is an equality constraint on a
534-- | A `SameField` constraint is an equality constraint on a
306-- `Generics.SOP.Type.Metadata.FieldInfo` and the column alias in a `:::` pair.
535-- `Generics.SOP.Type.Metadata.FieldInfo` and the column alias in a `:::` pair.
307class SameField
536class SameField
308− (fieldInfo :: Type.FieldInfo) (fieldty :: (Symbol,ColumnType)) where
537+ (fieldInfo :: Type.FieldInfo) (fieldty :: (Symbol,NullityType)) where
309instance field ~ column => SameField ('Type.FieldInfo field) (column ::: ty)
538instance field ~ column => SameField ('Type.FieldInfo field) (column ::: ty)
310
539
311-- | A `SameFields` constraint proves that a
540-- | A `SameFields` constraint proves that a
312-- `Generics.SOP.Type.Metadata.DatatypeInfo` of a record type has the same
541-- `Generics.SOP.Type.Metadata.DatatypeInfo` of a record type has the same
313−-- field names as the column aliases of a `ColumnsType`.
542+-- field names as the column AliasesOf of a `ColumnsType`.
314type family SameFields
543type family SameFields
315− (datatypeInfo :: Type.DatatypeInfo) (columns :: ColumnsType)
544+ (datatypeInfo :: Type.DatatypeInfo) (columns :: RelationType)
316 :: Constraint where
545 :: Constraint where
317 SameFields
546 SameFields
318 ('Type.ADT _module _datatype '[ 'Type.Record _constructor fields])
547 ('Type.ADT _module _datatype '[ 'Type.Record _constructor fields])
@@ -322,3 +551,19 @@ type family SameFields
322 ('Type.Newtype _module _datatype ('Type.Record _constructor fields))
551 ('Type.Newtype _module _datatype ('Type.Record _constructor fields))
323 columns
552 columns
324 = AllZip SameField fields columns
553 = AllZip SameField fields columns
554+
555+-- | Check if a `TableConstraint` involves a column
556+type family ConstraintInvolves column constraint where
557+ ConstraintInvolves column ('Check columns) = column `Elem` columns
558+ ConstraintInvolves column ('Unique columns) = column `Elem` columns
559+ ConstraintInvolves column ('PrimaryKey columns) = column `Elem` columns
560+ ConstraintInvolves column ('ForeignKey columns tab refcolumns)
561+ = column `Elem` columns
562+
563+-- | Drop all `TableConstraint`s that involve a column
564+type family DropIfConstraintsInvolve column constraints where
565+ DropIfConstraintsInvolve column '[] = '[]
566+ DropIfConstraintsInvolve column (alias ::: constraint ': constraints)
567+ = If (ConstraintInvolves column constraint)
568+ (DropIfConstraintsInvolve column constraints)
569+ (alias ::: constraint ': DropIfConstraintsInvolve column constraints)
squeal-postgresql/src/Squeal/PostgreSQL/Transaction.hsnew file+232
@@ -0,0 +1,232 @@
1+{-|
2+Module: Squeal.PostgreSQL.Transaction
3+Description: Squeal transaction control language
4+Copyright: (c) Eitan Chatav, 2017
5+Maintainer: eitan@morphism.tech
6+Stability: experimental
7+
8+Squeal transaction control language.
9+-}
10+
11+{-# LANGUAGE
12+ DataKinds
13+ , EmptyCase
14+ , FlexibleContexts
15+ , FlexibleInstances
16+ , LambdaCase
17+ , OverloadedStrings
18+ , MultiParamTypeClasses
19+ , ScopedTypeVariables
20+ , TypeFamilies
21+ , TypeInType
22+#-}
23+
24+module Squeal.PostgreSQL.Transaction
25+ ( -- * Transaction
26+ transactionally
27+ , transactionally_
28+ , begin
29+ , commit
30+ , rollback
31+ , transactionallySchema
32+ , transactionallySchema_
33+ -- * Transaction Mode
34+ , TransactionMode (..)
35+ , defaultMode
36+ , longRunningMode
37+ , IsolationLevel (..)
38+ , renderIsolationLevel
39+ , AccessMode (..)
40+ , renderAccessMode
41+ , DeferrableMode (..)
42+ , renderDeferrableMode
43+ ) where
44+
45+import Control.Exception.Lifted
46+import Control.Monad.Base
47+import Control.Monad.Trans.Control
48+import Data.ByteString
49+import Data.Monoid
50+import Generics.SOP
51+
52+import qualified Database.PostgreSQL.LibPQ as LibPQ
53+
54+import Squeal.PostgreSQL.Manipulation
55+import Squeal.PostgreSQL.Render
56+import Squeal.PostgreSQL.PQ
57+import Squeal.PostgreSQL.Schema
58+
59+-- | Run a schema invariant computation `transactionally`.
60+transactionally
61+ :: (MonadBaseControl IO tx, MonadPQ schema tx)
62+ => TransactionMode
63+ -> tx x -- ^ run inside a transaction
64+ -> tx x
65+transactionally mode tx = mask $ \restore -> do
66+ _ <- begin mode
67+ result <- restore tx `onException` rollback
68+ _ <- commit
69+ return result
70+
71+-- | Run a schema invariant computation `transactionally_` in `defaultMode`.
72+transactionally_
73+ :: (MonadBaseControl IO tx, MonadPQ schema tx)
74+ => tx x -- ^ run inside a transaction
75+ -> tx x
76+transactionally_ = transactionally defaultMode
77+
78+-- | @BEGIN@ a transaction.
79+begin :: MonadPQ schema tx => TransactionMode -> tx (K Result NilRelation)
80+begin mode = manipulate . UnsafeManipulation $
81+ "BEGIN" <+> renderTransactionMode mode <> ";"
82+
83+-- | @COMMIT@ a schema invariant transaction.
84+commit :: MonadPQ schema tx => tx (K Result NilRelation)
85+commit = manipulate $ UnsafeManipulation "COMMIT;"
86+
87+-- | @ROLLBACK@ a schema invariant transaction.
88+rollback :: MonadPQ schema tx => tx (K Result NilRelation)
89+rollback = manipulate $ UnsafeManipulation "ROLLBACK;"
90+
91+-- | Run a schema changing computation `transactionallySchema`.
92+transactionallySchema
93+ :: MonadBaseControl IO io
94+ => TransactionMode
95+ -> PQ schema0 schema1 io x
96+ -> PQ schema0 schema1 io x
97+transactionallySchema mode u = PQ $ \ conn -> mask $ \ restore -> do
98+ _ <- liftBase . LibPQ.exec (unK conn) $
99+ "BEGIN" <+> renderTransactionMode mode <> ";"
100+ x <- restore (unPQ u conn)
101+ `onException` (liftBase (LibPQ.exec (unK conn) "ROLLBACK"))
102+ _ <- liftBase $ LibPQ.exec (unK conn) "COMMIT"
103+ return x
104+
105+-- | Run a schema changing computation `transactionallySchema_` in `DefaultMode`.
106+transactionallySchema_
107+ :: MonadBaseControl IO io
108+ => PQ schema0 schema1 io x
109+ -> PQ schema0 schema1 io x
110+transactionallySchema_ = transactionallySchema defaultMode
111+
112+-- | The available transaction characteristics are the transaction `IsolationLevel`,
113+-- the transaction `AccessMode` (`ReadWrite` or `ReadOnly`), and the `DeferrableMode`.
114+data TransactionMode = TransactionMode
115+ { isolationLevel :: IsolationLevel
116+ , accessMode :: AccessMode
117+ , deferrableMode :: DeferrableMode
118+ } deriving (Show, Eq)
119+
120+-- | `TransactionMode` with a `Serializable` `IsolationLevel`,
121+-- `ReadWrite` `AccessMode` and `NotDeferrable` `DeferrableMode`.
122+defaultMode :: TransactionMode
123+defaultMode = TransactionMode Serializable ReadWrite NotDeferrable
124+
125+-- | `TransactionMode` with a `Serializable` `IsolationLevel`,
126+-- `ReadOnly` `AccessMode` and `Deferrable` `DeferrableMode`.
127+-- This mode is well suited for long-running reports or backups.
128+longRunningMode :: TransactionMode
129+longRunningMode = TransactionMode Serializable ReadOnly Deferrable
130+
131+-- | Render a `TransactionMode`.
132+renderTransactionMode :: TransactionMode -> ByteString
133+renderTransactionMode mode =
134+ "ISOLATION LEVEL"
135+ <+> renderIsolationLevel (isolationLevel mode)
136+ <+> renderAccessMode (accessMode mode)
137+ <+> renderDeferrableMode (deferrableMode mode)
138+
139+-- | The SQL standard defines four levels of transaction isolation.
140+-- The most strict is `Serializable`, which is defined by the standard in a paragraph
141+-- which says that any concurrent execution of a set of `Serializable` transactions is
142+-- guaranteed to produce the same effect as running them one at a time in some order.
143+-- The other three levels are defined in terms of phenomena, resulting from interaction
144+-- between concurrent transactions, which must not occur at each level.
145+-- The phenomena which are prohibited at various levels are:
146+--
147+-- __Dirty read__: A transaction reads data written by a concurrent uncommitted transaction.
148+--
149+-- __Nonrepeatable read__: A transaction re-reads data it has previously read and finds that data
150+-- has been modified by another transaction (that committed since the initial read).
151+--
152+-- __Phantom read__: A transaction re-executes a query returning a set of rows that satisfy
153+-- a search condition and finds that the set of rows satisfying the condition
154+-- has changed due to another recently-committed transaction.
155+--
156+-- __Serialization anomaly__: The result of successfully committing a group of transactions is inconsistent
157+-- with all possible orderings of running those transactions one at a time.
158+--
159+-- In PostgreSQL, you can request any of the four standard transaction
160+-- isolation levels, but internally only three distinct isolation levels are implemented,
161+-- i.e. PostgreSQL's `ReadUncommitted` mode behaves like `ReadCommitted`.
162+-- This is because it is the only sensible way to map the standard isolation levels to
163+-- PostgreSQL's multiversion concurrency control architecture.
164+data IsolationLevel
165+ = Serializable
166+ -- ^ Dirty read is not possible.
167+ -- Nonrepeatable read is not possible.
168+ -- Phantom read is not possible.
169+ -- Serialization anomaly is not possible.
170+ | RepeatableRead
171+ -- ^ Dirty read is not possible.
172+ -- Nonrepeatable read is not possible.
173+ -- Phantom read is not possible.
174+ -- Serialization anomaly is possible.
175+ | ReadCommitted
176+ -- ^ Dirty read is not possible.
177+ -- Nonrepeatable read is possible.
178+ -- Phantom read is possible.
179+ -- Serialization anomaly is possible.
180+ | ReadUncommitted
181+ -- ^ Dirty read is not possible.
182+ -- Nonrepeatable read is possible.
183+ -- Phantom read is possible.
184+ -- Serialization anomaly is possible.
185+ deriving (Show, Eq)
186+
187+-- | Render an `IsolationLevel`.
188+renderIsolationLevel :: IsolationLevel -> ByteString
189+renderIsolationLevel = \case
190+ Serializable -> "SERIALIZABLE"
191+ ReadCommitted -> "READ COMMITTED"
192+ ReadUncommitted -> "READ UNCOMMITTED"
193+ RepeatableRead -> "REPEATABLE READ"
194+
195+-- | The transaction access mode determines whether the transaction is `ReadWrite` or `ReadOnly`.
196+-- `ReadWrite` is the default. When a transaction is `ReadOnly`,
197+-- the following SQL commands are disallowed:
198+-- @INSERT@, @UPDATE@, @DELETE@, and @COPY FROM@
199+-- if the table they would write to is not a temporary table;
200+-- all @CREATE@, @ALTER@, and @DROP@ commands;
201+-- @COMMENT@, @GRANT@, @REVOKE@, @TRUNCATE@;
202+-- and @EXPLAIN ANALYZE@ and @EXECUTE@ if the command they would execute is among those listed.
203+-- This is a high-level notion of `ReadOnly` that does not prevent all writes to disk.
204+data AccessMode
205+ = ReadWrite
206+ | ReadOnly
207+ deriving (Show, Eq)
208+
209+-- | Render an `AccessMode`.
210+renderAccessMode :: AccessMode -> ByteString
211+renderAccessMode = \case
212+ ReadWrite -> "READ WRITE"
213+ ReadOnly -> "READ ONLY"
214+
215+-- | The `Deferrable` transaction property has no effect
216+-- unless the transaction is also `Serializable` and `ReadOnly`.
217+-- When all three of these properties are selected for a transaction,
218+-- the transaction may block when first acquiring its snapshot,
219+-- after which it is able to run without the normal overhead of a
220+-- `Serializable` transaction and without any risk of contributing
221+-- to or being canceled by a serialization failure.
222+-- This `longRunningMode` is well suited for long-running reports or backups.
223+data DeferrableMode
224+ = Deferrable
225+ | NotDeferrable
226+ deriving (Show, Eq)
227+
228+-- | Render a `DeferrableMode`.
229+renderDeferrableMode :: DeferrableMode -> ByteString
230+renderDeferrableMode = \case
231+ Deferrable -> "DEFERRABLE"
232+ NotDeferrable -> "NOT DEFERRABLE"
squeal-postgresql/test/DocTest.hs+4
@@ -5,10 +5,14 @@ import Test.DocTest
5main :: IO ()
5main :: IO ()
6main = doctest
6main = doctest
7 [ "-isrc"
7 [ "-isrc"
8+ , "src/Squeal/PostgreSQL.hs"
8 , "src/Squeal/PostgreSQL/Binary.hs"
9 , "src/Squeal/PostgreSQL/Binary.hs"
9 , "src/Squeal/PostgreSQL/Definition.hs"
10 , "src/Squeal/PostgreSQL/Definition.hs"
10 , "src/Squeal/PostgreSQL/Manipulation.hs"
11 , "src/Squeal/PostgreSQL/Manipulation.hs"
11 , "src/Squeal/PostgreSQL/Query.hs"
12 , "src/Squeal/PostgreSQL/Query.hs"
12 , "src/Squeal/PostgreSQL/Expression.hs"
13 , "src/Squeal/PostgreSQL/Expression.hs"
13 , "src/Squeal/PostgreSQL/PQ.hs"
14 , "src/Squeal/PostgreSQL/PQ.hs"
15+ , "src/Squeal/PostgreSQL/Migration.hs"
16+ , "src/Squeal/PostgreSQL/Transaction.hs"
17+ , "src/Squeal/PostgreSQL/Pool.hs"
14 ]
18 ]
squeal-postgresql/test/Spec.hsdeleted−1
@@ -1 +0,0 @@
1−{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
squeal-postgresql/test/Squeal/PostgreSQL/DefinitionSpec.hsdeleted−124
@@ -1,124 +0,0 @@
1−{-# LANGUAGE
2− DataKinds
3− , MagicHash
4− , OverloadedLabels
5− , OverloadedStrings
6− , TypeApplications
7− , TypeOperators
8−#-}
9−
10−module Squeal.PostgreSQL.DefinitionSpec where
11−
12−import Control.Category ((>>>))
13−import Data.Function
14−import Generics.SOP hiding (from)
15−import Test.Hspec
16−
17−import Squeal.PostgreSQL
18−
19−spec :: Spec
20−spec = do
21− let
22− definition `definitionRenders` str =
23− renderDefinition definition `shouldBe` str
24− it "should render CREATE TABLE statements" $ do
25− createTable #table1
26− ((int4 & notNull) `As` #col1 :* (int4 & notNull) `As` #col2 :* Nil)
27− [primaryKey (Column #col1 :* Column #col2 :* Nil)]
28− `definitionRenders`
29− "CREATE TABLE table1\
30− \ (col1 int4 NOT NULL, col2 int4 NOT NULL,\
31− \ PRIMARY KEY (col1, col2));"
32− createTable #table2
33− ( serial `As` #col1 :*
34− text `As` #col2 :*
35− (int8 & notNull & default_ 8) `As` #col3 :* Nil )
36− [check (#col3 .> 0)]
37− `definitionRenders`
38− "CREATE TABLE table2\
39− \ (col1 serial,\
40− \ col2 text,\
41− \ col3 int8 NOT NULL DEFAULT 8,\
42− \ CHECK ((col3 > 0)));"
43− let
44− statement :: Definition '[]
45− '[ "users" :::
46− '[ "id" ::: 'Optional ('NotNull 'PGint4)
47− , "username" ::: 'Required ('NotNull 'PGtext)
48− ]
49− , "emails" :::
50− '[ "id" ::: 'Optional ('NotNull 'PGint4)
51− , "userid" ::: 'Required ('NotNull 'PGint4)
52− , "email" ::: 'Required ('NotNull 'PGtext)
53− ]
54− ]
55− statement =
56− createTable #users
57− (serial `As` #id :* (text & notNull) `As` #username :* Nil)
58− [primaryKey (Column #id :* Nil)]
59− >>>
60− createTable #emails
61− ( serial `As` #id :*
62− (integer & notNull) `As` #userid :*
63− (text & notNull) `As` #email :* Nil )
64− [ primaryKey (Column #id :* Nil)
65− , foreignKey (Column #userid :* Nil) #users (Column #id :* Nil)
66− OnDeleteCascade OnUpdateRestrict
67− , unique (Column #email :* Nil)
68− , check (#email ./= "")
69− ]
70− statement `definitionRenders`
71− "CREATE TABLE users\
72− \ (id serial, username text NOT NULL,\
73− \ PRIMARY KEY (id));\
74− \ \
75− \CREATE TABLE emails\
76− \ (id serial,\
77− \ userid integer NOT NULL,\
78− \ email text NOT NULL,\
79− \ PRIMARY KEY (id),\
80− \ FOREIGN KEY (userid) REFERENCES users (id)\
81− \ ON DELETE CASCADE ON UPDATE RESTRICT,\
82− \ UNIQUE (email),\
83− \ CHECK ((email <> E'')));"
84− it "should render DROP TABLE statements" $ do
85− let
86− statement :: Definition Tables '[]
87− statement = dropTable #table1
88− statement `definitionRenders` "DROP TABLE table1;"
89−
90−type Columns =
91− '[ "col1" ::: 'Required ('NotNull 'PGint4)
92− , "col2" ::: 'Required ('NotNull 'PGint4)
93− ]
94−type Tables = '[ "table1" ::: Columns ]
95−type SumAndCol1 =
96− '[ "sum" ::: 'Required ('NotNull 'PGint4)
97− , "col1" ::: 'Required ('NotNull 'PGint4)
98− ]
99−type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
100−type StudentsTable = '["students" ::: StudentsColumns]
101−type OrderColumns =
102− '[ "orderID" ::: 'Required ('NotNull 'PGint4)
103− , "orderVal" ::: 'Required ('NotNull 'PGtext)
104− , "customerID" ::: 'Required ('NotNull 'PGint4)
105− , "shipperID" ::: 'Required ('NotNull 'PGint4)
106− ]
107−type CustomerColumns =
108− '[ "customerID" ::: 'Required ('NotNull 'PGint4)
109− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
110− ]
111−type ShipperColumns =
112− '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
113− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
114− ]
115−type JoinTables =
116− '[ "orders" ::: OrderColumns
117− , "customers" ::: CustomerColumns
118− , "shippers" ::: ShipperColumns
119− ]
120−type ValueColumns =
121− '[ "orderVal" ::: 'Required ('NotNull 'PGtext)
122− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
123− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
124− ]
squeal-postgresql/test/Squeal/PostgreSQL/ManipulationSpec.hsdeleted−128
@@ -1,128 +0,0 @@
1−{-# LANGUAGE
2− DataKinds
3− , MagicHash
4− , OverloadedLabels
5− , OverloadedStrings
6− , TypeApplications
7− , TypeOperators
8−#-}
9−
10−module Squeal.PostgreSQL.ManipulationSpec where
11−
12−import Generics.SOP hiding (from)
13−import Test.Hspec
14−
15−import Squeal.PostgreSQL
16−
17−spec :: Spec
18−spec = do
19− let
20− manipulation `manipulationRenders` str =
21− renderManipulation manipulation `shouldBe` str
22− it "correctly renders returning INSERTs" $ do
23− let
24− statement :: Manipulation Tables '[] SumAndCol1
25− statement =
26− insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
27− OnConflictDoRaise
28− (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
29− statement `manipulationRenders`
30− "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
31− \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
32− it "correctly renders simple UPDATEs" $ do
33− let
34− statement :: Manipulation Tables '[] '[]
35− statement =
36− update #table1 (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
37− (#col1 ./= #col2) (Returning Nil)
38− statement `manipulationRenders`
39− "UPDATE table1 SET col1 = 2\
40− \ WHERE (col1 <> col2);"
41− it "correctly renders returning UPDATEs" $ do
42− let
43− statement :: Manipulation Tables '[] SumAndCol1
44− statement =
45− update #table1 (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
46− (#col1 ./= #col2)
47− (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
48− statement `manipulationRenders`
49− "UPDATE table1 SET col1 = 2\
50− \ WHERE (col1 <> col2)\
51− \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
52− it "correctly renders upsert INSERTs" $ do
53− let
54− statement :: Manipulation Tables '[] '[]
55− statement =
56− insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
57− (OnConflictDoUpdate
58− (Set 2 `As` #col1 :* Same `As` #col2 :* Nil) Nothing)
59− (Returning Nil)
60− statement `manipulationRenders`
61− "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
62− \ ON CONFLICT DO UPDATE\
63− \ SET col1 = 2;"
64− it "correctly renders returning upsert INSERTs" $ do
65− let
66− statement :: Manipulation Tables '[] SumAndCol1
67− statement =
68− insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
69− (OnConflictDoUpdate
70− (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
71− (Just (#col1 ./= #col2)))
72− (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
73− statement `manipulationRenders`
74− "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
75− \ ON CONFLICT DO UPDATE\
76− \ SET col1 = 2\
77− \ WHERE (col1 <> col2)\
78− \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
79− it "correctly renders DELETEs" $ do
80− let
81− statement :: Manipulation Tables '[] '[]
82− statement = deleteFrom #table1 (#col1 .== #col2) (Returning Nil)
83− statement `manipulationRenders`
84− "DELETE FROM table1 WHERE (col1 = col2);"
85− it "should be safe against SQL injection in literal text" $ do
86− let
87− statement :: Manipulation StudentsTable '[] '[]
88− statement = insertInto #students
89− (Values ("Robert'); DROP TABLE students;" `As` #name :* Nil) [])
90− OnConflictDoRaise (Returning Nil)
91− statement `manipulationRenders`
92− "INSERT INTO students (name) VALUES (E'Robert''); DROP TABLE students;');"
93−
94−type Columns =
95− '[ "col1" ::: 'Required ('NotNull 'PGint4)
96− , "col2" ::: 'Required ('NotNull 'PGint4)
97− ]
98−type Tables = '[ "table1" ::: Columns ]
99−type SumAndCol1 =
100− '[ "sum" ::: 'Required ('NotNull 'PGint4)
101− , "col1" ::: 'Required ('NotNull 'PGint4)
102− ]
103−type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
104−type StudentsTable = '["students" ::: StudentsColumns]
105−type OrderColumns =
106− '[ "orderID" ::: 'Required ('NotNull 'PGint4)
107− , "orderVal" ::: 'Required ('NotNull 'PGtext)
108− , "customerID" ::: 'Required ('NotNull 'PGint4)
109− , "shipperID" ::: 'Required ('NotNull 'PGint4)
110− ]
111−type CustomerColumns =
112− '[ "customerID" ::: 'Required ('NotNull 'PGint4)
113− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
114− ]
115−type ShipperColumns =
116− '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
117− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
118− ]
119−type JoinTables =
120− '[ "orders" ::: OrderColumns
121− , "customers" ::: CustomerColumns
122− , "shippers" ::: ShipperColumns
123− ]
124−type ValueColumns =
125− '[ "orderVal" ::: 'Required ('NotNull 'PGtext)
126− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
127− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
128− ]
squeal-postgresql/test/Squeal/PostgreSQL/QuerySpec.hsdeleted−181
@@ -1,181 +0,0 @@
1−{-# LANGUAGE
2− DataKinds
3− , MagicHash
4− , OverloadedLabels
5− , OverloadedStrings
6− , TypeApplications
7− , TypeOperators
8−#-}
9−
10−module Squeal.PostgreSQL.QuerySpec where
11−
12−import Data.Function
13−import Generics.SOP hiding (from)
14−import Test.Hspec
15−
16−import Squeal.PostgreSQL
17−
18−spec :: Spec
19−spec = do
20− let
21− qry `queryRenders` str =
22− renderManipulation (queryStatement qry) `shouldBe` str
23− it "correctly renders a simple SELECT query" $ do
24− let
25− statement :: Query Tables '[] SumAndCol1
26− statement =
27− select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
28− (from (Table (#table1 `As` #table1)) & where_ (#col1 .> #col2))
29− statement `queryRenders`
30− "SELECT (col1 + col2) AS sum, col1 AS col1\
31− \ FROM table1 AS table1 WHERE (col1 > col2);"
32− it "combines WHEREs using AND" $ do
33− let
34− statement :: Query Tables '[] SumAndCol1
35− statement =
36− select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
37− (from (Table (#table1 `As` #table1)) & where_ true & where_ false)
38− statement `queryRenders`
39− "SELECT (col1 + col2) AS sum, col1 AS col1\
40− \ FROM table1 AS table1 WHERE (TRUE AND FALSE);"
41− it "performs sub SELECTs" $ do
42− let
43− statement :: Query Tables '[] SumAndCol1
44− statement =
45− selectStar (from (Subquery
46− (select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
47− (from (Table (#table1 `As` #table1))) `As` #sub)))
48− statement `queryRenders`
49− "SELECT * FROM\
50− \ (SELECT (col1 + col2) AS sum, col1 AS col1\
51− \ FROM table1 AS table1) AS sub;"
52− it "does LIMIT clauses" $ do
53− let
54− statement :: Query Tables '[] Columns
55− statement = selectStar (from (Table (#table1 `As` #table1)) & limit 1)
56− statement `queryRenders` "SELECT * FROM table1 AS table1 LIMIT 1;"
57− it "should use the minimum of given LIMITs" $ do
58− let
59− statement :: Query Tables '[] Columns
60− statement =
61− selectStar (from (Table (#table1 `As` #table1)) & limit 1 & limit 2)
62− statement `queryRenders` "SELECT * FROM table1 AS table1 LIMIT 1;"
63− it "should render parameters using $ signs" $ do
64− let
65− statement :: Query Tables '[ 'Required ('NotNull 'PGbool)] Columns
66− statement = selectStar
67− (from (Table (#table1 `As` #table1)) & where_ (param @1))
68− statement `queryRenders`
69− "SELECT * FROM table1 AS table1 WHERE ($1 :: bool);"
70− it "does OFFSET clauses" $ do
71− let
72− statement :: Query Tables '[] Columns
73− statement =
74− selectStar (from (Table (#table1 `As` #table1)) & offset 1)
75− statement `queryRenders` "SELECT * FROM table1 AS table1 OFFSET 1;"
76− it "should use the sum of given OFFSETs" $ do
77− let
78− statement :: Query Tables '[] Columns
79− statement = selectStar
80− (from (Table (#table1 `As` #table1)) & offset 1 & offset 2)
81− statement `queryRenders` "SELECT * FROM table1 AS table1 OFFSET 3;"
82− it "should render GROUP BY and HAVING clauses" $ do
83− let
84− statement :: Query Tables '[] SumAndCol1
85− statement =
86− select (sum_ #col2 `As` #sum :* #col1 `As` #col1 :* Nil)
87− ( from (Table (#table1 `As` #table1))
88− & group (By #col1 :* Nil)
89− & having (#col1 + sum_ #col2 .> 1) )
90− statement `queryRenders`
91− "SELECT sum(col2) AS sum, col1 AS col1\
92− \ FROM table1 AS table1\
93− \ GROUP BY col1\
94− \ HAVING ((col1 + sum(col2)) > 1);"
95− describe "JOINs" $ do
96− it "should render CROSS JOINs" $ do
97− let
98− statement :: Query JoinTables '[] ValueColumns
99− statement = select
100− ( #orders ! #orderVal `As` #orderVal :*
101− #customers ! #customerVal `As` #customerVal :*
102− #shippers ! #shipperVal `As` #shipperVal :* Nil )
103− ( from (Table (#orders `As` #orders)
104− & CrossJoin (Table (#customers `As` #customers))
105− & CrossJoin (Table (#shippers `As` #shippers))) )
106− statement `queryRenders`
107− "SELECT\
108− \ orders.orderVal AS orderVal,\
109− \ customers.customerVal AS customerVal,\
110− \ shippers.shipperVal AS shipperVal\
111− \ FROM orders AS orders\
112− \ CROSS JOIN customers AS customers\
113− \ CROSS JOIN shippers AS shippers;"
114− it "should render INNER JOINs" $ do
115− let
116− statement :: Query JoinTables '[] ValueColumns
117− statement = select
118− ( (#orders ! #orderVal) `As` #orderVal
119− :* (#customers ! #customerVal) `As` #customerVal
120− :* (#shippers ! #shipperVal) `As` #shipperVal :* Nil)
121− ( from (Table (#orders `As` #orders)
122− & InnerJoin (Table (#customers `As` #customers))
123− ((#orders ! #customerID) .== (#customers ! #customerID))
124− & InnerJoin (Table (#shippers `As` #shippers))
125− ((#orders ! #shipperID) .== (#shippers ! #shipperID))))
126− statement `queryRenders`
127− "SELECT\
128− \ orders.orderVal AS orderVal,\
129− \ customers.customerVal AS customerVal,\
130− \ shippers.shipperVal AS shipperVal\
131− \ FROM orders AS orders\
132− \ INNER JOIN customers AS customers\
133− \ ON (orders.customerID = customers.customerID)\
134− \ INNER JOIN shippers AS shippers\
135− \ ON (orders.shipperID = shippers.shipperID);"
136− it "should render self JOINs" $ do
137− let
138− statement :: Query JoinTables '[] OrderColumns
139− statement = selectDotStar #orders1
140− (from (Table (#orders `As` #orders1)
141− & CrossJoin (Table (#orders `As` #orders2))))
142− statement `queryRenders`
143− "SELECT orders1.*\
144− \ FROM orders AS orders1\
145− \ CROSS JOIN orders AS orders2;"
146−
147−type Columns =
148− '[ "col1" ::: 'Required ('NotNull 'PGint4)
149− , "col2" ::: 'Required ('NotNull 'PGint4)
150− ]
151−type Tables = '[ "table1" ::: Columns ]
152−type SumAndCol1 =
153− '[ "sum" ::: 'Required ('NotNull 'PGint4)
154− , "col1" ::: 'Required ('NotNull 'PGint4)
155− ]
156−type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
157−type StudentsTable = '["students" ::: StudentsColumns]
158−type OrderColumns =
159− '[ "orderID" ::: 'Required ('NotNull 'PGint4)
160− , "orderVal" ::: 'Required ('NotNull 'PGtext)
161− , "customerID" ::: 'Required ('NotNull 'PGint4)
162− , "shipperID" ::: 'Required ('NotNull 'PGint4)
163− ]
164−type CustomerColumns =
165− '[ "customerID" ::: 'Required ('NotNull 'PGint4)
166− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
167− ]
168−type ShipperColumns =
169− '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
170− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
171− ]
172−type JoinTables =
173− '[ "orders" ::: OrderColumns
174− , "customers" ::: CustomerColumns
175− , "shippers" ::: ShipperColumns
176− ]
177−type ValueColumns =
178− '[ "orderVal" ::: 'Required ('NotNull 'PGtext)
179− , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
180− , "shipperVal" ::: 'Required ('NotNull 'PGbool)
181− ]
stack.yaml+1−1
@@ -1,3 +1,3 @@
1−resolver: nightly-2017-08-25
1+resolver: lts-11.1
2packages:
2packages:
3- squeal-postgresql
3- squeal-postgresql