generated from tpl/purs
feat: cursor monad
This commit is contained in:
parent
c0ec6dec53
commit
e8d8fd24f3
@ -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
|
||||
|
@ -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
|
||||
|
121
src/Control.Monad.Postgres.Base.purs
Normal file
121
src/Control.Monad.Postgres.Base.purs
Normal 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
|
111
src/Control.Monad.Postgres.Cursor.purs
Normal file
111
src/Control.Monad.Postgres.Cursor.purs
Normal 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
|
34
src/Control.Monad.Postgres.Session.purs
Normal file
34
src/Control.Monad.Postgres.Session.purs
Normal 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
|
@ -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
|
||||
|
88
src/Data.Postgres.Custom.Enum.purs
Normal file
88
src/Data.Postgres.Custom.Enum.purs
Normal 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
|
29
src/Data.Postgres.Custom.purs
Normal file
29
src/Data.Postgres.Custom.purs
Normal 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)
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
107
test/Test.Data.Postgres.Generic.purs
Normal file
107
test/Test.Data.Postgres.Generic.purs
Normal 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"
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user