fix: some ergonomics, tests use postgres

This commit is contained in:
orion 2024-03-29 14:52:05 -05:00
parent 08dd5fe964
commit 340cee4745
Signed by: orion
GPG Key ID: 6D4165AE4C928719
11 changed files with 898 additions and 144 deletions

View File

@ -14,6 +14,7 @@ workspace:
- foldable-traversable - foldable-traversable
- foreign - foreign
- integers - integers
- js-bigints
- lists - lists
- maybe - maybe
- mmorph - mmorph
@ -72,6 +73,7 @@ workspace:
- identity - identity
- integers - integers
- invariant - invariant
- js-bigints
- js-date - js-date
- lazy - lazy
- lcg - lcg
@ -120,6 +122,487 @@ workspace:
- unicode - unicode
- unsafe-coerce - unsafe-coerce
- variant - 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: {} extra_packages: {}
packages: packages:
aff: aff:
@ -497,6 +980,14 @@ packages:
dependencies: dependencies:
- control - control
- prelude - prelude
js-bigints:
type: registry
version: 2.2.1
integrity: sha256-hKWZo9NxtsAaHmNXr6B8GY4c0olQbYLXPVGWm4TF2Ss=
dependencies:
- integers
- maybe
- prelude
js-date: js-date:
type: registry type: registry
version: 8.0.0 version: 8.0.0
@ -745,8 +1236,8 @@ packages:
- prelude - prelude
parallel: parallel:
type: registry type: registry
version: 6.0.0 version: 7.0.0
integrity: sha256-VJbkGD0rAKX+NUEeBJbYJ78bEKaZbgow+QwQEfPB6ko= integrity: sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=
dependencies: dependencies:
- control - control
- effect - effect

View File

@ -2,7 +2,7 @@ package:
name: pg name: pg
build: build:
strict: true strict: true
pedantic_packages: true pedanticPackages: true
dependencies: dependencies:
- aff - aff
- aff-promise - aff-promise
@ -15,6 +15,7 @@ package:
- foldable-traversable - foldable-traversable
- foreign - foreign
- integers - integers
- js-bigints
- lists - lists
- maybe - maybe
- mmorph - mmorph
@ -41,6 +42,6 @@ package:
- spec - spec
- spec-quickcheck - spec-quickcheck
workspace: workspace:
extra_packages: {} extraPackages: {}
packageSet: packageSet:
registry: 50.5.0 registry: 50.5.0

View File

