git.iko.soy/rowtype-yoga/purescript-yoga-postgres-om
on main help sign in
7 files changed+627−100
since splitsnapshots
unifiedsplit
.psc-ide-port+1−1 spago.lock+31−23 spago.yaml+11−11 src/Yoga/Postgres/ClientOm.js+21−1 src/Yoga/Postgres/ClientOm.purs+36−18 test/Test/ClientOm.purs+2−2 test/Test/SchemaQ.purs+525−44
.psc-ide-port+1−1
@@ -1 +1 @@
1−15826
1+15453
spago.lock+31−23
@@ -6,7 +6,7 @@
66 "core": {
77 "dependencies": [
88 {
9− "aff": ">=7.1.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
1010 },
1111 {
1212 "arrays": ">=7.3.0 <8.0.0"
@@ -20,6 +20,9 @@
2020 {
2121 "either": ">=6.1.0 <7.0.0"
2222 },
23+ {
24+ "exceptions": ">=6.1.0 <7.0.0"
25+ },
2326 {
2427 "foldable-traversable": ">=6.0.0 <7.0.0"
2528 },
@@ -27,11 +30,14 @@
2730 "foreign": ">=7.0.0 <8.0.0"
2831 },
2932 {
30− "heterogeneous": ">=0.6.0 <0.7.0"
33+ "heterogeneous": ">=0.7.0 <0.8.0"
3134 },
3235 {
3336 "maybe": ">=6.0.0 <7.0.0"
3437 },
38+ {
39+ "nullable": ">=6.0.0 <7.0.0"
40+ },
3541 {
3642 "ordered-collections": ">=3.2.0 <4.0.0"
3743 },
@@ -57,21 +63,24 @@
5763 "yoga-json": ">=5.2.1 <6.0.0"
5864 },
5965 {
60− "yoga-om": ">=1.5.2 <2.0.0"
66+ "yoga-om": ">=2.2.1 <3.0.0"
6167 },
6268 {
63− "yoga-om-layer": ">=1.2.2 <2.0.0"
69+ "yoga-om-layer": ">=2.1.0 <3.0.0"
6470 },
6571 {
66− "yoga-postgres": "*"
72+ "yoga-postgres": ">=0.3.2 <0.4.0"
6773 },
6874 {
69− "yoga-sql-types": ">=0.1.0 <0.2.0"
75+ "yoga-sql-types": ">=0.2.0 <0.3.0"
7076 }
7177 ]
7278 },
7379 "test": {
7480 "dependencies": [
81+ {
82+ "avar": ">=5.0.1 <6.0.0"
83+ },
7584 {
7685 "console": ">=6.0.0 <7.0.0"
7786 },
@@ -85,17 +94,13 @@
8594 }
8695 }
8796 },
88− "extra_packages": {
89− "yoga-postgres": {
90− "path": "../purescript-yoga-postgres"
91− }
92− }
97+ "extra_packages": {}
9398 },
9499 "packages": {
95100 "aff": {
96101 "type": "registry",
97− "version": "7.1.0",
98− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
102+ "version": "8.0.0",
103+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
99104 "dependencies": [
100105 "control",
101106 "datetime",
@@ -466,8 +471,8 @@
466471 },
467472 "heterogeneous": {
468473 "type": "registry",
469− "version": "0.6.0",
470− "integrity": "sha256-nQgk2iHYe5LIi6ZzREgWaCU3y8oCwdhDSukT6xVMWdk=",
474+ "version": "0.7.0",
475+ "integrity": "sha256-pQwcFvhWL8noktxaoD7FlLs2F2lYf3QHoyO0VOWO3I0=",
471476 "dependencies": [
472477 "either",
473478 "foldable-traversable",
@@ -1053,8 +1058,8 @@
10531058 },
10541059 "yoga-om": {
10551060 "type": "registry",
1056− "version": "1.5.2",
1057− "integrity": "sha256-2ys14BF6NPgzFDFUIFk0EDxmqkA8+yghIA9TXHlOmE0=",
1061+ "version": "2.2.1",
1062+ "integrity": "sha256-nahYX6UD7/7chFM5hxGGVITSbiLXKb0gbP3PV8CvsXY=",
10581063 "dependencies": [
10591064 "aff",
10601065 "aff-retry",
@@ -1085,12 +1090,13 @@
10851090 },
10861091 "yoga-om-layer": {
10871092 "type": "registry",
1088− "version": "1.2.2",
1089− "integrity": "sha256-Usw2ZysteSYNkEG3CDmVVXUk7cgr1mntkmhbR33nF6M=",
1093+ "version": "2.1.0",
1094+ "integrity": "sha256-4XxwLT/1JJQovIkH/qog5YRa4WcP8SbfKFrWs214/sQ=",
10901095 "dependencies": [
10911096 "aff",
10921097 "aff-retry",
10931098 "arrays",
1099+ "control",
10941100 "effect",
10951101 "exceptions",
10961102 "foldable-traversable",
@@ -1102,6 +1108,7 @@
11021108 "record",
11031109 "record-studio",
11041110 "refs",
1111+ "tailrec",
11051112 "transformers",
11061113 "type-equality",
11071114 "typelevel-prelude",
@@ -1111,8 +1118,9 @@
11111118 ]
11121119 },
11131120 "yoga-postgres": {
1114− "type": "local",
1115− "path": "../purescript-yoga-postgres",
1121+ "type": "registry",
1122+ "version": "0.3.2",
1123+ "integrity": "sha256-3xDBcPLrIRYbrj2k7Y+V6WcxD+I7yls90AjBCQS1NCk=",
11161124 "dependencies": [
11171125 "aff",
11181126 "arrays",
@@ -1148,8 +1156,8 @@
11481156 },
11491157 "yoga-sql-types": {
11501158 "type": "registry",
1151− "version": "0.1.0",
1152− "integrity": "sha256-vxOgnj3HK8F5gRHCNkIPNirORQtCBEKozG+CnrHtB84=",
1159+ "version": "0.2.0",
1160+ "integrity": "sha256-/fiSiEmSQ8UH2N7vp/E02Eo+Krbs6iGcre9haDV34Q8=",
11531161 "dependencies": [
11541162 "arrays",
11551163 "bifunctors",
spago.yaml+11−11
@@ -1,26 +1,25 @@
1−workspace:
2− extraPackages:
3− yoga-postgres:
4− path: ../purescript-yoga-postgres
1+workspace: {}
52
63package:
74 name: yoga-postgres-om
85 publish:
9− version: 0.1.0
6+ version: 0.1.1
107 license: MIT
118 location:
129 githubOwner: rowtype-yoga
1310 githubRepo: purescript-yoga-postgres-om
1411 dependencies:
15− - aff: ">=7.1.0 <8.0.0"
12+ - aff: ">=8.0.0 <9.0.0"
1613 - arrays: ">=7.3.0 <8.0.0"
1714 - console: ">=6.1.0 <7.0.0"
1815 - effect: ">=4.0.0 <5.0.0"
1916 - either: ">=6.1.0 <7.0.0"
17+ - exceptions: ">=6.1.0 <7.0.0"
2018 - foldable-traversable: ">=6.0.0 <7.0.0"
2119 - foreign: ">=7.0.0 <8.0.0"
22− - heterogeneous: ">=0.6.0 <0.7.0"
20+ - heterogeneous: ">=0.7.0 <0.8.0"
2321 - maybe: ">=6.0.0 <7.0.0"
22+ - nullable: ">=6.0.0 <7.0.0"
2423 - ordered-collections: ">=3.2.0 <4.0.0"
2524 - prelude: ">=6.0.2 <7.0.0"
2625 - record: ">=4.0.0 <5.0.0"
@@ -29,13 +28,14 @@ package:
2928 - typelevel-prelude: ">=7.0.0 <8.0.0"
3029 - unsafe-coerce: ">=6.0.0 <7.0.0"
3130 - yoga-json: ">=5.2.1 <6.0.0"
32− - yoga-om: ">=1.5.2 <2.0.0"
33− - yoga-om-layer: ">=1.2.2 <2.0.0"
34− - yoga-postgres: "*"
35− - yoga-sql-types: ">=0.1.0 <0.2.0"
31+ - yoga-om: ">=2.2.1 <3.0.0"
32+ - yoga-om-layer: ">=2.1.0 <3.0.0"
33+ - yoga-postgres: ">=0.3.2 <0.4.0"
34+ - yoga-sql-types: ">=0.2.0 <0.3.0"
3635 test:
3736 main: Test.Main
3837 dependencies:
38+ - avar: ">=5.0.1 <6.0.0"
3939 - console: ">=6.0.0 <7.0.0"
4040 - exceptions: ">=6.0.0 <7.0.0"
4141 - spec: ">=7.0.0 <8.0.0"
src/Yoga/Postgres/ClientOm.js+21−1
@@ -1 +1,21 @@
1−export const txToConnection = (tx) => tx._sql || tx._reserved || tx;
1+const transactionConnections = new WeakSet();
2+
3+const transactionFailure = Symbol("Yoga.Postgres.transactionFailure");
4+
5+export const transactionErrorImpl = (failure) => {
6+ const error = new Error("Yoga.Postgres transaction aborted");
7+ error[transactionFailure] = failure;
8+ return error;
9+};
10+
11+export const transactionFailureImpl = (error) =>
12+ error?.[transactionFailure] ?? null;
13+
14+export const txToConnection = (transaction) => {
15+ const connection = transaction._sql ?? transaction;
16+ transactionConnections.add(connection);
17+ return connection;
18+};
19+
20+export const isTransactionConnection = (connection) =>
21+ transactionConnections.has(connection);
src/Yoga/Postgres/ClientOm.purs+36−18
@@ -5,13 +5,15 @@ import Prelude
55import Data.Array (filter, intercalate, length, mapWithIndex, take)
66import Data.Tuple.Nested (type (/\), (/\))
77import Data.Either (Either(..))
8−import Data.Maybe (Maybe)
8+import Data.Maybe (Maybe(..))
9+import Data.Nullable as Nullable
910import Record (get) as Record
1011import Data.Symbol (class IsSymbol, reflectSymbol)
1112import Data.Traversable (traverse)
12−import Control.Monad.Reader.Class (local)
13+import Control.Monad.Error.Class (throwError, try)
1314import Effect.Aff.Class (liftAff)
14−import Foreign (Foreign, MultipleErrors)
15+import Effect.Exception (Error, error)
16+import Foreign (Foreign, MultipleErrors, unsafeFromForeign, unsafeToForeign)
1517import Prim.Boolean (True, False)
1618import Prim.Row (class Cons, class Lacks, class Union) as Row
1719import Prim.RowList as RL
@@ -33,11 +35,39 @@ import Yoga.Postgres.Schema
3335 , Table, PrimaryKey, AutoIncrement, Unique, Default
3436 , ForeignKey, References, Nullable
3537 , columnCountRL, columnNamesRL, fieldToPGValue, recordValuesRL, setClauseRL
36− , class ExtractWord, class SkipSpaces, class SplitOnDot
38+ , class ExtractWord, class SkipSpaces
3739 )
3840import Yoga.Postgres.Schema as Schema
3941
4042
43+
44+foreign import transactionErrorImpl :: Foreign -> Error
45+foreign import transactionFailureImpl :: Error -> Nullable.Nullable Foreign
46+foreign import isTransactionConnection :: PG.Connection -> Boolean
47+
48+transactionally
49+ :: forall r err a
50+ . Om.Om { postgresConnection :: PG.Connection | r } err a
51+ -> Om.Om { postgresConnection :: PG.Connection | r } err a
52+transactionally action = do
53+ context@{ postgresConnection } <- Om.ask
54+ Om.throwWhen
55+ { exception: error "Yoga.Postgres does not support nested transactions" }
56+ (isTransactionConnection postgresConnection)
57+ outcome <- liftAff $ try $ PG.withTransaction
58+ (\transaction -> do
59+ let transactionContext = context { postgresConnection = txToConnection transaction }
60+ Om.runReader transactionContext action >>= case _ of
61+ Left failure -> throwError (transactionErrorImpl (unsafeToForeign failure))
62+ Right value -> pure value
63+ )
64+ postgresConnection
65+ case outcome of
66+ Right value -> pure value
67+ Left cause -> case Nullable.toMaybe (transactionFailureImpl cause) of
68+ Just failure -> throwError (unsafeFromForeign failure)
69+ Nothing -> Om.throw { exception: cause }
70+
4171-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4272-- Query builder bridge (Schema.Q -> Om)
4373-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -73,9 +103,7 @@ exec
73103 => { | params }
74104 -> Schema.Q tables () params stage
75105 -> Om.Om { postgresConnection :: PG.Connection | r } err Unit
76−exec params q = do
77− { postgresConnection } <- Om.ask
78− Schema.runExecute postgresConnection params q # liftAff # void
106+exec params q = void (execCount params q)
79107
80108execCount
81109 :: forall tables params paramsRL stage r err
@@ -88,6 +116,7 @@ execCount params q = do
88116 { postgresConnection } <- Om.ask
89117 Schema.runExecute postgresConnection params q # liftAff
90118
119+
91120-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
92121-- WHERE operators
93122-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1833,17 +1862,6 @@ deleteWhereReturning whereRec _ = do
18331862-- Transaction
18341863-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18351864
1836−withTransaction
1837− :: forall r err a
1838− . Om.Om { postgresConnection :: PG.Connection | r } err a
1839− -> Om.Om { postgresConnection :: PG.Connection | r } err a
1840−withTransaction action = do
1841− { postgresConnection } <- Om.ask
1842− tx <- PG.begin postgresConnection # liftAff
1843− let txConn = txToConnection tx
1844− result <- local (_ { postgresConnection = txConn }) action
1845− PG.commit tx # liftAff
1846− pure result
18471865
18481866foreign import txToConnection :: PG.Transaction -> PG.Connection
18491867
test/Test/ClientOm.purs+2−2
@@ -31,7 +31,7 @@ import Yoga.Postgres.ClientOm
3131 , gt, gte, ilike, inArray, join, like_, lt, lte
3232 , maxOf, minOf, minOfWhere, ne, not_, or_
3333 , selectFrom, selectFromWhere, selectOneFrom, selectOneFromWhere
34− , sumOf, sumOfWhere, updateWhere, upsert, upsertMany, withTransaction
34+ , sumOf, sumOfWhere, transactionally, updateWhere, upsert, upsertMany
3535 )
3636import Yoga.Postgres.Schema (Table, PrimaryKey, AutoIncrement, Unique, Default, ForeignKey, References, PGUUID)
3737
@@ -541,7 +541,7 @@ spec = describe "ClientOm" do
541541
542542 describe "Transactions" do
543543 it "commits multiple inserts atomically" $ withUsersTable do
544− withTransaction do
544+ transactionally do
545545 _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
546546 _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
547547 pure unit
test/Test/SchemaQ.purs+525−44
@@ -2,106 +2,534 @@ module Test.SchemaQ where
22
33import Prelude
44
5−import Control.Monad.Error.Class (throwError)
5+import Control.Monad.Error.Class (throwError, try)
6+import Control.Monad.Except (throwError) as Except
7+import Data.Either (Either(..), isLeft)
68import Data.Array as Array
79import Data.Maybe (Maybe(..))
8−import Effect.Aff (Aff)
10+import JS.BigInt as BigInt
11+import Data.Time.Duration (Milliseconds(..))
12+import Data.Newtype (class Newtype)
13+import Effect.Aff (Aff, delay, forkAff, joinFiber, killFiber)
14+import Effect.Aff.Class (liftAff)
15+import Effect.Aff.AVar as AVar
916import Effect.Class (class MonadEffect, liftEffect)
1017import Effect.Exception as Exception
11−import Foreign (MultipleErrors)
18+import Effect.Ref as Ref
19+import Foreign (ForeignError(..), MultipleErrors)
1220import Test.Spec (Spec, describe, it)
1321import Type.Proxy (Proxy(..))
22+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
1423import Yoga.Om as Om
1524import Yoga.Postgres as PG
16−import Yoga.Postgres.ClientOm (run, runOne, exec)
17−import Yoga.Postgres.Schema (Table, PrimaryKey, AutoIncrement, Unique, Default, TimestampTZ, class CreateTableDDL, createTableDDL, from, into, update, select, where_, orderBy, limit, groupBy, insert, set, delete, returning, union)
25+import Yoga.Postgres.ClientOm (create, exec, run, runOne, transactionally)
26+import Yoga.Postgres.Schema (class PGTypeName, AutoIncrement, Default, ForeignKey, PrimaryKey, References, Table, TimestampTZ, Unique, andCTE, cte, createTableDDL, delete, except_, from, fromCTE, groupBy, having, innerJoin, innerJoinAs, insert, intersect, into, leftJoin, leftJoinAs, limit, offset, onConflict, onConflictDoNothing, orderBy, returning, returningAll, select, set, union, unionAll, update, where_, with_)
1827
1928spec :: Spec Unit
2029spec = describe "Query builder" do
2130
31+ -- ── SELECT ──────────────────────────────────────────────────
32+
2233 it "selects columns with a WHERE clause" $ withUsersTable do
23− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
24− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
34+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
35+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
2536 rows <- from usersTable
2637 # select @"name, email"
2738 # where_ @"role = $role"
28− # run { role: "admin" }
29− rows `shouldEqual` [ { name: "Alice", email: "a@t.com" } ]
39+ # run { role: Admin }
40+ rows `shouldEqual` [ { name: "Alice", email: Email "a@t.com" } ]
3041
3142 it "orders and limits results" $ withUsersTable do
32− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
33− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
34− _ <- into usersTable # insert { name: "Carol", email: "c@t.com", role: "user" } # exec {}
43+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
44+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
45+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: User } # exec {}
3546 rows <- from usersTable # select @"name" # orderBy @"name" # limit @"2" # run {}
3647 rows `shouldEqual` [ { name: "Alice" }, { name: "Bob" } ]
3748
49+ it "skips rows with offset" $ withUsersTable do
50+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
51+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
52+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: Mod } # exec {}
53+ rows <- from usersTable # select @"name" # orderBy @"name" # limit @"10" # offset @"1" # run {}
54+ rows `shouldEqual` [ { name: "Bob" }, { name: "Carol" } ]
55+
3856 it "groups results" $ withUsersTable do
39− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
40− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
41− _ <- into usersTable # insert { name: "Carol", email: "c@t.com", role: "user" } # exec {}
57+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
58+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
59+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: User } # exec {}
4260 rows <- from usersTable # select @"role" # groupBy @"role" # orderBy @"role" # run {}
43− rows `shouldEqual` [ { role: "admin" }, { role: "user" } ]
61+ rows `shouldEqual` [ { role: Admin }, { role: User } ]
62+
63+ it "filters with HAVING after GROUP BY" $ withUsersTable do
64+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
65+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
66+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: User } # exec {}
67+ rows <- from usersTable
68+ # select @"role, COUNT(*) AS count"
69+ # groupBy @"role"
70+ # having @"COUNT(*) > $min"
71+ # orderBy @"role"
72+ # run { min: 1 }
73+ rows `shouldEqual` [ { role: User, count: BigInt.fromInt 2 } ]
4474
4575 it "filters with a BETWEEN range" $ withUsersTable do
46− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
47− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
48− _ <- into usersTable # insert { name: "Carol", email: "c@t.com", role: "mod" } # exec {}
76+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
77+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
78+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: Mod } # exec {}
4979 rows <- from usersTable # select @"name" # where_ @"id BETWEEN $lo AND $hi" # orderBy @"id" # run { lo: 2, hi: 3 }
5080 rows `shouldEqual` [ { name: "Bob" }, { name: "Carol" } ]
5181
82+ -- ── INSERT ──────────────────────────────────────────────────
83+
5284 it "inserts a row and returns specific columns" $ withUsersTable do
53− rows <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # returning @"name" # run {}
85+ rows <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"name" # run {}
5486 rows `shouldEqual` [ { name: "Alice" } ]
5587
5688 it "inserts a row with default created_at and updated_at" $ withUsersTable do
57− row <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # returning @"created_at, updated_at" # runOne {}
89+ row <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"created_at, updated_at" # runOne {}
5890 r <- shouldBeJust row
5991 r.created_at `shouldEqual` r.updated_at
6092
93+ it "returns all columns with returningAll" $ withUsersTable do
94+ rows <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returningAll # run {}
95+ let row = Array.head rows
96+ r <- shouldBeJust row
97+ r.name `shouldEqual` "Alice"
98+ r.email `shouldEqual` Email "a@t.com"
99+ r.role `shouldEqual` Admin
100+
101+ it "skips duplicate on ON CONFLICT DO NOTHING" $ withUsersTable do
102+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
103+ _ <- into usersTable # insert { name: "Alice2", email: Email "a@t.com", role: User } # onConflictDoNothing @"email" # exec {}
104+ rows <- from usersTable # select @"name" # run {}
105+ rows `shouldEqual` [ { name: "Alice" } ]
106+
107+ it "upserts with ON CONFLICT DO UPDATE" $ withUsersTable do
108+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
109+ _ <- into usersTable # insert { name: "Alice2", email: Email "a@t.com", role: User }
110+ # onConflict @"email" @"DO UPDATE SET name = EXCLUDED.name"
111+ # exec {}
112+ rows <- from usersTable # select @"name" # run {}
113+ rows `shouldEqual` [ { name: "Alice2" } ]
114+
115+ describe "transactionally" do
116+ it "runs ordinary schema operations against the transaction and commits" $ withUsersTable do
117+ transactionally do
118+ into usersTable
119+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
120+ # exec {}
121+
122+ rows <- from usersTable # select @"name" # run {}
123+ rows `shouldEqual` [ { name: "Alice" } ]
124+
125+ it "reads its own writes before commit" $ withUsersTable do
126+ rows <- transactionally do
127+ _ <- into usersTable
128+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
129+ # exec {}
130+ from usersTable
131+ # select @"name, role"
132+ # where_ @"email = $email"
133+ # run { email: Email "a@t.com" }
134+
135+ rows `shouldEqual` [ { name: "Alice", role: Admin } ]
136+
137+ it "shares one transaction across ClientOm and schema operations" $ withUsersTable do
138+ rows <- transactionally do
139+ _ <- usersTable
140+ # create { name: "Alice", email: Email "a@t.com", role: Admin }
141+ from usersTable
142+ # select @"name, role"
143+ # where_ @"email = $email"
144+ # run { email: Email "a@t.com" }
145+
146+ rows `shouldEqual` [ { name: "Alice", role: Admin } ]
147+
148+ it "returns the application value after commit" $ withUsersTable do
149+ result <- transactionally do
150+ _ <- into usersTable
151+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
152+ # exec {}
153+ pure { status: "created", count: 1 }
154+
155+ result `shouldEqual` { status: "created", count: 1 }
156+ rows <- from usersTable # select @"name" # run {}
157+ rows `shouldEqual` [ { name: "Alice" } ]
158+
159+ it "rolls back typed Om failures" $ withUsersTable do
160+ result <- try $ transactionally do
161+ _ <- into usersTable
162+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
163+ # exec {}
164+ Om.fatal "abort transaction"
165+
166+ isLeft result `shouldEqual` true
167+ rows <- from usersTable # select @"name" # run {}
168+ rows `shouldEqual` []
169+
170+ it "rolls back writes made by composed helpers" $ withUsersTable do
171+ let
172+ createUser name email role =
173+ into usersTable
174+ # insert { name, email, role }
175+ # exec {}
176+ rejectBatch = Om.fatal "inventory unavailable"
177+
178+ result <- try $ transactionally do
179+ createUser "Alice" (Email "a@t.com") Admin
180+ createUser "Bob" (Email "b@t.com") User
181+ rejectBatch
182+
183+ isLeft result `shouldEqual` true
184+ rows <- from usersTable # select @"name" # run {}
185+ rows `shouldEqual` []
186+
187+ it "rolls back when an Om transaction fiber is cancelled" $ withUsersTable do
188+ wrote <- liftEffect $ Ref.new false
189+ transaction <- Om.unliftAff $ transactionally do
190+ _ <- into usersTable
191+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
192+ # exec {}
193+ liftEffect $ Ref.write true wrote
194+ liftAff $ delay (Milliseconds 10000.0)
195+
196+ fiber <- liftAff $ forkAff transaction
197+ liftAff $ delay (Milliseconds 50.0)
198+ didWrite <- liftEffect $ Ref.read wrote
199+ didWrite `shouldEqual` true
200+ liftAff $ killFiber (Exception.error "cancel transaction") fiber
201+
202+ rows <- from usersTable # select @"name" # run {}
203+ rows `shouldEqual` []
204+
205+ it "reuses a one-connection pool after commit and rollback" $ withSingleConnectionUsersTable do
206+ transactionally do
207+ _ <- into usersTable
208+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
209+ # exec {}
210+ pure unit
211+
212+ failed <- try $ transactionally do
213+ _ <- into usersTable
214+ # insert { name: "Bob", email: Email "b@t.com", role: User }
215+ # exec {}
216+ Om.fatal "roll back Bob"
217+ isLeft failed `shouldEqual` true
218+
219+ transactionally do
220+ _ <- into usersTable
221+ # insert { name: "Carol", email: Email "c@t.com", role: User }
222+ # exec {}
223+ pure unit
224+
225+ rows <- from usersTable # select @"name" # orderBy @"name" # run {}
226+ rows `shouldEqual` [ { name: "Alice" }, { name: "Carol" } ]
227+
228+ it "isolates concurrent transactions until each commits" $ withUsersTable do
229+ inserted <- liftAff AVar.empty
230+ readRows <- liftAff AVar.empty
231+ allowRead <- liftAff AVar.empty
232+ allowCommit <- liftAff AVar.empty
233+ let
234+ isolatedInsert name email role = transactionally do
235+ _ <- into usersTable
236+ # insert { name, email, role }
237+ # exec {}
238+ liftAff $ AVar.put unit inserted
239+ liftAff $ AVar.read allowRead
240+ rows <- from usersTable # select @"name" # run {}
241+ liftAff $ AVar.put unit readRows
242+ liftAff $ AVar.read allowCommit
243+ pure (Array.length rows)
244+
245+ alice <- Om.unliftAff $ isolatedInsert "Alice" (Email "a@t.com") Admin
246+ bob <- Om.unliftAff $ isolatedInsert "Bob" (Email "b@t.com") User
247+ aliceFiber <- liftAff $ forkAff alice
248+ bobFiber <- liftAff $ forkAff bob
249+
250+ liftAff $ AVar.take inserted
251+ liftAff $ AVar.take inserted
252+ liftAff $ AVar.put unit allowRead
253+ liftAff $ AVar.take readRows
254+ liftAff $ AVar.take readRows
255+ liftAff $ AVar.put unit allowCommit
256+
257+ aliceResult <- liftAff $ joinFiber aliceFiber
258+ bobResult <- liftAff $ joinFiber bobFiber
259+ case aliceResult, bobResult of
260+ Right aliceCount, Right bobCount ->
261+ [ aliceCount, bobCount ] `shouldEqual` [ 1, 1 ]
262+ _, _ ->
263+ liftEffect $ Exception.throwException $ Exception.error "concurrent transaction failed"
264+
265+ rows <- from usersTable # select @"name" # orderBy @"name" # run {}
266+ rows `shouldEqual` [ { name: "Alice" }, { name: "Bob" } ]
267+
268+ it "rejects nested transactions and rolls back the outer transaction" $ withUsersTable do
269+ failure <- transactionally
270+ (do
271+ _ <- into usersTable
272+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
273+ # exec {}
274+ transactionally do
275+ _ <- into usersTable
276+ # insert { name: "Bob", email: Email "b@t.com", role: User }
277+ # exec {}
278+ pure "nested transaction unexpectedly succeeded"
279+ )
280+ # Om.handleErrors
281+ { exception: \cause -> pure (Exception.message cause)
282+ , parseError: \_ -> pure "unexpected parse error"
283+ }
284+
285+ failure `shouldEqual` "Yoga.Postgres does not support nested transactions"
286+ rows <- from usersTable # select @"name" # run {}
287+ rows `shouldEqual` []
288+
289+ it "routes begin failures through the exception channel without running the action" do
290+ conn <- testConnection
291+ PG.end conn
292+ ran <- liftEffect $ Ref.new false
293+ result <- try $ Om.runOm { postgresConnection: conn }
294+ { exception: throwError }
295+ (transactionally do
296+ liftEffect $ Ref.write true ran
297+ )
298+
299+ isLeft result `shouldEqual` true
300+ liftEffect (Ref.read ran) >>= (_ `shouldEqual` false)
301+
302+ it "preserves the exact typed Om error payload" $ withUsersTable do
303+ let expected = { available: 10, requested: 20 }
304+ actual <- transactionally
305+ (Om.throw { insufficientFunds: expected })
306+ # Om.handleErrors
307+ { insufficientFunds: pure
308+ }
309+ actual `shouldEqual` expected
310+
311+ it "rolls back SQL exceptions and keeps the connection usable" $ withUsersTable do
312+ result <- try $ transactionally do
313+ _ <- into usersTable
314+ # insert { name: "Alice", email: Email "a@t.com", role: Admin }
315+ # exec {}
316+ { postgresConnection } <- Om.ask
317+ _ <- PG.executeSimple
318+ (PG.SQL "INSERT INTO missing_transaction_table VALUES (1)")
319+ postgresConnection
320+ # liftAff
321+ pure unit
322+
323+ isLeft result `shouldEqual` true
324+ rows <- from usersTable # select @"name" # run {}
325+ rows `shouldEqual` []
326+
327+ -- ── DELETE ──────────────────────────────────────────────────
328+
61329 it "deletes with a condition and returns the deleted rows" $ withUsersTable do
62− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
63− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
64− rows <- from usersTable # delete # where_ @"role = $role" # returning @"name" # run { role: "admin" }
330+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
331+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
332+ rows <- from usersTable # delete # where_ @"role = $role" # returning @"name" # run { role: Admin }
65333 rows `shouldEqual` [ { name: "Alice" } ]
66334
335+ -- ── UPDATE ──────────────────────────────────────────────────
336+
67337 it "updates a row with SET and WHERE" $ withUsersTable do
68− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
69− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
70− _ <- update usersTable # set { role: "mod" } # where_ @"name = $name" # exec { name: "Bob" }
338+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
339+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
340+ _ <- update usersTable # set { role: Mod } # where_ @"name = $name" # exec { name: "Bob" }
71341 rows <- from usersTable # select @"name, role" # orderBy @"name" # run {}
72− rows `shouldEqual` [ { name: "Alice", role: "admin" }, { name: "Bob", role: "mod" } ]
342+ rows `shouldEqual` [ { name: "Alice", role: Admin }, { name: "Bob", role: Mod } ]
73343
74344 it "updates multiple columns" $ withUsersTable do
75− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
76− _ <- update usersTable # set { name: "Alicia", role: "mod" } # where_ @"email = $email" # exec { email: "a@t.com" }
345+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
346+ _ <- update usersTable # set { name: "Alicia", role: Mod } # where_ @"email = $email" # exec { email: Email "a@t.com" }
77347 rows <- from usersTable # select @"name, role" # run {}
78− rows `shouldEqual` [ { name: "Alicia", role: "mod" } ]
348+ rows `shouldEqual` [ { name: "Alicia", role: Mod } ]
79349
80350 it "updates and returns the modified row" $ withUsersTable do
81− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
82− rows <- update usersTable # set { role: "mod" } # where_ @"email = $email" # returning @"name, role" # run { email: "a@t.com" }
83− rows `shouldEqual` [ { name: "Alice", role: "mod" } ]
351+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
352+ rows <- update usersTable
353+ # set { role: Mod }
354+ # where_ @"email = $email"
355+ # returning @"name, role"
356+ # run { email: Email "a@t.com" }
357+ rows `shouldEqual` [ { name: "Alice", role: Mod } ]
358+
359+ -- ── SET OPERATIONS ──────────────────────────────────────────
84360
85361 it "combines two queries with UNION" $ withUsersTable do
86− _ <- into usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # exec {}
87− _ <- into usersTable # insert { name: "Bob", email: "b@t.com", role: "user" } # exec {}
88− _ <- into usersTable # insert { name: "Carol", email: "c@t.com", role: "mod" } # exec {}
89− rows <-
90− from usersTable # select @"name" # where_ @"role = $a"
91− # union (from usersTable # select @"name" # where_ @"role = $b")
92− # run { a: "admin", b: "mod" }
93− Array.sort (rows <#> _.name) `shouldEqual` [ "Alice", "Carol" ]
362+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
363+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
364+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: Mod } # exec {}
365+ rows <- from usersTable # select @"name" # where_ @"role = $a"
366+ # union (from usersTable # select @"name" # where_ @"role = $b")
367+ # run { a: Admin, b: Mod }
368+ Array.sort rows `shouldEqual` [ { name: "Alice" }, { name: "Carol" } ]
369+
370+ it "combines with UNION ALL including duplicates" $ withUsersTable do
371+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
372+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
373+ rows <- from usersTable # select @"name" # where_ @"role = $r"
374+ # unionAll (from usersTable # select @"name" # where_ @"role = $r2")
375+ # run { r: Admin, r2: Admin }
376+ Array.sort rows `shouldEqual` [ { name: "Alice" }, { name: "Alice" } ]
377+
378+ it "returns common rows with INTERSECT" $ withUsersTable do
379+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
380+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
381+ rows <- from usersTable # select @"name" # where_ @"role = $a"
382+ # intersect (from usersTable # select @"name" # where_ @"name = $n")
383+ # run { a: Admin, n: "Alice" }
384+ rows `shouldEqual` [ { name: "Alice" } ]
385+
386+ it "excludes rows with EXCEPT" $ withUsersTable do
387+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
388+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
389+ _ <- into usersTable # insert { name: "Carol", email: Email "c@t.com", role: Mod } # exec {}
390+ rows <- (from usersTable # select @"name")
391+ `except_` (from usersTable # select @"name" # where_ @"role = $r")
392+ # run { r: User }
393+ Array.sort rows `shouldEqual` [ { name: "Alice" }, { name: "Carol" } ]
394+
395+ -- ── JOINs ───────────────────────────────────────────────────
396+
397+ it "inner joins posts to users" $ withUsersAndPostsTable do
398+ alice <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"id" # runOne {}
399+ { id: aliceId } <- shouldBeJust alice
400+ _ <- into postsTable # insert { user_id: aliceId, title: "Hello" } # exec {}
401+ rows <- from usersTable
402+ # innerJoin @"test_om_users.id = test_om_posts.user_id" postsTable
403+ # select @"test_om_users.name, test_om_posts.title"
404+ # run {}
405+ rows `shouldEqual` [ { name: "Alice", title: "Hello" } ]
406+
407+ it "inner join excludes users without posts" $ withUsersAndPostsTable do
408+ alice <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"id" # runOne {}
409+ { id: aliceId } <- shouldBeJust alice
410+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
411+ _ <- into postsTable # insert { user_id: aliceId, title: "Hello" } # exec {}
412+ rows <- from usersTable
413+ # innerJoin @"test_om_users.id = test_om_posts.user_id" postsTable
414+ # select @"test_om_users.name"
415+ # run {}
416+ rows `shouldEqual` [ { name: "Alice" } ]
417+
418+ it "left join includes users without posts as Nothing" $ withUsersAndPostsTable do
419+ alice <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"id" # runOne {}
420+ { id: aliceId } <- shouldBeJust alice
421+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
422+ _ <- into postsTable # insert { user_id: aliceId, title: "Hello" } # exec {}
423+ rows <- from usersTable
424+ # leftJoin @"test_om_users.id = test_om_posts.user_id" postsTable
425+ # select @"test_om_users.name, test_om_posts.title"
426+ # orderBy @"test_om_users.name"
427+ # run {}
428+ rows `shouldEqual`
429+ [ { name: "Alice", title: Just "Hello" }
430+ , { name: "Bob", title: Nothing }
431+ ]
432+
433+ it "inner join with alias" $ withUsersAndPostsTable do
434+ alice <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"id" # runOne {}
435+ { id: aliceId } <- shouldBeJust alice
436+ _ <- into postsTable # insert { user_id: aliceId, title: "Hello" } # exec {}
437+ rows <- from usersTable
438+ # innerJoinAs @"p" @"test_om_users.id = p.user_id" postsTable
439+ # select @"test_om_users.name, p.title"
440+ # run {}
441+ rows `shouldEqual` [ { name: "Alice", title: "Hello" } ]
442+
443+ it "left join with alias yields Nothing for unmatched" $ withUsersAndPostsTable do
444+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
445+ rows <- from usersTable
446+ # leftJoinAs @"p" @"test_om_users.id = p.user_id" postsTable
447+ # select @"test_om_users.name, p.title"
448+ # run {}
449+ rows `shouldEqual` [ { name: "Alice", title: Nothing } ]
450+
451+ -- ── CTEs ────────────────────────────────────────────────────
452+
453+ it "selects from a CTE" $ withUsersTable do
454+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
455+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: User } # exec {}
456+ let ctes = from usersTable # select @"name" # where_ @"role = $role" # cte @"active_users"
457+ rows <- with_ ctes
458+ (fromCTE @"active_users" ctes # select @"active_users.name")
459+ # run { role: Admin }
460+ rows `shouldEqual` [ { name: "Alice" } ]
461+
462+ it "CTE can be further filtered in the main query" $ withUsersTable do
463+ _ <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # exec {}
464+ _ <- into usersTable # insert { name: "Bob", email: Email "b@t.com", role: Admin } # exec {}
465+ let ctes = from usersTable # select @"name" # where_ @"role = $role" # cte @"admins"
466+ rows <- with_ ctes
467+ (fromCTE @"admins" ctes # select @"admins.name" # where_ @"admins.name = $name")
468+ # run { role: Admin, name: "Alice" }
469+ rows `shouldEqual` [ { name: "Alice" } ]
470+
471+ it "two CTEs with andCTE" $ withUsersAndPostsTable do
472+ alice <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"id" # runOne {}
473+ { id: aliceId } <- shouldBeJust alice
474+ _ <- into postsTable # insert { user_id: aliceId, title: "Hello" } # exec {}
475+ let adminsCTE = from usersTable # select @"name" # where_ @"role = $role" # cte @"admins"
476+ let ctes = andCTE @"posts" (from postsTable # select @"title") adminsCTE
477+ rows <- with_ ctes
478+ (fromCTE @"admins" ctes # select @"admins.name")
479+ # run { role: Admin }
480+ rows `shouldEqual` [ { name: "Alice" } ]
94481
95482-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96483-- Test schema
97484-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98485
486+newtype Email = Email String
487+
488+derive instance Newtype Email _
489+derive instance Eq Email
490+derive instance Ord Email
491+derive newtype instance Show Email
492+derive newtype instance ReadForeign Email
493+derive newtype instance WriteForeign Email
494+
495+data Role = Admin | User | Mod
496+
497+derive instance Eq Role
498+derive instance Ord Role
499+
500+instance Show Role where
501+ show = roleToString
502+
503+instance WriteForeign Role where
504+ writeImpl = roleToString >>> writeImpl
505+
506+instance ReadForeign Role where
507+ readImpl f = do
508+ s :: String <- readImpl f
509+ case s of
510+ "admin" -> pure Admin
511+ "user" -> pure User
512+ "mod" -> pure Mod
513+ _ -> Except.throwError $ pure $ ForeignError $ "Unknown role: " <> s
514+
515+roleToString :: Role -> String
516+roleToString = case _ of
517+ Admin -> "admin"
518+ User -> "user"
519+ Mod -> "mod"
520+
521+instance PGTypeName Email where
522+ pgTypeName _ = "TEXT"
523+
524+instance PGTypeName Role where
525+ pgTypeName _ = "TEXT"
526+
99527type UsersTable = Table "test_om_users"
100528 ( id :: PrimaryKey (AutoIncrement Int)
101529 , name :: String
102− , email :: Unique String
530+ , email :: Unique Email
103531 , active :: Default "true" Boolean
104− , role :: String
532+ , role :: Role
105533 , created_at :: Default "now()" TimestampTZ
106534 , updated_at :: Default "now()" TimestampTZ
107535 )
@@ -109,6 +537,15 @@ type UsersTable = Table "test_om_users"
109537usersTable :: Proxy UsersTable
110538usersTable = Proxy
111539
540+type PostsTable = Table "test_om_posts"
541+ ( id :: PrimaryKey (AutoIncrement Int)
542+ , user_id :: ForeignKey "test_om_users" References "id" Int
543+ , title :: String
544+ )
545+
546+postsTable :: Proxy PostsTable
547+postsTable = Proxy
548+
112549-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113550-- Helpers
114551-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -148,3 +585,47 @@ withUsersTable test = do
148585 dropStr = "DROP TABLE IF EXISTS test_om_users CASCADE"
149586 dropDDL = PG.SQL dropStr
150587 createDDL = PG.SQL (dropStr <> "; " <> createTableDDL @UsersTable)
588+
589+withSingleConnectionUsersTable :: TestOm -> Aff Unit
590+withSingleConnectionUsersTable test = do
591+ conn <- PG.postgres
592+ { host: PG.PostgresHost "localhost"
593+ , port: PG.PostgresPort 5433
594+ , database: PG.PostgresDatabase "test_playground"
595+ , username: PG.PostgresUsername "postgres"
596+ , password: PG.PostgresPassword "postgres"
597+ , max: PG.MaxConnections 1
598+ } # liftEffect
599+ _ <- PG.executeSimple createDDL conn
600+ Om.runOm { postgresConnection: conn }
601+ { exception: throwError
602+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
603+ }
604+ test
605+ _ <- PG.executeSimple dropDDL conn
606+ PG.end conn
607+ where
608+ dropStr = "DROP TABLE IF EXISTS test_om_users CASCADE"
609+ dropDDL = PG.SQL dropStr
610+ createDDL = PG.SQL (dropStr <> "; " <> createTableDDL @UsersTable)
611+
612+withUsersAndPostsTable :: TestOm -> Aff Unit
613+withUsersAndPostsTable test = do
614+ conn <- testConnection
615+ _ <- PG.executeSimple setupDDL conn
616+ Om.runOm { postgresConnection: conn }
617+ { exception: throwError
618+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
619+ }
620+ test
621+ _ <- PG.executeSimple teardownDDL conn
622+ PG.end conn
623+ where
624+ teardownDDL = PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE; DROP TABLE IF EXISTS test_om_users CASCADE"
625+ setupDDL = PG.SQL
626+ ( "DROP TABLE IF EXISTS test_om_posts CASCADE; "
627+ <> "DROP TABLE IF EXISTS test_om_users CASCADE; "
628+ <> createTableDDL @UsersTable
629+ <> "; "
630+ <> createTableDDL @PostsTable
631+ )