Compare commits

..

2 Commits

Author SHA1 Message Date
8a3f5a1061
docs: update readme 2024-08-20 21:42:58 -05:00
5f8be2e1f2
fix: improve error stringification 2024-07-21 16:51:12 -05:00
3 changed files with 110 additions and 81 deletions

166
README.md
View File

@ -5,7 +5,6 @@ Purescript PostgreSQL driver
## Table of Contents ## Table of Contents
- [Getting Started](#getting-started) - [Getting Started](#getting-started)
- [Data](#data) - [Data](#data)
- [Rows](#data---rows)
- [Ranges](#data---ranges) - [Ranges](#data---ranges)
- [Queries](#queries) - [Queries](#queries)
- [Builder](#queries---builder) - [Builder](#queries---builder)
@ -79,45 +78,54 @@ class MonadSession m where
``` ```
## Data ## Data
Single SQL values are serialized to and deserialized from JS via [`pg-types`] Scalar values are serialized & deserialized using the `Serialize` / `Deserialize` typeclasses.
(with some [tweaks][`modifyPgTypes`]). Implemented for `Int`, `String`, `DateTime`, `Buffer`, `BigInt`, `Boolean`, `Number`, `Array`, [`Range`]s, `Maybe`, [`Null`] and `Unit`.
The conversion between [`Raw`] JS values and purescript values is done There aren't any typeclass instances for unmarshalling rows by design. The rationale: as apps grow we often need to ask more of a relational database than just CRUD operations. Queries tend to be somewhat living, with joins and columns being added and removed, so it should be easy to modify queries and reflect the change in the purescript type they're unmarshalled into.
with the [`Serialize`] and [`Deserialize`] typeclasses.
The [`Rep`] class indicates a type is [`Rep`]resentable as a SQL value. The lib does this by transforming the js row array `Array<Array<unknown>>` with `FromRows` ("how many rows do you expect?"), then each row with `FromRow`, then each value with `Deserialize`.
[`Rep`] is automatically implemented for all types that are [`Serialize`]
and [`Deserialize`].
Implementations are provided for `Int`, `String`, `DateTime`, `Buffer`, Starting with querying directly into the loose rows as `Array (Array Raw)`:
`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 ```purescript
(fromRow [] :: Maybe Int) == Nothing a :: Array (Array Raw) <- query "select a, b, c from (values (1, 'foo', true), (4, 'bar', false)) as foo(a, b, c)"
(fromRow [1] :: Maybe Int) == Just 1 liftEffect $ log $ show a -- [[1, "foo", true], [4, "bar", false]]
(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`], We can tell the query to deserialize the rows as `Int /\ String /\ Boolean` (postgres shape of `(int, text, boolean)` ):
which is implemented for: ```purescript
- `Array a` where `a` is [`FromRow`] a :: Array (Int /\ String /\ Boolean) <- query "select a, b, c from (values (1, 'foo', true), (4, 'bar', false)) as foo(a, b, c)"
- `Maybe a` where `a` is [`FromRow`] (equivalent to `Array.head <<< fromRows`) liftEffect $ log $ show a -- [Tuple 1 (Tuple "foo" true), Tuple 1 (Tuple "foo" true)]
- `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`) From there we could unmarshal to `Maybe` to get 0 or 1, or directly into the row type if expect at least 1 row:
```purescript
a :: Maybe (Int /\ String /\ Boolean) <- query "select a, b, c from (values (1, 'foo', true), (4, 'bar', false)) as foo(a, b, c)"
liftEffect $ log $ show a -- Just (Tuple 1 (Tuple "foo" true))
b :: Int /\ String /\ Boolean <- query "select 1, 'foo', true"
liftEffect $ log $ show b -- Tuple 1 (Tuple "foo" true)
c :: Maybe (Int /\ String /\ Boolean) <- query "select null, null, null limit 0"
liftEffect $ log $ show c -- Nothing
```
`FromRows row` supports `Array row`, `Maybe row` or just `row` (failing if 0 returned).
`FromRow row` supports `Array a`, `Tuple a b`, `Maybe a`, or `a` (where `a` / `b` are `Deserialize`)
The idea is that you can deserialize query results directly to the purescript type you care about:
- `a :: Int <- query "select 1"`
- because there's no outer `Array`, we're saying this just returns 1 row. because there's no inner `Array`, we're saying the row just has 1 value; the int!
- `a :: Array Int <- query "select foo.a from (values (1), (2), (3)) as foo(a)"`
- Now there's an outer `Array`, so we expect the query to yield multiple rows of shape `(int)`
- `a :: Array (Maybe Int) <- query "select foo.a from (values (1), (null), (3)) as foo(a)"`
- Some of them are `NULL`!
- `a :: Int /\ Int <- query "select 1, 2"`
- 1 row of `(int, int)`
- `a :: Array (Int /\ String) <- query "select id, email from users"`
- Multiple rows of `(int, string)`
- `a :: Maybe (String /\ String /\ String) <- query $ "select first_name, last_name, email from users where id = $1" /\ userId`
- 0 or 1 rows of `(text, text, text)`
### Data - Ranges ### Data - Ranges
Postgres ranges are represented with [`Range`]. Postgres ranges are represented with [`Range`].
@ -259,59 +267,59 @@ the api of [`node-postgres`]:
- release clients with [`Pool.release`] or [`Pool.destroy`] - release clients with [`Pool.release`] or [`Pool.destroy`]
- release with [`Pool.end`] - release with [`Pool.end`]
[`Pool`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#t:Pool [`Pool`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#t:Pool
[`Config`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#t:Config [`Config`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#t:Config
[`Pool.make`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:make [`Pool.make`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:make
[`Pool.end`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:end [`Pool.end`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:end
[`Pool.connect`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:connect [`Pool.connect`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:connect
[`Pool.destroy`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:destroy [`Pool.destroy`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:destroy
[`Pool.release`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:release [`Pool.release`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Pool#v:release
[`Client`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#t:Client [`Client`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#t:Client
[`Client.end`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:end [`Client.end`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:end
[`Client.make`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:make [`Client.make`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:make
[`Client.connected`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:connected [`Client.connected`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:connected
[`Client.query`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:query [`Client.query`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:query
[`Client.queryRaw`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:queryRaw [`Client.queryRaw`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:queryRaw
[`Client.exec`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:exec [`Client.exec`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Effect.Aff.Postgres.Client#v:exec
[`Range`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Range#t:Range [`Range`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Range#t:Range
[`Range.gt`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:gt [`Range.gt`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:gt
[`Range.gte`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:gte [`Range.gte`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:gte
[`Range.lt`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:lt [`Range.lt`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:lt
[`Range.lte`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:lte [`Range.lte`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Range#v:lte
[`Raw`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Raw#t:Raw [`Raw`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Raw#t:Raw
[`Null`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Raw#t:Null [`Null`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Raw#t:Null
[`Serialize`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres#t:Serialize [`Serialize`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres#t:Serialize
[`Deserialize`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres#t:Deserialize [`Deserialize`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres#t:Deserialize
[`Rep`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres#t:Rep [`Rep`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres#t:Rep
[`modifyPgTypes`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres#v:modifyPgTypes [`modifyPgTypes`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres#v:modifyPgTypes
[`Result`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:Result [`Result`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:Result
[`FromRow`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:FromRow [`FromRow`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:FromRow
[`FromRows`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:FromRows [`FromRows`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Result#t:FromRows
[`Query`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Query#t:Query [`Query`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Query#t:Query
[`AsQuery`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Query#t:AsQuery [`AsQuery`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Query#t:AsQuery
[`Query.Builder`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#t:Builder [`Query.Builder`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#t:Builder
[`Query.Builder.param`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#v:param [`Query.Builder.param`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#v:param
[`Query.Builder.build`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#v:build [`Query.Builder.build`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Data.Postgres.Query.Builder#v:build
[`MonadCursor`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:MonadCursor [`MonadCursor`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:MonadCursor
[`MonadSession`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:MonadSession [`MonadSession`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:MonadSession
[`CursorT`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:CursorT [`CursorT`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:CursorT
[`SessionT`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:SessionT [`SessionT`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:SessionT
[`PostgresT`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:PostgresT [`PostgresT`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#t:PostgresT
[`cursor`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:cursor [`cursor`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:cursor
[`session`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:session [`session`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:session
[`transaction`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:transaction [`transaction`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:transaction
[`runPostgres`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:runPostgres [`runPostgres`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:runPostgres
[`query`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:query [`query`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:query
[`exec`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:exec [`exec`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:exec
[`exec_`]: https://pursuit.purescript.org////////////////////////////////////////////////////////////packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:exec_ [`exec_`]: https://pursuit.purescript.org/packages/purescript-postgresql/2.0.17/Control.Monad.Postgres#v:exec_
[`node-postgres`]: https://node-postgres.com/ [`node-postgres`]: https://node-postgres.com/
[`pg-types`]: https://github.com/brianc/node-pg-types/ [`pg-types`]: https://github.com/brianc/node-pg-types/

View File

@ -20,7 +20,7 @@ await writeFile('./spago.yaml', spagonew)
const readme = await readFile('./README.md', 'utf8') const readme = await readFile('./README.md', 'utf8')
const readmenew = readme.replace( const readmenew = readme.replace(
/packages\/purescript-postgresql\/.+?\//g, /\/packages\/purescript-postgresql\/.+?\//g,
`/packages/purescript-postgresql/${ver}/`, `/packages/purescript-postgresql/${ver}/`,
) )
await writeFile('./README.md', readmenew) await writeFile('./README.md', readmenew)

View File

@ -2,10 +2,15 @@ module Effect.Postgres.Error.Common where
import Prelude import Prelude
import Data.Array as Array
import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty (NonEmptyArray)
import Data.Foldable (fold)
import Data.Generic.Rep (class Generic) import Data.Generic.Rep (class Generic)
import Data.Maybe (maybe)
import Data.Newtype (wrap)
import Data.Postgres.Query (Query) import Data.Postgres.Query (Query)
import Data.Show.Generic (genericShow) import Data.Show.Generic (genericShow)
import Data.String as String
import Effect.Exception as Effect import Effect.Exception as Effect
import Foreign (MultipleErrors) import Foreign (MultipleErrors)
@ -21,7 +26,23 @@ data Error
derive instance Generic Error _ derive instance Generic Error _
instance Show Error where instance Show Error where
show = genericShow show = toString
toString :: Error -> String
toString =
let
indent n s = fold $ ((fold $ Array.replicate n " ") <> _) <$> String.split (wrap "\n") s
jsError n e =
indent n (Effect.message e)
<> maybe "" (\s -> "\n" <> indent n s) (Effect.stack e)
in
case _ of
Deserializing q es -> "Deserializing " <> show q <> "\n" <> indent 1 (show es)
Serializing es -> "Serializing" <> "\n" <> indent 1 (show es)
Executing q e -> "Executing " <> show q <> "\n" <> jsError 1 e
Connecting e -> "Connecting\n" <> jsError 1 e
Disconnecting e -> "Disconnecting\n" <> jsError 1 e
Other e -> "Other\n" <> jsError 1 e
toException' :: Error -> Effect.Error toException' :: Error -> Effect.Error
toException' = Effect.error <<< show toException' = Effect.error <<< show