65 Commits

Author SHA1 Message Date
f23eb4143a chore: prepare v1.7.1 2024-06-22 20:01:10 -05:00
d10d21b708 fix: bump node-stream-pipes to 2.0.2 (big perf improvement) 2024-06-22 20:00:58 -05:00
826c1e10b1 chore: prepare v1.7.0 2024-06-14 18:04:01 -05:00
fbb1f3b8a5 fix: add interval support 2024-06-14 17:00:22 -05:00
b4a84a3210 chore: prepare v1.6.5 2024-05-22 15:49:37 -05:00
a56add0ffc fix: update node-stream-pipes 2024-05-22 15:49:14 -05:00
7ed85ae22b fix: tagOf is not instanceof 2024-05-22 15:42:43 -05:00
bead3d81c3 chore: prepare v1.6.4 2024-05-11 23:00:24 -05:00
27a7abb329 fix: stdin txn 2024-05-11 23:00:05 -05:00
86263a7521 chore: prepare v1.6.3 2024-05-11 22:11:07 -05:00
2db29b8916 chore: prepare v1.6.2 2024-05-11 22:10:24 -05:00
cfb01608cd fix: ensure-ranges 2024-05-11 22:09:38 -05:00
aca76f7de3 chore: prepare v1.6.1 2024-05-11 20:45:41 -05:00
188403681a fix: pipes monad stuff 2024-05-11 20:45:04 -05:00
ca7ebb4337 chore: prepare v1.6.0 2024-05-11 13:26:44 -05:00
b3806d5f6e feat: add node-stream-pipes support 2024-05-11 13:25:27 -05:00
adb414662e chore: prepare v1.5.1 2024-04-30 19:26:35 -05:00
69721fcda4 chore: prepare v1.5.1 2024-04-30 19:26:21 -05:00
a616440abe chore: prepare v1.5.1 2024-04-30 19:26:20 -05:00
d66c3261b6 fix: impl Alternative 2024-04-30 19:26:01 -05:00
4e2fd8fa3f chore: prepare v1.5.0 2024-04-30 15:52:30 -05:00
e59266406b feat: streaming STDIO support 2024-04-30 15:46:10 -05:00
3be968da3a chore: prepare v1.4.0 2024-04-29 09:33:44 -05:00
20a0a6de31 chore: prepare v1.4.0 2024-04-29 09:32:23 -05:00
036a7b5de9 chore: prepare v1.4.0 2024-04-29 09:32:19 -05:00
639bee96d0 chore: prepare v 2024-04-29 09:32:05 -05:00
24b2156524 feat: MOVE support for cursors, RowsAffected 2024-04-29 09:31:49 -05:00
3e9f22397d chore: prepare v1.3.0 2024-04-11 16:45:35 -05:00
d3be053a8b chore: prepare v1.3.0 2024-04-11 16:45:27 -05:00
1319a4bce7 chore: prepare v1.3.0 2024-04-11 16:45:25 -05:00
8722e69013 feat: generalize PostgresT to MonadPostgres to allow mocking 2024-04-11 16:45:13 -05:00
5dc5912933 chore: prepare v1.2.7 2024-04-11 15:55:22 -05:00
a4d4e6bd75 fix: impl unlift 2024-04-11 15:55:11 -05:00
f0d2d764fe chore: prepare v1.2.6 2024-04-11 15:38:10 -05:00
ea3ff9b003 chore: lockfile 2024-04-11 15:38:01 -05:00
4832b594dc chore: prepare v1.2.5 2024-04-11 15:37:35 -05:00
aad6544658 chore: ranges 2024-04-11 15:37:29 -05:00
89dec85a31 chore: prepare v1.2.4 2024-04-11 15:36:40 -05:00
5a99e58062 fix: impl monadrec 2024-04-11 15:36:31 -05:00
02add2653a chore: prepare v1.2.3 2024-04-05 22:15:26 -05:00
de1aaccfb6 fix: pin unresult api to rept 2024-04-05 22:15:18 -05:00
43ce1354ea chore: prepare v1.2.2 2024-04-05 22:11:51 -05:00
87614611dd fix: relax cursor constraints, fix enum test 2024-04-05 22:11:44 -05:00
2ab53c43b2 chore: prepare v1.2.1 2024-04-05 21:53:42 -05:00
500e67d793 fix: FromRow t bound on cursor 2024-04-05 21:53:37 -05:00
ca95e0aa94 chore: prepare v1.2.0 2024-04-05 21:49:36 -05:00
bc120b072c feat: UnresultT 2024-04-05 21:49:31 -05:00
614db02470 chore: prepare v1.1.1 2024-04-05 20:56:58 -05:00
4aab05300b fix: improve custom enum api 2024-04-05 20:56:46 -05:00
651bc8cb8d chore: prepare v1.1.0 2024-04-05 20:06:43 -05:00
34fed4c369 chore: prepare v1.1.0 2024-04-05 20:06:27 -05:00
5fcf8c4549 feat: allow instances of serialize + deserialize outside this lib 2024-04-05 20:06:23 -05:00
07c2f5dd84 chore: prepare v1.1.0 2024-04-05 20:05:47 -05:00
bd25d12453 fix: prepare 2024-04-03 16:42:41 -05:00
7eb9a6e96a chore: prepare v1.0.6 2024-04-03 16:42:25 -05:00
6188a194cd chore: prepare 1.0.5 2024-04-03 16:41:55 -05:00
60f442fc21 fix: prepare 2024-04-03 16:41:53 -05:00
154fb0f1e7 fix: prepare 2024-04-03 16:40:17 -05:00
fb8e5af073 chore: prepare v1.0.4 2024-04-03 16:40:04 -05:00
dee8b85b84 fix: make publishing easier 2024-04-03 16:37:27 -05:00
8160660a98 feat: withPool 2024-04-03 16:27:41 -05:00
a0d2322441 docs: fix links 2024-04-03 13:22:34 -05:00
9f3aa6467f docs: improve 2024-04-03 13:17:15 -05:00
69e260e6fb docs: improve 2024-04-03 13:15:11 -05:00
d8ac527835 docs: improve 2024-04-03 13:13:37 -05:00
25 changed files with 1176 additions and 676 deletions

317
README.md Normal file
View File

