feat: cursor monad

This commit is contained in:
orion 2024-04-02 15:58:34 -05:00
parent c0ec6dec53
commit e8d8fd24f3
Signed by: orion
GPG Key ID: 6D4165AE4C928719
18 changed files with 651 additions and 199 deletions

View File

@ -10,9 +10,11 @@ workspace:
- control
- datetime
- effect
- either
- exceptions
- foldable-traversable
- foreign
- foreign-object
- fork
- integers
- js-bigints
@ -23,11 +25,14 @@ workspace:
- node-buffer
- node-event-emitter
- nullable
- parallel
- partial
- precise-datetime
- prelude
- profunctor
- record
- simple-json
- strings
- transformers
- tuples
- typelevel-prelude

View File

@ -2,6 +2,7 @@ package:
name: pg
build:
censorProjectWarnings:
- 'ImplicitQualifiedImport'
- 'ImplicitQualifiedImportReExport'
strict: true
pedanticPackages: true
@ -13,9 +14,11 @@ package:
- control
- datetime
- effect
- either
- exceptions
- foldable-traversable
- foreign
- foreign-object
- fork
- integers
- js-bigints
@ -26,11 +29,14 @@ package:
- node-buffer
- node-event-emitter
- nullable
- parallel
- partial
- precise-datetime
- prelude
- profunctor
- record
- simple-json
- strings
- transformers
- tuples
- typelevel-prelude

View File

@ -0,0 +1,121 @@
module Control.Monad.Postgres.Base where
import Prelude
import Control.Alt (class Alt)
import Control.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.Reader (class MonadAsk, class MonadReader, ReaderT, ask, local, runReaderT)
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 Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Pool (Pool)
import Effect.Aff.Postgres.Pool as Pool
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (Error)
import Prim.Row (class Union)
-- | Monad handling pool resource acquisition & release
-- |
-- | ```
-- | runPostgres
-- | {connectionString: "postgresql://postgres:postgres@localhost:5432"}
-- | $ session do
-- | exec_ "create table foo (bar int);"
-- | exec_ "insert into foo values (1);"
-- | res <- query "select * from foo"
-- | pure $ res == 1
-- | ```
-- |
-- | Is equivalent to:
-- | ```
-- | do
-- | pool <- liftEffect $ Pool.make {connectionString: "postgresql://postgres:postgres@localhost:5432"}
-- | finally (Pool.end pool) do
-- | client <- Pool.connect pool
-- | finally (liftEffect $ Pool.release pool client) do
-- | Client.exec_ "create table foo (bar int);" client
-- | Client.exec_ "insert into foo values (1);" client
-- | res <- Client.query "select * from foo" client
-- | pure $ res == 1
-- | ```
newtype PostgresT :: forall k. (k -> Type) -> k -> Type
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 (Applicative m) => Applicative (PostgresT m)
derive newtype instance (Plus m) => Plus (PostgresT m)
derive newtype instance (Alt m) => Alt (PostgresT m)
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 MonadTrans (PostgresT)
derive newtype instance (MonadThrow e m) => MonadThrow e (PostgresT m)
derive newtype instance (MonadError e m) => MonadError e (PostgresT m)
derive newtype instance (MonadFork f m) => MonadFork f (PostgresT m)
derive newtype instance MFunctor PostgresT
derive newtype instance MMonad PostgresT
instance (Apply m, Apply p, Parallel p m) => Parallel (PostgresT p) (PostgresT m) where
parallel = wrap <<< parallel <<< unwrap
sequential = wrap <<< sequential <<< unwrap
instance (Monad m, MonadKill e f m) => MonadKill e f (PostgresT m) where
kill a b = lift $ kill a b
instance (Monad m, MonadBracket e f (ReaderT Pool m), MonadBracket e f m) => MonadBracket e f (PostgresT m) where
bracket acq rel m = wrap $ bracket (unwrap acq) (\a b -> unwrap $ rel a b) (unwrap <<< m)
uninterruptible a = wrap $ uninterruptible $ unwrap a
never = lift $ never
instance Monad m => MonadAsk Pool (PostgresT m) where
ask = wrap ask
instance Monad m => MonadReader Pool (PostgresT m) where
local f m = wrap $ local f $ unwrap m
instance (MonadBracket e f m, MonadAff m) => MonadSession (PostgresT m) where
query = session <<< query
exec = session <<< exec
exec_ = session <<< exec_
-- | 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.
-- |
-- | 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
-- | Create a new connection pool from the provided config and execute
-- | the postgres monad, invoking `Effect.Aff.Postgres.Pool.end` afterwards.
runPostgres :: forall m a missing trash r e f. MonadBracket e f m => MonadAff m => Union r missing (Pool.Config trash) => Record r -> PostgresT m a -> m a
runPostgres cfg m =
let
acq = liftEffect $ Pool.make @r @missing @trash cfg
rel _ p = liftAff $ Pool.end p
in
bracket acq rel $ runReaderT $ unwrap m