@ -0,0 +1,107 @@
module Data.Postgres.Query.Builder where
import Prelude
import Control.Monad.State (StateT, get, modify, runStateT)
import Data.Array as Array
import Data.Newtype (unwrap)
import Data.Newtype as Newtype
import Data.Postgres (class Rep, serialize, smash)
import Data.Postgres.Query (Query, emptyQuery)
import Data.Tuple (snd)
import Data.Tuple.Nested (type (/\))
import Effect (Effect)
import Effect.Class (class MonadEffect, liftEffect)
import Record as Record
import Type.Prelude (Proxy(..))
-- | Monad for building parameterized queries without managing positional
-- | parameters directly
-- |
-- | For example, given the table `CREATE TABLE foo (id INT NOT NULL PRIMARY KEY, bar TEXT NOT NULL)`
-- |
-- | ```purescript
-- | updateFoo :: Int -> String -> Effect Query
-- | updateFoo id newBar =
-- | build do
-- | idP <- param id
-- | newBarP <- param newBar
-- | pure $
-- | [ "update foo"
-- | , "set bar = " <> newBarP
-- | , "where id = " <> idP
-- | ]
-- | ```
-- | `updateFoo 1 "test"` will yield:
-- | ```purescript
-- | { text: "update foo\nset bar = $2\nwhere id = $1"
-- | , values: ["test", 1]
-- | }
-- | ```
type QueryBuilderT m a = StateT Query m a
type QueryBuilder a = QueryBuilderT Effect a
-- | Yields a SQL string referencing the last parameter in the parameter list
-- |
-- | Examples:
-- | - if no parameters have been appended this will yield `"$0"` (invalid)
-- | - if 1 parameter has been appended this will yield `"$1"`
-- | - if 5 parameters have been appended this will yield `"$5"`
lastParamString :: forall m. Monad m => QueryBuilderT m String
lastParamString = map (("$" <> _) <<< show <<< Array.length <<< _.values <<< unwrap) $ get
-- | Append a serializable SQL value to the parameter list
appendParam :: forall m a. MonadEffect m => Rep a => a -> QueryBuilderT m Unit
appendParam a =
let
values = Proxy @"values"
in
do
a' <- liftEffect $ smash $ serialize a
void $ modify (Newtype.modify $ Record.modify values (_ <> [ a' ]))
-- | Replace the builder's query string with a new value
putText :: forall m. Monad m => String -> QueryBuilderT m Unit
putText t =
let
text = Proxy @"text"
in
void $ modify $ Newtype.modify $ Record.set text t
-- | Adds a parameter to the query
-- |
-- | This accepts any value `Rep`resentable in SQL, and
-- | yields the SQL string for the new parameter.
-- |
-- | ```purescript
-- | do
-- | p1 <- param 1 -- "$1"
-- | p2 <- param "foo" -- "$2"
-- | p3 <- param true -- "$3"
-- | pure unit
-- | ```
param :: forall m a. MonadEffect m => Rep a => a -> QueryBuilderT m String
param a = do
appendParam a
lastParamString
-- | Accepts a `QueryBuilder` monad that yields the built query string
-- | and yields the finished `Query`.
-- |
-- | ```
-- | build $ pure "select 1"
-- | -- Query {text: "select 1", values: [], name: Nothing}
-- | ```
-- | &nbsp;
-- | ```
-- | build do
-- | foo <- param "foo"
-- | pure $ "select " <> foo
-- | -- Query {text: "select $1", values: ["foo"], name: Nothing}
-- | ```
build :: QueryBuilder String -> Effect Query
build m = map snd $ build' $ putText =<< m
-- | Executes a `QueryBuilderT`
build' :: forall m a. MonadEffect m => QueryBuilderT m a -> m (a /\ Query)
build' = flip runStateT emptyQuery

View File

@ -0,0 +1,54 @@
module Data.Postgres.Query where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable, toNullable)
import Data.Postgres.Raw (Raw)
import Data.Tuple.Nested (type (/\), (/\))
import Effect (Effect)
import Record (insert, modify)
import Type.Prelude (Proxy(..))
-- | FFI Query type
type QueryRaw = { text :: String, values :: Array Raw, name :: Nullable String, rowMode :: String }
-- | SQL Query
-- |
-- | * `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 }
derive instance Newtype Query _
derive newtype instance Eq Query
derive newtype instance Show Query
-- | An empty query
emptyQuery :: Query
emptyQuery = Query { text: "", values: [], name: Nothing }
queryToRaw :: Query -> QueryRaw
queryToRaw (Query r) =
let
name = Proxy @"name"
rowMode = Proxy @"rowMode"
in
insert rowMode "array" $ modify name toNullable $ r
-- | Values that can be rendered as a SQL query
class AsQuery a where
asQuery :: a -> Effect Query
instance AsQuery a => AsQuery (Effect a) where
asQuery = flip bind asQuery
instance AsQuery Query where
asQuery = pure
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 }

View File

@ -1,9 +1,10 @@
import Pg from 'pg' import Pg from 'pg'
import Range from 'postgres-range' import Range from 'postgres-range'
export const null_ = null export const jsNull = null
export const modifyPgTypes = () => { export const modifyPgTypes = () => {
// https://github.com/brianc/node-pg-types/blob/master/lib/textParsers.js
const oid = { const oid = {
'text[]': 1009, 'text[]': 1009,
json: 114, json: 114,

View File

@ -21,9 +21,12 @@ import Effect (Effect)
import Effect.Exception (error) import Effect.Exception (error)
import Foreign (ForeignError(..)) import Foreign (ForeignError(..))
import Foreign as F import Foreign as F
import JS.BigInt (BigInt)
import JS.BigInt as BigInt
import Node.Buffer (Buffer) import Node.Buffer (Buffer)
import Simple.JSON (class ReadForeign, class WriteForeign, readJSON', writeJSON) import Simple.JSON (class ReadForeign, class WriteForeign, readJSON', writeJSON)
-- | Newtype hinting that this value should be serialized / deserialized as a JSON string.
newtype JSON a = JSON a newtype JSON a = JSON a
derive instance Newtype (JSON a) _ derive instance Newtype (JSON a) _
@ -33,7 +36,8 @@ derive newtype instance Ord a => Ord (JSON a)
derive newtype instance WriteForeign a => WriteForeign (JSON a) derive newtype instance WriteForeign a => WriteForeign (JSON a)
derive newtype instance ReadForeign a => ReadForeign (JSON a) derive newtype instance ReadForeign a => ReadForeign (JSON a)
foreign import null_ :: Raw -- | Literal javascript `null`
foreign import jsNull :: Raw
-- | This mutates `import('pg').types`, setting deserialization -- | This mutates `import('pg').types`, setting deserialization
-- | for some types to unmarshal as strings rather than JS values. -- | for some types to unmarshal as strings rather than JS values.
@ -51,7 +55,7 @@ instance Show Null where
-- | The serialization & deserialization monad. -- | The serialization & deserialization monad.
type RepT a = ExceptT (NonEmptyList ForeignError) Effect a type RepT a = ExceptT (NonEmptyList ForeignError) Effect a
-- | Flatten to an Effect, rendering any `RepError`s to `String` using `Show`. -- | Flatten to an Effect, `show`ing errors
smash :: forall a. RepT a -> Effect a smash :: forall a. RepT a -> Effect a
smash = liftEither <=< map (lmap (error <<< show)) <<< runExceptT smash = liftEither <=< map (lmap (error <<< show)) <<< runExceptT
@ -68,20 +72,26 @@ class (Serialize a, Deserialize a) <= Rep a
instance (Serialize a, Deserialize a) => Rep a instance (Serialize a, Deserialize a) => Rep a
-- | Coerces the value to `Raw` -- | Coerces the value to `Raw`.
-- |
-- | This is only safe for values whose javascript representation
-- | can be directly serialized by `node-postgres` to the corresponding
-- | SQL type.
unsafeSerializeCoerce :: forall m a. Monad m => a -> m Raw unsafeSerializeCoerce :: forall m a. Monad m => a -> m Raw
unsafeSerializeCoerce = pure <<< Raw.unsafeFromForeign <<< F.unsafeToForeign unsafeSerializeCoerce = pure <<< Raw.unsafeFromForeign <<< F.unsafeToForeign
instance Serialize Raw where instance Serialize Raw where
serialize = pure serialize = pure
-- | Serializes as `Null`. -- | `NULL`
instance Serialize Unit where instance Serialize Unit where
serialize _ = serialize Null serialize _ = serialize Null
-- | `NULL`
instance Serialize Null where instance Serialize Null where
serialize _ = unsafeSerializeCoerce null_ serialize _ = unsafeSerializeCoerce jsNull
-- | `json`, `jsonb`
instance WriteForeign a => Serialize (JSON a) where instance WriteForeign a => Serialize (JSON a) where
serialize = serialize <<< writeJSON <<< unwrap serialize = serialize <<< writeJSON <<< unwrap
@ -89,45 +99,51 @@ instance WriteForeign a => Serialize (JSON a) where
instance Serialize Buffer where instance Serialize Buffer where
serialize = unsafeSerializeCoerce serialize = unsafeSerializeCoerce
-- | `int2`, `int4`
instance Serialize Int where instance Serialize Int where
serialize = unsafeSerializeCoerce serialize = unsafeSerializeCoerce
-- | `int8`
instance Serialize BigInt where
serialize = serialize <<< BigInt.toString
-- | `bool`
instance Serialize Boolean where instance Serialize Boolean where
serialize = unsafeSerializeCoerce serialize = unsafeSerializeCoerce
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
instance Serialize String where instance Serialize String where
serialize = unsafeSerializeCoerce serialize = unsafeSerializeCoerce
-- | `float4`, `float8`
instance Serialize Number where instance Serialize Number where
serialize = unsafeSerializeCoerce serialize = unsafeSerializeCoerce
instance Serialize Char where -- | `timestamp`, `timestamptz`
serialize = unsafeSerializeCoerce
instance Serialize DateTime where instance Serialize DateTime where
serialize = serialize <<< unwrap <<< DateTime.ISO.fromDateTime serialize = serialize <<< unwrap <<< DateTime.ISO.fromDateTime
-- | `Just` -> `a`, `Nothing` -> `NULL`
instance Serialize a => Serialize (Maybe a) where instance Serialize a => Serialize (Maybe a) where
serialize (Just a) = serialize a serialize (Just a) = serialize a
serialize Nothing = unsafeSerializeCoerce null_ serialize Nothing = unsafeSerializeCoerce jsNull
-- | postgres `array`
instance Serialize a => Serialize (Array a) where instance Serialize a => Serialize (Array a) where
serialize = unsafeSerializeCoerce <=< traverse serialize serialize = unsafeSerializeCoerce <=< traverse serialize
instance Deserialize Raw where instance Deserialize Raw where
deserialize = pure deserialize = pure
-- | Note: this will always succeed, discarding -- | `NULL` (always succeeds)
-- | the actual raw value yielded.
-- |
-- | To explicitly deserialize NULL values and fail
-- | when the value is non-null, use `Null`.
instance Deserialize Unit where instance Deserialize Unit where
deserialize _ = pure unit deserialize _ = pure unit
-- | `NULL` (fails if non-null)
instance Deserialize Null where instance Deserialize Null where
deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.unsafeToForeign deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.unsafeToForeign
-- | `json`, `jsonb`
instance ReadForeign a => Deserialize (JSON a) where instance ReadForeign a => Deserialize (JSON a) where
deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String
@ -135,30 +151,43 @@ instance ReadForeign a => Deserialize (JSON a) where
instance Deserialize Buffer where instance Deserialize Buffer where
deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign
-- | `int2`, `int4`
instance Deserialize Int where instance Deserialize Int where
deserialize = F.readInt <<< Raw.unsafeToForeign deserialize = F.readInt <<< Raw.unsafeToForeign
-- | `int8`
instance Deserialize BigInt where
deserialize =
let
invalid s = pure $ ForeignError $ "Invalid bigint: " <> s
fromString s = liftMaybe (invalid s) $ BigInt.fromString s
in
fromString <=< deserialize @String
-- | `bool`
instance Deserialize Boolean where instance Deserialize Boolean where
deserialize = F.readBoolean <<< Raw.unsafeToForeign deserialize = F.readBoolean <<< Raw.unsafeToForeign
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
instance Deserialize String where instance Deserialize String where
deserialize = F.readString <<< Raw.unsafeToForeign deserialize = F.readString <<< Raw.unsafeToForeign
-- | `float4`, `float8`
instance Deserialize Number where instance Deserialize Number where
deserialize = F.readNumber <<< Raw.unsafeToForeign deserialize = F.readNumber <<< Raw.unsafeToForeign
instance Deserialize Char where -- | `timestamp`, `timestamptz`
deserialize = F.readChar <<< Raw.unsafeToForeign
instance Deserialize DateTime where instance Deserialize DateTime where
deserialize raw = do deserialize raw = do
s :: String <- deserialize raw s :: String <- deserialize raw
let invalid = pure $ ForeignError $ "Not a valid ISO8601 string: `" <> s <> "`" let invalid = pure $ ForeignError $ "Not a valid ISO8601 string: `" <> s <> "`"
liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s
-- | postgres `array`
instance Deserialize a => Deserialize (Array a) where instance Deserialize a => Deserialize (Array a) where
deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.unsafeToForeign deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.unsafeToForeign
-- | non-NULL -> `Just`, NULL -> `Nothing`
instance Deserialize a => Deserialize (Maybe a) where instance Deserialize a => Deserialize (Maybe a) where
deserialize raw = deserialize raw =
let let

View File

@ -4,51 +4,21 @@ import Prelude
import Control.Promise (Promise) import Control.Promise (Promise)
import Control.Promise as Promise import Control.Promise as Promise
import Data.Maybe (Maybe(..), fromMaybe) import Data.Maybe (fromMaybe)
import Data.Nullable (Nullable, toNullable)
import Data.Postgres (smash) import Data.Postgres (smash)
import Data.Postgres.Raw (Raw) import Data.Postgres.Query (class AsQuery, QueryRaw, asQuery, queryToRaw)
import Data.Postgres.Result (class FromResult, Result, fromRow, rows, rowsAffected) import Data.Postgres.Result (class FromResult, Result, fromRow, rows, rowsAffected)
import Data.Traversable (traverse) import Data.Traversable (traverse)
import Data.Tuple.Nested (type (/\), (/\))
import Effect (Effect) import Effect (Effect)
import Effect.Aff (Aff) import Effect.Aff (Aff)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Postgres.Client (Client, Config, make) import Effect.Postgres.Client (Client, Config, make)
import Prim.Row (class Union) import Prim.Row (class Union)
import Record (insert, modify)
import Type.Prelude (Proxy(..))
type QueryRaw = { text :: String, values :: Array Raw, name :: Nullable String, rowMode :: String }
foreign import connectImpl :: Client -> Effect (Promise Unit) foreign import connectImpl :: Client -> Effect (Promise Unit)
foreign import endImpl :: Client -> Effect (Promise Unit) foreign import endImpl :: Client -> Effect (Promise Unit)
foreign import queryImpl :: QueryRaw -> Client -> Effect (Promise Result) foreign import queryImpl :: QueryRaw -> Client -> Effect (Promise Result)
newtype Query = Query { text :: String, values :: Array Raw, name :: Maybe String }
queryToRaw :: Query -> QueryRaw
queryToRaw (Query r) =
let
name = Proxy @"name"
rowMode = Proxy @"rowMode"
in
insert rowMode "array"
$ modify name toNullable
$ r
class AsQuery a where
asQuery :: a -> Query
instance AsQuery Query where
asQuery = identity
instance AsQuery String where
asQuery text = Query { text, values: [], name: Nothing }
instance AsQuery (String /\ Array Raw) where
asQuery (text /\ values) = Query { text, values, name: Nothing }
connected :: forall r trash. Union r trash (Config ()) => Record r -> Aff Client connected :: forall r trash. Union r trash (Config ()) => Record r -> Aff Client
connected c = do connected c = do
client <- liftEffect $ make c client <- liftEffect $ make c
@ -62,7 +32,9 @@ end :: Client -> Aff Unit
end = Promise.toAffE <<< endImpl end = Promise.toAffE <<< endImpl
queryRaw :: forall q. AsQuery q => q -> Client -> Aff Result queryRaw :: forall q. AsQuery q => q -> Client -> Aff Result
queryRaw q = Promise.toAffE <<< queryImpl (queryToRaw $ asQuery q) queryRaw q c = do
q' <- queryToRaw <$> liftEffect (asQuery q)
Promise.toAffE $ queryImpl q' c
exec :: forall q. AsQuery q => q -> Client -> Aff Int exec :: forall q. AsQuery q => q -> Client -> Aff Int
exec q = map (fromMaybe 0 <<< rowsAffected) <<< queryRaw q exec q = map (fromMaybe 0 <<< rowsAffected) <<< queryRaw q

26
test/Test.Common.purs Normal file
View File

@ -0,0 +1,26 @@
module Test.Common where
import Prelude
import Effect (Effect)
import Effect.Aff (Aff, bracket)
import Effect.Aff.Postgres.Client (connected, end)
import Effect.Class (liftEffect)
import Effect.Postgres.Client (Client)
import Node.Path as Path
import Node.Process (cwd)
config
:: Effect
{ database :: String
, host :: String
, password :: String
, user :: String
}
config = do
cwd' <- liftEffect cwd
host <- liftEffect $ Path.resolve [ cwd' ] "./pg"
pure { host, user: "postgres", password: "password", database: "postgres" }
withClient :: (Client -> Aff Unit) -> Aff Unit
withClient = bracket (connected =<< liftEffect config) end

View File

@ -0,0 +1,4 @@
/** @type {(_: Buffer) => () => bigint} */
export const readBigInt64BE = buf => () => buf.readBigInt64BE(0)
export const dbg = a => () => typeof a === 'string' ? console.log(Buffer.from(a).toString('hex')) : undefined

View File

@ -2,30 +2,129 @@ module Test.Data.Postgres where
import Prelude import Prelude
import Control.Monad.Error.Class (liftEither) import Control.Monad.Gen (chooseInt, elements, oneOf)
import Control.Monad.Except (runExceptT) import Data.Array as Array
import Data.DateTime (DateTime(..)) import Data.Array.NonEmpty as Array.NonEmpty
import Data.DateTime (DateTime(..), canonicalDate)
import Data.DateTime.Instant as Instant 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.Int as Int
import Data.Maybe (Maybe, fromJust, fromMaybe, maybe) import Data.Maybe (Maybe(..), fromJust, maybe)
import Data.Newtype (unwrap, wrap) import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Postgres (class Rep, JSON(..), Null(..), deserialize, null_, serialize, smash) import Data.Number (abs) as Number
import Data.Postgres.Range as Range import Data.Postgres (class Rep, jsNull)
import Data.Postgres.Query.Builder as Q
import Data.Postgres.Raw (Raw) import Data.Postgres.Raw (Raw)
import Data.Postgres.Raw as Raw import Data.Postgres.Raw as Raw
import Data.Postgres.Result (class FromResult)
import Data.RFC3339String as DateTime.ISO import Data.RFC3339String as DateTime.ISO
import Data.Tuple.Nested (type (/\), (/\)) import Data.String as String
import Data.Time (Time(..))
import Data.Traversable (for, sequence)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Aff.Postgres.Client (exec, query)
import Effect.Class (liftEffect) import Effect.Class (liftEffect)
import Effect.Console (log) import Effect.Postgres.Client (Client)
import Effect.Unsafe (unsafePerformEffect) import Effect.Unsafe (unsafePerformEffect)
import Foreign (unsafeToForeign) import Foreign (Foreign, unsafeToForeign)
import Foreign.Object as Object 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 Partial.Unsafe (unsafePartial)
import Simple.JSON (writeImpl, writeJSON) import Simple.JSON (writeJSON)
import Test.QuickCheck (class Arbitrary, (==?)) import Test.Common (withClient)
import Test.Spec (Spec, describe, it) import Test.QuickCheck (class Arbitrary, arbitrary, randomSeed)
import Test.Spec.Assertions (shouldEqual) import Test.QuickCheck.Gen (sample, vectorOf)
import Test.Spec.QuickCheck (quickCheck) 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 GenSmallInt = GenSmallInt Int
derive instance Newtype GenSmallInt _
instance Arbitrary GenSmallInt where
arbitrary = wrap <$> chooseInt (-32768) 32767
newtype GenDateTime = GenDateTime DateTime
derive instance Newtype GenDateTime _
instance Arbitrary GenDateTime where
arbitrary = do
yr <- chooseInt 1970 2100
mo <- chooseInt 1 12
dy <- chooseInt 1 28
hr <- chooseInt 0 23
mn <- chooseInt 0 59
sc <- chooseInt 0 59
ms <- chooseInt 0 999
let
date = unsafePartial fromJust $ Just canonicalDate <*> toEnum yr <*> toEnum mo <*> toEnum dy
time = unsafePartial fromJust $ Just Time <*> toEnum hr <*> toEnum mn <*> toEnum sc <*> toEnum ms
pure $ wrap $ DateTime date time
newtype GenString = GenString String
derive instance Newtype GenString _
instance Arbitrary GenString where
arbitrary = do
let chars = unsafePartial fromJust $ Array.NonEmpty.fromArray $ String.split (wrap "") "abcdefghijklmnopqrstuvwxyz01234567890 _-=><|\\/"
len <- chooseInt 0 100
chars' <- vectorOf len (elements chars)
pure $ wrap $ fold chars'
newtype GenSmallFloat = GenSmallFloat Number
derive instance Newtype GenSmallFloat _
instance Arbitrary GenSmallFloat where
arbitrary = do
let byte = chooseInt 0 7
bytes <- sequence $ Array.replicate 4 byte
pure
$ wrap
$ unsafePerformEffect do
buf <- Buffer.fromArray bytes
Buffer.read Buffer.FloatBE 0 buf
newtype GenBigInt = GenBigInt BigInt
derive instance Newtype GenBigInt _
instance Arbitrary GenBigInt where
arbitrary = do
let byte = chooseInt 0 7
bytes <- sequence $ Array.replicate 8 byte
let
bigint = unsafePerformEffect do
buf <- Buffer.fromArray bytes
readBigInt64BE buf
pure $ wrap bigint
newtype GenJSON = GenJSON Foreign
derive instance Newtype GenJSON _
instance Arbitrary GenJSON where
arbitrary =
let
json _ = map wrap $ oneOf' [ prim, array unit, obj unit ]
oneOf' = oneOf <<< unsafePartial fromJust <<< Array.NonEmpty.fromArray
elements' = elements <<< unsafePartial fromJust <<< Array.NonEmpty.fromArray
prim = oneOf'
[ pure $ unsafeToForeign jsNull
, unsafeToForeign <$> arbitrary @Number
, unsafeToForeign <$> arbitrary @String
]
array _ = map unsafeToForeign $ vectorOf 3 prim
obj _ = do
keys <- vectorOf 3 (elements' [ "foo", "bar", "baz", "quux", "duck", "dog", "cat", "cow" ])
kvs <- for keys \k -> (k /\ _) <$> prim
pure $ unsafeToForeign $ Object.fromFoldable kvs
in
json unit
asRaw :: forall a. a -> Raw asRaw :: forall a. a -> Raw
asRaw = Raw.unsafeFromForeign <<< unsafeToForeign asRaw = Raw.unsafeFromForeign <<< unsafeToForeign
@ -33,59 +132,52 @@ asRaw = Raw.unsafeFromForeign <<< unsafeToForeign
spec :: Spec Unit spec :: Spec Unit
spec = spec =
let let
check :: forall @a @x. Eq a => Show a => Arbitrary x => Rep a => String -> (x -> a) -> (a -> Raw) -> Spec Unit check :: forall @a @x. Show a => Arbitrary x => Rep a => FromResult a => String -> String -> (x -> a) -> (a -> Raw) -> (a -> a -> Boolean) -> SpecT Aff Client Identity Unit
check s xa asRaw_ = check purs sql xa asRaw_ isEq =
describe s do it (purs <> " <> " <> sql) \c -> do
it "serialize" $ quickCheck \(x :: x) -> (unsafePerformEffect $ runExceptT $ serialize $ xa x) ==? pure (asRaw_ $ xa x) let
it "deserialize" $ quickCheck \(x :: x) -> (unsafePerformEffect $ runExceptT $ deserialize $ asRaw_ $ xa x) ==? pure (xa x) tab = String.replace (wrap " ") (wrap "_") $ String.replace (wrap "[") (wrap "") $ String.replace (wrap "]") (wrap "") $ sql <> "_is_" <> String.toLower purs
ser x =
Q.build do
x' <- Q.param $ xa x
pure $ "insert into " <> tab <> " values (" <> x' <> " :: " <> sql <> ")"
de x =
Q.build do
x' <- Q.param $ xa x
pure $ "select " <> x' <> " :: " <> sql
void $ exec ("create temp table " <> tab <> " (val " <> sql <> ")") c
seed <- liftEffect randomSeed
let
xs = sample seed 20 (arbitrary @x)
void $ for xs \x -> do
void $ exec (ser x) c
res :: Array a <- query (de x) c
let
exp = xa x
act = unsafePartial fromJust $ Array.head res
when (not $ isEq exp act) $ fail $ "expected " <> show exp <> " to equal " <> show act
check_ :: forall @a. Eq a => Show a => Arbitrary a => Rep a => String -> Spec Unit check_ :: forall @a. Eq a => Show a => Arbitrary a => FromResult a => Rep a => String -> String -> SpecT Aff Client Identity Unit
check_ s = check @a @a s identity asRaw check_ purs sql = check @a @a purs sql identity asRaw eq
dateTimeFromArbitrary :: Int -> DateTime dateTimeFromArbitrary :: Int -> DateTime
dateTimeFromArbitrary = Instant.toDateTime <<< unsafePartial fromJust <<< Instant.instant <<< wrap <<< Int.toNumber dateTimeFromArbitrary = Instant.toDateTime <<< unsafePartial fromJust <<< Instant.instant <<< wrap <<< Int.toNumber
in in
describe "Data.Postgres" do around withClient
check_ @Int "Int" $ describe "Data.Postgres"
check_ @String "String" $ do
check_ @Boolean "Boolean" check @Int @GenSmallInt "Int" "int2" unwrap asRaw eq
check_ @Number "Number" check_ @Int "Int" "int4"
check_ @Char "Char"
check @(Maybe String) "Maybe String" identity (maybe null_ asRaw) check @String @GenString "String" "text" unwrap asRaw eq
check @(Array String) "Array String" identity asRaw
check @DateTime "DateTime" dateTimeFromArbitrary (asRaw <<< DateTime.ISO.fromDateTime)
describe "JSON" do check_ @Boolean "Boolean" "bool"
describe "Record" do
it "deserialize" $
quickCheck \(a /\ b /\ c :: Int /\ String /\ Array { "foo" :: String }) -> unsafePerformEffect do
let
obj = { a, b, c }
json = writeJSON obj
act :: JSON _ <- smash $ deserialize $ asRaw json
pure $ obj ==? unwrap act
it "serialize" $
quickCheck \(a /\ b /\ c :: Int /\ String /\ Array { "foo" :: String }) -> unsafePerformEffect do
let obj = { a, b, c }
act <- smash $ serialize $ JSON obj
pure $ asRaw (writeJSON obj) ==? act
describe "Null" do check @Number @GenSmallFloat "Number" "float4" unwrap asRaw (\a b -> Number.abs (a - b) <= 0.0001)
it "serialize" $ liftEffect $ shouldEqual null_ =<< (smash $ serialize Null) check_ @Number "Number" "float8"
it "deserialize" $ liftEffect $ shouldEqual Null =<< (smash $ deserialize null_)
describe "Range" do check @BigInt @GenBigInt "BigInt" "int8" unwrap (asRaw <<< BigInt.toString) eq
it "deserialize" do check @(Maybe String) @(Maybe GenString) "Maybe String" "text" (map unwrap) (maybe jsNull asRaw) eq
quickCheck \(up /\ lo /\ uinc /\ linc :: Int /\ Int /\ Boolean /\ Boolean) -> unsafePerformEffect do check @(Array String) @(Array GenString) "Array String" "text[]" (map unwrap) asRaw eq
let check @DateTime @GenDateTime "DateTime" "timestamptz" unwrap (asRaw <<< DateTime.ISO.fromDateTime) eq
record = check @String @GenJSON "JSON" "json" (writeJSON <<< unwrap) asRaw eq
{ upper: unsafePerformEffect $ smash $ serialize up
, lower: unsafePerformEffect $ smash $ serialize lo
, upperIncl: uinc
, lowerIncl: linc
}
raw = asRaw $ Range.rangeRawFromRecord record
exp :: Range.Range Int <- smash $ Range.rangeFromRaw record
act :: Range.Range Int <- smash $ deserialize raw
pure $ exp ==? act

View File

@ -7,32 +7,12 @@ import Data.Either (Either, isLeft)
import Data.Newtype (wrap) import Data.Newtype (wrap)
import Data.Postgres (JSON(..)) import Data.Postgres (JSON(..))
import Data.PreciseDateTime (fromRFC3339String, toDateTimeLossy) import Data.PreciseDateTime (fromRFC3339String, toDateTimeLossy)
import Effect (Effect)
import Effect.Aff (Aff, bracket)
import Effect.Aff.Postgres.Client (query) import Effect.Aff.Postgres.Client (query)
import Effect.Aff.Postgres.Client as PG.Aff.Client import Effect.Aff.Postgres.Client as PG.Aff.Client
import Effect.Class (liftEffect) import Test.Common (withClient)
import Effect.Postgres.Client as PG
import Node.Path as Path
import Node.Process (cwd)
import Test.Spec (Spec, around, describe, it) import Test.Spec (Spec, around, describe, it)
import Test.Spec.Assertions (shouldEqual) import Test.Spec.Assertions (shouldEqual)
config
:: Effect
{ database :: String
, host :: String
, password :: String
, user :: String
}
config = do
cwd' <- liftEffect cwd
host <- liftEffect $ Path.resolve [ cwd' ] "./pg"
pure { host, user: "postgres", password: "password", database: "postgres" }
withClient :: (PG.Client -> Aff Unit) -> Aff Unit
withClient = bracket (PG.Aff.Client.connected =<< liftEffect config) PG.Aff.Client.end
spec :: Spec Unit spec :: Spec Unit
spec = spec =
around withClient do around withClient do
@ -46,12 +26,9 @@ spec =
isLeft res `shouldEqual` true isLeft res `shouldEqual` true
it "rowsAffected is correct" \c -> do it "rowsAffected is correct" \c -> do
void $ PG.Aff.Client.exec "create temp table foo (bar int);" c void $ PG.Aff.Client.exec "create temp table foo (bar int);" c
cta <- PG.Aff.Client.exec "insert into foo values (1);" c shouldEqual 1 =<< PG.Aff.Client.exec "insert into foo values (1);" c
cta `shouldEqual` 1 shouldEqual 3 =<< PG.Aff.Client.exec "insert into foo values (1), (2), (3);" c
ctb <- PG.Aff.Client.exec "insert into foo values (1), (2), (3);" c shouldEqual 4 =<< PG.Aff.Client.exec "update foo set bar = 10;" c
ctb `shouldEqual` 3
ctc <- PG.Aff.Client.exec "update foo set bar = 10;" c
ctc `shouldEqual` 4
describe "timestamp" do describe "timestamp" do
it "unmarshals" \c -> do it "unmarshals" \c -> do
let exp = toDateTimeLossy <$> fromRFC3339String (wrap "2020-01-01T00:00:00Z") let exp = toDateTimeLossy <$> fromRFC3339String (wrap "2020-01-01T00:00:00Z")