git.iko.soy/rowtype-yoga/purescript-yoga-postgres-om
on main help sign in
.psc-ide-port+1−1
since splitsnapshots
unifiedsplit
@@ -1 +1 @@
1−15396
1+15453
README.md+1
@@ -25,3 +25,4 @@ See [yoga-postgres](../yoga-postgres) for raw bindings.
25## License
25## License
26
26
27MIT
27MIT
28+
spago.lock+34−21
@@ -6,7 +6,7 @@
6 "core": {
6 "core": {
7 "dependencies": [
7 "dependencies": [
8 {
8 {
9− "aff": ">=7.1.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
10 },
10 },
11 {
11 {
12 "arrays": ">=7.3.0 <8.0.0"
12 "arrays": ">=7.3.0 <8.0.0"
@@ -20,6 +20,9 @@
20 {
20 {
21 "either": ">=6.1.0 <7.0.0"
21 "either": ">=6.1.0 <7.0.0"
22 },
22 },
23+ {
24+ "exceptions": ">=6.1.0 <7.0.0"
25+ },
23 {
26 {
24 "foldable-traversable": ">=6.0.0 <7.0.0"
27 "foldable-traversable": ">=6.0.0 <7.0.0"
25 },
28 },
@@ -27,11 +30,14 @@
27 "foreign": ">=7.0.0 <8.0.0"
30 "foreign": ">=7.0.0 <8.0.0"
28 },
31 },
29 {
32 {
30− "heterogeneous": ">=0.6.0 <0.7.0"
33+ "heterogeneous": ">=0.7.0 <0.8.0"
31 },
34 },
32 {
35 {
33 "maybe": ">=6.0.0 <7.0.0"
36 "maybe": ">=6.0.0 <7.0.0"
34 },
37 },
38+ {
39+ "nullable": ">=6.0.0 <7.0.0"
40+ },
35 {
41 {
36 "ordered-collections": ">=3.2.0 <4.0.0"
42 "ordered-collections": ">=3.2.0 <4.0.0"
37 },
43 },
@@ -54,24 +60,27 @@
54 "unsafe-coerce": ">=6.0.0 <7.0.0"
60 "unsafe-coerce": ">=6.0.0 <7.0.0"
55 },
61 },
56 {
62 {
57− "yoga-json": ">=5.2.0 <6.0.0"
63+ "yoga-json": ">=5.2.1 <6.0.0"
58 },
64 },
59 {
65 {
60− "yoga-om": ">=1.4.0 <2.0.0"
66+ "yoga-om": ">=2.2.1 <3.0.0"
61 },
67 },
62 {
68 {
63− "yoga-om-layer": ">=1.1.1 <2.0.0"
69+ "yoga-om-layer": ">=2.1.0 <3.0.0"
64 },
70 },
65 {
71 {
66− "yoga-postgres": ">=0.2.0 <0.3.0"
72+ "yoga-postgres": ">=0.3.2 <0.4.0"
67 },
73 },
68 {
74 {
69− "yoga-sql-types": ">=0.1.0 <0.2.0"
75+ "yoga-sql-types": ">=0.2.0 <0.3.0"
70 }
76 }
71 ]
77 ]
72 },
78 },
73 "test": {
79 "test": {
74 "dependencies": [
80 "dependencies": [
81+ {
82+ "avar": ">=5.0.1 <6.0.0"
83+ },
75 {
84 {
76 "console": ">=6.0.0 <7.0.0"
85 "console": ">=6.0.0 <7.0.0"
77 },
86 },
@@ -90,8 +99,8 @@
90 "packages": {
99 "packages": {
91 "aff": {
100 "aff": {
92 "type": "registry",
101 "type": "registry",
93− "version": "7.1.0",
102+ "version": "8.0.0",
94− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
103+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
95 "dependencies": [
104 "dependencies": [
96 "control",
105 "control",
97 "datetime",
106 "datetime",
@@ -462,8 +471,8 @@
462 },
471 },
463 "heterogeneous": {
472 "heterogeneous": {
464 "type": "registry",
473 "type": "registry",
465− "version": "0.6.0",
474+ "version": "0.7.0",
466− "integrity": "sha256-nQgk2iHYe5LIi6ZzREgWaCU3y8oCwdhDSukT6xVMWdk=",
475+ "integrity": "sha256-pQwcFvhWL8noktxaoD7FlLs2F2lYf3QHoyO0VOWO3I0=",
467 "dependencies": [
476 "dependencies": [
468 "either",
477 "either",
469 "foldable-traversable",
478 "foldable-traversable",
@@ -1010,8 +1019,8 @@
1010 },
1019 },
1011 "yoga-json": {
1020 "yoga-json": {
1012 "type": "registry",
1021 "type": "registry",
1013− "version": "5.2.0",
1022+ "version": "5.2.1",
1014− "integrity": "sha256-M+kkz6E1Gf+C4bKnhqoBFIoVcc74loi3rLULePbB204=",
1023+ "integrity": "sha256-12I7qIf27oKV/D/YRcHtGpN8n7GQ0Bm3uzvI4ZjOQrs=",
1015 "dependencies": [
1024 "dependencies": [
1016 "arrays",
1025 "arrays",
1017 "bifunctors",
1026 "bifunctors",
@@ -1049,8 +1058,8 @@
1049 },
1058 },
1050 "yoga-om": {
1059 "yoga-om": {
1051 "type": "registry",
1060 "type": "registry",
1052− "version": "1.4.0",
1061+ "version": "2.2.1",
1053− "integrity": "sha256-gTpW2XABBZbD3FtDR4gX1ktB1EtQUibIvGUuOhUuXRU=",
1062+ "integrity": "sha256-nahYX6UD7/7chFM5hxGGVITSbiLXKb0gbP3PV8CvsXY=",
1054 "dependencies": [
1063 "dependencies": [
1055 "aff",
1064 "aff",
1056 "aff-retry",
1065 "aff-retry",
@@ -1062,6 +1071,7 @@
1062 "either",
1071 "either",
1063 "exceptions",
1072 "exceptions",
1064 "functions",
1073 "functions",
1074+ "heterogeneous",
1065 "maybe",
1075 "maybe",
1066 "newtype",
1076 "newtype",
1067 "parallel",
1077 "parallel",
@@ -1080,12 +1090,13 @@
1080 },
1090 },
1081 "yoga-om-layer": {
1091 "yoga-om-layer": {
1082 "type": "registry",
1092 "type": "registry",
1083− "version": "1.1.1",
1093+ "version": "2.1.0",
1084− "integrity": "sha256-9Hl4M0p/+6X98slJAv/Zg5oitWQBYb9ndD7rQlkfXTo=",
1094+ "integrity": "sha256-4XxwLT/1JJQovIkH/qog5YRa4WcP8SbfKFrWs214/sQ=",
1085 "dependencies": [
1095 "dependencies": [
1086 "aff",
1096 "aff",
1087 "aff-retry",
1097 "aff-retry",
1088 "arrays",
1098 "arrays",
1099+ "control",
1089 "effect",
1100 "effect",
1090 "exceptions",
1101 "exceptions",
1091 "foldable-traversable",
1102 "foldable-traversable",
@@ -1097,8 +1108,10 @@
1097 "record",
1108 "record",
1098 "record-studio",
1109 "record-studio",
1099 "refs",
1110 "refs",
1111+ "tailrec",
1100 "transformers",
1112 "transformers",
1101 "type-equality",
1113 "type-equality",
1114+ "typelevel-prelude",
1102 "unsafe-coerce",
1115 "unsafe-coerce",
1103 "variant",
1116 "variant",
1104 "yoga-om"
1117 "yoga-om"
@@ -1106,8 +1119,8 @@
1106 },
1119 },
1107 "yoga-postgres": {
1120 "yoga-postgres": {
1108 "type": "registry",
1121 "type": "registry",
1109− "version": "0.2.0",
1122+ "version": "0.3.2",
1110− "integrity": "sha256-63pzEyMW6OrvCiDYD/RTk+T0LBmsMDtgZWeLYefNH/c=",
1123+ "integrity": "sha256-3xDBcPLrIRYbrj2k7Y+V6WcxD+I7yls90AjBCQS1NCk=",
1111 "dependencies": [
1124 "dependencies": [
1112 "aff",
1125 "aff",
1113 "arrays",
1126 "arrays",
@@ -1143,8 +1156,8 @@
1143 },
1156 },
1144 "yoga-sql-types": {
1157 "yoga-sql-types": {
1145 "type": "registry",
1158 "type": "registry",
1146− "version": "0.1.0",
1159+ "version": "0.2.0",
1147− "integrity": "sha256-vxOgnj3HK8F5gRHCNkIPNirORQtCBEKozG+CnrHtB84=",
1160+ "integrity": "sha256-/fiSiEmSQ8UH2N7vp/E02Eo+Krbs6iGcre9haDV34Q8=",
1148 "dependencies": [
1161 "dependencies": [
1149 "arrays",
1162 "arrays",
1150 "bifunctors",
1163 "bifunctors",
spago.yaml+11−8
@@ -3,21 +3,23 @@ workspace: {}
3package:
3package:
4 name: yoga-postgres-om
4 name: yoga-postgres-om
5 publish:
5 publish:
6− version: 0.1.0
6+ version: 0.1.1
7 license: MIT
7 license: MIT
8 location:
8 location:
9 githubOwner: rowtype-yoga
9 githubOwner: rowtype-yoga
10 githubRepo: purescript-yoga-postgres-om
10 githubRepo: purescript-yoga-postgres-om
11 dependencies:
11 dependencies:
12− - aff: ">=7.1.0 <8.0.0"
12+ - aff: ">=8.0.0 <9.0.0"
13 - arrays: ">=7.3.0 <8.0.0"
13 - arrays: ">=7.3.0 <8.0.0"
14 - console: ">=6.1.0 <7.0.0"
14 - console: ">=6.1.0 <7.0.0"
15 - effect: ">=4.0.0 <5.0.0"
15 - effect: ">=4.0.0 <5.0.0"
16 - either: ">=6.1.0 <7.0.0"
16 - either: ">=6.1.0 <7.0.0"
17+ - exceptions: ">=6.1.0 <7.0.0"
17 - foldable-traversable: ">=6.0.0 <7.0.0"
18 - foldable-traversable: ">=6.0.0 <7.0.0"
18 - foreign: ">=7.0.0 <8.0.0"
19 - foreign: ">=7.0.0 <8.0.0"
19− - heterogeneous: ">=0.6.0 <0.7.0"
20+ - heterogeneous: ">=0.7.0 <0.8.0"
20 - maybe: ">=6.0.0 <7.0.0"
21 - maybe: ">=6.0.0 <7.0.0"
22+ - nullable: ">=6.0.0 <7.0.0"
21 - ordered-collections: ">=3.2.0 <4.0.0"
23 - ordered-collections: ">=3.2.0 <4.0.0"
22 - prelude: ">=6.0.2 <7.0.0"
24 - prelude: ">=6.0.2 <7.0.0"
23 - record: ">=4.0.0 <5.0.0"
25 - record: ">=4.0.0 <5.0.0"
@@ -25,14 +27,15 @@ package:
25 - tuples: ">=7.0.0 <8.0.0"
27 - tuples: ">=7.0.0 <8.0.0"
26 - typelevel-prelude: ">=7.0.0 <8.0.0"
28 - typelevel-prelude: ">=7.0.0 <8.0.0"
27 - unsafe-coerce: ">=6.0.0 <7.0.0"
29 - unsafe-coerce: ">=6.0.0 <7.0.0"
28− - yoga-json: ">=5.2.0 <6.0.0"
30+ - yoga-json: ">=5.2.1 <6.0.0"
29− - yoga-om: ">=1.4.0 <2.0.0"
31+ - yoga-om: ">=2.2.1 <3.0.0"
30− - yoga-om-layer: ">=1.1.1 <2.0.0"
32+ - yoga-om-layer: ">=2.1.0 <3.0.0"
31− - yoga-postgres: ">=0.2.0 <0.3.0"
33+ - yoga-postgres: ">=0.3.2 <0.4.0"
32− - yoga-sql-types: ">=0.1.0 <0.2.0"
34+ - yoga-sql-types: ">=0.2.0 <0.3.0"
33 test:
35 test:
34 main: Test.Main
36 main: Test.Main
35 dependencies:
37 dependencies:
38+ - avar: ">=5.0.1 <6.0.0"
36 - console: ">=6.0.0 <7.0.0"
39 - console: ">=6.0.0 <7.0.0"
37 - exceptions: ">=6.0.0 <7.0.0"
40 - exceptions: ">=6.0.0 <7.0.0"
38 - spec: ">=7.0.0 <8.0.0"
41 - spec: ">=7.0.0 <8.0.0"
src/Yoga/Postgres/ClientOm.js+21−1
@@ -1 +1,21 @@
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+
1−export const txToConnection = (tx) => tx._sql || tx._reserved || tx;
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+47−19
@@ -5,13 +5,15 @@ import Prelude
5import Data.Array (filter, intercalate, length, mapWithIndex, take)
5import Data.Array (filter, intercalate, length, mapWithIndex, take)
6import Data.Tuple.Nested (type (/\), (/\))
6import Data.Tuple.Nested (type (/\), (/\))
7import Data.Either (Either(..))
7import Data.Either (Either(..))
8−import Data.Maybe (Maybe)
8+import Data.Maybe (Maybe(..))
9+import Data.Nullable as Nullable
9import Record (get) as Record
10import Record (get) as Record
10import Data.Symbol (class IsSymbol, reflectSymbol)
11import Data.Symbol (class IsSymbol, reflectSymbol)
11import Data.Traversable (traverse)
12import Data.Traversable (traverse)
12−import Control.Monad.Reader.Class (local)
13+import Control.Monad.Error.Class (throwError, try)
13import Effect.Aff.Class (liftAff)
14import Effect.Aff.Class (liftAff)
15+import Effect.Exception (Error, error)
14−import Foreign (Foreign, MultipleErrors)
16+import Foreign (Foreign, MultipleErrors, unsafeFromForeign, unsafeToForeign)
15import Prim.Boolean (True, False)
17import Prim.Boolean (True, False)
16import Prim.Row (class Cons, class Lacks, class Union) as Row
18import Prim.Row (class Cons, class Lacks, class Union) as Row
17import Prim.RowList as RL
19import Prim.RowList as RL
@@ -30,14 +32,42 @@ import Yoga.Postgres.Schema
30 , class FieldToPGValue, class InsertableColumnsRL, class RecordValuesRL
32 , class FieldToPGValue, class InsertableColumnsRL, class RecordValuesRL
31 , class RequiredColumnsRL, class SetClauseRL, class StripColumnsRL
33 , class RequiredColumnsRL, class SetClauseRL, class StripColumnsRL
32 , class ValidateSetColumnsRL
34 , class ValidateSetColumnsRL
33− , Table, PrimaryKey, AutoIncrement, Unique, Default, DefaultExpr
35+ , Table, PrimaryKey, AutoIncrement, Unique, Default
34 , ForeignKey, References, Nullable
36 , ForeignKey, References, Nullable
35 , columnCountRL, columnNamesRL, fieldToPGValue, recordValuesRL, setClauseRL
37 , columnCountRL, columnNamesRL, fieldToPGValue, recordValuesRL, setClauseRL
36− , class ExtractWord, class SkipSpaces, class SplitOnDot
38+ , class ExtractWord, class SkipSpaces
37 )
39 )
38import Yoga.Postgres.Schema as Schema
40import Yoga.Postgres.Schema as Schema
39
41
40
42
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+
41-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42-- Query builder bridge (Schema.Q -> Om)
72-- Query builder bridge (Schema.Q -> Om)
43-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -66,17 +96,27 @@ runOne params q = do
66 { postgresConnection } <- Om.ask
96 { postgresConnection } <- Om.ask
67 Schema.runQueryOne postgresConnection params q # liftAff
97 Schema.runQueryOne postgresConnection params q # liftAff
68
98
69−runExec
99+exec
100+ :: forall tables params paramsRL stage r err
101+ . RowToList params paramsRL
102+ => Schema.ParamsToArray paramsRL params
103+ => { | params }
104+ -> Schema.Q tables () params stage
105+ -> Om.Om { postgresConnection :: PG.Connection | r } err Unit
106+exec params q = void (execCount params q)
107+
108+execCount
70 :: forall tables params paramsRL stage r err
109 :: forall tables params paramsRL stage r err
71 . RowToList params paramsRL
110 . RowToList params paramsRL
72 => Schema.ParamsToArray paramsRL params
111 => Schema.ParamsToArray paramsRL params
73 => { | params }
112 => { | params }
74 -> Schema.Q tables () params stage
113 -> Schema.Q tables () params stage
75 -> Om.Om { postgresConnection :: PG.Connection | r } err Int
114 -> Om.Om { postgresConnection :: PG.Connection | r } err Int
76−runExec params q = do
115+execCount params q = do
77 { postgresConnection } <- Om.ask
116 { postgresConnection } <- Om.ask
78 Schema.runExecute postgresConnection params q # liftAff
117 Schema.runExecute postgresConnection params q # liftAff
79
118
119+
80-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81-- WHERE operators
121-- WHERE operators
82-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
122-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -299,7 +339,6 @@ instance HasPrimaryKeyConstraint (PrimaryKey a) True
299else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (AutoIncrement a) result
339else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (AutoIncrement a) result
300else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Unique a) result
340else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Unique a) result
301else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Default s a) result
341else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Default s a) result
302−else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (DefaultExpr s a) result
303else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (ForeignKey t r c a) result
342else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (ForeignKey t r c a) result
304else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Nullable a) result
343else instance HasPrimaryKeyConstraint a result => HasPrimaryKeyConstraint (Nullable a) result
305else instance HasPrimaryKeyConstraint a False
344else instance HasPrimaryKeyConstraint a False
@@ -1823,17 +1862,6 @@ deleteWhereReturning whereRec _ = do
1823-- Transaction
1862-- Transaction
1824-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1863-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1825
1864
1826−withTransaction
1827− :: forall r err a
1828− . Om.Om { postgresConnection :: PG.Connection | r } err a
1829− -> Om.Om { postgresConnection :: PG.Connection | r } err a
1830−withTransaction action = do
1831− { postgresConnection } <- Om.ask
1832− tx <- PG.begin postgresConnection # liftAff
1833− let txConn = txToConnection tx
1834− result <- local (_ { postgresConnection = txConn }) action
1835− PG.commit tx # liftAff
1836− pure result
1837
1865
1838foreign import txToConnection :: PG.Transaction -> PG.Connection
1866foreign import txToConnection :: PG.Transaction -> PG.Connection
1839
1867
test/Test/ClientOm.purs+512−525
@@ -6,11 +6,10 @@ import Control.Monad.Error.Class (throwError)
6import Data.Array as Array
6import Data.Array as Array
7import Data.Maybe (Maybe(..), isNothing)
7import Data.Maybe (Maybe(..), isNothing)
8import Effect.Aff (Aff)
8import Effect.Aff (Aff)
9−import Effect.Class (liftEffect)
9+import Effect.Class (class MonadEffect, liftEffect)
10−import Effect.Exception (error)
10+import Effect.Exception as Exception
11import Foreign (MultipleErrors)
11import Foreign (MultipleErrors)
12import Test.Spec (Spec, describe, it)
12import Test.Spec (Spec, describe, it)
13−import Test.Spec.Assertions (shouldEqual)
14import Type.Function (type (#))
13import Type.Function (type (#))
15import Type.Proxy (Proxy(..))
14import Type.Proxy (Proxy(..))
16import Yoga.Om as Om
15import Yoga.Om as Om
@@ -32,624 +31,400 @@ import Yoga.Postgres.ClientOm
32 , gt, gte, ilike, inArray, join, like_, lt, lte
31 , gt, gte, ilike, inArray, join, like_, lt, lte
33 , maxOf, minOf, minOfWhere, ne, not_, or_
32 , maxOf, minOf, minOfWhere, ne, not_, or_
34 , selectFrom, selectFromWhere, selectOneFrom, selectOneFromWhere
33 , selectFrom, selectFromWhere, selectOneFrom, selectOneFromWhere
35− , sumOf, sumOfWhere, updateWhere, upsert, upsertMany, withTransaction
34+ , sumOf, sumOfWhere, transactionally, updateWhere, upsert, upsertMany
36 )
35 )
37−import Yoga.Postgres.Schema (Table, PrimaryKey, AutoIncrement, Unique, Default, DefaultExpr, ForeignKey, References, PGUUID)
36+import Yoga.Postgres.Schema (Table, PrimaryKey, AutoIncrement, Unique, Default, ForeignKey, References, PGUUID)
38−
39−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40−-- Test schemas
41−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42−
43−type UsersTable = Table "test_om_users"
44− ( id :: PrimaryKey (AutoIncrement Int)
45− , name :: String
46− , email :: Unique String
47− , active :: Default "true" Boolean
48− , role :: String
49− )
50−
51−usersTable :: Proxy UsersTable
52−usersTable = Proxy
53−
54−type SettingsTable = Table "test_om_settings"
55− ( key :: PrimaryKey String
56− , value :: String
57− )
58−
59−settingsTable :: Proxy SettingsTable
60−settingsTable = Proxy
61−
62−type UuidTable = Table "test_om_uuid_things"
63− ( id :: PGUUID # PrimaryKey # DefaultExpr "gen_random_uuid()"
64− , label :: String
65− )
66−
67−uuidTable :: Proxy UuidTable
68−uuidTable = Proxy
69−
70−type PostsTable = Table "test_om_posts"
71− ( id :: PrimaryKey (AutoIncrement Int)
72− , title :: String
73− , body :: String
74− , user_id :: ForeignKey "test_om_users" References "id" Int
75− )
76−
77−postsTable :: Proxy PostsTable
78−postsTable = Proxy
79−
80−type CommentsTable = Table "test_om_comments"
81− ( id :: PrimaryKey (AutoIncrement Int)
82− , text :: String
83− , user_id :: ForeignKey "test_om_users" References "id" Int
84− )
85−
86−commentsTable :: Proxy CommentsTable
87−commentsTable = Proxy
88−
89−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90−-- Helpers
91−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
92−
93−testConnection :: Aff PG.Connection
94−testConnection =
95− PG.postgres
96− { host: PG.PostgresHost "localhost"
97− , port: PG.PostgresPort 5433
98− , database: PG.PostgresDatabase "test_playground"
99− , username: PG.PostgresUsername "postgres"
100− , password: PG.PostgresPassword "postgres"
101− } # liftEffect
102−
103−run
104− :: forall a
105− . Om.Om { postgresConnection :: PG.Connection } (parseError :: MultipleErrors) a
106− -> PG.Connection
107− -> Aff a
108−run om conn = Om.runOm { postgresConnection: conn }
109− { exception: \e -> throwError e
110− , parseError: \errs -> throwError (error ("Parse error: " <> show errs))
111− }
112− om
113−
114−runNoErr
115− :: forall a
116− . Om.Om { postgresConnection :: PG.Connection } () a
117− -> PG.Connection
118− -> Aff a
119−runNoErr om conn = Om.runOm { postgresConnection: conn }
120− { exception: \e -> throwError e }
121− om
122−
123−withUsersTable :: (PG.Connection -> Aff Unit) -> Aff Unit
124−withUsersTable test = do
125− conn <- testConnection
126− _ <- PG.executeSimple createDDL conn
127− test conn
128− _ <- PG.executeSimple dropDDL conn
129− PG.end conn
130− where
131− dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE"
132− createDDL = PG.SQL
133− """DROP TABLE IF EXISTS test_om_users CASCADE;
134− CREATE TABLE test_om_users (
135− id SERIAL PRIMARY KEY,
136− name TEXT NOT NULL,
137− email TEXT UNIQUE NOT NULL,
138− active BOOLEAN DEFAULT true NOT NULL,
139− role TEXT NOT NULL
140− )"""
141−
142−withSettingsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
143−withSettingsTable test = do
144− conn <- testConnection
145− _ <- PG.executeSimple createDDL conn
146− test conn
147− _ <- PG.executeSimple dropDDL conn
148− PG.end conn
149− where
150− dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_settings CASCADE"
151− createDDL = PG.SQL
152− """DROP TABLE IF EXISTS test_om_settings CASCADE;
153− CREATE TABLE test_om_settings (
154− key TEXT PRIMARY KEY,
155− value TEXT NOT NULL
156− )"""
157−
158−withUuidTable :: (PG.Connection -> Aff Unit) -> Aff Unit
159−withUuidTable test = do
160− conn <- testConnection
161− _ <- PG.executeSimple createDDL conn
162− test conn
163− _ <- PG.executeSimple dropDDL conn
164− PG.end conn
165− where
166− dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_uuid_things CASCADE"
167− createDDL = PG.SQL
168− """DROP TABLE IF EXISTS test_om_uuid_things CASCADE;
169− CREATE TABLE test_om_uuid_things (
170− id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
171− label TEXT NOT NULL
172− )"""
173−
174−withPostsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
175−withPostsTable test = do
176− conn <- testConnection
177− _ <- PG.executeSimple createDDL conn
178− test conn
179− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
180− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
181− PG.end conn
182− where
183− createDDL = PG.SQL
184− """DROP TABLE IF EXISTS test_om_posts CASCADE;
185− DROP TABLE IF EXISTS test_om_users CASCADE;
186− CREATE TABLE test_om_users (
187− id SERIAL PRIMARY KEY,
188− name TEXT NOT NULL,
189− email TEXT UNIQUE NOT NULL,
190− active BOOLEAN DEFAULT true NOT NULL,
191− role TEXT NOT NULL
192− );
193− CREATE TABLE test_om_posts (
194− id SERIAL PRIMARY KEY,
195− title TEXT NOT NULL,
196− body TEXT NOT NULL,
197− user_id INTEGER NOT NULL REFERENCES test_om_users(id)
198− )"""
199−
200−withPostsAndCommentsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
201−withPostsAndCommentsTable test = do
202− conn <- testConnection
203− _ <- PG.executeSimple createDDL conn
204− test conn
205− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_comments CASCADE") conn
206− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
207− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
208− PG.end conn
209− where
210− createDDL = PG.SQL
211− """DROP TABLE IF EXISTS test_om_comments CASCADE;
212− DROP TABLE IF EXISTS test_om_posts CASCADE;
213− DROP TABLE IF EXISTS test_om_users CASCADE;
214− CREATE TABLE test_om_users (
215− id SERIAL PRIMARY KEY,
216− name TEXT NOT NULL,
217− email TEXT UNIQUE NOT NULL,
218− active BOOLEAN DEFAULT true NOT NULL,
219− role TEXT NOT NULL
220− );
221− CREATE TABLE test_om_posts (
222− id SERIAL PRIMARY KEY,
223− title TEXT NOT NULL,
224− body TEXT NOT NULL,
225− user_id INTEGER NOT NULL REFERENCES test_om_users(id)
226− );
227− CREATE TABLE test_om_comments (
228− id SERIAL PRIMARY KEY,
229− text TEXT NOT NULL,
230− user_id INTEGER NOT NULL REFERENCES test_om_users(id)
231− )"""
232−
233−shouldBeJust :: forall a. Maybe a -> Aff a
234−shouldBeJust (Just a) = pure a
235−shouldBeJust Nothing = throwError $ error "Expected Just but got Nothing"
236−
237−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
238−-- Spec
239−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240
37
241spec :: Spec Unit
38spec :: Spec Unit
242spec = describe "ClientOm" do
39spec = describe "ClientOm" do
243
40
244 describe "Creating records" do
41 describe "Creating records" do
245− it "inserts a record and returns it with generated fields" $ withUsersTable \conn -> do
42+ it "inserts a record and returns it with generated fields" $ withUsersTable do
246− created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
43+ created <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
247 alice <- shouldBeJust created
44 alice <- shouldBeJust created
248 alice.name `shouldEqual` "Alice"
45 alice.name `shouldEqual` "Alice"
249 (alice.id > 0) `shouldEqual` true
46 (alice.id > 0) `shouldEqual` true
250
47
251− it "lets the database fill in defaults" $ withUsersTable \conn -> do
48+ it "lets the database fill in defaults" $ withUsersTable do
252− created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
49+ created <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
253 alice <- shouldBeJust created
50 alice <- shouldBeJust created
254 alice.active `shouldEqual` true
51 alice.active `shouldEqual` true
255
52
256− it "inserts many records at once" $ withUsersTable \conn -> do
53+ it "inserts many records at once" $ withUsersTable do
257 let noUsers = [] :: Array { name :: String, email :: String, role :: String }
54 let noUsers = [] :: Array { name :: String, email :: String, role :: String }
258− empty <- conn # run do usersTable # createMany noUsers
55+ empty <- usersTable # createMany noUsers
259 Array.length empty `shouldEqual` 0
56 Array.length empty `shouldEqual` 0
260− users <- conn # run do
261− usersTable # createMany
57+ users <- usersTable # createMany
262− [ { name: "Alice", email: "a@t.com", role: "admin" }
58+ [ { name: "Alice", email: "a@t.com", role: "admin" }
263− , { name: "Bob", email: "b@t.com", role: "user" }
59+ , { name: "Bob", email: "b@t.com", role: "user" }
264− , { name: "Carol", email: "c@t.com", role: "user" }
60+ , { name: "Carol", email: "c@t.com", role: "user" }
265− ]
61+ ]
266 Array.length users `shouldEqual` 3
62 Array.length users `shouldEqual` 3
267− n <- conn # runNoErr do usersTable # count
63+ n <- usersTable # count
268 n `shouldEqual` 3
64 n `shouldEqual` 3
269
65
270− it "skips insert when a conflict would occur" $ withSettingsTable \conn -> do
66+ it "skips insert when a conflict would occur" $ withSettingsTable do
271− inserted <- conn # run do settingsTable # createIfNotExists { key: "theme", value: "dark" }
67+ inserted <- settingsTable # createIfNotExists { key: "theme", value: "dark" }
272 r1 <- shouldBeJust inserted
68 r1 <- shouldBeJust inserted
273 r1.value `shouldEqual` "dark"
69 r1.value `shouldEqual` "dark"
274− skipped <- conn # run do settingsTable # createIfNotExists { key: "theme", value: "light" }
70+ skipped <- settingsTable # createIfNotExists { key: "theme", value: "light" }
275 isNothing skipped `shouldEqual` true
71 isNothing skipped `shouldEqual` true
276− found <- conn # run do settingsTable # findOneBy @"key" "theme"
72+ found <- settingsTable # findOneBy @"key" "theme"
277 existing <- shouldBeJust found
73 existing <- shouldBeJust found
278 existing.value `shouldEqual` "dark"
74 existing.value `shouldEqual` "dark"
279
75
280− it "upserts: inserts or updates on conflict" $ withSettingsTable \conn -> do
76+ it "upserts: inserts or updates on conflict" $ withSettingsTable do
281− inserted <- conn # run do settingsTable # upsert { key: "theme", value: "dark" }
77+ inserted <- settingsTable # upsert { key: "theme", value: "dark" }
282 r1 <- shouldBeJust inserted
78 r1 <- shouldBeJust inserted
283 r1.value `shouldEqual` "dark"
79 r1.value `shouldEqual` "dark"
284− updated <- conn # run do settingsTable # upsert { key: "theme", value: "light" }
80+ updated <- settingsTable # upsert { key: "theme", value: "light" }
285 r2 <- shouldBeJust updated
81 r2 <- shouldBeJust updated
286 r2.value `shouldEqual` "light"
82 r2.value `shouldEqual` "light"
287− n <- conn # runNoErr do settingsTable # count
83+ n <- settingsTable # count
288 n `shouldEqual` 1
84 n `shouldEqual` 1
289
85
290− it "bulk upserts a batch" $ withSettingsTable \conn -> do
86+ it "bulk upserts a batch" $ withSettingsTable do
291− _ <- conn # run do
292− settingsTable # upsertMany
87+ _ <- settingsTable # upsertMany
293− [ { key: "theme", value: "dark" }
88+ [ { key: "theme", value: "dark" }
294− , { key: "lang", value: "en" }
89+ , { key: "lang", value: "en" }
295− ]
90+ ]
296− n1 <- conn # runNoErr do settingsTable # count
91+ n1 <- settingsTable # count
297 n1 `shouldEqual` 2
92 n1 `shouldEqual` 2
298− updated <- conn # run do
299− settingsTable # upsertMany
93+ updated <- settingsTable # upsertMany
300− [ { key: "theme", value: "light" }
94+ [ { key: "theme", value: "light" }
301− , { key: "lang", value: "de" }
95+ , { key: "lang", value: "de" }
302− , { key: "tz", value: "UTC" }
96+ , { key: "tz", value: "UTC" }
303− ]
97+ ]
304 Array.length updated `shouldEqual` 3
98 Array.length updated `shouldEqual` 3
305− n2 <- conn # runNoErr do settingsTable # count
99+ n2 <- settingsTable # count
306 n2 `shouldEqual` 3
100 n2 `shouldEqual` 3
307− found <- conn # run do settingsTable # findOneBy @"key" "theme"
101+ found <- settingsTable # findOneBy @"key" "theme"
308 setting <- shouldBeJust found
102 setting <- shouldBeJust found
309 setting.value `shouldEqual` "light"
103 setting.value `shouldEqual` "light"
310
104
311 describe "Finding records" do
105 describe "Finding records" do
312− it "finds a single record by primary key" $ withUsersTable \conn -> do
106+ it "finds a single record by primary key" $ withUsersTable do
313− created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
107+ created <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
314 alice <- shouldBeJust created
108 alice <- shouldBeJust created
315− found <- conn # run do usersTable # findById alice.id
109+ found <- usersTable # findById alice.id
316 r <- shouldBeJust found
110 r <- shouldBeJust found
317 r.name `shouldEqual` "Alice"
111 r.name `shouldEqual` "Alice"
318
112
319− it "finds a single record by any unique column" $ withUsersTable \conn -> do
113+ it "finds a single record by any unique column" $ withUsersTable do
320− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
114+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
321− found <- conn # run do usersTable # findOneBy @"email" "a@t.com"
115+ found <- usersTable # findOneBy @"email" "a@t.com"
322 alice <- shouldBeJust found
116 alice <- shouldBeJust found
323 alice.name `shouldEqual` "Alice"
117 alice.name `shouldEqual` "Alice"
324
118
325− it "finds all records matching a column value" $ withUsersTable \conn -> do
119+ it "finds all records matching a column value" $ withUsersTable do
326− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
120+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
327− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
121+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
328− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
122+ _ <- usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
329− admins <- conn # run do usersTable # findBy @"role" "admin"
123+ admins <- usersTable # findBy @"role" "admin"
330 Array.length admins `shouldEqual` 2
124 Array.length admins `shouldEqual` 2
331
125
332− it "finds all records matching multiple conditions" $ withUsersTable \conn -> do
126+ it "finds all records matching multiple conditions" $ withUsersTable do
333− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
127+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
334− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
128+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
335− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
129+ _ <- usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
336− alices <- conn # run do usersTable # findAllWhere { name: "Alice", role: "admin" }
130+ alices <- usersTable # findAllWhere { name: "Alice", role: "admin" }
337 Array.length alices `shouldEqual` 1
131 Array.length alices `shouldEqual` 1
338 alice <- shouldBeJust (Array.head alices)
132 alice <- shouldBeJust (Array.head alices)
339 alice.email `shouldEqual` "a@t.com"
133 alice.email `shouldEqual` "a@t.com"
340
134
341− it "returns Nothing when no record matches" $ withUsersTable \conn -> do
135+ it "returns Nothing when no record matches" $ withUsersTable do
342− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
136+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
343− missing <- conn # run do usersTable # findOneBy @"email" "nobody@t.com"
137+ missing <- usersTable # findOneBy @"email" "nobody@t.com"
344 isNothing missing `shouldEqual` true
138 isNothing missing `shouldEqual` true
345− alsoMissing <- conn # run do usersTable # findOneWhere { name: "Alice", role: "user" }
139+ alsoMissing <- usersTable # findOneWhere { name: "Alice", role: "user" }
346 isNothing alsoMissing `shouldEqual` true
140 isNothing alsoMissing `shouldEqual` true
347
141
348− it "finds records by a list of primary keys" $ withUsersTable \conn -> do
142+ it "finds records by a list of primary keys" $ withUsersTable do
349− created1 <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
143+ created1 <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
350 u1 <- shouldBeJust created1
144 u1 <- shouldBeJust created1
351− created2 <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
145+ created2 <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
352 u2 <- shouldBeJust created2
146 u2 <- shouldBeJust created2
353− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
147+ _ <- usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
354− found <- conn # run do usersTable # findByIdIn [ u1.id, u2.id ]
148+ found <- usersTable # findByIdIn [ u1.id, u2.id ]
355 Array.length found `shouldEqual` 2
149 Array.length found `shouldEqual` 2
356− empty <- conn # run do usersTable # findByIdIn ([] :: Array Int)
150+ empty <- usersTable # findByIdIn ([] :: Array Int)
357 Array.length empty `shouldEqual` 0
151 Array.length empty `shouldEqual` 0
358
152
359− it "returns all records in the table" $ withUsersTable \conn -> do
153+ it "returns all records in the table" $ withUsersTable do
360− _ <- conn # run do
361− usersTable # createMany
154+ _ <- usersTable # createMany
362− [ { name: "Alice", email: "a@t.com", role: "admin" }
155+ [ { name: "Alice", email: "a@t.com", role: "admin" }
363− , { name: "Bob", email: "b@t.com", role: "user" }
156+ , { name: "Bob", email: "b@t.com", role: "user" }
364− ]
157+ ]
365− all <- conn # run do usersTable # findAll
158+ all <- usersTable # findAll
366 Array.length all `shouldEqual` 2
159 Array.length all `shouldEqual` 2
367
160
368 describe "Filtering with operators" do
161 describe "Filtering with operators" do
369− it "compares with gt, gte, lt, lte" $ withUsersTable \conn -> do
162+ it "compares with gt, gte, lt, lte" $ withUsersTable do
370− _ <- conn # run do
371− usersTable # createMany
163+ _ <- usersTable # createMany
372− [ { name: "Alice", email: "a@t.com", role: "admin" }
164+ [ { name: "Alice", email: "a@t.com", role: "admin" }
373− , { name: "Bob", email: "b@t.com", role: "user" }
165+ , { name: "Bob", email: "b@t.com", role: "user" }
374− , { name: "Carol", email: "c@t.com", role: "user" }
166+ , { name: "Carol", email: "c@t.com", role: "user" }
375− ]
167+ ]
376− all <- conn # run do usersTable # findAll
168+ all <- usersTable # findAll
377 let
169 let
378 id1 = case Array.head all of
170 id1 = case Array.head all of
379 Just u -> u.id
171 Just u -> u.id
380 Nothing -> 0
172 Nothing -> 0
381− gtRows <- conn # run do usersTable # findAllWhere { id: gt id1 }
173+ gtRows <- usersTable # findAllWhere { id: gt id1 }
382 Array.length gtRows `shouldEqual` 2
174 Array.length gtRows `shouldEqual` 2
383− gteRows <- conn # run do usersTable # findAllWhere { id: gte id1 }
175+ gteRows <- usersTable # findAllWhere { id: gte id1 }
384 Array.length gteRows `shouldEqual` 3
176 Array.length gteRows `shouldEqual` 3
385− ltRows <- conn # run do usersTable # findAllWhere { id: lt (id1 + 3) }
177+ ltRows <- usersTable # findAllWhere { id: lt (id1 + 3) }
386 Array.length ltRows `shouldEqual` 3
178 Array.length ltRows `shouldEqual` 3
387− lteRows <- conn # run do usersTable # findAllWhere { id: lte id1 }
179+ lteRows <- usersTable # findAllWhere { id: lte id1 }
388 Array.length lteRows `shouldEqual` 1
180 Array.length lteRows `shouldEqual` 1
389
181
390− it "excludes with ne" $ withUsersTable \conn -> do
182+ it "excludes with ne" $ withUsersTable do
391− _ <- conn # run do
392− usersTable # createMany
183+ _ <- usersTable # createMany
393− [ { name: "Alice", email: "a@t.com", role: "admin" }
184+ [ { name: "Alice", email: "a@t.com", role: "admin" }
394− , { name: "Bob", email: "b@t.com", role: "user" }
185+ , { name: "Bob", email: "b@t.com", role: "user" }
395− , { name: "Carol", email: "c@t.com", role: "user" }
186+ , { name: "Carol", email: "c@t.com", role: "user" }
396− ]
187+ ]
397− rows <- conn # run do usersTable # findAllWhere { role: ne "admin" }
188+ rows <- usersTable # findAllWhere { role: ne "admin" }
398 Array.length rows `shouldEqual` 2
189 Array.length rows `shouldEqual` 2
399
190
400− it "pattern-matches with like_ and ilike" $ withUsersTable \conn -> do
191+ it "pattern-matches with like_ and ilike" $ withUsersTable do
401− _ <- conn # run do
402− usersTable # createMany
192+ _ <- usersTable # createMany
403− [ { name: "Alice", email: "a@t.com", role: "admin" }
193+ [ { name: "Alice", email: "a@t.com", role: "admin" }
404− , { name: "Bob", email: "b@t.com", role: "user" }
194+ , { name: "Bob", email: "b@t.com", role: "user" }
405− , { name: "Carol", email: "c@t.com", role: "user" }
195+ , { name: "Carol", email: "c@t.com", role: "user" }
406− ]
196+ ]
407− likeRows <- conn # run do usersTable # findAllWhere { name: like_ "A%" }
197+ likeRows <- usersTable # findAllWhere { name: like_ "A%" }
408 Array.length likeRows `shouldEqual` 1
198 Array.length likeRows `shouldEqual` 1
409− ilikeRows <- conn # run do usersTable # findAllWhere { name: ilike "a%" }
199+ ilikeRows <- usersTable # findAllWhere { name: ilike "a%" }
410 Array.length ilikeRows `shouldEqual` 1
200 Array.length ilikeRows `shouldEqual` 1
411
201
412− it "checks membership with inArray" $ withUsersTable \conn -> do
202+ it "checks membership with inArray" $ withUsersTable do
413− _ <- conn # run do
414− usersTable # createMany
203+ _ <- usersTable # createMany
415− [ { name: "Alice", email: "a@t.com", role: "admin" }
204+ [ { name: "Alice", email: "a@t.com", role: "admin" }
416− , { name: "Bob", email: "b@t.com", role: "user" }
205+ , { name: "Bob", email: "b@t.com", role: "user" }
417− , { name: "Carol", email: "c@t.com", role: "mod" }
206+ , { name: "Carol", email: "c@t.com", role: "mod" }
418− ]
207+ ]
419− rows <- conn # run do usersTable # findAllWhere { role: inArray [ "admin", "mod" ] }
208+ rows <- usersTable # findAllWhere { role: inArray [ "admin", "mod" ] }
420 Array.length rows `shouldEqual` 2
209 Array.length rows `shouldEqual` 2
421
210
422− it "combines conditions with or_" $ withUsersTable \conn -> do
211+ it "combines conditions with or_" $ withUsersTable do
423− _ <- conn # run do
424− usersTable # createMany
212+ _ <- usersTable # createMany
425− [ { name: "Alice", email: "a@t.com", role: "admin" }
213+ [ { name: "Alice", email: "a@t.com", role: "admin" }
426− , { name: "Bob", email: "b@t.com", role: "user" }
214+ , { name: "Bob", email: "b@t.com", role: "user" }
427− , { name: "Carol", email: "c@t.com", role: "mod" }
215+ , { name: "Carol", email: "c@t.com", role: "mod" }
428− ]
216+ ]
429− rows <- conn # run do usersTable # findAllWhere { role: or_ (eq_ "admin") (eq_ "mod") }
217+ rows <- usersTable # findAllWhere { role: or_ (eq_ "admin") (eq_ "mod") }
430 Array.length rows `shouldEqual` 2
218 Array.length rows `shouldEqual` 2
431
219
432− it "negates conditions with not_" $ withUsersTable \conn -> do
220+ it "negates conditions with not_" $ withUsersTable do
433− _ <- conn # run do
434− usersTable # createMany
221+ _ <- usersTable # createMany
435− [ { name: "Alice", email: "a@t.com", role: "admin" }
222+ [ { name: "Alice", email: "a@t.com", role: "admin" }
436− , { name: "Bob", email: "b@t.com", role: "user" }
223+ , { name: "Bob", email: "b@t.com", role: "user" }
437− , { name: "Carol", email: "c@t.com", role: "user" }
224+ , { name: "Carol", email: "c@t.com", role: "user" }
438− ]
225+ ]
439− rows <- conn # run do usersTable # findAllWhere { role: not_ (eq_ "admin") }
226+ rows <- usersTable # findAllWhere { role: not_ (eq_ "admin") }
440 Array.length rows `shouldEqual` 2
227 Array.length rows `shouldEqual` 2
441
228
442 describe "Updating and deleting" do
229 describe "Updating and deleting" do
443− it "updates rows matching a condition" $ withUsersTable \conn -> do
230+ it "updates rows matching a condition" $ withUsersTable do
444− _ <- conn # run do
445− usersTable # createMany
231+ _ <- usersTable # createMany
446− [ { name: "Alice", email: "a@t.com", role: "admin" }
232+ [ { name: "Alice", email: "a@t.com", role: "admin" }
447− , { name: "Bob", email: "b@t.com", role: "user" }
233+ , { name: "Bob", email: "b@t.com", role: "user" }
448− , { name: "Carol", email: "c@t.com", role: "user" }
234+ , { name: "Carol", email: "c@t.com", role: "user" }
449− ]
235+ ]
450− updated <- conn # run do usersTable # updateWhere { role: "user" } { role: "member" }
236+ updated <- usersTable # updateWhere { role: "user" } { role: "member" }
451 Array.length updated `shouldEqual` 2
237 Array.length updated `shouldEqual` 2
452− members <- conn # run do usersTable # findBy @"role" "member"
238+ members <- usersTable # findBy @"role" "member"
453 Array.length members `shouldEqual` 2
239 Array.length members `shouldEqual` 2
454
240
455− it "deletes rows matching a condition" $ withUsersTable \conn -> do
241+ it "deletes rows matching a condition" $ withUsersTable do
456− _ <- conn # run do
457− usersTable # createMany
242+ _ <- usersTable # createMany
458− [ { name: "Alice", email: "a@t.com", role: "admin" }
243+ [ { name: "Alice", email: "a@t.com", role: "admin" }
459− , { name: "Bob", email: "b@t.com", role: "user" }
244+ , { name: "Bob", email: "b@t.com", role: "user" }
460− , { name: "Carol", email: "c@t.com", role: "user" }
245+ , { name: "Carol", email: "c@t.com", role: "user" }
461− ]
246+ ]
462− deleted <- conn # runNoErr do usersTable # deleteWhere { role: "user" }
247+ deleted <- usersTable # deleteWhere { role: "user" }
463 deleted `shouldEqual` 2
248 deleted `shouldEqual` 2
464− remaining <- conn # runNoErr do usersTable # count
249+ remaining <- usersTable # count
465 remaining `shouldEqual` 1
250 remaining `shouldEqual` 1
466
251
467− it "deletes and returns the deleted rows" $ withUsersTable \conn -> do
252+ it "deletes and returns the deleted rows" $ withUsersTable do
468− _ <- conn # run do
469− usersTable # createMany
253+ _ <- usersTable # createMany
470− [ { name: "Alice", email: "a@t.com", role: "admin" }
254+ [ { name: "Alice", email: "a@t.com", role: "admin" }
471− , { name: "Bob", email: "b@t.com", role: "user" }
255+ , { name: "Bob", email: "b@t.com", role: "user" }
472− , { name: "Carol", email: "c@t.com", role: "user" }
256+ , { name: "Carol", email: "c@t.com", role: "user" }
473− ]
257+ ]
474− deleted <- conn # run do usersTable # deleteWhereReturning { role: "user" }
258+ deleted <- usersTable # deleteWhereReturning { role: "user" }
475 Array.length deleted `shouldEqual` 2
259 Array.length deleted `shouldEqual` 2
476 Array.sort (map _.name deleted) `shouldEqual` [ "Bob", "Carol" ]
260 Array.sort (map _.name deleted) `shouldEqual` [ "Bob", "Carol" ]
477− remaining <- conn # runNoErr do usersTable # count
261+ remaining <- usersTable # count
478 remaining `shouldEqual` 1
262 remaining `shouldEqual` 1
479
263
480− it "deletes everything" $ withUsersTable \conn -> do
264+ it "deletes everything" $ withUsersTable do
481− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
265+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
482− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
266+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
483− deleted <- conn # runNoErr do usersTable # deleteAll
267+ deleted <- usersTable # deleteAll
484 deleted `shouldEqual` 2
268 deleted `shouldEqual` 2
485− n <- conn # runNoErr do usersTable # count
269+ n <- usersTable # count
486 n `shouldEqual` 0
270 n `shouldEqual` 0
487
271
488 describe "Ordering and pagination" do
272 describe "Ordering and pagination" do
489− it "orders results by a column" $ withUsersTable \conn -> do
273+ it "orders results by a column" $ withUsersTable do
490− _ <- conn # run do
491− usersTable # createMany
274+ _ <- usersTable # createMany
492− [ { name: "Carol", email: "c@t.com", role: "user" }
275+ [ { name: "Carol", email: "c@t.com", role: "user" }
493− , { name: "Alice", email: "a@t.com", role: "admin" }
276+ , { name: "Alice", email: "a@t.com", role: "admin" }
494− , { name: "Bob", email: "b@t.com", role: "user" }
277+ , { name: "Bob", email: "b@t.com", role: "user" }
495− ]
278+ ]
496− asc <- conn # run do usersTable # findOrdered @"name" Asc
279+ asc <- usersTable # findOrdered @"name" Asc
497 (map _.name asc) `shouldEqual` [ "Alice", "Bob", "Carol" ]
280 (map _.name asc) `shouldEqual` [ "Alice", "Bob", "Carol" ]
498− desc <- conn # run do usersTable # findOrdered @"name" Desc
281+ desc <- usersTable # findOrdered @"name" Desc
499 (map _.name desc) `shouldEqual` [ "Carol", "Bob", "Alice" ]
282 (map _.name desc) `shouldEqual` [ "Carol", "Bob", "Alice" ]
500
283
501− it "filters, orders, and limits in one call" $ withUsersTable \conn -> do
284+ it "filters, orders, and limits in one call" $ withUsersTable do
502− _ <- conn # run do
503− usersTable # createMany
285+ _ <- usersTable # createMany
504− [ { name: "Alice", email: "a@t.com", role: "user" }
286+ [ { name: "Alice", email: "a@t.com", role: "user" }
505− , { name: "Bob", email: "b@t.com", role: "user" }
287+ , { name: "Bob", email: "b@t.com", role: "user" }
506− , { name: "Carol", email: "c@t.com", role: "user" }
288+ , { name: "Carol", email: "c@t.com", role: "user" }
507− , { name: "Dave", email: "d@t.com", role: "user" }
289+ , { name: "Dave", email: "d@t.com", role: "user" }
508− ]
290+ ]
509− page1 <- conn # run do usersTable # findWhereLimited @"name" { role: "user" } Asc { limit: 2, offset: 0 }
291+ page1 <- usersTable # findWhereLimited @"name" { role: "user" } Asc { limit: 2, offset: 0 }
510 (map _.name page1) `shouldEqual` [ "Alice", "Bob" ]
292 (map _.name page1) `shouldEqual` [ "Alice", "Bob" ]
511− page2 <- conn # run do usersTable # findWhereLimited @"name" { role: "user" } Asc { limit: 2, offset: 2 }
293+ page2 <- usersTable # findWhereLimited @"name" { role: "user" } Asc { limit: 2, offset: 2 }
512 (map _.name page2) `shouldEqual` [ "Carol", "Dave" ]
294 (map _.name page2) `shouldEqual` [ "Carol", "Dave" ]
513
295
514− it "paginates forward with cursors" $ withUsersTable \conn -> do
296+ it "paginates forward with cursors" $ withUsersTable do
515− _ <- conn # run do
516− usersTable # createMany
297+ _ <- usersTable # createMany
517− [ { name: "Alice", email: "a@t.com", role: "admin" }
298+ [ { name: "Alice", email: "a@t.com", role: "admin" }
518− , { name: "Bob", email: "b@t.com", role: "user" }
299+ , { name: "Bob", email: "b@t.com", role: "user" }
519− , { name: "Carol", email: "c@t.com", role: "user" }
300+ , { name: "Carol", email: "c@t.com", role: "user" }
520− , { name: "Dave", email: "d@t.com", role: "user" }
301+ , { name: "Dave", email: "d@t.com", role: "user" }
521− , { name: "Eve", email: "e@t.com", role: "admin" }
302+ , { name: "Eve", email: "e@t.com", role: "admin" }
522− ]
303+ ]
523− page1 <- conn # run do usersTable # findFirst @"id" 2
304+ page1 <- usersTable # findFirst @"id" 2
524 Array.length page1.items `shouldEqual` 2
305 Array.length page1.items `shouldEqual` 2
525 page1.hasMore `shouldEqual` true
306 page1.hasMore `shouldEqual` true
526 let
307 let
527 lastId = case Array.last page1.items of
308 lastId = case Array.last page1.items of
528 Just u -> u.id
309 Just u -> u.id
529 Nothing -> 0
310 Nothing -> 0
530− page2 <- conn # run do usersTable # findAfter @"id" lastId 2
311+ page2 <- usersTable # findAfter @"id" lastId 2
531 Array.length page2.items `shouldEqual` 2
312 Array.length page2.items `shouldEqual` 2
532 page2.hasMore `shouldEqual` true
313 page2.hasMore `shouldEqual` true
533 let
314 let
534 lastId2 = case Array.last page2.items of
315 lastId2 = case Array.last page2.items of
535 Just u -> u.id
316 Just u -> u.id
536 Nothing -> 0
317 Nothing -> 0
537− page3 <- conn # run do usersTable # findAfter @"id" lastId2 2
318+ page3 <- usersTable # findAfter @"id" lastId2 2
538 Array.length page3.items `shouldEqual` 1
319 Array.length page3.items `shouldEqual` 1
539 page3.hasMore `shouldEqual` false
320 page3.hasMore `shouldEqual` false
540
321
541− it "paginates backward with cursors" $ withUsersTable \conn -> do
322+ it "paginates backward with cursors" $ withUsersTable do
542− _ <- conn # run do
543− usersTable # createMany
323+ _ <- usersTable # createMany
544− [ { name: "Alice", email: "a@t.com", role: "admin" }
324+ [ { name: "Alice", email: "a@t.com", role: "admin" }
545− , { name: "Bob", email: "b@t.com", role: "user" }
325+ , { name: "Bob", email: "b@t.com", role: "user" }
546− , { name: "Carol", email: "c@t.com", role: "user" }
326+ , { name: "Carol", email: "c@t.com", role: "user" }
547− , { name: "Dave", email: "d@t.com", role: "user" }
327+ , { name: "Dave", email: "d@t.com", role: "user" }
548− , { name: "Eve", email: "e@t.com", role: "admin" }
328+ , { name: "Eve", email: "e@t.com", role: "admin" }
549− ]
329+ ]
550− all <- conn # run do usersTable # findAll
330+ all <- usersTable # findAll
551 let
331 let
552 lastUser = case Array.last all of
332 lastUser = case Array.last all of
553 Just u -> u
333 Just u -> u
554 Nothing -> { id: 0, name: "", email: "", active: true, role: "" }
334 Nothing -> { id: 0, name: "", email: "", active: true, role: "" }
555− page <- conn # run do usersTable # findBefore @"id" lastUser.id 2
335+ page <- usersTable # findBefore @"id" lastUser.id 2
556 Array.length page.items `shouldEqual` 2
336 Array.length page.items `shouldEqual` 2
557 page.hasMore `shouldEqual` true
337 page.hasMore `shouldEqual` true
558
338
559− it "paginates with a filter applied" $ withUsersTable \conn -> do
339+ it "paginates with a filter applied" $ withUsersTable do
560− _ <- conn # run do
561− usersTable # createMany
340+ _ <- usersTable # createMany
562− [ { name: "Alice", email: "a@t.com", role: "admin" }
341+ [ { name: "Alice", email: "a@t.com", role: "admin" }
563− , { name: "Bob", email: "b@t.com", role: "user" }
342+ , { name: "Bob", email: "b@t.com", role: "user" }
564− , { name: "Carol", email: "c@t.com", role: "user" }
343+ , { name: "Carol", email: "c@t.com", role: "user" }
565− , { name: "Dave", email: "d@t.com", role: "user" }
344+ , { name: "Dave", email: "d@t.com", role: "user" }
566− , { name: "Eve", email: "e@t.com", role: "admin" }
345+ , { name: "Eve", email: "e@t.com", role: "admin" }
567− ]
346+ ]
568− page1 <- conn # run do usersTable # findFirstWhere @"id" { role: "user" } 2
347+ page1 <- usersTable # findFirstWhere @"id" { role: "user" } 2
569 Array.length page1.items `shouldEqual` 2
348 Array.length page1.items `shouldEqual` 2
570 page1.hasMore `shouldEqual` true
349 page1.hasMore `shouldEqual` true
571 let
350 let
572 lastId = case Array.last page1.items of
351 lastId = case Array.last page1.items of
573 Just u -> u.id
352 Just u -> u.id
574 Nothing -> 0
353 Nothing -> 0
575− page2 <- conn # run do usersTable # findAfterWhere @"id" { role: "user" } lastId 2
354+ page2 <- usersTable # findAfterWhere @"id" { role: "user" } lastId 2
576 Array.length page2.items `shouldEqual` 1
355 Array.length page2.items `shouldEqual` 1
577 page2.hasMore `shouldEqual` false
356 page2.hasMore `shouldEqual` false
578
357
579 describe "Aggregations and checks" do
358 describe "Aggregations and checks" do
580− it "counts rows, optionally with a filter" $ withUsersTable \conn -> do
359+ it "counts rows, optionally with a filter" $ withUsersTable do
581− n0 <- conn # runNoErr do usersTable # count
360+ n0 <- usersTable # count
582 n0 `shouldEqual` 0
361 n0 `shouldEqual` 0
583− _ <- conn # run do
584− usersTable # createMany
362+ _ <- usersTable # createMany
585− [ { name: "Alice", email: "a@t.com", role: "admin" }
363+ [ { name: "Alice", email: "a@t.com", role: "admin" }
586− , { name: "Bob", email: "b@t.com", role: "user" }
364+ , { name: "Bob", email: "b@t.com", role: "user" }
587− , { name: "Carol", email: "c@t.com", role: "admin" }
365+ , { name: "Carol", email: "c@t.com", role: "admin" }
588− ]
366+ ]
589− n3 <- conn # runNoErr do usersTable # count
367+ n3 <- usersTable # count
590 n3 `shouldEqual` 3
368 n3 `shouldEqual` 3
591− nAdmins <- conn # runNoErr do usersTable # countWhere { role: "admin" }
369+ nAdmins <- usersTable # countWhere { role: "admin" }
592 nAdmins `shouldEqual` 2
370 nAdmins `shouldEqual` 2
593
371
594− it "checks whether a record exists" $ withUsersTable \conn -> do
372+ it "checks whether a record exists" $ withUsersTable do
595− created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
373+ created <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
596 alice <- shouldBeJust created
374 alice <- shouldBeJust created
597− yes <- conn # runNoErr do usersTable # exists alice.id
375+ yes <- usersTable # exists alice.id
598 yes `shouldEqual` true
376 yes `shouldEqual` true
599− no <- conn # runNoErr do usersTable # exists 99999
377+ no <- usersTable # exists 99999
600 no `shouldEqual` false
378 no `shouldEqual` false
601− yesWhere <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "admin" }
379+ yesWhere <- usersTable # existsWhere { name: "Alice", role: "admin" }
602 yesWhere `shouldEqual` true
380 yesWhere `shouldEqual` true
603− noWhere <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "user" }
381+ noWhere <- usersTable # existsWhere { name: "Alice", role: "user" }
604 noWhere `shouldEqual` false
382 noWhere `shouldEqual` false
605
383
606− it "sums and averages a numeric column" $ withUsersTable \conn -> do
384+ it "sums and averages a numeric column" $ withUsersTable do
607− _ <- conn # run do
608− usersTable # createMany
385+ _ <- usersTable # createMany
609− [ { name: "Alice", email: "a@t.com", role: "admin" }
386+ [ { name: "Alice", email: "a@t.com", role: "admin" }
610− , { name: "Bob", email: "b@t.com", role: "user" }
387+ , { name: "Bob", email: "b@t.com", role: "user" }
611− ]
388+ ]
612− total <- conn # runNoErr do usersTable # sumOf @"id"
389+ total <- usersTable # sumOf @"id"
613 (total > 0.0) `shouldEqual` true
390 (total > 0.0) `shouldEqual` true
614− emptySum <- conn # runNoErr do usersTable # sumOfWhere @"id" { role: "nobody" }
391+ emptySum <- usersTable # sumOfWhere @"id" { role: "nobody" }
615 emptySum `shouldEqual` 0.0
392 emptySum `shouldEqual` 0.0
616− avg <- conn # runNoErr do usersTable # avgOf @"id"
393+ avg <- usersTable # avgOf @"id"
617 isNothing avg `shouldEqual` false
394 isNothing avg `shouldEqual` false
618− emptyAvg <- conn # runNoErr do usersTable # avgOfWhere @"id" { role: "nobody" }
395+ emptyAvg <- usersTable # avgOfWhere @"id" { role: "nobody" }
619 isNothing emptyAvg `shouldEqual` true
396 isNothing emptyAvg `shouldEqual` true
620
397
621− it "finds the min and max of a column" $ withUsersTable \conn -> do
398+ it "finds the min and max of a column" $ withUsersTable do
622− _ <- conn # run do
623− usersTable # createMany
399+ _ <- usersTable # createMany
624− [ { name: "Alice", email: "a@t.com", role: "admin" }
400+ [ { name: "Alice", email: "a@t.com", role: "admin" }
625− , { name: "Bob", email: "b@t.com", role: "user" }
401+ , { name: "Bob", email: "b@t.com", role: "user" }
626− , { name: "Carol", email: "c@t.com", role: "user" }
402+ , { name: "Carol", email: "c@t.com", role: "user" }
627− ]
403+ ]
628− minName <- conn # run do usersTable # minOf @"name"
404+ minName <- usersTable # minOf @"name"
629 minName `shouldEqual` (Just "Alice")
405 minName `shouldEqual` (Just "Alice")
630− maxName <- conn # run do usersTable # maxOf @"name"
406+ maxName <- usersTable # maxOf @"name"
631 maxName `shouldEqual` (Just "Carol")
407 maxName `shouldEqual` (Just "Carol")
632− noMin <- conn # run do usersTable # minOfWhere @"name" { role: "nobody" }
408+ noMin <- usersTable # minOfWhere @"name" { role: "nobody" }
633 isNothing noMin `shouldEqual` true
409 isNothing noMin `shouldEqual` true
634
410
635− it "lists distinct values of a column" $ withUsersTable \conn -> do
411+ it "lists distinct values of a column" $ withUsersTable do
636− _ <- conn # run do
637− usersTable # createMany
412+ _ <- usersTable # createMany
638− [ { name: "Alice", email: "a@t.com", role: "admin" }
413+ [ { name: "Alice", email: "a@t.com", role: "admin" }
639− , { name: "Bob", email: "b@t.com", role: "user" }
414+ , { name: "Bob", email: "b@t.com", role: "user" }
640− , { name: "Carol", email: "c@t.com", role: "user" }
415+ , { name: "Carol", email: "c@t.com", role: "user" }
641− ]
416+ ]
642− roles <- conn # run do usersTable # findDistinct @"role"
417+ roles <- usersTable # findDistinct @"role"
643 Array.sort roles `shouldEqual` [ "admin", "user" ]
418 Array.sort roles `shouldEqual` [ "admin", "user" ]
644− activeRoles <- conn # run do usersTable # findDistinctWhere @"role" { active: true }
419+ activeRoles <- usersTable # findDistinctWhere @"role" { active: true }
645 Array.sort activeRoles `shouldEqual` [ "admin", "user" ]
420 Array.sort activeRoles `shouldEqual` [ "admin", "user" ]
646
421
647 describe "Selecting specific columns" do
422 describe "Selecting specific columns" do
648− it "fetches only the columns you need" $ withUsersTable \conn -> do
423+ it "fetches only the columns you need" $ withUsersTable do
649− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
424+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
650− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
425+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
651− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
426+ _ <- usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
652− rows <- conn # run do usersTable # selectFrom @"name, email"
427+ rows <- usersTable # selectFrom @"name, email"
653 Array.length rows `shouldEqual` 3
428 Array.length rows `shouldEqual` 3
654 let
429 let
655 first = case Array.head rows of
430 first = case Array.head rows of
@@ -657,31 +432,31 @@ spec = describe "ClientOm" do
657 Nothing -> { name: "", email: "" }
432 Nothing -> { name: "", email: "" }
658 first.name `shouldEqual` "Alice"
433 first.name `shouldEqual` "Alice"
659 first.email `shouldEqual` "a@t.com"
434 first.email `shouldEqual` "a@t.com"
660− filtered <- conn # run do usersTable # selectFromWhere @"name" { role: "user" }
435+ filtered <- usersTable # selectFromWhere @"name" { role: "user" }
661 Array.length filtered `shouldEqual` 2
436 Array.length filtered `shouldEqual` 2
662 Array.sort (map _.name filtered) `shouldEqual` [ "Bob", "Carol" ]
437 Array.sort (map _.name filtered) `shouldEqual` [ "Bob", "Carol" ]
663
438
664− it "fetches one row with specific columns and a filter" $ withUsersTable \conn -> do
439+ it "fetches one row with specific columns and a filter" $ withUsersTable do
665− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
440+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
666− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
441+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
667− found <- conn # run do usersTable # selectOneFrom @"name, role"
442+ found <- usersTable # selectOneFrom @"name, role"
668 r <- shouldBeJust found
443 r <- shouldBeJust found
669 r.name `shouldEqual` "Alice"
444 r.name `shouldEqual` "Alice"
670 r.role `shouldEqual` "admin"
445 r.role `shouldEqual` "admin"
671− byName <- conn # run do usersTable # selectOneFromWhere @"email" { name: "Bob" }
446+ byName <- usersTable # selectOneFromWhere @"email" { name: "Bob" }
672 r2 <- shouldBeJust byName
447 r2 <- shouldBeJust byName
673 r2.email `shouldEqual` "b@t.com"
448 r2.email `shouldEqual` "b@t.com"
674− missing <- conn # run do usersTable # selectOneFromWhere @"email" { name: "Nobody" }
449+ missing <- usersTable # selectOneFromWhere @"email" { name: "Nobody" }
675 isNothing missing `shouldEqual` true
450 isNothing missing `shouldEqual` true
676
451
677 describe "Relationships" do
452 describe "Relationships" do
678− it "joins a child table to its parent (flat)" $ withPostsTable \conn -> do
453+ it "joins a child table to its parent (flat)" $ withPostsTable do
679− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
454+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
680− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
455+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
681− _ <- conn # run do postsTable # create { title: "First Post", body: "Hello", user_id: 1 }
456+ _ <- postsTable # create { title: "First Post", body: "Hello", user_id: 1 }
682− _ <- conn # run do postsTable # create { title: "Second Post", body: "World", user_id: 1 }
457+ _ <- postsTable # create { title: "Second Post", body: "World", user_id: 1 }
683− _ <- conn # run do postsTable # create { title: "Bob's Post", body: "Hi", user_id: 2 }
458+ _ <- postsTable # create { title: "Bob's Post", body: "Hi", user_id: 2 }
684− rows <- conn # run do findWith postsTable usersTable
459+ rows <- findWith postsTable usersTable
685 Array.length rows `shouldEqual` 3
460 Array.length rows `shouldEqual` 3
686 let
461 let
687 r = case Array.head rows of
462 r = case Array.head rows of
@@ -690,20 +465,20 @@ spec = describe "ClientOm" do
690 r.test_om_posts.title `shouldEqual` "First Post"
465 r.test_om_posts.title `shouldEqual` "First Post"
691 r.test_om_users.name `shouldEqual` "Alice"
466 r.test_om_users.name `shouldEqual` "Alice"
692
467
693− it "finds a joined record by primary key" $ withPostsTable \conn -> do
468+ it "finds a joined record by primary key" $ withPostsTable do
694− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
469+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
695− _ <- conn # run do postsTable # create { title: "My Post", body: "Content", user_id: 1 }
470+ _ <- postsTable # create { title: "My Post", body: "Content", user_id: 1 }
696− found <- conn # run do findByIdWith 1 postsTable usersTable
471+ found <- findByIdWith 1 postsTable usersTable
697 r <- shouldBeJust found
472 r <- shouldBeJust found
698 r.test_om_posts.title `shouldEqual` "My Post"
473 r.test_om_posts.title `shouldEqual` "My Post"
699 r.test_om_users.name `shouldEqual` "Alice"
474 r.test_om_users.name `shouldEqual` "Alice"
700
475
701− it "joins with a filter" $ withPostsTable \conn -> do
476+ it "joins with a filter" $ withPostsTable do
702− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
477+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
703− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
478+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
704− _ <- conn # run do postsTable # create { title: "Post A", body: "A", user_id: 1 }
479+ _ <- postsTable # create { title: "Post A", body: "A", user_id: 1 }
705− _ <- conn # run do postsTable # create { title: "Post B", body: "B", user_id: 2 }
480+ _ <- postsTable # create { title: "Post B", body: "B", user_id: 2 }
706− rows <- conn # run do findWithWhere { user_id: 1 } postsTable usersTable
481+ rows <- findWithWhere { user_id: 1 } postsTable usersTable
707 Array.length rows `shouldEqual` 1
482 Array.length rows `shouldEqual` 1
708 let
483 let
709 r = case Array.head rows of
484 r = case Array.head rows of
@@ -711,12 +486,12 @@ spec = describe "ClientOm" do
711 Nothing -> { test_om_posts: { id: 0, title: "", body: "", user_id: 0 }, test_om_users: { id: 0, name: "", email: "", active: false, role: "" } }
486 Nothing -> { test_om_posts: { id: 0, title: "", body: "", user_id: 0 }, test_om_users: { id: 0, name: "", email: "", active: false, role: "" } }
712 r.test_om_posts.title `shouldEqual` "Post A"
487 r.test_om_posts.title `shouldEqual` "Post A"
713
488
714− it "nests children under their parent" $ withPostsTable \conn -> do
489+ it "nests children under their parent" $ withPostsTable do
715− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
490+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
716− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
491+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
717− _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
492+ _ <- postsTable # create { title: "Post 1", body: "A", user_id: 1 }
718− _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
493+ _ <- postsTable # create { title: "Post 2", body: "B", user_id: 1 }
719− rows <- conn # run do findIncluding @"posts" usersTable postsTable
494+ rows <- findIncluding @"posts" usersTable postsTable
720 Array.length rows `shouldEqual` 2
495 Array.length rows `shouldEqual` 2
721 let
496 let
722 alice = case Array.head rows of
497 alice = case Array.head rows of
@@ -725,22 +500,22 @@ spec = describe "ClientOm" do
725 alice.name `shouldEqual` "Alice"
500 alice.name `shouldEqual` "Alice"
726 Array.length alice.posts `shouldEqual` 2
501 Array.length alice.posts `shouldEqual` 2
727
502
728− it "nests children for a single parent by id" $ withPostsTable \conn -> do
503+ it "nests children for a single parent by id" $ withPostsTable do
729− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
504+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
730− _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
505+ _ <- postsTable # create { title: "Post 1", body: "A", user_id: 1 }
731− _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
506+ _ <- postsTable # create { title: "Post 2", body: "B", user_id: 1 }
732− found <- conn # run do findByIdIncluding @"posts" 1 usersTable postsTable
507+ found <- findByIdIncluding @"posts" 1 usersTable postsTable
733 r <- shouldBeJust found
508 r <- shouldBeJust found
734 r.name `shouldEqual` "Alice"
509 r.name `shouldEqual` "Alice"
735 Array.length r.posts `shouldEqual` 2
510 Array.length r.posts `shouldEqual` 2
736
511
737− it "uses the join combinator to compose tables" $ withPostsTable \conn -> do
512+ it "uses the join combinator to compose tables" $ withPostsTable do
738− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
513+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
739− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
514+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
740− _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
515+ _ <- postsTable # create { title: "Post 1", body: "A", user_id: 1 }
741− _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
516+ _ <- postsTable # create { title: "Post 2", body: "B", user_id: 1 }
742− _ <- conn # run do postsTable # create { title: "Post 3", body: "C", user_id: 2 }
517+ _ <- postsTable # create { title: "Post 3", body: "C", user_id: 2 }
743− rows <- conn # run do (usersTable # join postsTable) # findAll
518+ rows <- (usersTable # join postsTable) # findAll
744 Array.length rows `shouldEqual` 2
519 Array.length rows `shouldEqual` 2
745 let
520 let
746 alice = case Array.head rows of
521 alice = case Array.head rows of
@@ -749,12 +524,12 @@ spec = describe "ClientOm" do
749 alice.name `shouldEqual` "Alice"
524 alice.name `shouldEqual` "Alice"
750 Array.length alice.test_om_posts `shouldEqual` 2
525 Array.length alice.test_om_posts `shouldEqual` 2
751
526
752− it "joins multiple child tables at once" $ withPostsAndCommentsTable \conn -> do
527+ it "joins multiple child tables at once" $ withPostsAndCommentsTable do
753− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
528+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
754− _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
529+ _ <- postsTable # create { title: "Post 1", body: "A", user_id: 1 }
755− _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
530+ _ <- postsTable # create { title: "Post 2", body: "B", user_id: 1 }
756− _ <- conn # run do commentsTable # create { text: "Nice!", user_id: 1 }
531+ _ <- commentsTable # create { text: "Nice!", user_id: 1 }
757− rows <- conn # run do (usersTable # join postsTable # join commentsTable) # findAll
532+ rows <- (usersTable # join postsTable # join commentsTable) # findAll
758 Array.length rows `shouldEqual` 1
533 Array.length rows `shouldEqual` 1
759 let
534 let
760 r = case Array.head rows of
535 r = case Array.head rows of
@@ -765,27 +540,239 @@ spec = describe "ClientOm" do
765 Array.length r.test_om_comments `shouldEqual` 1
540 Array.length r.test_om_comments `shouldEqual` 1
766
541
767 describe "Transactions" do
542 describe "Transactions" do
768− it "commits multiple inserts atomically" $ withUsersTable \conn -> do
543+ it "commits multiple inserts atomically" $ withUsersTable do
769− conn # run do
544+ transactionally do
770− withTransaction do
771− _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
545+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
772− _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
546+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
773− pure unit
547+ pure unit
774− n <- conn # runNoErr do usersTable # count
548+ n <- usersTable # count
775 n `shouldEqual` 2
549 n `shouldEqual` 2
776
550
777 describe "UUIDs and newtypes" do
551 describe "UUIDs and newtypes" do
778− it "round-trips a UUID primary key through create/find/exists" $ withUuidTable \conn -> do
552+ it "round-trips a UUID primary key through create/find/exists" $ withUuidTable do
779− created <- conn # run do uuidTable # create { label: "first" }
553+ created <- uuidTable # create { label: "first" }
780 row <- shouldBeJust created
554 row <- shouldBeJust created
781 row.label `shouldEqual` "first"
555 row.label `shouldEqual` "first"
782− found <- conn # run do uuidTable # findById row.id
556+ found <- uuidTable # findById row.id
783 r2 <- shouldBeJust found
557 r2 <- shouldBeJust found
784 r2.label `shouldEqual` "first"
558 r2.label `shouldEqual` "first"
785− byCol <- conn # run do uuidTable # findBy @"label" "first"
559+ byCol <- uuidTable # findBy @"label" "first"
786 Array.length byCol `shouldEqual` 1
560 Array.length byCol `shouldEqual` 1
787− byId <- conn # run do uuidTable # findOneBy @"id" row.id
561+ byId <- uuidTable # findOneBy @"id" row.id
788 r3 <- shouldBeJust byId
562 r3 <- shouldBeJust byId
789 r3.label `shouldEqual` "first"
563 r3.label `shouldEqual` "first"
790− yes <- conn # runNoErr do uuidTable # exists row.id
564+ yes <- uuidTable # exists row.id
791 yes `shouldEqual` true
565 yes `shouldEqual` true
566+
567+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
568+-- Test schemas
569+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
570+
571+type UsersTable = Table "test_om_users"
572+ ( id :: PrimaryKey (AutoIncrement Int)
573+ , name :: String
574+ , email :: Unique String
575+ , active :: Default "true" Boolean
576+ , role :: String
577+ )
578+
579+usersTable :: Proxy UsersTable
580+usersTable = Proxy
581+
582+type SettingsTable = Table "test_om_settings"
583+ ( key :: PrimaryKey String
584+ , value :: String
585+ )
586+
587+settingsTable :: Proxy SettingsTable
588+settingsTable = Proxy
589+
590+type UuidTable = Table "test_om_uuid_things"
591+ ( id :: PGUUID # PrimaryKey # Default "gen_random_uuid()"
592+ , label :: String
593+ )
594+
595+uuidTable :: Proxy UuidTable
596+uuidTable = Proxy
597+
598+type PostsTable = Table "test_om_posts"
599+ ( id :: PrimaryKey (AutoIncrement Int)
600+ , title :: String
601+ , body :: String
602+ , user_id :: ForeignKey "test_om_users" References "id" Int
603+ )
604+
605+postsTable :: Proxy PostsTable
606+postsTable = Proxy
607+
608+type CommentsTable = Table "test_om_comments"
609+ ( id :: PrimaryKey (AutoIncrement Int)
610+ , text :: String
611+ , user_id :: ForeignKey "test_om_users" References "id" Int
612+ )
613+
614+commentsTable :: Proxy CommentsTable
615+commentsTable = Proxy
616+
617+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
618+-- Helpers
619+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
620+
621+type TestOm = Om.Om { postgresConnection :: PG.Connection } (parseError :: MultipleErrors) Unit
622+
623+shouldEqual :: forall m t. MonadEffect m => Show t => Eq t => t -> t -> m Unit
624+shouldEqual v1 v2 = when (v1 /= v2) $ liftEffect $ Exception.throwException $ Exception.error $ show v1 <> " ≠ " <> show v2
625+
626+shouldBeJust :: forall m a. MonadEffect m => Maybe a -> m a
627+shouldBeJust (Just a) = pure a
628+shouldBeJust Nothing = liftEffect $ Exception.throwException $ Exception.error "Expected Just but got Nothing"
629+
630+testConnection :: Aff PG.Connection
631+testConnection =
632+ PG.postgres
633+ { host: PG.PostgresHost "localhost"
634+ , port: PG.PostgresPort 5433
635+ , database: PG.PostgresDatabase "test_playground"
636+ , username: PG.PostgresUsername "postgres"
637+ , password: PG.PostgresPassword "postgres"
638+ } # liftEffect
639+
640+runOm :: TestOm -> Aff Unit
641+runOm om = do
642+ conn <- testConnection
643+ Om.runOm { postgresConnection: conn }
644+ { exception: \e -> throwError e
645+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
646+ }
647+ om
648+ PG.end conn
649+
650+withUsersTable :: TestOm -> Aff Unit
651+withUsersTable test = do
652+ conn <- testConnection
653+ _ <- PG.executeSimple createDDL conn
654+ Om.runOm { postgresConnection: conn }
655+ { exception: \e -> throwError e
656+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
657+ }
658+ test
659+ _ <- PG.executeSimple dropDDL conn
660+ PG.end conn
661+ where
662+ dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE"
663+ createDDL = PG.SQL
664+ """DROP TABLE IF EXISTS test_om_users CASCADE;
665+ CREATE TABLE test_om_users (
666+ id SERIAL PRIMARY KEY,
667+ name TEXT NOT NULL,
668+ email TEXT UNIQUE NOT NULL,
669+ active BOOLEAN DEFAULT true NOT NULL,
670+ role TEXT NOT NULL
671+ )"""
672+
673+withSettingsTable :: TestOm -> Aff Unit
674+withSettingsTable test = do
675+ conn <- testConnection
676+ _ <- PG.executeSimple createDDL conn
677+ Om.runOm { postgresConnection: conn }
678+ { exception: \e -> throwError e
679+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
680+ }
681+ test
682+ _ <- PG.executeSimple dropDDL conn
683+ PG.end conn
684+ where
685+ dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_settings CASCADE"
686+ createDDL = PG.SQL
687+ """DROP TABLE IF EXISTS test_om_settings CASCADE;
688+ CREATE TABLE test_om_settings (
689+ key TEXT PRIMARY KEY,
690+ value TEXT NOT NULL
691+ )"""
692+
693+withUuidTable :: TestOm -> Aff Unit
694+withUuidTable test = do
695+ conn <- testConnection
696+ _ <- PG.executeSimple createDDL conn
697+ Om.runOm { postgresConnection: conn }
698+ { exception: \e -> throwError e
699+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
700+ }
701+ test
702+ _ <- PG.executeSimple dropDDL conn
703+ PG.end conn
704+ where
705+ dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_uuid_things CASCADE"
706+ createDDL = PG.SQL
707+ """DROP TABLE IF EXISTS test_om_uuid_things CASCADE;
708+ CREATE TABLE test_om_uuid_things (
709+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
710+ label TEXT NOT NULL
711+ )"""
712+
713+withPostsTable :: TestOm -> Aff Unit
714+withPostsTable test = do
715+ conn <- testConnection
716+ _ <- PG.executeSimple createDDL conn
717+ Om.runOm { postgresConnection: conn }
718+ { exception: \e -> throwError e
719+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
720+ }
721+ test
722+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
723+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
724+ PG.end conn
725+ where
726+ createDDL = PG.SQL
727+ """DROP TABLE IF EXISTS test_om_posts CASCADE;
728+ DROP TABLE IF EXISTS test_om_users CASCADE;
729+ CREATE TABLE test_om_users (
730+ id SERIAL PRIMARY KEY,
731+ name TEXT NOT NULL,
732+ email TEXT UNIQUE NOT NULL,
733+ active BOOLEAN DEFAULT true NOT NULL,
734+ role TEXT NOT NULL
735+ );
736+ CREATE TABLE test_om_posts (
737+ id SERIAL PRIMARY KEY,
738+ title TEXT NOT NULL,
739+ body TEXT NOT NULL,
740+ user_id INTEGER NOT NULL REFERENCES test_om_users(id)
741+ )"""
742+
743+withPostsAndCommentsTable :: TestOm -> Aff Unit
744+withPostsAndCommentsTable test = do
745+ conn <- testConnection
746+ _ <- PG.executeSimple createDDL conn
747+ Om.runOm { postgresConnection: conn }
748+ { exception: \e -> throwError e
749+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
750+ }
751+ test
752+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_comments CASCADE") conn
753+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
754+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
755+ PG.end conn
756+ where
757+ createDDL = PG.SQL
758+ """DROP TABLE IF EXISTS test_om_comments CASCADE;
759+ DROP TABLE IF EXISTS test_om_posts CASCADE;
760+ DROP TABLE IF EXISTS test_om_users CASCADE;
761+ CREATE TABLE test_om_users (
762+ id SERIAL PRIMARY KEY,
763+ name TEXT NOT NULL,
764+ email TEXT UNIQUE NOT NULL,
765+ active BOOLEAN DEFAULT true NOT NULL,
766+ role TEXT NOT NULL
767+ );
768+ CREATE TABLE test_om_posts (
769+ id SERIAL PRIMARY KEY,
770+ title TEXT NOT NULL,
771+ body TEXT NOT NULL,
772+ user_id INTEGER NOT NULL REFERENCES test_om_users(id)
773+ );
774+ CREATE TABLE test_om_comments (
775+ id SERIAL PRIMARY KEY,
776+ text TEXT NOT NULL,
777+ user_id INTEGER NOT NULL REFERENCES test_om_users(id)
778+ )"""
test/Test/SchemaQ.purs+585−104
@@ -2,40 +2,564 @@ module Test.SchemaQ where
2
2
3import Prelude
3import Prelude
4
4
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)
6import Data.Array as Array
8import Data.Array as Array
7import Data.Maybe (Maybe(..))
9import 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)
9−import Effect.Class (liftEffect)
14+import Effect.Aff.Class (liftAff)
15+import Effect.Aff.AVar as AVar
16+import Effect.Class (class MonadEffect, liftEffect)
10−import Effect.Exception (error)
17+import Effect.Exception as Exception
18+import Effect.Ref as Ref
11−import Foreign (MultipleErrors)
19+import Foreign (ForeignError(..), MultipleErrors)
12import Test.Spec (Spec, describe, it)
20import Test.Spec (Spec, describe, it)
13−import Test.Spec.Assertions (shouldEqual)
14import Type.Proxy (Proxy(..))
21import Type.Proxy (Proxy(..))
22+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
15import Yoga.Om as Om
23import Yoga.Om as Om
16import Yoga.Postgres as PG
24import Yoga.Postgres as PG
17−import Yoga.Postgres.ClientOm (run, runOne, create) as Q
25+import Yoga.Postgres.ClientOm (create, exec, run, runOne, transactionally)
18−import Yoga.Postgres.Schema (Table, PrimaryKey, AutoIncrement, Unique, Default, from, select, where_, orderBy, limit, groupBy, insert, delete, returning, returningAll, union)
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_)
27+
28+spec :: Spec Unit
29+spec = describe "Query builder" do
30+
31+ -- ── SELECT ──────────────────────────────────────────────────
32+
33+ it "selects columns with a WHERE clause" $ withUsersTable do
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 {}
36+ rows <- from usersTable
37+ # select @"name, email"
38+ # where_ @"role = $role"
39+ # run { role: Admin }
40+ rows `shouldEqual` [ { name: "Alice", email: Email "a@t.com" } ]
41+
42+ it "orders and limits results" $ withUsersTable do
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 {}
46+ rows <- from usersTable # select @"name" # orderBy @"name" # limit @"2" # run {}
47+ rows `shouldEqual` [ { name: "Alice" }, { name: "Bob" } ]
48+
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+
56+ it "groups results" $ withUsersTable do
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 {}
60+ rows <- from usersTable # select @"role" # groupBy @"role" # orderBy @"role" # run {}
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 } ]
74+
75+ it "filters with a BETWEEN range" $ withUsersTable do
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 {}
79+ rows <- from usersTable # select @"name" # where_ @"id BETWEEN $lo AND $hi" # orderBy @"id" # run { lo: 2, hi: 3 }
80+ rows `shouldEqual` [ { name: "Bob" }, { name: "Carol" } ]
81+
82+ -- ── INSERT ──────────────────────────────────────────────────
83+
84+ it "inserts a row and returns specific columns" $ withUsersTable do
85+ rows <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"name" # run {}
86+ rows `shouldEqual` [ { name: "Alice" } ]
87+
88+ it "inserts a row with default created_at and updated_at" $ withUsersTable do
89+ row <- into usersTable # insert { name: "Alice", email: Email "a@t.com", role: Admin } # returning @"created_at, updated_at" # runOne {}
90+ r <- shouldBeJust row
91+ r.created_at `shouldEqual` r.updated_at
92+
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+
329+ it "deletes with a condition and returns the deleted rows" $ withUsersTable do
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 }
333+ rows `shouldEqual` [ { name: "Alice" } ]
334+
335+ -- ── UPDATE ──────────────────────────────────────────────────
336+
337+ it "updates a row with SET and WHERE" $ withUsersTable do
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" }
341+ rows <- from usersTable # select @"name, role" # orderBy @"name" # run {}
342+ rows `shouldEqual` [ { name: "Alice", role: Admin }, { name: "Bob", role: Mod } ]
343+
344+ it "updates multiple columns" $ withUsersTable do
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" }
347+ rows <- from usersTable # select @"name, role" # run {}
348+ rows `shouldEqual` [ { name: "Alicia", role: Mod } ]
349+
350+ it "updates and returns the modified row" $ withUsersTable do
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 ──────────────────────────────────────────
360+
361+ it "combines two queries with UNION" $ withUsersTable do
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" } ]
19
481
20-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
482-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21-- Test schema
483-- Test schema
22-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
484-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
485
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+
24type UsersTable = Table "test_om_users"
527type UsersTable = Table "test_om_users"
25 ( id :: PrimaryKey (AutoIncrement Int)
528 ( id :: PrimaryKey (AutoIncrement Int)
26 , name :: String
529 , name :: String
27− , email :: Unique String
530+ , email :: Unique Email
28 , active :: Default "true" Boolean
531 , active :: Default "true" Boolean
29− , role :: String
532+ , role :: Role
533+ , created_at :: Default "now()" TimestampTZ
534+ , updated_at :: Default "now()" TimestampTZ
30 )
535 )
31
536
32usersTable :: Proxy UsersTable
537usersTable :: Proxy UsersTable
33usersTable = Proxy
538usersTable = Proxy
34
539
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+
35-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
549-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36-- Helpers
550-- Helpers
37-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
551-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38
552
553+type TestOm = Om.Om { postgresConnection :: PG.Connection } (parseError :: MultipleErrors) Unit
554+
555+shouldEqual :: forall m t. MonadEffect m => Show t => Eq t => t -> t -> m Unit
556+shouldEqual v1 v2 = liftEffect $ when (v1 /= v2) do
557+ Exception.throwException $ Exception.error $ show v1 <> " ≠ " <> show v2
558+
559+shouldBeJust :: forall m a. MonadEffect m => Maybe a -> m a
560+shouldBeJust (Just a) = pure a
561+shouldBeJust Nothing = liftEffect $ Exception.throwException $ Exception.error "Expected Just but got Nothing"
562+
39testConnection :: Aff PG.Connection
563testConnection :: Aff PG.Connection
40testConnection =
564testConnection =
41 PG.postgres
565 PG.postgres
@@ -46,105 +570,62 @@ testConnection =
46 , password: PG.PostgresPassword "postgres"
570 , password: PG.PostgresPassword "postgres"
47 } # liftEffect
571 } # liftEffect
48
572
49−exec
50− :: forall a
51− . Om.Om { postgresConnection :: PG.Connection } (parseError :: MultipleErrors) a
52− -> PG.Connection
53− -> Aff a
54−exec om conn = Om.runOm { postgresConnection: conn }
55− { exception: \e -> throwError e
56− , parseError: \errs -> throwError (error ("Parse error: " <> show errs))
57− }
58− om
59−
60−shouldBeJust :: forall a. Maybe a -> Aff a
61−shouldBeJust (Just a) = pure a
62−shouldBeJust Nothing = throwError $ error "Expected Just but got Nothing"
63−
64−withUsersTable :: (PG.Connection -> Aff Unit) -> Aff Unit
573+withUsersTable :: TestOm -> Aff Unit
65withUsersTable test = do
574withUsersTable test = do
66 conn <- testConnection
575 conn <- testConnection
67 _ <- PG.executeSimple createDDL conn
576 _ <- PG.executeSimple createDDL conn
68− test conn
577+ Om.runOm { postgresConnection: conn }
578+ { exception: throwError
579+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
580+ }
581+ test
69 _ <- PG.executeSimple dropDDL conn
582 _ <- PG.executeSimple dropDDL conn
70 PG.end conn
583 PG.end conn
71 where
584 where
72− dropDDL = PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE"
585+ dropStr = "DROP TABLE IF EXISTS test_om_users CASCADE"
586+ dropDDL = PG.SQL dropStr
73− createDDL = PG.SQL
587+ createDDL = PG.SQL (dropStr <> "; " <> createTableDDL @UsersTable)
74− """DROP TABLE IF EXISTS test_om_users CASCADE;
75− CREATE TABLE test_om_users (
76− id SERIAL PRIMARY KEY,
77− name TEXT NOT NULL,
78− email TEXT UNIQUE NOT NULL,
79− active BOOLEAN DEFAULT true NOT NULL,
80− role TEXT NOT NULL
81− )"""
82
588
83−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
589+withSingleConnectionUsersTable :: TestOm -> Aff Unit
84−-- Spec
590+withSingleConnectionUsersTable test = do
85−-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
591+ conn <- PG.postgres
86−
592+ { host: PG.PostgresHost "localhost"
87−spec :: Spec Unit
593+ , port: PG.PostgresPort 5433
88−spec = describe "Query builder" do
594+ , database: PG.PostgresDatabase "test_playground"
89−
595+ , username: PG.PostgresUsername "postgres"
90− it "selects columns with a WHERE clause" $ withUsersTable \conn -> do
596+ , password: PG.PostgresPassword "postgres"
91− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
597+ , max: PG.MaxConnections 1
92− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
598+ } # liftEffect
93− rows <- conn # exec do Q.run { role: "admin" } (from usersTable # select @"name, email" # where_ @"role = $role")
599+ _ <- PG.executeSimple createDDL conn
94− Array.length rows `shouldEqual` 1
600+ Om.runOm { postgresConnection: conn }
95− let
601+ { exception: throwError
96− r = case Array.head rows of
602+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
97− Just x -> x
603+ }
98− Nothing -> { name: "", email: "" }
604+ test
99− r.name `shouldEqual` "Alice"
605+ _ <- PG.executeSimple dropDDL conn
100−
606+ PG.end conn
101− it "orders and limits results" $ withUsersTable \conn -> do
607+ where
102− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
608+ dropStr = "DROP TABLE IF EXISTS test_om_users CASCADE"
103− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
609+ dropDDL = PG.SQL dropStr
104− _ <- conn # exec do usersTable # Q.create { name: "Carol", email: "c@t.com", role: "user" }
610+ createDDL = PG.SQL (dropStr <> "; " <> createTableDDL @UsersTable)
105− rows <- conn # exec do Q.run {} (from usersTable # select @"name" # orderBy @"name" # limit @"2")
106− Array.length rows `shouldEqual` 2
107− (map _.name rows) `shouldEqual` [ "Alice", "Bob" ]
108−
109− it "groups results" $ withUsersTable \conn -> do
110− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
111− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
112− _ <- conn # exec do usersTable # Q.create { name: "Carol", email: "c@t.com", role: "user" }
113− rows <- conn # exec do Q.run {} (from usersTable # select @"role" # groupBy @"role" # orderBy @"role")
114− (map _.role rows) `shouldEqual` [ "admin", "user" ]
115−
116− it "filters with a BETWEEN range" $ withUsersTable \conn -> do
117− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
118− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
119− _ <- conn # exec do usersTable # Q.create { name: "Carol", email: "c@t.com", role: "mod" }
120− rows <- conn # exec do Q.run { lo: 2, hi: 3 } (from usersTable # select @"name" # where_ @"id BETWEEN $lo AND $hi" # orderBy @"id")
121− (map _.name rows) `shouldEqual` [ "Bob", "Carol" ]
122−
123− it "inserts a row and returns specific columns" $ withUsersTable \conn -> do
124− row <- conn # exec do Q.runOne {} (from usersTable # insert { name: "Alice", email: "a@t.com", role: "admin" } # returning @"id, name")
125− r <- shouldBeJust row
126− r.name `shouldEqual` "Alice"
127− (r.id > 0) `shouldEqual` true
128−
129− it "deletes with a condition and returns the deleted rows" $ withUsersTable \conn -> do
130− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
131− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
132− rows <- conn # exec do Q.run { role: "admin" } (from usersTable # delete # where_ @"role = $role" # returningAll)
133− Array.length rows `shouldEqual` 1
134− let
135− r = case Array.head rows of
136− Just x -> x
137− Nothing -> { id: 0, name: "", email: "", active: false, role: "" }
138− r.name `shouldEqual` "Alice"
139
611
140− it "combines two queries with UNION" $ withUsersTable \conn -> do
612+withUsersAndPostsTable :: TestOm -> Aff Unit
141− _ <- conn # exec do usersTable # Q.create { name: "Alice", email: "a@t.com", role: "admin" }
613+withUsersAndPostsTable test = do
142− _ <- conn # exec do usersTable # Q.create { name: "Bob", email: "b@t.com", role: "user" }
614+ conn <- testConnection
143− _ <- conn # exec do usersTable # Q.create { name: "Carol", email: "c@t.com", role: "mod" }
615+ _ <- PG.executeSimple setupDDL conn
144− rows <- conn # exec do
616+ Om.runOm { postgresConnection: conn }
145− Q.run { a: "admin", b: "mod" }
617+ { exception: throwError
146− ( (from usersTable # select @"name" # where_ @"role = $a")
618+ , parseError: \errs -> throwError (Exception.error ("Parse error: " <> show errs))
147− `union`
619+ }
148− (from usersTable # select @"name" # where_ @"role = $b")
620+ test
149− )
621+ _ <- PG.executeSimple teardownDDL conn
150− Array.sort (map _.name rows) `shouldEqual` [ "Alice", "Carol" ]
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+ )