View File

@ -0,0 +1,111 @@
module Control.Monad.Postgres.Cursor where
import Prelude
import Control.Alt (class Alt)
import Control.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.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 Effect.Aff.Class (class MonadAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (Error)
newtype CursorT :: forall k. Type -> (k -> Type) -> k -> Type
newtype CursorT t m a = CursorT (ReaderT String m a)
derive instance Newtype (CursorT t m a) _
derive newtype instance (Functor m) => Functor (CursorT t m)
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 (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 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)
derive newtype instance (MonadFork f m) => MonadFork f (CursorT t m)
instance (Monad m, MonadKill e f m) => MonadKill e f (CursorT t m) where
kill a b = lift $ kill a b
instance (Monad m, MonadBracket e f (ReaderT String m), MonadBracket e f m) => MonadBracket e f (CursorT t m) where
bracket acq rel m = wrap $ bracket (unwrap acq) (\a b -> unwrap $ rel a b) (unwrap <<< m)
uninterruptible a = wrap $ uninterruptible $ unwrap a
never = lift $ never
instance Monad m => MonadAsk String (CursorT t m) where
ask = wrap ask
instance Monad m => MonadReader String (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
parallel = wrap <<< parallel <<< unwrap
sequential = wrap <<< sequential <<< unwrap
-- | A monad representing a handle to a server-side cursor
-- |
-- | ```
-- | runPostgres {connectionString: "..."} do
-- | exec_ "create table foo (id int not null primary key);"
-- | exec_
-- | $ intercalate "\n "
-- | [ "insert into foo (id)"
-- | , "values"
-- | , intercalate ", "
-- | $ map (\n -> "(" <> show n <> ")")
-- | $ Array.range 1 100
-- | ]
-- |
-- | cursor @Int "foo_cursor" "select id from foo" do
-- | a <- fetchOne -- 1
-- | b <- fetchOne -- 2
-- | c <- fetchOne -- 3
-- | d <- fetch 10 -- 4..14
-- | e <- fetchAll -- 15..100
-- | pure unit
-- | ```
class (MonadSession m, FromRow t) <= 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)
instance (FromRow t, MonadSession m) => MonadCursor (CursorT t m) t where
fetch n = do
cur <- ask
query $ "fetch forward " <> show n <> " from " <> cur
fetchAll = do
cur <- ask
query $ "fetch all from " <> cur
instance (MonadSession m) => MonadSession (CursorT t m) where
query = lift <<< query
exec = lift <<< exec
exec_ = lift <<< exec_
-- | 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

@ -0,0 +1,34 @@
module Control.Monad.Postgres.Session where
import Prelude
import Control.Monad.Fork.Class (class MonadBracket)
import Control.Monad.Reader (class MonadReader, ReaderT, ask)
import Data.Postgres.Query (class AsQuery)
import Data.Postgres.Result (class FromRows)
import Effect.Aff (Fiber)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Client (Client)
import Effect.Aff.Postgres.Client as Client
import Effect.Exception (Error)
type SessionT :: forall k. (k -> Type) -> k -> Type
type SessionT = ReaderT Client
-- | A monad representing a connected session to a database
class MonadAff m <= MonadSession m where
-- | Executes a query and unmarshals the result into `r`
query :: forall q r. AsQuery q => FromRows r => q -> m r
-- | Executes a query and returns the number of rows affected
exec :: forall q. AsQuery q => q -> m Int
-- | Executes a query and discards the result
exec_ :: forall q. AsQuery q => q -> m Unit
instance MonadAff m => MonadSession (SessionT m) where
query q = do
client <- ask
liftAff $ Client.query q client
exec q = do
client <- ask
liftAff $ Client.exec q client
exec_ = void <<< exec

View File

@ -1,100 +1,5 @@
module Control.Monad.Postgres where
module Control.Monad.Postgres (module X) where
import Prelude
import Control.Monad.Error.Class (catchError, throwError)
import Control.Monad.Fork.Class (class MonadBracket, bracket)
import Control.Monad.Reader (class MonadReader, ReaderT, ask, runReaderT)
import Control.Monad.Trans.Class (lift)
import Data.Postgres.Query (class AsQuery)
import Data.Postgres.Result (class FromRows)
import Effect.Aff (Fiber)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Aff.Postgres.Client (Client)
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 Prim.Row (class Union)
-- | Monad handling pool resource acquisition & release
-- |
-- | ```
-- | runPostgres
-- | {connectionString: "postgresql://postgres:postgres@localhost:5432"}
-- | $ session do
-- | exec_ "create table foo (bar int);"
-- | exec_ "insert into foo values (1);"
-- | res <- query "select * from foo"
-- | pure $ res == 1
-- | ```
-- |
-- | Is equivalent to:
-- | ```
-- | do
-- | pool <- liftEffect $ Pool.make {connectionString: "postgresql://postgres:postgres@localhost:5432"}
-- | finally (Pool.end pool) do
-- | client <- Pool.connect pool
-- | finally (liftEffect $ Pool.release pool client) do
-- | Client.exec_ "create table foo (bar int);" client
-- | Client.exec_ "insert into foo values (1);" client
-- | res <- Client.query "select * from foo" client
-- | pure $ res == 1
-- | ```
type PostgresT :: forall k. (k -> Type) -> k -> Type
type PostgresT = ReaderT Pool
type SessionT :: forall k. (k -> Type) -> k -> Type
type SessionT = ReaderT Client
-- | A monad representing a connected session to a database
class (MonadBracket Error Fiber m, MonadAff m, MonadReader Client m) <= MonadSession m where
-- | Executes a query and unmarshals the result into `r`
query :: forall q r. AsQuery q => FromRows r => q -> m r
-- | Executes a query and returns the number of rows affected
exec :: forall q. AsQuery q => q -> m Int
-- | Executes a query and discards the result
exec_ :: forall q. AsQuery q => q -> m Unit
instance (MonadBracket Error Fiber m, MonadAff m, MonadReader Client m) => MonadSession m where
query q = do
client <- ask
liftAff $ Client.query q client
exec q = do
client <- ask
liftAff $ Client.exec q client
exec_ = void <<< exec
-- | Lifts a session to `PostgresT`, releasing the client to the pool
-- | after execution.
session :: forall m a. MonadBracket Error Fiber 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.
-- |
-- | 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
-- | Runs a `PostgresT` with a pool created with the provided config, invoking `Pool.end` afterwards.
runPostgres :: forall m a missing trash r e f. MonadBracket e f m => MonadAff m => Union r missing (Pool.Config trash) => Record r -> PostgresT m a -> m a
runPostgres cfg m =
let
acq = liftEffect $ Pool.make @r @missing @trash cfg
rel _ p = liftAff $ Pool.end p
in
bracket acq rel $ runReaderT m
import Control.Monad.Postgres.Cursor as X
import Control.Monad.Postgres.Session as X
import Control.Monad.Postgres.Base as X

View File

@ -0,0 +1,88 @@
module Data.Postgres.Custom.Enum where
import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (liftMaybe)
import Data.Array.NonEmpty.Internal (NonEmptyArray)
import Data.Either (hush)
import Data.Foldable (intercalate)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep as G
import Data.Maybe (Maybe(..), fromJust)
import Data.Newtype as Newtype
import Data.Postgres (RepT, deserialize, serialize)
import Data.Postgres.Custom (class CustomRep, quoted, typeName)
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 Foreign (ForeignError(..))
import Partial.Unsafe (unsafePartial)
import Type.Prelude (Proxy(..), reflectSymbol)
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"
class CustomRep a ty <= CustomEnum a ty | a -> ty where
enumVariants :: NonEmptyArray a
parseEnum :: String -> Maybe a
printEnum :: a -> String
class GenericCustomEnum a where
genericEnumVariants' :: NonEmptyArray a
genericParseEnum' :: String -> Maybe a
genericPrintEnum' :: a -> String
instance IsSymbol n => GenericCustomEnum (G.Constructor n G.NoArguments) where
genericEnumVariants' = pure (G.Constructor @n G.NoArguments)
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)
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'
genericParseEnum :: forall a g. Generic a g => GenericCustomEnum g => String -> Maybe a
genericParseEnum = map G.to <<< genericParseEnum'
genericPrintEnum :: forall a g. Generic a g => GenericCustomEnum g => a -> String
genericPrintEnum = genericPrintEnum' <<< G.from
create :: forall @a ty. CustomEnum a ty => Query
create =
let
variants' :: NonEmptyArray a
variants' = enumVariants
variants = intercalate ", " $ quoted <$> printEnum <$> variants'
q = "create type " <> typeName @a <> " as enum (" <> variants <> ");"
in
Newtype.modify (_ { text = q }) emptyQuery