@@ -0,0 +1,317 @@
# postgresql
Purescript PostgreSQL driver
## Table of Contents
- [Getting Started](#getting-started)
- [Data](#data)
- [Rows](#data---rows)
- [Ranges](#data---ranges)
- [Queries](#queries)
- [Builder](#queries---builder)
- [Monads](#monads)
- [`PostgresT`](#monads---postgrest)
- [`SessionT`](#monads---sessiont)
- [`CursorT`](#monads---cursort)
- [`node-postgres` style](#node-postgres-style)
## Getting Started
Install with:
```bash
> spago install postgresql
# (npm | yarn | bun) install pg
```
Next, create a pool [`Config`] object:
```purescript
-- from a connection string:
pgConfig =
{ connectionString: "postgresql://postgres:password@localhost:5432/postgres"
}
-- or explicitly:
pgConfig =
{ username: "postgres"
, password: "password"
, host: "localhost"
, port: 5432
, database: "postgres"
}
```
Then in an `Aff`, use `runPostgres` to connect to the database and execute
queries:
```purescript
module Main where
import Prelude
import Effect (Effect)
import Effect.Class (liftEffect)
import Effect.Aff (launchAff_)
import Effect.Console (log)
import Control.Monad.Postgres (runPostgres, query)
main :: Effect Unit
main =
launchAff_ do
msg <- runPostgres pgConfig $ query "select 'hello, world!'"
liftEffect $ log msg -- logs 'hello, world!'
```
[`runPostgres`] creates a connection pool, then executes a
[`PostgresT`](#monads-postgrest) monad, which is an `Aff` with access
to a connection pool.
```purescript
runPostgres :: forall a. <partial config> -> PostgresT Aff a -> Aff a
```
[`query`] accepts any `q` that can be turned [into a query][`AsQuery`]
(here just a `String`), and [unmarshals][`FromRows`] the result into a
destination type `r`.
`query` is from [`MonadSession`], which [`PostgresT`] implements:
```purescript
class MonadSession m where
query :: forall q r. AsQuery q => FromRows r => q -> m r
-- ...
```
## Data
Single SQL values are serialized to and deserialized from JS via [`pg-types`]
(with some [tweaks][`modifyPgTypes`]).
The conversion between [`Raw`] JS values and purescript values is done
with the [`Serialize`] and [`Deserialize`] typeclasses.
The [`Rep`] class indicates a type is [`Rep`]resentable as a SQL value.
[`Rep`] is automatically implemented for all types that are [`Serialize`]
and [`Deserialize`].
Implementations are provided for `Int`, `String`, `DateTime`, `Buffer`,
`BigInt`, `Boolean`, `Number`, `Array`, [`Range`]s, `Maybe`, [`Null`]
and `Unit`.
### Data - Rows
A single row (multiple SQL values) are deserialized using [`FromRow`],
which is implemented for:
- n-tuples of [`Rep`] types
- `Array a` where `a` is [`Rep`]
- A single [`Rep`] type
Examples:
```purescript
(fromRow [] :: Maybe Int) == Nothing
(fromRow [1] :: Maybe Int) == Just 1
(fromRow [1, 2] :: Maybe Int) == Just 1
(fromRow [] :: Int /\ Int) == Error
(fromRow [1, 2] :: Int /\ Int) == 1 /\ 2
(fromRow [] :: Array Int) == []
(fromRow [1, 2] :: Array Int) == [1, 2]
```
Multiple rows are deserialized using [`FromRows`],
which is implemented for:
- `Array a` where `a` is [`FromRow`]
- `Maybe a` where `a` is [`FromRow`] (equivalent to `Array.head <<< fromRows`)
- `a` where `a` is [`FromRow`] (throws if 0 rows yielded)
- `RowsAffected`
- Extracts the number of rows processed by the last command in the query (ex. `INSERT INTO foo (bar) VALUES ('a'), ('b')` -> `INSERT 2` -> `RowsAffected 2`)
### Data - Ranges
Postgres ranges are represented with [`Range`].
[`Range`]s can be created with:
- `mempty` - an unbounded range
- [`Range.lt`]` a` - `(,a)`
- [`Range.lte`]` a` - `(,a]`
- [`Range.gt`]` a` - `(a,)`
- [`Range.gte`]` a` - `[a,)`
and combined with `append`:
```purescript
mempty <> lt 100 -- (,100)
gte 10 <> lt 100 -- [10,100)
```
## Queries
Queries can be executed with any type that implements [`AsQuery`],
which converts it into a [`Query`]:
```purescript
newtype Query = Query { text :: String, values :: Array Raw, name :: Maybe String }
class AsQuery a where
asQuery :: a -> Effect Query
```
[`AsQuery`] is implemented for:
- [`Query`]
- `String`
- `String /\ n` where `n` is:
- n-tuple of [`Rep`] query parameters
- a single [`Rep`] query parameter
- `Array `[`Raw`]
### Queries - Builder
For complex parameterized queries, there is a provided [`Query.Builder`]:
```purescript
runPostgres {} do
exec_ "create table person (id int, first_name text, last_name text, age int, born timestamptz);"
exec_
$ Query.Builder.build
$ do
id <- Query.Builder.param 1
firstName <- Query.Builder.param "Henry"
lastName <- Query.Builder.param "Cavill"
age <- Query.Builder.param 38
born <- Query.Builder.param "1985-05-05"
pure
$ intercalate "\n"
[ "insert into person (id, first_name, last_name, age, born)"
, "values"
, "("
, intercalate ", " [id, firstName, lastName, age, born])
, ")"
]
```
[`Query.Builder.param`] accepts any [`Rep`] value and returns a string (ex. `"$2"`)
that will reference that value in the query.
[`Query.Builder.build`] renders the query to [`Query`]
## Monads
### Monads - `PostgresT`
[`PostgresT`] is the database driver's main entry point,
and is just an `Aff` with access to a [`Pool`].
Run in `Aff` with [`runPostgres`]:
```purescript
main :: Effect Unit
main =
launchAff_ do
hi <- runPostgres {} $ query "select 'hi!'"
liftEffect $ log hi
```
Execute [`SessionT`] monads with [`session`] or [`transaction`]:
```purescript
dbMain :: PostgresT Aff Unit
dbMain = do
transaction do
exec_ """
create table persons
( id int primary key generated always as identity
, name text not null unique
);
"""
exec_ $ "insert into persons (name) values ($1);" /\ "Henry"
pure unit
```
Implements [`MonadSession`] as a shorthand for single-query [`session`]s:
```purescript
dbMain :: PostgresT Aff Int
dbMain = exec_ $ "insert into persons (name) values ($1);" /\ "Sarah"
-- equivalent to:
-- dbMain = session $ exec_ ...
```
Execute [`CursorT`] monads with [`cursor`]:
```purescript
dbMain :: PostgresT Aff Int
dbMain =
cursor @(Int /\ String) "people_cursor" "select id, name from persons" do
a <- fetchOne -- Just (1 /\ "Henry")
b <- fetchOne -- Just (2 /\ "Sarah")
void $ move (MoveRelative -2)
c <- fetchAll -- [1 /\ "Henry", 2 /\ "Sarah"]
d <- fetchOne -- Nothing
```
### Monads - `SessionT`
[`SessionT`] is an `Aff` with access to a [`Client`]
issued by a [`Pool`], connected to the database.
Run in [`PostgresT`] with [`session`] or [`transaction`]
Perform queries with [`query`], [`exec`] or [`exec_`]
### Monads - `CursorT`
[`CursorT`] is a transaction [`SessionT`] with access to a named server-side cursor.
Run in [`PostgresT`] with [`cursor`]
## `node-postgres` style
You may also choose to use the `Aff` API directly, which closely mirrors
the api of [`node-postgres`]:
- [`Client`]
- create with [`Client.make`] or [`Client.connected`]
- execute queries with [`Client.query`], [`Client.queryRaw`] or [`Client.exec`]
- release with [`Client.end`]
- [`Pool`]
- create with [`Pool.make`]
- issue clients with [`Pool.connect`]
- release clients with [`Pool.release`] or [`Pool.destroy`]
- release with [`Pool.end`]
[`Pool`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#t:Pool
[`Config`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#t:Config
[`Pool.make`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#v:make
[`Pool.end`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#v:end
[`Pool.connect`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#v:connect
[`Pool.destroy`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#v:destroy
[`Pool.release`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Pool#v:release
[`Client`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#t:Client
[`Client.end`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:end
[`Client.make`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:make
[`Client.connected`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:connected
[`Client.query`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:query
[`Client.queryRaw`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:queryRaw
[`Client.exec`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Effect.Aff.Postgres.Client#v:exec
[`Range`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Range#t:Range
[`Range.gt`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Range#v:gt
[`Range.gte`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Range#v:gte
[`Range.lt`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Range#v:lt
[`Range.lte`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Range#v:lte
[`Raw`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Raw#t:Raw
[`Null`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Raw#t:Null
[`Serialize`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres#t:Serialize
[`Deserialize`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres#t:Deserialize
[`Rep`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres#t:Rep
[`modifyPgTypes`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres#v:modifyPgTypes
[`Result`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Result#t:Result
[`FromRow`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Result#t:FromRow
[`FromRows`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Result#t:FromRows
[`Query`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Query#t:Query
[`AsQuery`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Query#t:AsQuery
[`Query.Builder`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Query.Builder#t:Builder
[`Query.Builder.param`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Query.Builder#v:param
[`Query.Builder.build`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Data.Postgres.Query.Builder#v:build
[`MonadCursor`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#t:MonadCursor
[`MonadSession`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#t:MonadSession
[`CursorT`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#t:CursorT
[`SessionT`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#t:SessionT
[`PostgresT`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#t:PostgresT
[`cursor`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:cursor
[`session`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:session
[`transaction`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:transaction
[`runPostgres`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:runPostgres
[`query`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:query
[`exec`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:exec
[`exec_`]: https://pursuit.purescript.org/////////////////////////////////////////packages/purescript-postgresql/1.7.1/Control.Monad.Postgres#v:exec_
[`node-postgres`]: https://node-postgres.com/
[`pg-types`]: https://github.com/brianc/node-pg-types/

BIN
bun.lockb

Binary file not shown.

32
bun/prepare.js Normal file
View File

@@ -0,0 +1,32 @@
import { readFile, writeFile } from 'fs/promises'
import { execSync } from 'child_process'
let ver = process.argv[2]
if (!ver) {
console.error(`tag required: bun bun/prepare.js v1.0.0`)
} else if (!/v\d+\.\d+\.\d+/.test(ver)) {
console.error(`invalid tag: ${ver}`)
}
ver = (/\d+\.\d+\.\d+/.exec(ver) || [])[0] || ''
const pkg = await readFile('./package.json', 'utf8')
const pkgnew = pkg.replace(/"version": ".+"/, `"version": "v${ver}"`)
await writeFile('./package.json', pkgnew)
const spago = await readFile('./spago.yaml', 'utf8')
const spagonew = spago.replace(/version: .+/, `version: '${ver}'`)
await writeFile('./spago.yaml', spagonew)
const readme = await readFile('./README.md', 'utf8')
const readmenew = readme.replace(
/packages\/purescript-postgresql\/.+?\//g,
`/packages/purescript-postgresql/${ver}/`,
)
await writeFile('./README.md', readmenew)
execSync(`git add spago.yaml package.json README.md`)
execSync(`git commit -m 'chore: prepare v${ver}'`)
execSync(`git tag v${ver}`)
execSync(`git push --tags`)
execSync(`git push --mirror github-mirror`)

View File

@@ -1,5 +1,5 @@
{
"name": "purs",
"name": "purescript-postgresql",
"private": true,
"module": "index.js",
"type": "module",
@@ -14,6 +14,9 @@
"dependencies": {
"decimal.js": "^10.4.3",
"pg": "^8.11.3",
"pg-copy-streams": "^6.0.6",
"pg-listen": "^1.7.0",
"postgres-interval": "1.2.0",
"postgres-range": "^1.1.4"
}
}

View File

@@ -23,24 +23,30 @@ workspace:
- newtype: ">=5.0.0 <6.0.0"
- node-buffer: ">=9.0.0 <10.0.0"
- node-event-emitter: ">=3.0.0 <4.0.0"
- node-stream-pipes: ">=2.0.2 <3.0.0"
- node-streams: ">=9.0.0 <10.0.0"
- nullable: ">=6.0.0 <7.0.0"
- parallel: ">=6.0.0 <7.0.0"
- partial: ">=4.0.0 <5.0.0"
- pipes: ">=8.0.0 <9.0.0"
- precise-datetime: ">=7.0.0 <8.0.0"
- prelude: ">=6.0.1 <7.0.0"
- profunctor: ">=6.0.1 <7.0.0"
- record: ">=4.0.0 <5.0.0"
- simple-json: ">=9.0.0 <10.0.0"
- strings: ">=6.0.1 <7.0.0"
- tailrec: ">=6.1.0 <7.0.0"
- transformers: ">=6.0.0 <7.0.0"
- tuples: ">=7.0.0 <8.0.0"
- typelevel-prelude: ">=7.0.0 <8.0.0"
- unlift: ">=1.0.1 <2.0.0"
- unsafe-coerce: ">=6.0.0 <7.0.0"
test_dependencies:
- filterable
- foreign-object
- node-child-process
- node-process
- precise-datetime
- quickcheck
- spec
- spec-quickcheck
@@ -73,6 +79,7 @@ workspace:
- fork
- formatters
- free
- freet
- functions
- functors
- gen
@@ -86,6 +93,7 @@ workspace:
- lists
- maybe
- mmorph
- monad-control
- newtype
- node-buffer
- node-child-process
@@ -94,7 +102,9 @@ workspace:
- node-os
- node-path
- node-process
- node-stream-pipes
- node-streams
- node-zlib
- nonempty
- now
- nullable
@@ -126,489 +136,10 @@ workspace:
- typelevel-prelude
- unfoldable
- unicode
- unlift
- unordered-collections
- unsafe-coerce
- variant
package_set:
address:
registry: 50.5.0
compiler: ">=0.15.15 <0.16.0"
content:
abc-parser: 2.0.1
ace: 9.1.0
aff: 7.1.0
aff-bus: 6.0.0
aff-coroutines: 9.0.0
aff-promise: 4.0.0
aff-retry: 2.0.0
affjax: 13.0.0
affjax-node: 1.0.0
affjax-web: 1.0.0
ansi: 7.0.0
applicative-phases: 1.0.0
argonaut: 9.0.0
argonaut-aeson-generic: 0.4.1
argonaut-codecs: 9.1.0
argonaut-core: 7.0.0
argonaut-generic: 8.0.0
argonaut-traversals: 10.0.0
argparse-basic: 2.0.0
array-builder: 0.1.2
array-search: 0.5.6
arraybuffer: 13.2.0
arraybuffer-builder: 3.1.0
arraybuffer-types: 3.0.2
arrays: 7.3.0
arrays-extra: 0.6.1
arrays-zipper: 2.0.1
ask: 1.0.0
assert: 6.0.0
assert-multiple: 0.3.4
avar: 5.0.0
b64: 0.0.8
barbies: 1.0.1
barlow-lens: 0.9.0
bifunctors: 6.0.0
bigints: 7.0.1
bolson: 0.3.9
bookhound: 0.1.7
bower-json: 3.0.0
call-by-name: 4.0.1
canvas: 6.0.0
canvas-action: 9.0.0
cartesian: 1.0.6
catenable-lists: 7.0.0
chameleon: 1.0.0
chameleon-halogen: 1.0.3
chameleon-react-basic: 1.1.0
chameleon-styled: 2.5.0
chameleon-transformers: 1.0.0
channel: 1.0.0
checked-exceptions: 3.1.1
choku: 1.0.1
classless: 0.1.1
classless-arbitrary: 0.1.1
classless-decode-json: 0.1.1
classless-encode-json: 0.1.3
classnames: 2.0.0
codec: 6.1.0
codec-argonaut: 10.0.0
codec-json: 1.1.0
colors: 7.0.1
concur-core: 0.5.0
concur-react: 0.5.0
concurrent-queues: 3.0.0
console: 6.1.0
const: 6.0.0
contravariant: 6.0.0
control: 6.0.0
convertable-options: 1.0.0
coroutines: 7.0.0
css: 6.0.0
css-frameworks: 1.0.1
data-mvc: 0.0.2
datetime: 6.1.0
datetime-parsing: 0.2.0
debug: 6.0.2
decimals: 7.1.0
default-values: 1.0.1
deku: 0.9.23
deno: 0.0.5
dissect: 1.0.0
distributive: 6.0.0
dom-filereader: 7.0.0
dom-indexed: 12.0.0
dotenv: 4.0.3
droplet: 0.6.0
dts: 1.0.0
dual-numbers: 1.0.2
dynamic-buffer: 3.0.1
echarts-simple: 0.0.1
effect: 4.0.0
either: 6.1.0
elmish: 0.11.3
elmish-enzyme: 0.1.1
elmish-hooks: 0.10.0
elmish-html: 0.8.2
elmish-testing-library: 0.3.2
email-validate: 7.0.0
encoding: 0.0.9
enums: 6.0.1
env-names: 0.3.4
error: 2.0.0
eta-conversion: 0.3.2
exceptions: 6.0.0
exists: 6.0.0
exitcodes: 4.0.0
expect-inferred: 3.0.0
fahrtwind: 2.0.0
fallback: 0.1.0
fast-vect: 1.2.0
fetch: 4.1.0
fetch-argonaut: 1.0.1
fetch-core: 5.1.0
fetch-yoga-json: 1.1.0
fft-js: 0.1.0
filterable: 5.0.0
fix-functor: 0.1.0
fixed-points: 7.0.0
fixed-precision: 5.0.0
flame: 1.3.0
float32: 2.0.0
fmt: 0.2.1
foldable-traversable: 6.0.0
foldable-traversable-extra: 0.0.6
foreign: 7.0.0
foreign-object: 4.1.0
foreign-readwrite: 3.4.0
forgetmenot: 0.1.0
fork: 6.0.0
form-urlencoded: 7.0.0
formatters: 7.0.0
framer-motion: 1.0.1
free: 7.1.0
freeap: 7.0.0
freer-free: 0.0.1
freet: 7.0.0
functions: 6.0.0
functor1: 3.0.0
functors: 5.0.0
fuzzy: 0.4.0
gen: 4.0.0
generate-values: 1.0.1
generic-router: 0.0.1
geojson: 0.0.5
geometry-plane: 1.0.3
gojs: 0.1.1
grain: 3.0.0
grain-router: 3.0.0
grain-virtualized: 3.0.0
graphs: 8.1.0
group: 4.1.1
halogen: 7.0.0
halogen-bootstrap5: 5.3.2
halogen-canvas: 1.0.0
halogen-css: 10.0.0
halogen-echarts-simple: 0.0.4
halogen-formless: 4.0.3
halogen-helix: 1.0.0
halogen-hooks: 0.6.3
halogen-hooks-extra: 0.9.0
halogen-infinite-scroll: 1.1.0
halogen-store: 0.5.4
halogen-storybook: 2.0.0
halogen-subscriptions: 2.0.0
halogen-svg-elems: 8.0.0
halogen-typewriter: 1.0.4
halogen-vdom: 8.0.0
halogen-vdom-string-renderer: 0.5.0
halogen-xterm: 2.0.0
heckin: 2.0.1
heterogeneous: 0.6.0
homogeneous: 0.4.0
http-methods: 6.0.0
httpurple: 4.0.0
humdrum: 0.0.1
hyrule: 2.3.8
identity: 6.0.0
identy: 4.0.1
indexed-db: 1.0.0
indexed-monad: 3.0.0
int64: 3.0.0
integers: 6.0.0
interpolate: 5.0.2
intersection-observer: 1.0.1
invariant: 6.0.0
jarilo: 1.0.1
jelly: 0.10.0
jelly-router: 0.3.0
jelly-signal: 0.4.0
jest: 1.0.0
js-abort-controller: 1.0.0
js-bigints: 2.2.1
js-date: 8.0.0
js-fetch: 0.2.1
js-fileio: 3.0.0
js-intl: 1.0.4
js-iterators: 0.1.1
js-maps: 0.1.2
js-promise: 1.0.0
js-promise-aff: 1.0.0
js-timers: 6.1.0
js-uri: 3.1.0
json: 1.0.0
json-codecs: 5.0.0
justifill: 0.5.0
jwt: 0.0.9
labeled-data: 0.2.0
language-cst-parser: 0.14.0
lazy: 6.0.0
lazy-joe: 1.0.0
lcg: 4.0.0
leibniz: 5.0.0
leveldb: 1.0.1
liminal: 1.0.1
linalg: 6.0.0
lists: 7.0.0
literals: 1.0.2
logging: 3.0.0
logging-journald: 0.4.0
lumi-components: 18.0.0
machines: 7.0.0
maps-eager: 0.4.1
marionette: 1.0.0
marionette-react-basic-hooks: 0.1.1
marked: 0.1.0
matrices: 5.0.1
matryoshka: 1.0.0
maybe: 6.0.0
media-types: 6.0.0
meowclient: 1.0.0
midi: 4.0.0
milkis: 9.0.0
minibench: 4.0.1
mmorph: 7.0.0
monad-control: 5.0.0
monad-logger: 1.3.1
monad-loops: 0.5.0
monad-unlift: 1.0.1
monoid-extras: 0.0.1
monoidal: 0.16.0
morello: 0.4.0
mote: 3.0.0
motsunabe: 2.0.0
mvc: 0.0.1
mysql: 6.0.1
n3: 0.1.0
nano-id: 1.1.0
nanoid: 0.1.0
naturals: 3.0.0
nested-functor: 0.2.1
newtype: 5.0.0
nextjs: 0.1.1
nextui: 0.2.0
node-buffer: 9.0.0
node-child-process: 11.1.0
node-event-emitter: 3.0.0
node-execa: 5.0.0
node-fs: 9.1.0
node-glob-basic: 1.3.0
node-http: 9.1.0
node-http2: 1.1.1
node-human-signals: 1.0.0
node-net: 5.1.0
node-os: 5.1.0
node-path: 5.0.0
node-process: 11.2.0
node-readline: 8.1.0
node-sqlite3: 8.0.0
node-streams: 9.0.0
node-tls: 0.3.1
node-url: 7.0.1
node-zlib: 0.4.0
nonempty: 7.0.0
now: 6.0.0
npm-package-json: 2.0.0
nullable: 6.0.0
numberfield: 0.1.0
numbers: 9.0.1
oak: 3.1.1
oak-debug: 1.2.2
object-maps: 0.3.0
ocarina: 1.5.4
open-folds: 6.3.0
open-memoize: 6.1.0
open-pairing: 6.1.0
options: 7.0.0
optparse: 5.0.1
ordered-collections: 3.2.0
ordered-set: 0.4.0
orders: 6.0.0
owoify: 1.2.0
pairs: 9.0.1
parallel: 7.0.0
parsing: 10.2.0
parsing-dataview: 3.2.4
partial: 4.0.0
pathy: 9.0.0
pha: 0.13.0
phaser: 0.7.0
phylio: 1.1.2
pipes: 8.0.0
pirates-charm: 0.0.1
pmock: 0.9.0
point-free: 1.0.0
pointed-list: 0.5.1
polymorphic-vectors: 4.0.0
posix-types: 6.0.0
precise: 6.0.0
precise-datetime: 7.0.0
prelude: 6.0.1
prettier-printer: 3.0.0
profunctor: 6.0.1
profunctor-lenses: 8.0.0
protobuf: 4.3.0
psa-utils: 8.0.0
psci-support: 6.0.0
punycode: 1.0.0
qualified-do: 2.2.0
quantities: 12.2.0
quickcheck: 8.0.1
quickcheck-combinators: 0.1.3
quickcheck-laws: 7.0.0
quickcheck-utf8: 0.0.0
random: 6.0.0
rationals: 6.0.0
rdf: 0.1.0
react: 11.0.0
react-aria: 0.2.0
react-basic: 17.0.0
react-basic-classic: 3.0.0
react-basic-dnd: 10.1.0
react-basic-dom: 6.1.0
react-basic-emotion: 7.1.0
react-basic-hooks: 8.2.0
react-basic-storybook: 2.0.0
react-dom: 8.0.0
react-halo: 3.0.0
react-icons: 1.1.4
react-markdown: 0.1.0
react-testing-library: 4.0.1
react-virtuoso: 1.0.0
read: 1.0.1
recharts: 1.1.0
record: 4.0.0
record-extra: 5.0.1
record-ptional-fields: 0.1.2
record-studio: 1.0.4
refs: 6.0.0
remotedata: 5.0.1
resize-observer: 1.0.0
resource: 2.0.1
resourcet: 1.0.0
result: 1.0.3
return: 0.2.0
ring-modules: 5.0.1
rito: 0.3.4
rough-notation: 1.0.2
routing: 11.0.0
routing-duplex: 0.7.0
run: 5.0.0
safe-coerce: 2.0.0
safely: 4.0.1
school-of-music: 1.3.0
selection-foldable: 0.2.0
selective-functors: 1.0.1
semirings: 7.0.0
signal: 13.0.0
simple-emitter: 3.0.1
simple-i18n: 2.0.1
simple-json: 9.0.0
simple-ulid: 3.0.0
sized-matrices: 1.0.0
sized-vectors: 5.0.2
slug: 3.0.8
small-ffi: 4.0.1
soundfonts: 4.1.0
sparse-matrices: 1.3.0
sparse-polynomials: 2.0.5
spec: 7.6.0
spec-mocha: 5.1.0
spec-quickcheck: 5.0.0
splitmix: 2.1.0
ssrs: 1.0.0
st: 6.2.0
statistics: 0.3.2
strictlypositiveint: 1.0.1
string-parsers: 8.0.0
strings: 6.0.1
strings-extra: 4.0.0
stringutils: 0.0.12
substitute: 0.2.3
supply: 0.2.0
svg-parser: 3.0.0
systemd-journald: 0.3.0
tagged: 4.0.2
tailrec: 6.1.0
tecton: 0.2.1
tecton-halogen: 0.2.0
test-unit: 17.0.0
thermite: 6.3.1
thermite-dom: 0.3.1
these: 6.0.0
transformation-matrix: 1.0.1
transformers: 6.0.0
tree-rose: 4.0.2
ts-bridge: 4.0.0
tuples: 7.0.0
two-or-more: 1.0.0
type-equality: 4.0.1
typedenv: 2.0.1
typelevel: 6.0.0
typelevel-lists: 2.1.0
typelevel-peano: 1.0.1
typelevel-prelude: 7.0.0
typelevel-regex: 0.0.3
typelevel-rows: 0.1.0
uint: 7.0.0
ulid: 3.0.1
uncurried-transformers: 1.1.0
undefined: 2.0.0
undefined-is-not-a-problem: 1.1.0
unfoldable: 6.0.0
unicode: 6.0.0
unique: 0.6.1
unlift: 1.0.1
unordered-collections: 3.1.0
unsafe-coerce: 6.0.0
unsafe-reference: 5.0.0
untagged-to-tagged: 0.1.4
untagged-union: 1.0.0
uri: 9.0.0
uuid: 9.0.0
uuidv4: 1.0.0
validation: 6.0.0
variant: 8.0.0
variant-encodings: 2.0.0
vectorfield: 1.0.1
vectors: 2.1.0
versions: 7.0.0
visx: 0.0.2
web-clipboard: 5.0.0
web-cssom: 2.0.0
web-cssom-view: 0.1.0
web-dom: 6.0.0
web-dom-parser: 8.0.0
web-dom-xpath: 3.0.0
web-encoding: 3.0.0
web-events: 4.0.0
web-fetch: 4.0.1
web-file: 4.0.0
web-geometry: 0.1.0
web-html: 4.1.0
web-pointerevents: 2.0.0
web-proletarian: 1.0.0
web-promise: 3.2.0
web-resize-observer: 2.1.0
web-router: 1.0.0
web-socket: 4.0.0
web-storage: 5.0.0
web-streams: 4.0.0
web-touchevents: 4.0.0
web-uievents: 5.0.0
web-url: 2.0.0
web-workers: 1.1.0
web-xhr: 5.0.1
webextension-polyfill: 0.1.0
webgpu: 0.0.1
which: 2.0.0
xterm: 1.0.0
yoga-fetch: 1.0.1
yoga-json: 5.1.0
yoga-om: 0.1.0
yoga-postgres: 6.0.0
yoga-tree: 1.0.0
z3: 0.0.2
zipperarray: 2.0.0
extra_packages: {}
packages:
aff:
@@ -923,6 +454,21 @@ packages:
- transformers
- tuples
- unsafe-coerce
freet:
type: registry
version: 7.0.0
integrity: sha256-zkL6wU4ZPq8xz1kGFxoliWqyhBksepMJTyA68VEBaJo=
dependencies:
- aff
- bifunctors
- effect
- either
- exists
- free
- prelude
- tailrec
- transformers
- tuples
functions:
type: registry
version: 6.0.0
@@ -1059,6 +605,15 @@ packages:
- free
- functors
- transformers
monad-control:
type: registry
version: 5.0.0
integrity: sha256-bgfDW30wbIm70NR1Tvvh9P+VFQMDh1wK2sSJXCj/dZc=
dependencies:
- aff
- freet
- identity
- lists
newtype:
type: registry
version: 5.0.0
@@ -1171,6 +726,45 @@ packages:
- posix-types
- prelude
- unsafe-coerce
node-stream-pipes:
type: registry
version: 2.0.2
integrity: sha256-IwkFgzWVwqjZkQRLYBGaRukKqYIw2I7wKHwIXRcdBWI=
dependencies:
- aff
- arrays
- console
- control
- datetime
- effect
- either
- exceptions
- foldable-traversable
- foreign-object
- fork
- lists
- maybe
- mmorph
- newtype
- node-buffer
- node-event-emitter
- node-fs
- node-path
- node-streams
- node-zlib
- now
- ordered-collections
- parallel
- pipes
- prelude
- profunctor
- st
- strings
- tailrec
- transformers
- tuples
- unordered-collections
- unsafe-coerce
node-streams:
type: registry
version: 9.0.0
@@ -1184,6 +778,19 @@ packages:
- node-event-emitter
- nullable
- prelude
node-zlib:
type: registry
version: 0.4.0
integrity: sha256-kYSajFQFzWVg71l5/y4w4kXdTr5EJoqyV3D2RqmAjQ4=
dependencies:
- aff
- effect
- either
- functions
- node-buffer
- node-streams
- prelude
- unsafe-coerce
nonempty:
type: registry
version: 7.0.0
@@ -1242,8 +849,8 @@ packages:
- prelude
parallel:
type: registry
version: 7.0.0
integrity: sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=
version: 6.0.0
integrity: sha256-VJbkGD0rAKX+NUEeBJbYJ78bEKaZbgow+QwQEfPB6ko=
dependencies:
- control
- effect
@@ -1566,6 +1173,38 @@ packages:
- foldable-traversable
- maybe
- strings
unlift:
type: registry
version: 1.0.1
integrity: sha256-nbBCVV0fZz/3UHKoW11dcTwBYmQOIgK31ht2BN47RPw=
dependencies:
- aff
- effect
- either
- freet
- identity
- lists
- maybe
- monad-control
- prelude
- st
- transformers
- tuples
unordered-collections:
type: registry
version: 3.1.0
integrity: sha256-H2eQR+ylI+cljz4XzWfEbdF7ee+pnw2IZCeq69AuJ+Q=
dependencies:
- arrays
- enums
- functions
- integers
- lists
- prelude
- record
- tuples
- typelevel-prelude
- unfoldable
unsafe-coerce:
type: registry
version: 6.0.0

View File

@@ -1,7 +1,7 @@
package:
name: postgresql
publish:
version: '1.0.0'
version: '1.7.1'
license: 'GPL-3.0-or-later'
location:
githubOwner: 'cakekindel'
@@ -33,18 +33,23 @@ package:
- newtype: ">=5.0.0 <6.0.0"
- node-buffer: ">=9.0.0 <10.0.0"
- node-event-emitter: ">=3.0.0 <4.0.0"
- node-stream-pipes: ">=2.0.2 <3.0.0"
- node-streams: ">=9.0.0 <10.0.0"
- nullable: ">=6.0.0 <7.0.0"
- parallel: ">=6.0.0 <7.0.0"
- partial: ">=4.0.0 <5.0.0"
- pipes: ">=8.0.0 <9.0.0"
- precise-datetime: ">=7.0.0 <8.0.0"
- prelude: ">=6.0.1 <7.0.0"
- profunctor: ">=6.0.1 <7.0.0"
- record: ">=4.0.0 <5.0.0"
- simple-json: ">=9.0.0 <10.0.0"
- strings: ">=6.0.1 <7.0.0"
- tailrec: ">=6.1.0 <7.0.0"
- transformers: ">=6.0.0 <7.0.0"
- tuples: ">=7.0.0 <8.0.0"
- typelevel-prelude: ">=7.0.0 <8.0.0"
- unlift: ">=1.0.1 <2.0.0"
- unsafe-coerce: ">=6.0.0 <7.0.0"
test:
main: Test.Main
@@ -53,10 +58,9 @@ package:
- foreign-object
- node-child-process
- node-process
- precise-datetime
- quickcheck
- spec
- spec-quickcheck
workspace:
extraPackages: {}
packageSet:
registry: 50.5.0

View File

@@ -3,21 +3,28 @@ module Control.Monad.Postgres.Base where
import Prelude
import Control.Alt (class Alt)
import Control.Alternative (class Plus)
import Control.Alternative (class Alternative, class Plus)
import Control.Monad.Error.Class (class MonadError, class MonadThrow, catchError, throwError)
import Control.Monad.Fork.Class (class MonadBracket, class MonadFork, class MonadKill, bracket, kill, never, uninterruptible)
import Control.Monad.Morph (class MFunctor, class MMonad)
import Control.Monad.Postgres.Session (class MonadSession, SessionT, exec, exec_, query)
import Control.Monad.Postgres.Cursor (class MonadCursor, CursorT)
import Control.Monad.Postgres.Session (class MonadSession, SessionT, exec, exec_, query, streamIn, streamOut)
import Control.Monad.Reader (class MonadAsk, class MonadReader, ReaderT, ask, local, runReaderT)
import Control.Monad.Rec.Class (class MonadRec)
import Control.Monad.Trans.Class (class MonadTrans, lift)
import Control.Parallel (class Parallel, parallel, sequential)
import Data.Newtype (class Newtype, unwrap, wrap)
import Effect.Aff (Fiber)
import Data.Postgres (RepT)
import Data.Postgres.Query (class AsQuery, asQuery)
import Data.Postgres.Raw (Raw)
import Data.Postgres.Result (class FromRow, fromRow)
import Data.Tuple.Nested ((/\))
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Pool (Pool)
import Effect.Aff.Postgres.Pool as Pool
import Effect.Aff.Unlift (class MonadUnliftAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (Error)
import Effect.Unlift (class MonadUnliftEffect)
import Prim.Row (class Union)
-- | Monad handling pool resource acquisition & release
@@ -50,6 +57,7 @@ newtype PostgresT m a = PostgresT (ReaderT Pool m a)
derive instance Newtype (PostgresT m a) _
derive newtype instance (Functor m) => Functor (PostgresT m)
derive newtype instance (Apply m) => Apply (PostgresT m)
derive newtype instance (Alternative m) => Alternative (PostgresT m)
derive newtype instance (Applicative m) => Applicative (PostgresT m)
derive newtype instance (Plus m) => Plus (PostgresT m)
derive newtype instance (Alt m) => Alt (PostgresT m)
@@ -57,6 +65,9 @@ derive newtype instance (Bind m) => Bind (PostgresT m)
derive newtype instance (Monad m) => Monad (PostgresT m)
derive newtype instance (MonadEffect m) => MonadEffect (PostgresT m)
derive newtype instance (MonadAff m) => MonadAff (PostgresT m)
derive newtype instance (MonadUnliftEffect m) => MonadUnliftEffect (PostgresT m)
derive newtype instance (MonadUnliftAff m) => MonadUnliftAff (PostgresT m)
derive newtype instance MonadRec m => MonadRec (PostgresT m)
derive newtype instance MonadTrans (PostgresT)
derive newtype instance (MonadThrow e m) => MonadThrow e (PostgresT m)
derive newtype instance (MonadError e m) => MonadError e (PostgresT m)
@@ -85,30 +96,57 @@ instance (MonadBracket e f m, MonadAff m) => MonadSession (PostgresT m) where
query = session <<< query
exec = session <<< exec
exec_ = session <<< exec_
streamIn = session <<< streamIn
streamOut = session <<< streamOut
-- | Lifts a session to `PostgresT`, releasing the client to the pool
-- | after execution.
session :: forall e f m a. MonadBracket e f m => MonadAff m => MonadSession (SessionT m) => SessionT m a -> PostgresT m a
session m = do
pool <- ask
let
acq = liftAff $ Pool.connect pool
rel _ c = liftEffect $ Pool.release pool c
lift $ bracket acq rel (runReaderT m)
-- | Lifts a session to `PostgresT`, running the session
-- | in a transaction.
-- | Typeclass generalizing `PostgresT`. Allows for dependency-injecting different
-- | implementations of the idea of a postgres connection.
-- |
-- | If the session throws an error, the transaction will be
-- | rolled back and the error rethrown.
transaction :: forall m a. MonadBracket Error Fiber m => MonadAff m => MonadSession (SessionT m) => SessionT m a -> PostgresT m a
transaction m =
let
begin = void $ exec "begin;"
commit = m <* exec "commit;"
rollback e = exec "rollback;" *> throwError e
in
session $ begin *> catchError commit rollback
-- | - `session` - Session monad (for `PostgresT` this is `SessionT`)
-- | - `cursor` - Cursor session monad (for `PostgresT` this is `CursorT`)
-- | - `ct` - Open type parameter for cursor type. Don't pin this to a concrete type.
class (Monad m, MonadSession session, MonadCursor cursor ct) <= MonadPostgres m session cursor ct | m -> ct cursor session where
-- | Run a session in `m`.
session :: session ~> m
-- | Run a session in `m`, wrapped in a transaction.
-- |
-- | If any errors are raised, the transaction is rolled back and
-- | the error rethrown.
transaction :: session ~> m
-- | `cursor`, but using a custom deserialize function for the data
-- | yielded by the cursor
cursorWith :: forall q. AsQuery q => (Array Raw -> RepT ct) -> String -> q -> cursor ~> m
instance (MonadBracket e f m, MonadAff m, MonadSession (SessionT m), MonadCursor (CursorT t (SessionT m)) t) => MonadPostgres (PostgresT m) (SessionT m) (CursorT ct (SessionT m)) ct where
session m = do
pool <- ask
let
acq = liftAff $ Pool.connect pool
rel _ c = liftEffect $ Pool.release pool c
lift $ bracket acq rel (runReaderT m)
transaction m =
let
begin = void $ exec "begin;"
commit = m <* exec "commit;"
rollback e = exec "rollback;" *> throwError e
in
session $ begin *> catchError commit rollback
cursorWith f cur q m =
transaction do
q' <- liftEffect $ asQuery q
exec_ $ "declare " <> cur <> " cursor for (" <> (unwrap q').text <> ");"
runReaderT (unwrap m) (cur /\ f)
-- | Create a server-side cursor for a query in a transaction,
-- | and execute a `CursorT` with a view to the new cursor.
cursor :: forall @cursort t session cursor q a. MonadPostgres t session cursor cursort => AsQuery q => FromRow cursort => String -> q -> cursor a -> t a
cursor = cursorWith fromRow
-- | Execute a `PostgresT` using an existing connection pool.
-- |
-- | This will not invoke `Pool.end` after executing.
withPool :: forall m a. PostgresT m a -> Pool -> m a
withPool = runReaderT <<< unwrap
-- | Create a new connection pool from the provided config and execute
-- | the postgres monad, invoking `Effect.Aff.Postgres.Pool.end` afterwards.
@@ -118,4 +156,4 @@ runPostgres cfg m =
acq = liftEffect $ Pool.make @r @missing @trash cfg
rel _ p = liftAff $ Pool.end p
in
bracket acq rel $ runReaderT $ unwrap m
bracket acq rel $ withPool m

View File

@@ -3,26 +3,35 @@ module Control.Monad.Postgres.Cursor where
import Prelude
import Control.Alt (class Alt)
import Control.Alternative (class Plus)
import Control.Alternative (class Alternative, class Plus)
import Control.Monad.Error.Class (class MonadError, class MonadThrow)
import Control.Monad.Fork.Class (class MonadBracket, class MonadFork, class MonadKill, bracket, kill, never, uninterruptible)
import Control.Monad.Postgres.Base (PostgresT, transaction)
import Control.Monad.Postgres.Session (class MonadSession, SessionT, exec, exec_, query)
import Control.Monad.Reader (class MonadAsk, class MonadReader, ReaderT, ask, local, runReaderT)
import Control.Monad.Postgres.Session (class MonadSession, exec, exec_, query, streamIn, streamOut)
import Control.Monad.Reader (class MonadAsk, class MonadReader, ReaderT, ask, local)
import Control.Monad.Rec.Class (class MonadRec)
import Control.Monad.Trans.Class (class MonadTrans, lift)
import Control.Parallel (class Parallel, parallel, sequential)
import Data.Array as Array
import Data.Maybe (Maybe)
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Postgres.Query (class AsQuery, asQuery)
import Data.Postgres.Result (class FromRow)
import Effect.Aff (Fiber)
import Data.Postgres (RepT, smash)
import Data.Postgres.Raw (Raw)
import Data.Postgres.Result (RowsAffected(..))
import Data.Traversable (traverse)
import Data.Tuple.Nested (type (/\), (/\))
import Effect.Aff.Class (class MonadAff)
import Effect.Aff.Unlift (class MonadUnliftAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (Error)
import Effect.Unlift (class MonadUnliftEffect)
data Move
-- | `MOVE RELATIVE`
= MoveRelative Int
-- | `MOVE ABSOLUTE`
| MoveTo Int
newtype CursorT :: forall k. Type -> (k -> Type) -> k -> Type
newtype CursorT t m a = CursorT (ReaderT String m a)
newtype CursorT t m a = CursorT (ReaderT (String /\ (Array Raw -> RepT t)) m a)
derive instance Newtype (CursorT t m a) _
derive newtype instance (Functor m) => Functor (CursorT t m)
@@ -30,10 +39,14 @@ derive newtype instance (Apply m) => Apply (CursorT t m)
derive newtype instance (Applicative m) => Applicative (CursorT t m)
derive newtype instance (Plus m) => Plus (CursorT t m)
derive newtype instance (Alt m) => Alt (CursorT t m)
derive newtype instance (Alternative m) => Alternative (CursorT t m)
derive newtype instance (Bind m) => Bind (CursorT t m)
derive newtype instance (Monad m) => Monad (CursorT t m)
derive newtype instance (MonadEffect m) => MonadEffect (CursorT t m)
derive newtype instance (MonadAff m) => MonadAff (CursorT t m)
derive newtype instance MonadRec m => MonadRec (CursorT t m)
derive newtype instance (MonadUnliftEffect m) => MonadUnliftEffect (CursorT t m)
derive newtype instance (MonadUnliftAff m) => MonadUnliftAff (CursorT t m)
derive newtype instance MonadTrans (CursorT t)
derive newtype instance (MonadThrow e m) => MonadThrow e (CursorT t m)
derive newtype instance (MonadError e m) => MonadError e (CursorT t m)
@@ -46,10 +59,10 @@ instance (Monad m, MonadBracket e f (ReaderT String m), MonadBracket e f m) => M
uninterruptible a = wrap $ uninterruptible $ unwrap a
never = lift $ never
instance Monad m => MonadAsk String (CursorT t m) where
instance Monad m => MonadAsk (String /\ (Array Raw -> RepT t)) (CursorT t m) where
ask = wrap ask
instance Monad m => MonadReader String (CursorT t m) where
instance Monad m => MonadReader (String /\ (Array Raw -> RepT t)) (CursorT t m) where
local f m = wrap $ local f $ unwrap m
instance (Apply m, Apply p, Parallel p m) => Parallel (CursorT t p) (CursorT t m) where
@@ -78,34 +91,40 @@ instance (Apply m, Apply p, Parallel p m) => Parallel (CursorT t p) (CursorT t m
-- | e <- fetchAll -- 15..100
-- | pure unit
-- | ```
class (MonadSession m, FromRow t) <= MonadCursor m t where
class (MonadSession m) <= MonadCursor m t where
-- | Fetch a specified number of rows from the cursor
fetch :: Int -> m (Array t)
-- | Fetch all remaining rows from the cursor
fetchAll :: m (Array t)
-- | Change the cursor's position without fetching any data,
-- | returning the number of rows skipped.
move :: Move -> m Int
instance (FromRow t, MonadSession m) => MonadCursor (CursorT t m) t where
instance (MonadSession m) => MonadCursor (CursorT t m) t where
fetch n = do
cur <- ask
query $ "fetch forward " <> show n <> " from " <> cur
cur /\ f <- ask
raw :: Array (Array Raw) <- query $ "fetch forward " <> show n <> " from " <> cur
liftEffect $ smash $ traverse f raw
fetchAll = do
cur <- ask
query $ "fetch all from " <> cur
cur /\ f <- ask
raw :: Array (Array Raw) <- query $ "fetch all from " <> cur
liftEffect $ smash $ traverse f raw
move (MoveTo n) = do
cur /\ _ <- ask
RowsAffected n' <- query $ ("move absolute $1 from " <> cur) /\ n
pure n'
move (MoveRelative n) = do
cur /\ _ <- ask
RowsAffected n' <- query $ ("move relative $1 from " <> cur) /\ n
pure n'
instance (MonadSession m) => MonadSession (CursorT t m) where
query = lift <<< query
exec = lift <<< exec
exec_ = lift <<< exec_
streamIn = lift <<< streamIn
streamOut = lift <<< streamOut
-- | Fetch the next row from the cursor
fetchOne :: forall m t. MonadCursor m t => m (Maybe t)
fetchOne = Array.head <$> fetch 1
-- | Create a server-side cursor for a query in a transaction,
-- | and execute a `CursorT` with a view to the new cursor.
cursor :: forall m @t a q. AsQuery q => MonadAff m => MonadBracket Error Fiber m => MonadSession (SessionT m) => String -> q -> CursorT t (SessionT m) a -> PostgresT m a
cursor cur q m =
transaction do
q' <- liftEffect $ asQuery q
exec_ $ "declare " <> cur <> " cursor for (" <> (unwrap q').text <> ");"
runReaderT (unwrap m) cur

View File

@@ -8,6 +8,8 @@ import Data.Postgres.Result (class FromRows)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Client (Client)
import Effect.Aff.Postgres.Client as Client
import Effect.Class (liftEffect)
import Node.Stream (Writable, Readable)
type SessionT :: forall k. (k -> Type) -> k -> Type
type SessionT = ReaderT Client
@@ -20,6 +22,24 @@ class MonadAff m <= MonadSession m where
exec :: forall q. AsQuery q => q -> m Int
-- | Executes a query and discards the result
exec_ :: forall q. AsQuery q => q -> m Unit
-- | Execute a query with a `Writable` stream to `STDIN`
-- |
-- | Use with `COPY .. FROM` like so:
-- |
-- | ```purescript
-- | w <- streamIn "COPY foo FROM STDIN WITH (FORMAT CSV, HEADER true)"
-- | liftEffect $ Stream.writeString "bar\n\"my bar column\"" UTF8 w
-- | ```
streamIn :: String -> m (Writable ())
-- | Execute a query with a `Readable` stream from `STDOUT`
-- |
-- | Use with `COPY .. TO` like so:
-- |
-- | ```purescript
-- | r <- streamIn "COPY foo TO STDIN WITH (FORMAT CSV, HEADER true)"
-- | liftEffect $ Stream.readString r -- "bar\n\"my bar column\""
-- | ```
streamOut :: String -> m (Readable ())
instance MonadAff m => MonadSession (SessionT m) where
query q = do
@@ -29,3 +49,9 @@ instance MonadAff m => MonadSession (SessionT m) where
client <- ask
liftAff $ Client.exec q client
exec_ = void <<< exec
streamIn q = do
client <- ask
liftEffect $ Client.execWithStdin q client
streamOut q = do
client <- ask
liftEffect $ Client.queryWithStdout q client

View File

@@ -5,71 +5,69 @@ import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (liftMaybe)
import Data.Array.NonEmpty.Internal (NonEmptyArray)
import Data.Either (hush)
import Data.Bifunctor (lmap)
import Data.Foldable (intercalate)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep as G
import Data.Maybe (Maybe(..), fromJust)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype as Newtype
import Data.Postgres (RepT, deserialize, serialize)
import Data.Postgres.Custom (class CustomRep, quoted, typeName)
import Data.Postgres (class Rep, RepT, deserialize, serialize)
import Data.Postgres.Custom (quoted)
import Data.Postgres.Query (Query, emptyQuery)
import Data.Postgres.Raw (Raw)
import Data.String as String
import Data.String.Regex (Regex)
import Data.String.Regex as Regex
import Data.String.Regex.Flags as Regex.Flags
import Data.Symbol (class IsSymbol)
import Data.Traversable (find)
import Data.Tuple (fst, snd)
import Data.Tuple.Nested (type (/\), (/\))
import Foreign (ForeignError(..))
import Partial.Unsafe (unsafePartial)
import Type.Prelude (Proxy(..), reflectSymbol)
upperRe :: Regex
upperRe = unsafePartial fromJust $ hush $ Regex.regex "[A-Z]" Regex.Flags.global
typeName :: forall @a ty. CustomEnum a ty => String
typeName = reflectSymbol (Proxy @ty)
leadingUnderRe :: Regex
leadingUnderRe = unsafePartial fromJust $ hush $ Regex.regex "^_" Regex.Flags.noFlags
pascalToSnake :: String -> String
pascalToSnake = String.toLower <<< Regex.replace leadingUnderRe "" <<< Regex.replace upperRe "_$1"
class CustomRep a ty <= CustomEnum a ty | a -> ty where
enumVariants :: NonEmptyArray a
class (IsSymbol ty, Rep a, Eq a) <= CustomEnum a ty | a -> ty where
enumVariants :: NonEmptyArray (a /\ String)
parseEnum :: String -> Maybe a
printEnum :: a -> String
defaultParseEnum :: forall a ty. CustomEnum a ty => String -> Maybe a
defaultParseEnum s = map fst $ find (eq s <<< snd) enumVariants
defaultPrintEnum :: forall a ty. CustomEnum a ty => a -> String
defaultPrintEnum a = fromMaybe "ERROR: CustomEnum enumVariants was not exhaustive" $ map snd $ find (eq a <<< fst) enumVariants
defaultDeserializeEnum :: forall @a ty. CustomEnum a ty => Raw -> RepT a
defaultDeserializeEnum raw = do
s <- deserialize raw
let e = pure $ ForeignError $ "unsupported enum variant for " <> typeName @a <> ": " <> quoted s
liftMaybe e $ parseEnum s
defaultSerializeEnum :: forall @a ty. CustomEnum a ty => a -> RepT Raw
defaultSerializeEnum = serialize <<< printEnum
class GenericCustomEnum a where
genericEnumVariants' :: NonEmptyArray a
genericEnumVariants' :: NonEmptyArray (a /\ String)
genericParseEnum' :: String -> Maybe a
genericPrintEnum' :: a -> String
instance IsSymbol n => GenericCustomEnum (G.Constructor n G.NoArguments) where
genericEnumVariants' = pure (G.Constructor @n G.NoArguments)
genericEnumVariants' = pure (G.Constructor @n G.NoArguments /\ reflectSymbol (Proxy @n))
genericParseEnum' s
| s == reflectSymbol (Proxy @n) = Just (G.Constructor @n G.NoArguments)
| otherwise = Nothing
genericPrintEnum' _ = reflectSymbol (Proxy @n)
instance (GenericCustomEnum a, GenericCustomEnum b) => GenericCustomEnum (G.Sum a b) where
genericEnumVariants' = (G.Inl <$> genericEnumVariants' @a) <> (G.Inr <$> genericEnumVariants' @b)
genericEnumVariants' = (lmap G.Inl <$> genericEnumVariants' @a) <> (lmap G.Inr <$> genericEnumVariants' @b)
genericParseEnum' s = (G.Inl <$> genericParseEnum' @a s) <|> (G.Inr <$> genericParseEnum' @b s)
genericPrintEnum' (G.Inl a) = genericPrintEnum' a
genericPrintEnum' (G.Inr a) = genericPrintEnum' a
enumDeserialize :: forall @a ty. CustomEnum a ty => Raw -> RepT a
enumDeserialize raw = do
s <- deserialize raw
let e = pure $ ForeignError $ "unsupported enum variant for " <> typeName @a <> ": " <> quoted s
liftMaybe e $ parseEnum s
enumSerialize :: forall @a ty. CustomEnum a ty => a -> RepT Raw
enumSerialize = serialize <<< printEnum
enumPrintExpr :: forall @a ty. CustomEnum a ty => a -> Maybe String
enumPrintExpr = Just <<< (\s -> quoted s <> " :: " <> typeName @a) <<< printEnum
genericEnumVariants :: forall a g. Generic a g => GenericCustomEnum g => NonEmptyArray a
genericEnumVariants = G.to <$> genericEnumVariants'
genericEnumVariants :: forall a g. Generic a g => GenericCustomEnum g => NonEmptyArray (a /\ String)
genericEnumVariants = lmap G.to <$> genericEnumVariants'
genericParseEnum :: forall a g. Generic a g => GenericCustomEnum g => String -> Maybe a
genericParseEnum = map G.to <<< genericParseEnum'
@@ -80,9 +78,7 @@ genericPrintEnum = genericPrintEnum' <<< G.from
create :: forall @a ty. CustomEnum a ty => Query
create =
let
variants' :: NonEmptyArray a
variants' = enumVariants
variants = intercalate ", " $ quoted <$> printEnum <$> variants'
variants = intercalate ", " $ quoted <$> snd <$> enumVariants @a
q = "create type " <> typeName @a <> " as enum (" <> variants <> ");"
in
Newtype.modify (_ { text = q }) emptyQuery

View File

@@ -2,28 +2,22 @@ module Data.Postgres.Custom where
import Prelude
import Control.Monad.Except (ExceptT)
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe)
import Data.Postgres.Raw (Raw)
import Effect (Effect)
import Foreign (ForeignError)
import Type.Data.Symbol (reflectSymbol)
import Type.Prelude (class IsSymbol, Proxy(..))
class (IsSymbol ty) <= CustomSerialize a ty | a -> ty where
customPrintExpr :: a -> Maybe String
customSerialize :: a -> ExceptT (NonEmptyList ForeignError) Effect Raw
class (IsSymbol ty) <= CustomDeserialize a ty | a -> ty where
customDeserialize :: Raw -> ExceptT (NonEmptyList ForeignError) Effect a
class (IsSymbol ty, CustomSerialize a ty, CustomDeserialize a ty) <= CustomRep a ty | a -> ty
instance (IsSymbol ty, CustomSerialize a ty, CustomDeserialize a ty) => CustomRep a ty
import Data.Either (hush)
import Data.Maybe (fromJust)
import Data.String as String
import Data.String.Regex (Regex)
import Data.String.Regex as Regex
import Data.String.Regex.Flags as Regex.Flags
import Partial.Unsafe (unsafePartial)
quoted :: String -> String
quoted s = "'" <> s <> "'"
typeName :: forall @a ty. CustomRep a ty => String
typeName = reflectSymbol (Proxy @ty)
upperRe :: Regex
upperRe = unsafePartial fromJust $ hush $ Regex.regex "[A-Z]" Regex.Flags.global
leadingUnderRe :: Regex
leadingUnderRe = unsafePartial fromJust $ hush $ Regex.regex "^_" Regex.Flags.noFlags
pascalToSnake :: String -> String
pascalToSnake = String.toLower <<< Regex.replace leadingUnderRe "" <<< Regex.replace upperRe "_$1"

View File

@@ -0,0 +1,30 @@
import PostgresInterval from 'postgres-interval'
/** @typedef {import('postgres-interval').IPostgresInterval} I */
/** @type {(o: {years: number, months: number, days: number, hours: number, minutes: number, seconds: number, milliseconds: number}) => I} */
export const make = o => Object.assign(PostgresInterval(''), o)
/** @type {(s: string) => () => I} */
export const parse = s => () => PostgresInterval(s)
/** @type {(a: I) => number} */
export const getYears = i => i.years || 0.0
/** @type {(a: I) => number} */
export const getMonths = i => i.months || 0.0
/** @type {(a: I) => number} */
export const getDays = i => i.days || 0.0
/** @type {(a: I) => number} */
export const getMinutes = i => i.minutes || 0.0
/** @type {(a: I) => number} */
export const getHours = i => i.hours || 0.0
/** @type {(a: I) => number} */
export const getSeconds = i => i.seconds || 0.0
/** @type {(a: I) => number} */
export const getMilliseconds = i => i.milliseconds || 0.0

View File

@@ -0,0 +1,88 @@
module Data.Postgres.Interval where
import Prelude
import Data.Int as Int
import Data.Maybe (Maybe(..))
import Data.Newtype (unwrap)
import Data.Time.Duration (class Duration, Days(..), Hours(..), Milliseconds(..), Minutes(..), Seconds(..), convertDuration)
import Effect (Effect)
zero :: IntervalRecord
zero = {years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0.0}
type IntervalRecord =
{ years :: Int
, months :: Int
, days :: Int
, hours :: Int
, minutes :: Int
, seconds :: Int
, milliseconds :: Number
}
foreign import data Interval :: Type
foreign import make :: IntervalRecord -> Interval
foreign import parse :: String -> Effect Interval
foreign import getYears :: Interval -> Int
foreign import getMonths :: Interval -> Int
foreign import getDays :: Interval -> Int
foreign import getHours :: Interval -> Int
foreign import getMinutes :: Interval -> Int
foreign import getSeconds :: Interval -> Int
foreign import getMilliseconds :: Interval -> Number
toDuration :: forall d. Semigroup d => Duration d => Interval -> Maybe d
toDuration a =
let
includesMonths = getYears a > 0 || getMonths a > 0
days :: d
days = convertDuration $ Days $ Int.toNumber $ getDays a
hours :: d
hours = convertDuration $ Hours $ Int.toNumber $ getHours a
minutes :: d
minutes = convertDuration $ Minutes $ Int.toNumber $ getMinutes a
seconds :: d
seconds = convertDuration $ Seconds $ Int.toNumber $ getSeconds a
milliseconds :: d
milliseconds = convertDuration $ Milliseconds $ getMilliseconds a
in
if includesMonths then Nothing else Just (days <> hours <> minutes <> seconds <> milliseconds)
toRecord :: Interval -> IntervalRecord
toRecord a =
{ years: getYears a
, months: getMonths a
, days: getDays a
, hours: getHours a
, minutes: getMinutes a
, seconds: getSeconds a
, milliseconds: getMilliseconds a
}
fromDuration :: forall d. Duration d => d -> Interval
fromDuration a =
let
millisTotal :: Number
millisTotal = (unwrap :: Milliseconds -> Number) $ convertDuration a
secondFactor = 1000.0
minuteFactor = 60.0 * secondFactor
hourFactor = 60.0 * minuteFactor
dayFactor = 24.0 * hourFactor
days = Int.trunc $ millisTotal / dayFactor
daysRem = millisTotal - (Int.toNumber days * dayFactor)
hours = Int.trunc $ daysRem / hourFactor
hoursRem = daysRem - (Int.toNumber hours * hourFactor)
minutes = Int.trunc $ hoursRem / minuteFactor
minutesRem = hoursRem - (Int.toNumber minutes * minuteFactor)
seconds = Int.trunc $ minutesRem / secondFactor
milliseconds = minutesRem - (Int.toNumber seconds * secondFactor)
in
make {years: 0, months: 0, days, hours, minutes, seconds, milliseconds}

View File

@@ -5,6 +5,7 @@ import Prelude
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable, toNullable)
import Data.Postgres (class Rep, serialize, smash)
import Data.Postgres.Raw (Raw)
import Data.Tuple.Nested (type (/\), (/\))
import Effect (Effect)
@@ -16,7 +17,11 @@ import Type.Prelude (Proxy(..))
-- | * `text` - the query string
-- | * `values` - query parameter values
-- | * `name` (optional) - providing this will create this query as a [prepared statement](https://node-postgres.com/features/queries#prepared-statements)
newtype Query = Query { text :: String, values :: Array Raw, name :: Maybe String }
newtype Query = Query
{ text :: String
, values :: Array Raw
, name :: Maybe String
}
derive instance Newtype Query _
derive newtype instance Show Query
@@ -25,6 +30,20 @@ derive newtype instance Show Query
emptyQuery :: Query
emptyQuery = Query { text: "", values: [], name: Nothing }
-- | Any value that can be converted to an array of query parameters
class AsQueryParams a where
asQueryParams :: a -> Effect (Array Raw)
instance AsQueryParams (Array Raw) where
asQueryParams = pure
else instance (Rep a, AsQueryParams b) => AsQueryParams (a /\ b) where
asQueryParams (a /\ tail) = do
a' <- map pure $ smash $ serialize a
tail' <- asQueryParams tail
pure $ a' <> tail'
else instance (Rep a) => AsQueryParams a where
asQueryParams = map pure <<< smash <<< serialize
-- | Values that can be rendered as a SQL query
class AsQuery a where
asQuery :: a -> Effect Query
@@ -38,8 +57,10 @@ instance AsQuery Query where
instance AsQuery String where
asQuery text = pure $ Query { text, values: [], name: Nothing }
instance AsQuery (String /\ Array Raw) where
asQuery (text /\ values) = pure $ Query { text, values, name: Nothing }
instance AsQueryParams ps => AsQuery (String /\ ps) where
asQuery (text /\ ps) = do
ps' <- asQueryParams ps
pure $ Query { text, values: ps', name: Nothing }
-- | FFI
type QueryRaw = { text :: String, values :: Array Raw, name :: Nullable String, rowMode :: String }

View File

@@ -4,11 +4,13 @@ import Prelude
import Control.Monad.Error.Class (liftMaybe, throwError)
import Data.Array as Array
import Data.Generic.Rep (class Generic)
import Data.Int as Int
import Data.Maybe (Maybe)
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Data.Postgres (class Rep, RepT, deserialize)
import Data.Postgres (class Deserialize, RepT, deserialize)
import Data.Postgres.Raw (Raw)
import Data.Traversable (traverse)
import Data.Tuple (Tuple)
@@ -27,17 +29,29 @@ foreign import data Result :: Type
rowsAffected :: Result -> Maybe Int
rowsAffected = Int.fromNumber <=< Nullable.toMaybe <<< __rowsAffected
class FromRows a where
fromRows :: Array (Array Raw) -> RepT a
newtype RowsAffected = RowsAffected Int
instance (FromRow a) => FromRows (Array a) where
fromRows = traverse fromRow
derive instance Newtype RowsAffected _
derive instance Generic RowsAffected _
derive newtype instance Eq RowsAffected
derive newtype instance Ord RowsAffected
derive newtype instance Show RowsAffected
class FromRows a where
fromRows :: RowsAffected -> Array (Array Raw) -> RepT a
instance FromRows RowsAffected where
fromRows a _ = pure a
else instance (FromRow a) => FromRows (Array a) where
fromRows _ = traverse fromRow
else instance (FromRow a) => FromRows (Maybe a) where
fromRows _ = map Array.head <<< traverse fromRow
else instance (FromRow a) => FromRows a where
fromRows =
fromRows a =
let
e = pure $ ForeignError $ "Expected at least 1 row"
in
liftMaybe e <=< map Array.head <<< traverse fromRow
liftMaybe e <=< fromRows @(Maybe a) a
-- | Can be unmarshalled from a queried row
-- |
@@ -77,7 +91,7 @@ class FromRow (a :: Type) where
-- | Performs the conversion
fromRow :: Array Raw -> RepT a
instance (Rep a, FromRow b) => FromRow (a /\ b) where
instance (Deserialize a, FromRow b) => FromRow (a /\ b) where
minColumnCount _ = minColumnCount (Proxy @b) + 1
fromRow r =
let
@@ -95,7 +109,7 @@ else instance FromRow (Array Raw) where
else instance FromRow Unit where
minColumnCount _ = 0
fromRow _ = pure unit
else instance Rep a => FromRow a where
else instance Deserialize a => FromRow a where
minColumnCount _ = 1
fromRow r =
let

View File

@@ -0,0 +1,27 @@
module Data.Postgres.Unresult where
import Prelude
import Control.Monad.Error.Class (liftMaybe)
import Control.Monad.State (StateT, runStateT, state)
import Data.Array as Array
import Data.Postgres (class Deserialize, RepT, deserialize, smash)
import Data.Postgres.Raw (Raw)
import Data.Tuple (fst)
import Data.Tuple.Nested ((/\))
import Effect.Class (liftEffect)
import Foreign (ForeignError(..))
-- | Monad used to incrementally deserialize columns from a row
type Unresult a = StateT { ix :: Int, row :: Array Raw } RepT a
-- | Run an `UnresultT`
unresult :: forall a. Unresult a -> Array Raw -> RepT a
unresult m row = fst <$> runStateT m { ix: 0, row }
-- | Take the next column from the row, unmarshalling into `a`
take :: forall a. Deserialize a => Unresult a
take = do
raw <- state (\r -> Array.index r.row r.ix /\ r { ix = r.ix + 1 })
raw' <- liftMaybe (pure $ ForeignError $ "Ran out of columns!") raw
liftEffect $ smash $ deserialize raw'

View File

@@ -1,5 +1,24 @@
import Pg from 'pg'
import Range from 'postgres-range'
import { Buffer } from 'buffer'
import PostgresInterval from 'postgres-interval'
/** @type {(a: unknown) => boolean} */
export const isInstanceOfBuffer = a => a instanceof Buffer
/** @type {(a: unknown) => boolean} */
export const isInstanceOfInterval = a => {
return typeof a === 'object'
&& a !== null
&& ('years' in a
|| 'months' in a
|| 'days' in a
|| 'hours' in a
|| 'minutes' in a
|| 'seconds' in a
|| 'milliseconds' in a
)
}
export const modifyPgTypes = () => {
// https://github.com/brianc/node-pg-types/blob/master/lib/textParsers.js

View File

@@ -3,24 +3,28 @@ module Data.Postgres where
import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (liftEither, liftMaybe)
import Control.Monad.Error.Class (liftEither, liftMaybe, throwError)
import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.Morph (hoist)
import Control.Monad.Trans.Class (lift)
import Data.Bifunctor (lmap)
import Data.DateTime (DateTime)
import Data.DateTime.Instant (Instant)
import Data.DateTime.Instant as Instant
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Postgres.Custom (class CustomDeserialize, class CustomSerialize, customDeserialize, customSerialize)
import Data.Postgres.Interval (Interval)
import Data.Postgres.Interval as Interval
import Data.Postgres.Range (Range, __rangeFromRecord, __rangeRawFromRaw, __rangeRawFromRecord, __rangeRawToRecord, __rangeToRecord)
import Data.Postgres.Raw (Null(..), Raw, jsNull)
import Data.Postgres.Raw (unsafeFromForeign, asForeign) as Raw
import Data.RFC3339String as DateTime.ISO
import Data.Time.Duration (Days, Hours, Milliseconds, Minutes, Seconds)
import Data.Traversable (traverse)
import Effect (Effect)
import Effect.Exception (error)
import Foreign (ForeignError(..), unsafeToForeign)
import Foreign (ForeignError(..), tagOf, unsafeFromForeign, unsafeToForeign)
import Foreign as F
import JS.BigInt (BigInt)
import JS.BigInt as BigInt
@@ -41,8 +45,11 @@ derive newtype instance ReadForeign a => ReadForeign (JSON a)
-- | for some types to unmarshal as strings rather than JS values.
foreign import modifyPgTypes :: Effect Unit
foreign import isInstanceOfBuffer :: F.Foreign -> Boolean
foreign import isInstanceOfInterval :: F.Foreign -> Boolean
-- | The serialization & deserialization monad.
type RepT a = ExceptT (NonEmptyList ForeignError) Effect a
type RepT = ExceptT (NonEmptyList ForeignError) Effect
-- | Flatten to an Effect, `show`ing errors
smash :: forall a. RepT a -> Effect a
@@ -69,91 +76,153 @@ instance (Serialize a, Deserialize a) => Rep a
unsafeSerializeCoerce :: forall m a. Monad m => a -> m Raw
unsafeSerializeCoerce = pure <<< Raw.unsafeFromForeign <<< F.unsafeToForeign
invalidDuration :: NonEmptyList ForeignError
invalidDuration = pure $ ForeignError $ "Can't convert interval with year or month components to Milliseconds"
instance Serialize Raw where
serialize = pure
-- | `NULL`
else instance Serialize Unit where
instance Serialize Unit where
serialize _ = serialize Null
-- | `NULL`
else instance Serialize Null where
instance Serialize Null where
serialize _ = unsafeSerializeCoerce jsNull
-- | `json`, `jsonb`
else instance WriteForeign a => Serialize (JSON a) where
instance WriteForeign a => Serialize (JSON a) where
serialize = serialize <<< writeJSON <<< unwrap
-- | `bytea`
else instance Serialize Buffer where
instance Serialize Buffer where
serialize = unsafeSerializeCoerce
-- | `int2`, `int4`
else instance Serialize Int where
instance Serialize Int where
serialize = unsafeSerializeCoerce
-- | `int8`
else instance Serialize BigInt where
instance Serialize BigInt where
serialize = serialize <<< BigInt.toString
-- | `bool`
else instance Serialize Boolean where
instance Serialize Boolean where
serialize = unsafeSerializeCoerce
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
else instance Serialize String where
instance Serialize String where
serialize = unsafeSerializeCoerce
-- | `float4`, `float8`
else instance Serialize Number where
instance Serialize Number where
serialize = unsafeSerializeCoerce
-- | `timestamp`, `timestamptz`
else instance Serialize DateTime where
-- | `interval`
instance Serialize DateTime where
serialize = serialize <<< unwrap <<< DateTime.ISO.fromDateTime
-- | `interval`
instance Serialize Interval where
serialize = unsafeSerializeCoerce
-- | `interval`
instance Serialize Milliseconds where
serialize = serialize <<< Interval.fromDuration
-- | `interval`
instance Serialize Seconds where
serialize = serialize <<< Interval.fromDuration
-- | `interval`
instance Serialize Minutes where
serialize = serialize <<< Interval.fromDuration
-- | `interval`
instance Serialize Hours where
serialize = serialize <<< Interval.fromDuration
-- | `interval`
instance Serialize Days where
serialize = serialize <<< Interval.fromDuration
-- | `timestamp`, `timestamptz`
instance Serialize Instant where
serialize = serialize <<< Instant.toDateTime
-- | `Just` -> `a`, `Nothing` -> `NULL`
else instance Serialize a => Serialize (Maybe a) where
instance Serialize a => Serialize (Maybe a) where
serialize (Just a) = serialize a
serialize Nothing = unsafeSerializeCoerce jsNull
-- | postgres `array`
else instance Serialize a => Serialize (Array a) where
instance Serialize a => Serialize (Array a) where
serialize = unsafeSerializeCoerce <=< traverse serialize
else instance (Ord a, Rep a) => Serialize (Range a) where
instance (Ord a, Rep a) => Serialize (Range a) where
serialize =
map (Raw.unsafeFromForeign <<< unsafeToForeign <<< __rangeRawFromRecord <<< __rangeToRecord)
<<< traverse serialize
else instance (CustomSerialize a ty) => Serialize a where
serialize = customSerialize
instance Deserialize Raw where
deserialize = pure
-- | `NULL` (always succeeds)
else instance Deserialize Unit where
instance Deserialize Unit where
deserialize _ = pure unit
-- | `NULL` (fails if non-null)
else instance Deserialize Null where
instance Deserialize Null where
deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.asForeign
-- | `json`, `jsonb`
else instance ReadForeign a => Deserialize (JSON a) where
instance ReadForeign a => Deserialize (JSON a) where
deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String
-- | `bytea`
else instance Deserialize Buffer where
deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.asForeign
instance Deserialize Buffer where
deserialize =
let
notBuffer a = pure $ TypeMismatch (tagOf a) "Buffer"
readBuffer a = when (not $ isInstanceOfBuffer a) (throwError $ notBuffer a) $> unsafeFromForeign a
in
readBuffer <<< Raw.asForeign
-- | `interval`
instance Deserialize Interval where
deserialize =
let
notInterval a = pure $ TypeMismatch (tagOf a) "Interval"
readInterval a = when (not $ isInstanceOfInterval a) (throwError $ notInterval a) $> unsafeFromForeign a
in
readInterval <<< Raw.asForeign
-- | `interval`
instance Deserialize Milliseconds where
deserialize = flip bind (liftMaybe invalidDuration) <<< map Interval.toDuration <<< deserialize
-- | `interval`
instance Deserialize Seconds where
deserialize = flip bind (liftMaybe invalidDuration) <<< map Interval.toDuration <<< deserialize
-- | `interval`
instance Deserialize Minutes where
deserialize = flip bind (liftMaybe invalidDuration) <<< map Interval.toDuration <<< deserialize
-- | `interval`
instance Deserialize Hours where
deserialize = flip bind (liftMaybe invalidDuration) <<< map Interval.toDuration <<< deserialize
-- | `interval`
instance Deserialize Days where
deserialize = flip bind (liftMaybe invalidDuration) <<< map Interval.toDuration <<< deserialize
-- | `int2`, `int4`
else instance Deserialize Int where
instance Deserialize Int where
deserialize = F.readInt <<< Raw.asForeign
-- | `int8`
else instance Deserialize BigInt where
instance Deserialize BigInt where
deserialize =
let
invalid s = pure $ ForeignError $ "Invalid bigint: " <> s
@@ -162,30 +231,34 @@ else instance Deserialize BigInt where
fromString <=< deserialize @String
-- | `bool`
else instance Deserialize Boolean where
instance Deserialize Boolean where
deserialize = F.readBoolean <<< Raw.asForeign
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
else instance Deserialize String where
instance Deserialize String where
deserialize = F.readString <<< Raw.asForeign
-- | `float4`, `float8`
else instance Deserialize Number where
instance Deserialize Number where
deserialize = F.readNumber <<< Raw.asForeign
-- | `timestamp`, `timestamptz`
else instance Deserialize DateTime where
instance Deserialize DateTime where
deserialize raw = do
s :: String <- deserialize raw
let invalid = pure $ ForeignError $ "Not a valid ISO8601 string: `" <> s <> "`"
liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s
-- | `timestamp`, `timestamptz`
instance Deserialize Instant where
deserialize = map Instant.fromDateTime <<< deserialize
-- | postgres `array`
else instance Deserialize a => Deserialize (Array a) where
instance Deserialize a => Deserialize (Array a) where
deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.asForeign
-- | non-NULL -> `Just`, NULL -> `Nothing`
else instance Deserialize a => Deserialize (Maybe a) where
instance Deserialize a => Deserialize (Maybe a) where
deserialize raw =
let
nothing = const Nothing <$> deserialize @Null raw
@@ -193,8 +266,5 @@ else instance Deserialize a => Deserialize (Maybe a) where
in
just <|> nothing
else instance (Ord a, Rep a) => Deserialize (Range a) where
instance (Ord a, Rep a) => Deserialize (Range a) where
deserialize = traverse deserialize <=< map (__rangeFromRecord <<< __rangeRawToRecord) <<< lift <<< __rangeRawFromRaw
else instance (CustomDeserialize a ty) => Deserialize a where
deserialize = customDeserialize

View File

@@ -1,3 +1,5 @@
import QueryStream from 'pg-copy-streams'
/** @type {(c: import('pg').Client) => () => Promise<void>} */
export const __connect = c => () => c.connect()
@@ -6,3 +8,9 @@ export const __end = c => () => c.end()
/** @type {(q: import('pg').QueryConfig) => (c: import('pg').Client) => () => Promise<import('pg').QueryResult>} */
export const __query = q => c => () => c.query(q)
/** @type {(q: string) => (c: import('pg').Client) => () => import('stream').Readable} */
export const __execStreamStdout = q => c => () => c.query(QueryStream.to(q))
/** @type {(q: string) => (c: import('pg').Client) => () => import('stream').Writable} */
export const __execStreamStdin = q => c => () => c.query(QueryStream.from(q))

View File

@@ -1,4 +1,4 @@
module Effect.Aff.Postgres.Client (connected, connect, end, exec, query, queryRaw, __connect, __end, __query, module X) where
module Effect.Aff.Postgres.Client (connected, connect, end, exec, execWithStdin, queryWithStdout, query, queryRaw, __connect, __end, __query, __execStreamStdin, __execStreamStdout, module X) where
import Prelude
@@ -6,14 +6,16 @@ import Control.Promise (Promise)
import Control.Promise as Promise
import Data.Functor (voidRight)
import Data.Maybe (fromMaybe)
import Data.Newtype (wrap)
import Data.Postgres (smash)
import Data.Postgres.Query (class AsQuery, QueryRaw, asQuery, __queryToRaw)
import Data.Postgres.Result (class FromRows, Result, fromRows, rows, rowsAffected)
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Effect.Postgres.Client (Client, Config, make)
import Effect.Postgres.Client (Client, ClientConfigRaw, Config, Notification, NotificationRaw, __make, __uncfg, endE, errorE, make, noticeE, notificationE) as X
import Effect.Postgres.Client (Client, Config, make)
import Node.Stream (Readable, Writable)
import Prim.Row (class Union)
-- | Create a client and immediately connect it to the database
@@ -56,7 +58,18 @@ exec q = map (fromMaybe 0 <<< rowsAffected) <<< queryRaw q
-- |
-- | <https://node-postgres.com/apis/client#clientquery>
query :: forall q r. AsQuery q => FromRows r => q -> Client -> Aff r
query q = (liftEffect <<< smash <<< fromRows) <=< map rows <<< queryRaw q
query q c = do
raw <- queryRaw q c
let
affected = rowsAffected raw
rows' = rows raw
liftEffect $ smash $ fromRows (wrap $ fromMaybe 0 affected) rows'
execWithStdin :: String -> Client -> Effect (Writable ())
execWithStdin q c = __execStreamStdin q c
queryWithStdout :: String -> Client -> Effect (Readable ())
queryWithStdout q c = __execStreamStdout q c
-- | FFI binding to `Client#connect`
foreign import __connect :: Client -> Effect (Promise Unit)
@@ -66,3 +79,9 @@ foreign import __end :: Client -> Effect (Promise Unit)
-- | FFI binding to `Client#query`
foreign import __query :: QueryRaw -> Client -> Effect (Promise Result)
-- | FFI binding to `import('pg-copy-streams').to`
foreign import __execStreamStdout :: String -> Client -> Effect (Readable ())
-- | FFI binding to `import('pg-copy-streams').from`
foreign import __execStreamStdin :: String -> Client -> Effect (Writable ())

65
src/Pipes.Postgres.purs Normal file
View File

@@ -0,0 +1,65 @@
module Pipes.Postgres where
import Prelude
import Control.Monad.Error.Class (class MonadError, class MonadThrow, catchError, throwError)
import Control.Monad.Postgres (class MonadPostgres)
import Control.Monad.Reader (class MonadAsk, ask)
import Data.Maybe (Maybe(..))
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Client as Client
import Effect.Aff.Postgres.Pool (Pool)
import Effect.Aff.Postgres.Pool as Pool
import Effect.Class (liftEffect)
import Effect.Exception (Error)
import Node.Buffer (Buffer)
import Node.Stream.Object as O
import Pipes ((>->))
import Pipes.Core (Consumer, Producer)
import Pipes.Node.Stream (fromReadable, fromWritable)
import Pipes.Prelude as Pipes
stdin
:: forall m s c ct
. MonadAff m
=> MonadError Error m
=> MonadAsk Pool m
=> MonadPostgres m s c ct
=> String
-> Consumer (Maybe Buffer) m Unit
stdin q = do
pool <- ask
client <- liftAff $ Pool.connect pool
stream <- liftEffect $ Client.execWithStdin q client
liftAff $ void $ Client.exec "begin" client
let
releaseOnEOS Nothing = do
liftAff $ void $ Client.exec "commit" client
liftEffect $ Pool.release pool client
pure Nothing
releaseOnEOS (Just a) = pure (Just a)
pipe = Pipes.mapM releaseOnEOS >-> fromWritable (O.unsafeFromBufferWritable stream)
err e = do
liftAff $ void $ Client.exec "rollback" client
liftEffect $ Pool.release pool client
throwError e
catchError pipe err
stdout
:: forall m s c ct
. MonadAff m
=> MonadThrow Error m
=> MonadAsk Pool m
=> MonadPostgres m s c ct
=> String
-> Producer (Maybe Buffer) m Unit
stdout q = do
pool <- ask
client <- liftAff $ Pool.connect pool
stream <- liftEffect $ Client.queryWithStdout q client
let
releaseOnEOS Nothing = liftEffect $ Pool.release pool client $> Nothing
releaseOnEOS (Just a) = pure (Just a)
fromReadable (O.unsafeFromBufferReadable stream) >-> Pipes.mapM releaseOnEOS

View File

@@ -9,9 +9,8 @@ import Data.DateTime (DateTime(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Newtype (unwrap)
import Data.Postgres (deserialize, serialize, smash)
import Data.Postgres.Custom (class CustomDeserialize, class CustomSerialize, customDeserialize)
import Data.Postgres.Custom.Enum (class CustomEnum, create, enumDeserialize, enumPrintExpr, enumSerialize, genericEnumVariants, genericParseEnum, genericPrintEnum, parseEnum, printEnum)
import Data.Postgres (class Deserialize, class Serialize, deserialize, serialize, smash)
import Data.Postgres.Custom.Enum (class CustomEnum, create, defaultDeserializeEnum, defaultSerializeEnum, enumPrintExpr, genericEnumVariants, genericParseEnum, genericPrintEnum, parseEnum, printEnum)
import Data.Show.Generic (genericShow)
import Effect.Class (liftEffect)
import Test.Spec (Spec, describe, it)
@@ -25,12 +24,11 @@ derive instance Eq Enum1
instance Show Enum1 where
show = genericShow
instance CustomSerialize Enum1 "enum_1" where
customPrintExpr a = enumPrintExpr a
customSerialize a = enumSerialize a
instance Serialize Enum1 where
serialize a = defaultSerializeEnum a
instance CustomDeserialize Enum1 "enum_1" where
customDeserialize a = enumDeserialize a
instance Deserialize Enum1 where
deserialize a = defaultDeserializeEnum a
instance CustomEnum Enum1 "enum_1" where
printEnum = genericPrintEnum
@@ -44,12 +42,11 @@ derive instance Eq Enum2
instance Show Enum2 where
show = genericShow
instance CustomSerialize Enum2 "enum_2" where
customPrintExpr a = enumPrintExpr a
customSerialize a = enumSerialize a
instance Serialize Enum2 where
serialize a = defaultSerializeEnum a
instance CustomDeserialize Enum2 "enum_2" where
customDeserialize a = enumDeserialize a
instance Deserialize Enum2 where
deserialize a = defaultDeserializeEnum a
instance CustomEnum Enum2 "enum_2" where
printEnum a = genericPrintEnum a
@@ -63,12 +60,11 @@ derive instance Eq Enum5
instance Show Enum5 where
show = genericShow
instance CustomSerialize Enum5 "enum_5" where
customPrintExpr a = enumPrintExpr a
customSerialize a = enumSerialize a
instance Serialize Enum5 where
serialize a = defaultSerializeEnum a
instance CustomDeserialize Enum5 "enum_5" where
customDeserialize a = enumDeserialize a
instance Deserialize Enum5 where
deserialize a = defaultDeserializeEnum a
instance CustomEnum Enum5 "enum_5" where
printEnum a = genericPrintEnum a

View File

@@ -0,0 +1,37 @@
module Test.Data.Postgres.Interval where
import Prelude
import Data.Postgres.Interval as Interval
import Data.Time.Duration (Milliseconds(..))
import Data.Traversable (for_)
import Data.Tuple.Nested (type (/\), (/\))
import Effect.Class (liftEffect)
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (shouldEqual)
spec :: Spec Unit
spec =
describe "Data.Postgres.Interval" do
it "parse & toRecord" do
p <- liftEffect $ Interval.parse "3 days 04:05:06"
Interval.toRecord p `shouldEqual` Interval.zero {days = 3, hours = 4, minutes = 5, seconds = 6}
it "make & toRecord" do
let p = Interval.make $ Interval.zero {days = 3, hours = 4, minutes = 5, seconds = 6}
Interval.toRecord p `shouldEqual` Interval.zero {days = 3, hours = 4, minutes = 5, seconds = 6}
describe "fromDuration" do
for_
[ Milliseconds 100.0 /\ Interval.zero {milliseconds = 100.0}
, Milliseconds 1000.0 /\ Interval.zero {seconds = 1}
, Milliseconds 1100.0 /\ Interval.zero {seconds = 1, milliseconds = 100.0}
, Milliseconds 60000.0 /\ Interval.zero {minutes = 1}
, Milliseconds 61100.0 /\ Interval.zero {minutes = 1, seconds = 1, milliseconds = 100.0}
, Milliseconds 3600000.0 /\ Interval.zero {hours = 1}
, Milliseconds 3661100.0 /\ Interval.zero {hours = 1, minutes = 1, seconds = 1, milliseconds = 100.0}
, Milliseconds 86400000.0 /\ Interval.zero {days = 1}
, Milliseconds 90061100.0 /\ Interval.zero {days = 1, hours = 1, minutes = 1, seconds = 1, milliseconds = 100.0}
]
\(i /\ o) -> it ("converts " <> show i) do
Interval.toRecord (Interval.fromDuration i) `shouldEqual` o

View File

@@ -2,28 +2,28 @@ module Test.Data.Postgres where
import Prelude
import Control.Monad.Gen (chooseInt, elements, oneOf)
import Control.Monad.Gen (chooseFloat, chooseInt, elements, oneOf)
import Control.Parallel (parTraverse_)
import Data.Array (intercalate)
import Data.Array as Array
import Data.Array.NonEmpty as Array.NonEmpty
import Data.DateTime (DateTime(..), canonicalDate)
import Data.DateTime.Instant as Instant
import Data.Enum (toEnum)
import Data.Foldable (fold)
import Data.Identity (Identity)
import Data.Int as Int
import Data.Maybe (Maybe(..), fromJust, maybe)
import Data.Maybe (Maybe(..), fromJust, fromMaybe)
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Number (abs) as Number
import Data.Postgres (class Rep)
import Data.Postgres.Interval (Interval)
import Data.Postgres.Interval as Interval
import Data.Postgres.Query.Builder as Q
import Data.Postgres.Raw (Raw, jsNull)
import Data.Postgres.Raw as Raw
import Data.Postgres.Result (class FromRow)
import Data.RFC3339String as DateTime.ISO
import Data.String as String
import Data.Time (Time(..))
import Data.Time.Duration (class Duration, Days, Hours, Milliseconds, Minutes, Seconds)
import Data.Traversable (for, sequence)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
@@ -35,20 +35,45 @@ import Effect.Unsafe (unsafePerformEffect)
import Foreign (Foreign, unsafeToForeign)
import Foreign.Object as Object
import JS.BigInt (BigInt)
import JS.BigInt as BigInt
import Node.Buffer (Buffer)
import Node.Buffer as Buffer
import Partial.Unsafe (unsafePartial)
import Simple.JSON (writeJSON)
import Test.Common (withClient, withPoolClient)
import Test.Common (withPoolClient)
import Test.QuickCheck (class Arbitrary, arbitrary, randomSeed)
import Test.QuickCheck.Gen (sample, vectorOf)
import Test.Spec (Spec, SpecT, around, describe, it, parallel)
import Test.Spec (Spec, SpecT, around, describe, it)
import Test.Spec.Assertions (fail)
foreign import readBigInt64BE :: Buffer -> Effect BigInt
foreign import dbg :: forall a. a -> Effect Unit
newtype GenIntervalSubMonth = GenIntervalSubMonth Interval
derive instance Newtype GenIntervalSubMonth _
instance Arbitrary GenIntervalSubMonth where
arbitrary = do
days <- chooseInt 0 30
hours <- chooseInt 0 23
minutes <- chooseInt 0 59
seconds <- chooseInt 0 59
milliseconds <- chooseFloat 0.0 999.9
pure $ wrap $ Interval.make $ Interval.zero {days = days, hours = hours, minutes = minutes, seconds = seconds, milliseconds = milliseconds}
newtype GenInterval = GenInterval Interval
derive instance Newtype GenInterval _
instance Arbitrary GenInterval where
arbitrary = do
years <- chooseInt 0 10
months <- chooseInt 0 11
days <- chooseInt 0 30
hours <- chooseInt 0 23
minutes <- chooseInt 0 59
seconds <- chooseInt 0 59
milliseconds <- chooseFloat 0.0 999.9
pure $ wrap $ Interval.make {years, months, days, hours, minutes, seconds, milliseconds}
newtype GenSmallInt = GenSmallInt Int
derive instance Newtype GenSmallInt _
@@ -196,6 +221,17 @@ spec =
around withPoolClient
$ describe "Data.Postgres"
$ do
let
durationFromGenInterval :: forall d. Semigroup d => Duration d => Newtype d Number => GenIntervalSubMonth -> d
durationFromGenInterval = fromMaybe (wrap 0.0) <<< Interval.toDuration <<< unwrap
durationEq :: forall d. Duration d => Newtype d Number => d -> d -> Boolean
durationEq a b = Number.abs (unwrap a - unwrap b) <= 0.001
check @Milliseconds @GenIntervalSubMonth { purs: "Milliseconds", sql: "interval", fromArb: durationFromGenInterval, isEq: durationEq}
check @Seconds @GenIntervalSubMonth { purs: "Seconds", sql: "interval", fromArb: durationFromGenInterval, isEq: durationEq}
check @Minutes @GenIntervalSubMonth { purs: "Minutes", sql: "interval", fromArb: durationFromGenInterval, isEq: durationEq}
check @Hours @GenIntervalSubMonth { purs: "Hours", sql: "interval", fromArb: durationFromGenInterval, isEq: durationEq}
check @Days @GenIntervalSubMonth { purs: "Days", sql: "interval", fromArb: durationFromGenInterval, isEq: durationEq}
check @Int @GenSmallInt { purs: "Int", sql: "int2", fromArb: unwrap, isEq: eq }
check @Int { purs: "Int", sql: "int4", fromArb: identity, isEq: eq }
check @String @GenString { purs: "String", sql: "text", fromArb: unwrap, isEq: eq }

View File

@@ -23,6 +23,7 @@ import Node.EventEmitter as Event
import Test.Control.Monad.Postgres as Test.Control.Monad.Postgres
import Test.Data.Postgres as Test.Data.Postgres
import Test.Data.Postgres.Custom as Test.Data.Postgres.Custom
import Test.Data.Postgres.Interval as Test.Data.Postgres.Interval
import Test.Effect.Postgres.Client as Test.Effect.Postgres.Client
import Test.Effect.Postgres.Pool as Test.Effect.Postgres.Pool
import Test.Spec.Reporter (specReporter)
@@ -65,6 +66,7 @@ main = launchAff_ do
$ runSpec [ specReporter ] do
Test.Data.Postgres.Custom.spec
Test.Data.Postgres.spec
Test.Data.Postgres.Interval.spec
Test.Effect.Postgres.Client.spec
Test.Effect.Postgres.Pool.spec
Test.Control.Monad.Postgres.spec