View File

@ -0,0 +1,29 @@
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
quoted :: String -> String
quoted s = "'" <> s <> "'"
typeName :: forall @a ty. CustomRep a ty => String
typeName = reflectSymbol (Proxy @ty)

View File

@ -3,16 +3,17 @@ module Data.Postgres.Range where
import Prelude
import Control.Alt ((<|>))
import Control.Monad.Trans.Class (lift)
import Data.Array as Array
import Data.Foldable (class Foldable, foldl)
import Data.FoldableWithIndex (foldMapDefault)
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (class Newtype, unwrap)
import Data.Postgres (class Deserialize, class Rep, class Serialize, RepT, deserialize, serialize, smash)
import Data.Postgres.Raw (Raw)
import Data.Postgres.Raw as Raw
import Data.Postgres.Raw (Raw, rawMaybeNull, rawNullMaybe)
import Data.Show.Generic (genericShow)
import Data.Traversable (class Traversable, foldMapDefaultL, foldrDefault, sequenceDefault, traverse)
import Effect (Effect)
import Foreign (unsafeToForeign)
import Foreign.Object (foldMap)
-- | A range of values with optional upper & lower bounds.
-- |
@ -22,18 +23,27 @@ import Foreign (unsafeToForeign)
-- | * `gte 1 <> lt 2 -> '[1,2)'`
newtype Range a = Range { upper :: Maybe (Bound a), lower :: Maybe (Bound a) }
derive instance Functor Range
instance Foldable Range where
foldl f b r = foldl f b $ boundValue <$> Array.catMaybes [ upper r, lower r ]
foldr f b r = foldrDefault f b r
foldMap f r = foldMapDefaultL f r
instance Traversable Range where
traverse f r =
let
build u l = Range { upper: u, lower: l }
fToBound = traverse (traverse f)
in
pure build <*> fToBound (upper r) <*> fToBound (lower r)
sequence = sequenceDefault
derive instance Generic (Range a) _
derive instance Newtype (Range a) _
derive instance Eq a => Eq (Range a)
instance Show a => Show (Range a) where
show = genericShow
instance (Ord a, Rep a) => Serialize (Range a) where
serialize = map (Raw.unsafeFromForeign <<< unsafeToForeign <<< __rangeRawFromRecord) <<< __rangeToRecord
instance (Ord a, Rep a) => Deserialize (Range a) where
deserialize = __rangeFromRecord <=< map __rangeRawToRecord <<< lift <<< __rangeRawFromRaw
instance Monoid (Range a) where
mempty = Range { upper: Nothing, lower: Nothing }
@ -43,6 +53,18 @@ instance Semigroup (Range a) where
-- | An upper or lower range bound
data Bound a = BoundIncl a | BoundExcl a
instance Foldable Bound where
foldl f b (BoundIncl a) = f b a
foldl f b (BoundExcl a) = f b a
foldr f b a = foldrDefault f b a
foldMap f r = foldMapDefaultL f r
instance Traversable Bound where
traverse f (BoundIncl a) = BoundIncl <$> f a
traverse f (BoundExcl a) = BoundExcl <$> f a
sequence = sequenceDefault
derive instance Functor Bound
derive instance Generic (Bound a) _
derive instance Eq a => Eq (Bound a)
instance Show a => Show (Bound a) where
@ -73,18 +95,6 @@ makeBound i a
| i = BoundIncl a
| otherwise = BoundExcl a
-- | Attempt to parse a SQL string of a range as `Range a`
parseSQL :: forall a. Rep a => (String -> RepT a) -> String -> RepT (Range a)
parseSQL fromString sql = do
range <- lift $ __rangeRawParse sql $ smash <<< (serialize <=< fromString)
__rangeFromRecord $ __rangeRawToRecord range
-- | Serialize a `Range` as a SQL string
printSQL :: forall a. Rep a => Range a -> RepT String
printSQL range = do
record <- __rangeToRecord range
lift $ __rangeRawSerialize $ __rangeRawFromRecord record
-- | Returns whether the range contains value `a`
contains :: forall a. Ord a => a -> Range a -> Boolean
contains a r =
@ -133,16 +143,9 @@ foreign import __rangeRawParse :: String -> (String -> Effect Raw) -> Effect Ran
foreign import __rangeRawSerialize :: RangeRaw -> Effect String
-- | FFI
__rangeFromRecord :: forall a. Deserialize a => RangeRecord -> RepT (Range a)
__rangeFromRecord raw = do
upper' :: Maybe a <- deserialize raw.upper
lower' :: Maybe a <- deserialize raw.lower
pure $ Range { upper: makeBound raw.upperIncl <$> upper', lower: makeBound raw.lowerIncl <$> lower' }
__rangeFromRecord :: RangeRecord -> Range Raw
__rangeFromRecord raw = Range { upper: makeBound raw.upperIncl <$> rawNullMaybe raw.upper, lower: makeBound raw.lowerIncl <$> rawNullMaybe raw.lower }
-- | FFI
__rangeToRecord :: forall a. Serialize a => Range a -> RepT RangeRecord
__rangeToRecord r = do
upper' <- serialize $ boundValue <$> upper r
lower' <- serialize $ boundValue <$> lower r
pure $ { upper: upper', lower: lower', upperIncl: fromMaybe false $ boundIsInclusive <$> upper r, lowerIncl: fromMaybe false $ boundIsInclusive <$> lower r }
__rangeToRecord :: Range Raw -> RangeRecord
__rangeToRecord r = { upper: rawMaybeNull $ boundValue <$> upper r, lower: rawMaybeNull $ boundValue <$> lower r, upperIncl: fromMaybe false $ boundIsInclusive <$> upper r, lowerIncl: fromMaybe false $ boundIsInclusive <$> lower r }

View File

@ -28,3 +28,8 @@ export const rawDebugEq = a => b =>
? a.every((a_, ix) => rawDebugEq(a_)(b[ix]))
: false
: false
export const jsNull = null
/** @type {(a: unknown) => a is null} */
export const isNull = a => a === null

View File

@ -2,10 +2,28 @@ module Data.Postgres.Raw where
import Prelude
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Show.Generic (genericShow)
import Foreign (Foreign)
import Prim.TypeError (class Warn, Text)
import Unsafe.Coerce (unsafeCoerce)
-- | Literal javascript `null`
foreign import jsNull :: Raw
-- | `a === null`
foreign import isNull :: Raw -> Boolean
-- | The SQL value NULL
data Null = Null
derive instance Generic Null _
derive instance Eq Null
derive instance Ord Null
instance Show Null where
show = genericShow
-- | A raw JS value converted from SQL
-- |
-- | In practice, this is an alias for `Foreign` with
@ -32,6 +50,15 @@ instance Show Raw where
instance (Warn (Text "`Eq Raw` only checks equality for JS primitives, and is always `false` for objects.")) => Eq Raw where
eq = rawDebugEq
rawMaybeNull :: Maybe Raw -> Raw
rawMaybeNull (Just a) = a
rawMaybeNull Nothing = unsafeCoerce jsNull
rawNullMaybe :: Raw -> Maybe Raw
rawNullMaybe raw
| isNull raw = Nothing
| otherwise = Just raw
-- | Coerce a `Foreign` value to `Raw`.
-- |
-- | This is only safe if the `Foreign` value

View File

@ -1,8 +1,6 @@
import Pg from 'pg'
import Range from 'postgres-range'
export const jsNull = null
export const modifyPgTypes = () => {
// https://github.com/brianc/node-pg-types/blob/master/lib/textParsers.js
const oid = {

View File

@ -6,20 +6,21 @@ import Control.Alt ((<|>))
import Control.Monad.Error.Class (liftEither, liftMaybe)
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.Generic.Rep (class Generic)
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Postgres.Raw (Raw)
import Data.Postgres.Custom (class CustomDeserialize, class CustomSerialize, customDeserialize, customSerialize)
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.Show.Generic (genericShow)
import Data.Traversable (traverse)
import Effect (Effect)
import Effect.Exception (error)
import Foreign (ForeignError(..))
import Foreign (ForeignError(..), unsafeToForeign)
import Foreign as F
import JS.BigInt (BigInt)
import JS.BigInt as BigInt
@ -36,22 +37,10 @@ derive newtype instance Ord a => Ord (JSON a)
derive newtype instance WriteForeign a => WriteForeign (JSON a)
derive newtype instance ReadForeign a => ReadForeign (JSON a)
-- | Literal javascript `null`
foreign import jsNull :: Raw
-- | This mutates `import('pg').types`, setting deserialization
-- | for some types to unmarshal as strings rather than JS values.
foreign import modifyPgTypes :: Effect Unit
-- | The SQL value NULL
data Null = Null
derive instance Generic Null _
derive instance Eq Null
derive instance Ord Null
instance Show Null where
show = genericShow
-- | The serialization & deserialization monad.
type RepT a = ExceptT (NonEmptyList ForeignError) Effect a
@ -84,79 +73,87 @@ instance Serialize Raw where
serialize = pure
-- | `NULL`
instance Serialize Unit where
else instance Serialize Unit where
serialize _ = serialize Null
-- | `NULL`
instance Serialize Null where
else instance Serialize Null where
serialize _ = unsafeSerializeCoerce jsNull
-- | `json`, `jsonb`
instance WriteForeign a => Serialize (JSON a) where
else instance WriteForeign a => Serialize (JSON a) where
serialize = serialize <<< writeJSON <<< unwrap
-- | `bytea`
instance Serialize Buffer where
else instance Serialize Buffer where
serialize = unsafeSerializeCoerce
-- | `int2`, `int4`
instance Serialize Int where
else instance Serialize Int where
serialize = unsafeSerializeCoerce
-- | `int8`
instance Serialize BigInt where
else instance Serialize BigInt where
serialize = serialize <<< BigInt.toString
-- | `bool`
instance Serialize Boolean where
else instance Serialize Boolean where
serialize = unsafeSerializeCoerce
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
instance Serialize String where
else instance Serialize String where
serialize = unsafeSerializeCoerce
-- | `float4`, `float8`
instance Serialize Number where
else instance Serialize Number where
serialize = unsafeSerializeCoerce
-- | `timestamp`, `timestamptz`
instance Serialize DateTime where
else instance Serialize DateTime where
serialize = serialize <<< unwrap <<< DateTime.ISO.fromDateTime
-- | `Just` -> `a`, `Nothing` -> `NULL`
instance Serialize a => Serialize (Maybe a) where
else instance Serialize a => Serialize (Maybe a) where
serialize (Just a) = serialize a
serialize Nothing = unsafeSerializeCoerce jsNull
-- | postgres `array`
instance Serialize a => Serialize (Array a) where
else instance Serialize a => Serialize (Array a) where
serialize = unsafeSerializeCoerce <=< traverse serialize
else 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)
instance Deserialize Unit where
else instance Deserialize Unit where
deserialize _ = pure unit
-- | `NULL` (fails if non-null)
instance Deserialize Null where
else instance Deserialize Null where
deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.asForeign
-- | `json`, `jsonb`
instance ReadForeign a => Deserialize (JSON a) where
else instance ReadForeign a => Deserialize (JSON a) where
deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String
-- | `bytea`
instance Deserialize Buffer where
else instance Deserialize Buffer where
deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.asForeign
-- | `int2`, `int4`
instance Deserialize Int where
else instance Deserialize Int where
deserialize = F.readInt <<< Raw.asForeign
-- | `int8`
instance Deserialize BigInt where
else instance Deserialize BigInt where
deserialize =
let
invalid s = pure $ ForeignError $ "Invalid bigint: " <> s
@ -165,33 +162,39 @@ instance Deserialize BigInt where
fromString <=< deserialize @String
-- | `bool`
instance Deserialize Boolean where
else instance Deserialize Boolean where
deserialize = F.readBoolean <<< Raw.asForeign
-- | `text`, `inet`, `tsquery`, `tsvector`, `uuid`, `xml`, `cidr`, `time`, `timetz`
instance Deserialize String where
else instance Deserialize String where
deserialize = F.readString <<< Raw.asForeign
-- | `float4`, `float8`
instance Deserialize Number where
else instance Deserialize Number where
deserialize = F.readNumber <<< Raw.asForeign
-- | `timestamp`, `timestamptz`
instance Deserialize DateTime where
else 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
-- | postgres `array`
instance Deserialize a => Deserialize (Array a) where
else instance Deserialize a => Deserialize (Array a) where
deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.asForeign
-- | non-NULL -> `Just`, NULL -> `Nothing`
instance Deserialize a => Deserialize (Maybe a) where
else instance Deserialize a => Deserialize (Maybe a) where
deserialize raw =
let
nothing = const Nothing <$> deserialize @Null raw
just = Just <$> deserialize raw
in
just <|> nothing
else 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

@ -4,12 +4,14 @@ import Prelude
import Control.Monad.Error.Class (throwError)
import Control.Monad.Fork.Class (class MonadBracket, bracket)
import Control.Monad.Postgres (PostgresT, exec_, query, runPostgres, session, transaction)
import Control.Monad.Postgres (PostgresT, exec_, query, runPostgres, session, transaction, cursor, fetch, fetchAll, fetchOne)
import Control.Parallel (parTraverse_)
import Data.Array as Array
import Data.Array.NonEmpty as Array.NonEmpty
import Data.Maybe (fromJust, maybe)
import Data.Maybe (Maybe(..), fromJust, maybe)
import Data.String.Regex as Regex
import Data.String.Regex.Flags as Regex.Flag
import Data.Traversable (for_)
import Effect.Aff (Fiber)
import Effect.Aff.Class (class MonadAff)
import Effect.Exception (Error, error)
@ -23,29 +25,35 @@ withTable s m =
let
tabname = unsafePartial fromJust $ join $ Array.index (maybe [] Array.NonEmpty.toArray $ Regex.match (re "create table (\\w+)" Regex.Flag.ignoreCase) s) 1
in
bracket (session $ exec_ s) (\_ _ -> session $ exec_ $ "drop table " <> tabname <> ";") (const m)
bracket (exec_ s) (\_ _ -> exec_ $ "drop table " <> tabname <> ";") (const m)
spec :: Spec Unit
spec =
around withConfig $ describe "Control.Monad.Postgres" do
it "empty works" \cfg -> runPostgres cfg $ pure unit
it "connects" \cfg -> runPostgres cfg do
act <- session $ query "select 1"
act `shouldEqual` 1
it "connects multiple" \cfg -> runPostgres cfg do
a <- session $ query "select 1"
b <- session $ query "select 2"
a `shouldEqual` 1
b `shouldEqual` 2
it "connects" \cfg -> runPostgres cfg $ shouldEqual 1 =<< query "select 1"
it "multiple sessions serially" \cfg -> runPostgres cfg do
shouldEqual 1 =<< query "select 1"
shouldEqual 2 =<< query "select 2"
it "multiple sessions concurrently" \cfg -> runPostgres cfg do
flip parTraverse_ [ 1, 2, 3 ] \_ -> shouldEqual 1 =<< query "select 1"
it "transaction commits" \cfg -> runPostgres cfg do
withTable "create table test_txn_commits (id int);" do
transaction $ exec_ "insert into test_txn_commits values (1);"
act <- session $ query "select * from test_txn_commits"
act `shouldEqual` [ 1 ]
exec_ "create temporary table test_txn_commits (id int);"
transaction $ exec_ "insert into test_txn_commits values (1);"
shouldEqual [ 1 ] =<< query "select * from test_txn_commits"
it "transaction rolls back" \cfg -> runPostgres cfg do
withTable "create table test_txn_rolls_back (id int);" do
expectError $ transaction do
exec_ "insert into test_txn_rolls_back values (1);"
throwError $ error "foo"
act :: Array Int <- session $ query "select * from test_txn_rolls_back"
act `shouldEqual` []
exec_ "create temporary table test_txn_rolls_back (id int);"
exec_ "insert into test_txn_rolls_back values (1);"
expectError $ transaction do
exec_ "insert into test_txn_rolls_back values (2);"
throwError $ error "foo"
shouldEqual [ 1 ] =<< query "select * from test_txn_rolls_back"
it "cursor" \cfg -> runPostgres cfg do
exec_ $ "create temporary table test_cursor_data (id int primary key generated always as identity)"
for_ (Array.range 1 50) $ const $ exec_ "insert into test_cursor_data (id) values (default);"
cursor @Int "test_cursor" "select id from test_cursor_data" do
shouldEqual (Just 1) =<< fetchOne
shouldEqual (Just 2) =<< fetchOne
shouldEqual (Just 3) =<< fetchOne
shouldEqual [ 4, 5, 6, 7, 8 ] =<< fetch 5
shouldEqual (Array.range 9 50) =<< fetchAll

View File

@ -0,0 +1,107 @@
module Test.Data.Postgres.Custom where
import Prelude
import Control.Monad.Error.Class (liftEither)
import Control.Monad.Except (runExceptT)
import Data.Bifunctor (lmap)
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.Show.Generic (genericShow)
import Effect.Class (liftEffect)
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (shouldEqual)
import Unsafe.Coerce (unsafeCoerce)
data Enum1 = E1A
derive instance Generic Enum1 _
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 CustomDeserialize Enum1 "enum_1" where
customDeserialize a = enumDeserialize a
instance CustomEnum Enum1 "enum_1" where
printEnum = genericPrintEnum
parseEnum = genericParseEnum
enumVariants = genericEnumVariants
data Enum2 = E2A | E2B
derive instance Generic Enum2 _
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 CustomDeserialize Enum2 "enum_2" where
customDeserialize a = enumDeserialize a
instance CustomEnum Enum2 "enum_2" where
printEnum a = genericPrintEnum a
parseEnum a = genericParseEnum a
enumVariants = genericEnumVariants
data Enum5 = E5A | E5B | E5C | E5D | E5E
derive instance Generic Enum5 _
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 CustomDeserialize Enum5 "enum_5" where
customDeserialize a = enumDeserialize a
instance CustomEnum Enum5 "enum_5" where
printEnum a = genericPrintEnum a
parseEnum a = genericParseEnum a
enumVariants = genericEnumVariants
spec :: Spec Unit
spec =
describe "Test.Data.Postgres.Custom" do
describe "Enum" do
it "serialize" do
act <- liftEffect $ smash $ serialize E5A
exp <- liftEffect $ smash $ serialize "E5A"
act `shouldEqual` exp
it "deserialize" do
act <- liftEffect $ smash $ deserialize $ unsafeCoerce "E5A"
act `shouldEqual` E5A
it "create" do
(_.text $ unwrap $ create @Enum1) `shouldEqual` "create type enum_1 as enum ('E1A');"
(_.text $ unwrap $ create @Enum2) `shouldEqual` "create type enum_2 as enum ('E2A', 'E2B');"
(_.text $ unwrap $ create @Enum5) `shouldEqual` "create type enum_5 as enum ('E5A', 'E5B', 'E5C', 'E5D', 'E5E');"
it "parseEnum" do
parseEnum "E1A" `shouldEqual` Just E1A
parseEnum "E2A" `shouldEqual` Just E2A
parseEnum "E2B" `shouldEqual` Just E2B
parseEnum "E5B" `shouldEqual` Just E5B
it "printEnum" do
printEnum E1A `shouldEqual` "E1A"
printEnum E2A `shouldEqual` "E2A"
printEnum E2B `shouldEqual` "E2B"
printEnum E5D `shouldEqual` "E5D"
it "enumPrintExpr" do
enumPrintExpr E1A `shouldEqual` Just "'E1A' :: enum_1"
enumPrintExpr E2A `shouldEqual` Just "'E2A' :: enum_2"
enumPrintExpr E2B `shouldEqual` Just "'E2B' :: enum_2"
enumPrintExpr E5D `shouldEqual` Just "'E5D' :: enum_5"

View File

@ -16,9 +16,9 @@ import Data.Int as Int
import Data.Maybe (Maybe(..), fromJust, maybe)
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Number (abs) as Number
import Data.Postgres (class Rep, jsNull)
import Data.Postgres (class Rep)
import Data.Postgres.Query.Builder as Q
import Data.Postgres.Raw (Raw)
import Data.Postgres.Raw (Raw, jsNull)
import Data.Postgres.Raw as Raw
import Data.Postgres.Result (class FromRow)
import Data.RFC3339String as DateTime.ISO

View File

@ -59,4 +59,4 @@ spec =
describe "json" do
it "unmarshals" \c -> shouldEqual (JSON { foo: "bar" }) =<< query "select '{\"foo\": \"bar\"}' :: json" c
it "is string" \c -> shouldEqual "{\"foo\": \"bar\"}" =<< query "select '{\"foo\": \"bar\"}' :: json" c
it "array is string" \c -> shouldEqual [["{\"foo\": \"bar\"}"]] =<< query "select array['{\"foo\": \"bar\"}' :: json]" c
it "array is string" \c -> shouldEqual [ [ "{\"foo\": \"bar\"}" ] ] =<< query "select array['{\"foo\": \"bar\"}' :: json]" c

View File

@ -22,6 +22,7 @@ import Node.Encoding (Encoding(..))
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.Effect.Postgres.Client as Test.Effect.Postgres.Client
import Test.Effect.Postgres.Pool as Test.Effect.Postgres.Pool
import Test.Spec.Reporter (specReporter)
@ -62,6 +63,7 @@ main = launchAff_ do
bracket spawnDb killDb
$ const
$ runSpec [ specReporter ] do
Test.Data.Postgres.Custom.spec
Test.Data.Postgres.spec
Test.Effect.Postgres.Client.spec
Test.Effect.Postgres.Pool.spec