@@ -25,9 +25,9 @@ import Yoga.Postgres.ClientOm
2525 , findByIdIncluding, findByIdWith
2626 , findDistinct, findDistinctWhere
2727 , findFirst, findFirstWhere
28− , findIncluding, findIncludingWhere
28+ , findIncluding
2929 , findOneBy, findOneWhere, findOrdered
30− , findWhereLimited, findWhereOrdered
30+ , findWhereLimited
3131 , findWith, findWithWhere
3232 , gt, gte, ilike, inArray, join, like_, lt, lte
3333 , maxOf, minOf, minOfWhere, ne, not_, or_
@@ -171,19 +171,17 @@ withUuidTable test = do
171171 label TEXT NOT NULL
172172 )"""
173173
174−withPostsAndCommentsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
175−withPostsAndCommentsTable test = do
174+withPostsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
175+withPostsTable test = do
176176 conn <- testConnection
177177 _ <- PG.executeSimple createDDL conn
178178 test conn
179− _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_comments CASCADE") conn
180179 _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
181180 _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
182181 PG.end conn
183182 where
184183 createDDL = PG.SQL
185− """DROP TABLE IF EXISTS test_om_comments CASCADE;
186− DROP TABLE IF EXISTS test_om_posts CASCADE;
184+ """DROP TABLE IF EXISTS test_om_posts CASCADE;
187185 DROP TABLE IF EXISTS test_om_users CASCADE;
188186 CREATE TABLE test_om_users (
189187 id SERIAL PRIMARY KEY,
@@ -197,28 +195,21 @@ withPostsAndCommentsTable test = do
197195 title TEXT NOT NULL,
198196 body TEXT NOT NULL,
199197 user_id INTEGER NOT NULL REFERENCES test_om_users(id)
200− );
201− CREATE TABLE test_om_comments (
202− id SERIAL PRIMARY KEY,
203− text TEXT NOT NULL,
204− user_id INTEGER NOT NULL REFERENCES test_om_users(id)
205198 )"""
206199
207−shouldBeJust :: forall a. Maybe a -> Aff a
208−shouldBeJust (Just a) = pure a
209−shouldBeJust Nothing = throwError $ error "Expected Just but got Nothing"
210−
211−withPostsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
212−withPostsTable test = do
200+withPostsAndCommentsTable :: (PG.Connection -> Aff Unit) -> Aff Unit
201+withPostsAndCommentsTable test = do
213202 conn <- testConnection
214203 _ <- PG.executeSimple createDDL conn
215204 test conn
205+ _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_comments CASCADE") conn
216206 _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_posts CASCADE") conn
217207 _ <- PG.executeSimple (PG.SQL "DROP TABLE IF EXISTS test_om_users CASCADE") conn
218208 PG.end conn
219209 where
220210 createDDL = PG.SQL
221− """DROP TABLE IF EXISTS test_om_posts CASCADE;
211+ """DROP TABLE IF EXISTS test_om_comments CASCADE;
212+ DROP TABLE IF EXISTS test_om_posts CASCADE;
222213 DROP TABLE IF EXISTS test_om_users CASCADE;
223214 CREATE TABLE test_om_users (
224215 id SERIAL PRIMARY KEY,
@@ -232,8 +223,17 @@ withPostsTable test = do
232223 title TEXT NOT NULL,
233224 body TEXT NOT NULL,
234225 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)
235231 )"""
236232
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+
237237-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
238238-- Spec
239239-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -241,79 +241,19 @@ withPostsTable test = do
241241spec :: Spec Unit
242242spec = describe "ClientOm" do
243243
244− describe "findBy" do
245− it "returns rows matching a column value" $ withUsersTable \conn -> do
246− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
247− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
248− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
249− admins <- conn # run do usersTable # findBy @"role" "admin"
250− Array.length admins `shouldEqual` 2
251−
252− describe "findOneBy" do
253− it "returns Just for matching row, Nothing for missing" $ withUsersTable \conn -> do
254− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
255− found <- conn # run do usersTable # findOneBy @"email" "a@t.com"
256− user <- shouldBeJust found
257− user.name `shouldEqual` "Alice"
258− missing <- conn # run do usersTable # findOneBy @"email" "nobody@t.com"
259− isNothing missing `shouldEqual` true
260−
261− describe "findAllWhere" do
262− it "returns rows matching multiple column conditions" $ withUsersTable \conn -> do
263− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
264− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
265− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
266− alices <- conn # run do usersTable # findAllWhere { name: "Alice", role: "admin" }
267− Array.length alices `shouldEqual` 1
268− alice <- shouldBeJust (Array.head alices)
269− alice.email `shouldEqual` "a@t.com"
270−
271− describe "findOneWhere" do
272− it "returns Just/Nothing for multi-column conditions" $ withUsersTable \conn -> do
273− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
274− found <- conn # run do usersTable # findOneWhere { name: "Alice", role: "admin" }
275− user <- shouldBeJust found
276− user.email `shouldEqual` "a@t.com"
277− missing <- conn # run do usersTable # findOneWhere { name: "Alice", role: "user" }
278− isNothing missing `shouldEqual` true
279−
280− describe "count" do
281− it "counts all rows in a table" $ withUsersTable \conn -> do
282− n0 <- conn # runNoErr do usersTable # count
283− n0 `shouldEqual` 0
284− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
285− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
286− n2 <- conn # runNoErr do usersTable # count
287− n2 `shouldEqual` 2
288−
289− describe "countWhere" do
290− it "counts rows matching conditions" $ withUsersTable \conn -> do
291− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
292− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
293− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "admin" }
294− n <- conn # runNoErr do usersTable # countWhere { role: "admin" }
295− n `shouldEqual` 2
296−
297− describe "exists" do
298− it "checks existence by primary key" $ withUsersTable \conn -> do
244+ describe "Creating records" do
245+ it "inserts a record and returns it with generated fields" $ withUsersTable \conn -> do
299246 created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
300− user <- shouldBeJust created
301− yes <- conn # runNoErr do usersTable # exists user.id
302− yes `shouldEqual` true
303− no <- conn # runNoErr do usersTable # exists 99999
304− no `shouldEqual` false
247+ alice <- shouldBeJust created
248+ alice.name `shouldEqual` "Alice"
249+ (alice.id > 0) `shouldEqual` true
305250
306− describe "existsWhere" do
307− it "checks existence by multiple conditions" $ withUsersTable \conn -> do
308− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
309− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
310− yes <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "admin" }
311− yes `shouldEqual` true
312− no <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "user" }
313− no `shouldEqual` false
251+ it "lets the database fill in defaults" $ withUsersTable \conn -> do
252+ created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
253+ alice <- shouldBeJust created
254+ alice.active `shouldEqual` true
314255
315− describe "createMany" do
316− it "bulk inserts records and handles empty arrays" $ withUsersTable \conn -> do
256+ it "inserts many records at once" $ withUsersTable \conn -> do
317257 let noUsers = [] :: Array { name :: String, email :: String, role :: String }
318258 empty <- conn # run do usersTable # createMany noUsers
319259 Array.length empty `shouldEqual` 0
@@ -327,48 +267,7 @@ spec = describe "ClientOm" do
327267 n <- conn # runNoErr do usersTable # count
328268 n `shouldEqual` 3
329269
330− describe "upsert" do
331− it "inserts or updates on primary key conflict" $ withSettingsTable \conn -> do
332− inserted <- conn # run do settingsTable # upsert { key: "theme", value: "dark" }
333− r1 <- shouldBeJust inserted
334− r1.value `shouldEqual` "dark"
335− updated <- conn # run do settingsTable # upsert { key: "theme", value: "light" }
336− r2 <- shouldBeJust updated
337− r2.value `shouldEqual` "light"
338− n <- conn # runNoErr do settingsTable # count
339− n `shouldEqual` 1
340−
341− describe "deleteAll" do
342− it "deletes all rows and returns count" $ withUsersTable \conn -> do
343− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
344− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
345− deleted <- conn # runNoErr do usersTable # deleteAll
346− deleted `shouldEqual` 2
347− n <- conn # runNoErr do usersTable # count
348− n `shouldEqual` 0
349−
350− describe "deleteWhere" do
351− it "deletes rows matching conditions and returns count" $ withUsersTable \conn -> do
352− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
353− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
354− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
355− deleted <- conn # runNoErr do usersTable # deleteWhere { role: "user" }
356− deleted `shouldEqual` 2
357− remaining <- conn # runNoErr do usersTable # count
358− remaining `shouldEqual` 1
359−
360− describe "updateWhere" do
361− it "updates rows matching conditions and returns updated rows" $ withUsersTable \conn -> do
362− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
363− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
364− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
365− updated <- conn # run do usersTable # updateWhere { role: "user" } { role: "member" }
366− Array.length updated `shouldEqual` 2
367− members <- conn # run do usersTable # findBy @"role" "member"
368− Array.length members `shouldEqual` 2
369−
370− describe "createIfNotExists" do
371− it "inserts new row or returns Nothing on conflict" $ withSettingsTable \conn -> do
270+ it "skips insert when a conflict would occur" $ withSettingsTable \conn -> do
372271 inserted <- conn # run do settingsTable # createIfNotExists { key: "theme", value: "dark" }
373272 r1 <- shouldBeJust inserted
374273 r1.value `shouldEqual` "dark"
@@ -378,8 +277,17 @@ spec = describe "ClientOm" do
378277 existing <- shouldBeJust found
379278 existing.value `shouldEqual` "dark"
380279
381− describe "upsertMany" do
382− it "bulk upserts with ON CONFLICT DO UPDATE" $ withSettingsTable \conn -> do
280+ it "upserts: inserts or updates on conflict" $ withSettingsTable \conn -> do
281+ inserted <- conn # run do settingsTable # upsert { key: "theme", value: "dark" }
282+ r1 <- shouldBeJust inserted
283+ r1.value `shouldEqual` "dark"
284+ updated <- conn # run do settingsTable # upsert { key: "theme", value: "light" }
285+ r2 <- shouldBeJust updated
286+ r2.value `shouldEqual` "light"
287+ n <- conn # runNoErr do settingsTable # count
288+ n `shouldEqual` 1
289+
290+ it "bulk upserts a batch" $ withSettingsTable \conn -> do
383291 _ <- conn # run do
384292 settingsTable # upsertMany
385293 [ { key: "theme", value: "dark" }
@@ -400,8 +308,44 @@ spec = describe "ClientOm" do
400308 setting <- shouldBeJust found
401309 setting.value `shouldEqual` "light"
402310
403− describe "findByIdIn" do
404− it "finds rows by array of primary keys" $ withUsersTable \conn -> do
311+ describe "Finding records" do
312+ it "finds a single record by primary key" $ withUsersTable \conn -> do
313+ created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
314+ alice <- shouldBeJust created
315+ found <- conn # run do usersTable # findById alice.id
316+ r <- shouldBeJust found
317+ r.name `shouldEqual` "Alice"
318+
319+ it "finds a single record by any unique column" $ withUsersTable \conn -> do
320+ _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
321+ found <- conn # run do usersTable # findOneBy @"email" "a@t.com"
322+ alice <- shouldBeJust found
323+ alice.name `shouldEqual` "Alice"
324+
325+ it "finds all records matching a column value" $ withUsersTable \conn -> do
326+ _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
327+ _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
328+ _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
329+ admins <- conn # run do usersTable # findBy @"role" "admin"
330+ Array.length admins `shouldEqual` 2
331+
332+ it "finds all records matching multiple conditions" $ withUsersTable \conn -> do
333+ _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
334+ _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "admin" }
335+ _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
336+ alices <- conn # run do usersTable # findAllWhere { name: "Alice", role: "admin" }
337+ Array.length alices `shouldEqual` 1
338+ alice <- shouldBeJust (Array.head alices)
339+ alice.email `shouldEqual` "a@t.com"
340+
341+ it "returns Nothing when no record matches" $ withUsersTable \conn -> do
342+ _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
343+ missing <- conn # run do usersTable # findOneBy @"email" "nobody@t.com"
344+ isNothing missing `shouldEqual` true
345+ alsoMissing <- conn # run do usersTable # findOneWhere { name: "Alice", role: "user" }
346+ isNothing alsoMissing `shouldEqual` true
347+
348+ it "finds records by a list of primary keys" $ withUsersTable \conn -> do
405349 created1 <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
406350 u1 <- shouldBeJust created1
407351 created2 <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
@@ -412,101 +356,17 @@ spec = describe "ClientOm" do
412356 empty <- conn # run do usersTable # findByIdIn ([] :: Array Int)
413357 Array.length empty `shouldEqual` 0
414358
415− describe "cursor pagination" do
416− it "findFirst returns first page with hasMore" $ withUsersTable \conn -> do
417− _ <- conn # run do
418− usersTable # createMany
419− [ { name: "Alice", email: "a@t.com", role: "admin" }
420− , { name: "Bob", email: "b@t.com", role: "user" }
421− , { name: "Carol", email: "c@t.com", role: "user" }
422− , { name: "Dave", email: "d@t.com", role: "user" }
423− , { name: "Eve", email: "e@t.com", role: "admin" }
424− ]
425− page1 <- conn # run do usersTable # findFirst @"id" 2
426− Array.length page1.items `shouldEqual` 2
427− page1.hasMore `shouldEqual` true
428−
429− it "findAfter paginates forward through all pages" $ withUsersTable \conn -> do
359+ it "returns all records in the table" $ withUsersTable \conn -> do
430360 _ <- conn # run do
431361 usersTable # createMany
432362 [ { name: "Alice", email: "a@t.com", role: "admin" }
433363 , { name: "Bob", email: "b@t.com", role: "user" }
434− , { name: "Carol", email: "c@t.com", role: "user" }
435− , { name: "Dave", email: "d@t.com", role: "user" }
436− , { name: "Eve", email: "e@t.com", role: "admin" }
437− ]
438− page1 <- conn # run do usersTable # findFirst @"id" 2
439− let
440− lastId = case Array.last page1.items of
441− Just u -> u.id
442− Nothing -> 0
443− page2 <- conn # run do usersTable # findAfter @"id" lastId 2
444− Array.length page2.items `shouldEqual` 2
445− page2.hasMore `shouldEqual` true
446− let
447− lastId2 = case Array.last page2.items of
448− Just u -> u.id
449− Nothing -> 0
450− page3 <- conn # run do usersTable # findAfter @"id" lastId2 2
451− Array.length page3.items `shouldEqual` 1
452− page3.hasMore `shouldEqual` false
453−
454− it "findBefore paginates backward" $ withUsersTable \conn -> do
455− _ <- conn # run do
456− usersTable # createMany
457− [ { name: "Alice", email: "a@t.com", role: "admin" }
458− , { name: "Bob", email: "b@t.com", role: "user" }
459− , { name: "Carol", email: "c@t.com", role: "user" }
460− , { name: "Dave", email: "d@t.com", role: "user" }
461− , { name: "Eve", email: "e@t.com", role: "admin" }
462364 ]
463365 all <- conn # run do usersTable # findAll
464− let
465− lastUser = case Array.last all of
466− Just u -> u
467− Nothing -> { id: 0, name: "", email: "", active: true, role: "" }
468− page <- conn # run do usersTable # findBefore @"id" lastUser.id 2
469− Array.length page.items `shouldEqual` 2
470− page.hasMore `shouldEqual` true
366+ Array.length all `shouldEqual` 2
471367
472− it "findAfterWhere paginates with conditions" $ withUsersTable \conn -> do
473− _ <- conn # run do
474− usersTable # createMany
475− [ { name: "Alice", email: "a@t.com", role: "admin" }
476− , { name: "Bob", email: "b@t.com", role: "user" }
477− , { name: "Carol", email: "c@t.com", role: "user" }
478− , { name: "Dave", email: "d@t.com", role: "user" }
479− , { name: "Eve", email: "e@t.com", role: "admin" }
480− ]
481− page1 <- conn # run do usersTable # findFirstWhere @"id" { role: "user" } 2
482− Array.length page1.items `shouldEqual` 2
483− page1.hasMore `shouldEqual` true
484− let
485− lastId = case Array.last page1.items of
486− Just u -> u.id
487− Nothing -> 0
488− page2 <- conn # run do usersTable # findAfterWhere @"id" { role: "user" } lastId 2
489− Array.length page2.items `shouldEqual` 1
490− page2.hasMore `shouldEqual` false
491−
492− describe "newtypes" do
493− it "PGUUID round-trip: create, findById, findBy, findOneBy, exists" $ withUuidTable \conn -> do
494− created <- conn # run do uuidTable # create { label: "first" }
495− row <- shouldBeJust created
496− row.label `shouldEqual` "first"
497− found <- conn # run do uuidTable # findById row.id
498− r2 <- shouldBeJust found
499− r2.label `shouldEqual` "first"
500− byCol <- conn # run do uuidTable # findBy @"label" "first"
501− Array.length byCol `shouldEqual` 1
502− byId <- conn # run do uuidTable # findOneBy @"id" row.id
503− r3 <- shouldBeJust byId
504− r3.label `shouldEqual` "first"
505− yes <- conn # runNoErr do uuidTable # exists row.id
506− yes `shouldEqual` true
507−
508− describe "WHERE operators" do
509− it "gt, gte, lt, lte filter by comparison" $ withUsersTable \conn -> do
368+ describe "Filtering with operators" do
369+ it "compares with gt, gte, lt, lte" $ withUsersTable \conn -> do
510370 _ <- conn # run do
511371 usersTable # createMany
512372 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -527,7 +387,7 @@ spec = describe "ClientOm" do
527387 lteRows <- conn # run do usersTable # findAllWhere { id: lte id1 }
528388 Array.length lteRows `shouldEqual` 1
529389
530− it "ne filters out matching rows" $ withUsersTable \conn -> do
390+ it "excludes with ne" $ withUsersTable \conn -> do
531391 _ <- conn # run do
532392 usersTable # createMany
533393 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -537,7 +397,7 @@ spec = describe "ClientOm" do
537397 rows <- conn # run do usersTable # findAllWhere { role: ne "admin" }
538398 Array.length rows `shouldEqual` 2
539399
540− it "like_ and ilike match patterns" $ withUsersTable \conn -> do
400+ it "pattern-matches with like_ and ilike" $ withUsersTable \conn -> do
541401 _ <- conn # run do
542402 usersTable # createMany
543403 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -549,7 +409,7 @@ spec = describe "ClientOm" do
549409 ilikeRows <- conn # run do usersTable # findAllWhere { name: ilike "a%" }
550410 Array.length ilikeRows `shouldEqual` 1
551411
552− it "inArray matches values in list" $ withUsersTable \conn -> do
412+ it "checks membership with inArray" $ withUsersTable \conn -> do
553413 _ <- conn # run do
554414 usersTable # createMany
555415 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -559,7 +419,7 @@ spec = describe "ClientOm" do
559419 rows <- conn # run do usersTable # findAllWhere { role: inArray [ "admin", "mod" ] }
560420 Array.length rows `shouldEqual` 2
561421
562− it "or_ combines conditions" $ withUsersTable \conn -> do
422+ it "combines conditions with or_" $ withUsersTable \conn -> do
563423 _ <- conn # run do
564424 usersTable # createMany
565425 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -569,7 +429,7 @@ spec = describe "ClientOm" do
569429 rows <- conn # run do usersTable # findAllWhere { role: or_ (eq_ "admin") (eq_ "mod") }
570430 Array.length rows `shouldEqual` 2
571431
572− it "not_ negates conditions" $ withUsersTable \conn -> do
432+ it "negates conditions with not_" $ withUsersTable \conn -> do
573433 _ <- conn # run do
574434 usersTable # createMany
575435 [ { name: "Alice", email: "a@t.com", role: "admin" }
@@ -579,42 +439,66 @@ spec = describe "ClientOm" do
579439 rows <- conn # run do usersTable # findAllWhere { role: not_ (eq_ "admin") }
580440 Array.length rows `shouldEqual` 2
581441
582− it "operators compose with countWhere and deleteWhere" $ withUsersTable \conn -> do
442+ describe "Updating and deleting" do
443+ it "updates rows matching a condition" $ withUsersTable \conn -> do
583444 _ <- conn # run do
584445 usersTable # createMany
585446 [ { name: "Alice", email: "a@t.com", role: "admin" }
586447 , { name: "Bob", email: "b@t.com", role: "user" }
587448 , { name: "Carol", email: "c@t.com", role: "user" }
588449 ]
589− n <- conn # runNoErr do usersTable # countWhere { role: ne "admin" }
590− n `shouldEqual` 2
591− deleted <- conn # runNoErr do usersTable # deleteWhere { role: ne "admin" }
450+ updated <- conn # run do usersTable # updateWhere { role: "user" } { role: "member" }
451+ Array.length updated `shouldEqual` 2
452+ members <- conn # run do usersTable # findBy @"role" "member"
453+ Array.length members `shouldEqual` 2
454+
455+ it "deletes rows matching a condition" $ withUsersTable \conn -> do
456+ _ <- conn # run do
457+ usersTable # createMany
458+ [ { name: "Alice", email: "a@t.com", role: "admin" }
459+ , { name: "Bob", email: "b@t.com", role: "user" }
460+ , { name: "Carol", email: "c@t.com", role: "user" }
461+ ]
462+ deleted <- conn # runNoErr do usersTable # deleteWhere { role: "user" }
592463 deleted `shouldEqual` 2
464+ remaining <- conn # runNoErr do usersTable # count
465+ remaining `shouldEqual` 1
593466
594− describe "ordering" do
595− it "findOrdered sorts results" $ withUsersTable \conn -> do
467+ it "deletes and returns the deleted rows" $ withUsersTable \conn -> do
596468 _ <- conn # run do
597469 usersTable # createMany
598− [ { name: "Carol", email: "c@t.com", role: "user" }
599− , { name: "Alice", email: "a@t.com", role: "admin" }
470+ [ { name: "Alice", email: "a@t.com", role: "admin" }
600471 , { name: "Bob", email: "b@t.com", role: "user" }
472+ , { name: "Carol", email: "c@t.com", role: "user" }
601473 ]
602− asc <- conn # run do usersTable # findOrdered @"name" Asc
603− (map _.name asc) `shouldEqual` [ "Alice", "Bob", "Carol" ]
604− desc <- conn # run do usersTable # findOrdered @"name" Desc
605− (map _.name desc) `shouldEqual` [ "Carol", "Bob", "Alice" ]
474+ deleted <- conn # run do usersTable # deleteWhereReturning { role: "user" }
475+ Array.length deleted `shouldEqual` 2
476+ Array.sort (map _.name deleted) `shouldEqual` [ "Bob", "Carol" ]
477+ remaining <- conn # runNoErr do usersTable # count
478+ remaining `shouldEqual` 1
479+
480+ it "deletes everything" $ withUsersTable \conn -> do
481+ _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
482+ _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
483+ deleted <- conn # runNoErr do usersTable # deleteAll
484+ deleted `shouldEqual` 2
485+ n <- conn # runNoErr do usersTable # count
486+ n `shouldEqual` 0
606487
607− it "findWhereOrdered filters and sorts" $ withUsersTable \conn -> do
488+ describe "Ordering and pagination" do
489+ it "orders results by a column" $ withUsersTable \conn -> do
608490 _ <- conn # run do
609491 usersTable # createMany
610492 [ { name: "Carol", email: "c@t.com", role: "user" }
611493 , { name: "Alice", email: "a@t.com", role: "admin" }
612494 , { name: "Bob", email: "b@t.com", role: "user" }
613495 ]
614− rows <- conn # run do usersTable # findWhereOrdered @"name" { role: "user" } Asc
615− (map _.name rows) `shouldEqual` [ "Bob", "Carol" ]
496+ asc <- conn # run do usersTable # findOrdered @"name" Asc
497+ (map _.name asc) `shouldEqual` [ "Alice", "Bob", "Carol" ]
498+ desc <- conn # run do usersTable # findOrdered @"name" Desc
499+ (map _.name desc) `shouldEqual` [ "Carol", "Bob", "Alice" ]
616500
617− it "findWhereLimited paginates with offset" $ withUsersTable \conn -> do
501+ it "filters, orders, and limits in one call" $ withUsersTable \conn -> do
618502 _ <- conn # run do
619503 usersTable # createMany
620504 [ { name: "Alice", email: "a@t.com", role: "user" }
@@ -627,138 +511,171 @@ spec = describe "ClientOm" do
627511 page2 <- conn # run do usersTable # findWhereLimited @"name" { role: "user" } Asc { limit: 2, offset: 2 }
628512 (map _.name page2) `shouldEqual` [ "Carol", "Dave" ]
629513
630− describe "aggregations" do
631− it "sumOf sums a numeric column" $ withUsersTable \conn -> do
514+ it "paginates forward with cursors" $ withUsersTable \conn -> do
632515 _ <- conn # run do
633516 usersTable # createMany
634517 [ { name: "Alice", email: "a@t.com", role: "admin" }
635518 , { name: "Bob", email: "b@t.com", role: "user" }
636519 , { name: "Carol", email: "c@t.com", role: "user" }
520+ , { name: "Dave", email: "d@t.com", role: "user" }
521+ , { name: "Eve", email: "e@t.com", role: "admin" }
637522 ]
638− total <- conn # runNoErr do usersTable # sumOf @"id"
639− (total > 0.0) `shouldEqual` true
640− emptySum <- conn # runNoErr do usersTable # sumOfWhere @"id" { role: "nobody" }
641− emptySum `shouldEqual` 0.0
523+ page1 <- conn # run do usersTable # findFirst @"id" 2
524+ Array.length page1.items `shouldEqual` 2
525+ page1.hasMore `shouldEqual` true
526+ let
527+ lastId = case Array.last page1.items of
528+ Just u -> u.id
529+ Nothing -> 0
530+ page2 <- conn # run do usersTable # findAfter @"id" lastId 2
531+ Array.length page2.items `shouldEqual` 2
532+ page2.hasMore `shouldEqual` true
533+ let
534+ lastId2 = case Array.last page2.items of
535+ Just u -> u.id
536+ Nothing -> 0
537+ page3 <- conn # run do usersTable # findAfter @"id" lastId2 2
538+ Array.length page3.items `shouldEqual` 1
539+ page3.hasMore `shouldEqual` false
642540
643− it "avgOf averages a numeric column" $ withUsersTable \conn -> do
541+ it "paginates backward with cursors" $ withUsersTable \conn -> do
644542 _ <- conn # run do
645543 usersTable # createMany
646544 [ { name: "Alice", email: "a@t.com", role: "admin" }
647545 , { name: "Bob", email: "b@t.com", role: "user" }
546+ , { name: "Carol", email: "c@t.com", role: "user" }
547+ , { name: "Dave", email: "d@t.com", role: "user" }
548+ , { name: "Eve", email: "e@t.com", role: "admin" }
648549 ]
649− avg <- conn # runNoErr do usersTable # avgOf @"id"
650− isNothing avg `shouldEqual` false
651− emptyAvg <- conn # runNoErr do usersTable # avgOfWhere @"id" { role: "nobody" }
652− isNothing emptyAvg `shouldEqual` true
550+ all <- conn # run do usersTable # findAll
551+ let
552+ lastUser = case Array.last all of
553+ Just u -> u
554+ Nothing -> { id: 0, name: "", email: "", active: true, role: "" }
555+ page <- conn # run do usersTable # findBefore @"id" lastUser.id 2
556+ Array.length page.items `shouldEqual` 2
557+ page.hasMore `shouldEqual` true
653558
654− it "minOf and maxOf find extremes" $ withUsersTable \conn -> do
559+ it "paginates with a filter applied" $ withUsersTable \conn -> do
655560 _ <- conn # run do
656561 usersTable # createMany
657562 [ { name: "Alice", email: "a@t.com", role: "admin" }
658563 , { name: "Bob", email: "b@t.com", role: "user" }
659564 , { name: "Carol", email: "c@t.com", role: "user" }
565+ , { name: "Dave", email: "d@t.com", role: "user" }
566+ , { name: "Eve", email: "e@t.com", role: "admin" }
660567 ]
661− minName <- conn # run do usersTable # minOf @"name"
662− minName `shouldEqual` (Just "Alice")
663− maxName <- conn # run do usersTable # maxOf @"name"
664− maxName `shouldEqual` (Just "Carol")
665− noMin <- conn # run do usersTable # minOfWhere @"name" { role: "nobody" }
666− isNothing noMin `shouldEqual` true
568+ page1 <- conn # run do usersTable # findFirstWhere @"id" { role: "user" } 2
569+ Array.length page1.items `shouldEqual` 2
570+ page1.hasMore `shouldEqual` true
571+ let
572+ lastId = case Array.last page1.items of
573+ Just u -> u.id
574+ Nothing -> 0
575+ page2 <- conn # run do usersTable # findAfterWhere @"id" { role: "user" } lastId 2
576+ Array.length page2.items `shouldEqual` 1
577+ page2.hasMore `shouldEqual` false
578+
579+ describe "Aggregations and checks" do
580+ it "counts rows, optionally with a filter" $ withUsersTable \conn -> do
581+ n0 <- conn # runNoErr do usersTable # count
582+ n0 `shouldEqual` 0
583+ _ <- conn # run do
584+ usersTable # createMany
585+ [ { name: "Alice", email: "a@t.com", role: "admin" }
586+ , { name: "Bob", email: "b@t.com", role: "user" }
587+ , { name: "Carol", email: "c@t.com", role: "admin" }
588+ ]
589+ n3 <- conn # runNoErr do usersTable # count
590+ n3 `shouldEqual` 3
591+ nAdmins <- conn # runNoErr do usersTable # countWhere { role: "admin" }
592+ nAdmins `shouldEqual` 2
667593
668− describe "distinct" do
669− it "findDistinct returns unique column values" $ withUsersTable \conn -> do
594+ it "checks whether a record exists" $ withUsersTable \conn -> do
595+ created <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
596+ alice <- shouldBeJust created
597+ yes <- conn # runNoErr do usersTable # exists alice.id
598+ yes `shouldEqual` true
599+ no <- conn # runNoErr do usersTable # exists 99999
600+ no `shouldEqual` false
601+ yesWhere <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "admin" }
602+ yesWhere `shouldEqual` true
603+ noWhere <- conn # runNoErr do usersTable # existsWhere { name: "Alice", role: "user" }
604+ noWhere `shouldEqual` false
605+
606+ it "sums and averages a numeric column" $ withUsersTable \conn -> do
670607 _ <- conn # run do
671608 usersTable # createMany
672609 [ { name: "Alice", email: "a@t.com", role: "admin" }
673610 , { name: "Bob", email: "b@t.com", role: "user" }
674− , { name: "Carol", email: "c@t.com", role: "user" }
675611 ]
676− roles <- conn # run do usersTable # findDistinct @"role"
677− Array.sort roles `shouldEqual` [ "admin", "user" ]
612+ total <- conn # runNoErr do usersTable # sumOf @"id"
613+ (total > 0.0) `shouldEqual` true
614+ emptySum <- conn # runNoErr do usersTable # sumOfWhere @"id" { role: "nobody" }
615+ emptySum `shouldEqual` 0.0
616+ avg <- conn # runNoErr do usersTable # avgOf @"id"
617+ isNothing avg `shouldEqual` false
618+ emptyAvg <- conn # runNoErr do usersTable # avgOfWhere @"id" { role: "nobody" }
619+ isNothing emptyAvg `shouldEqual` true
678620
679− it "findDistinctWhere with conditions" $ withUsersTable \conn -> do
621+ it "finds the min and max of a column" $ withUsersTable \conn -> do
680622 _ <- conn # run do
681623 usersTable # createMany
682624 [ { name: "Alice", email: "a@t.com", role: "admin" }
683625 , { name: "Bob", email: "b@t.com", role: "user" }
684626 , { name: "Carol", email: "c@t.com", role: "user" }
685627 ]
686− roles <- conn # run do usersTable # findDistinctWhere @"role" { active: true }
687− Array.sort roles `shouldEqual` [ "admin", "user" ]
628+ minName <- conn # run do usersTable # minOf @"name"
629+ minName `shouldEqual` (Just "Alice")
630+ maxName <- conn # run do usersTable # maxOf @"name"
631+ maxName `shouldEqual` (Just "Carol")
632+ noMin <- conn # run do usersTable # minOfWhere @"name" { role: "nobody" }
633+ isNothing noMin `shouldEqual` true
688634
689− describe "deleteWhereReturning" do
690− it "returns deleted rows" $ withUsersTable \conn -> do
635+ it "lists distinct values of a column" $ withUsersTable \conn -> do
691636 _ <- conn # run do
692637 usersTable # createMany
693638 [ { name: "Alice", email: "a@t.com", role: "admin" }
694639 , { name: "Bob", email: "b@t.com", role: "user" }
695640 , { name: "Carol", email: "c@t.com", role: "user" }
696641 ]
697− deleted <- conn # run do usersTable # deleteWhereReturning { role: "user" }
698− Array.length deleted `shouldEqual` 2
699− Array.sort (map _.name deleted) `shouldEqual` [ "Bob", "Carol" ]
700− remaining <- conn # runNoErr do usersTable # count
701− remaining `shouldEqual` 1
702−
703− describe "withTransaction" do
704− it "commits on success" $ withUsersTable \conn -> do
705− conn # run do
706− withTransaction do
707− _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
708− _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
709− pure unit
710− n <- conn # runNoErr do usersTable # count
711− n `shouldEqual` 2
642+ roles <- conn # run do usersTable # findDistinct @"role"
643+ Array.sort roles `shouldEqual` [ "admin", "user" ]
644+ activeRoles <- conn # run do usersTable # findDistinctWhere @"role" { active: true }
645+ Array.sort activeRoles `shouldEqual` [ "admin", "user" ]
712646
713− describe "partial select" do
714− it "selectFrom picks specific columns" $ withUsersTable \conn -> do
647+ describe "Selecting specific columns" do
648+ it "fetches only the columns you need" $ withUsersTable \conn -> do
715649 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
716650 _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
651+ _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
717652 rows <- conn # run do usersTable # selectFrom @"name, email"
718− Array.length rows `shouldEqual` 2
653+ Array.length rows `shouldEqual` 3
719654 let
720655 first = case Array.head rows of
721656 Just r -> r
722657 Nothing -> { name: "", email: "" }
723658 first.name `shouldEqual` "Alice"
724659 first.email `shouldEqual` "a@t.com"
660+ filtered <- conn # run do usersTable # selectFromWhere @"name" { role: "user" }
661+ Array.length filtered `shouldEqual` 2
662+ Array.sort (map _.name filtered) `shouldEqual` [ "Bob", "Carol" ]
725663
726− it "selectOneFrom returns Maybe with picked columns" $ withUsersTable \conn -> do
664+ it "fetches one row with specific columns and a filter" $ withUsersTable \conn -> do
727665 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
666+ _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
728667 found <- conn # run do usersTable # selectOneFrom @"name, role"
729668 r <- shouldBeJust found
730669 r.name `shouldEqual` "Alice"
731670 r.role `shouldEqual` "admin"
732−
733− it "selectFromWhere filters and picks columns" $ withUsersTable \conn -> do
734− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
735− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
736− _ <- conn # run do usersTable # create { name: "Carol", email: "c@t.com", role: "user" }
737− rows <- conn # run do usersTable # selectFromWhere @"name" { role: "user" }
738− Array.length rows `shouldEqual` 2
739− Array.sort (map _.name rows) `shouldEqual` [ "Bob", "Carol" ]
740−
741− it "selectFromWhere works with operators" $ withUsersTable \conn -> do
742− _ <- conn # run do
743− usersTable # createMany
744− [ { name: "Alice", email: "a@t.com", role: "admin" }
745− , { name: "Bob", email: "b@t.com", role: "user" }
746− , { name: "Carol", email: "c@t.com", role: "mod" }
747− ]
748− rows <- conn # run do usersTable # selectFromWhere @"name" { role: ne "admin" }
749− Array.length rows `shouldEqual` 2
750−
751− it "selectOneFromWhere returns Maybe with conditions" $ withUsersTable \conn -> do
752− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
753− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
754− found <- conn # run do usersTable # selectOneFromWhere @"email" { name: "Bob" }
755− r <- shouldBeJust found
756− r.email `shouldEqual` "b@t.com"
671+ byName <- conn # run do usersTable # selectOneFromWhere @"email" { name: "Bob" }
672+ r2 <- shouldBeJust byName
673+ r2.email `shouldEqual` "b@t.com"
757674 missing <- conn # run do usersTable # selectOneFromWhere @"email" { name: "Nobody" }
758675 isNothing missing `shouldEqual` true
759676
760− describe "joins (ORMy)" do
761− it "findWith joins child to parent" $ withPostsTable \conn -> do
677+ describe "Relationships" do
678+ it "joins a child table to its parent (flat)" $ withPostsTable \conn -> do
762679 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
763680 _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
764681 _ <- conn # run do postsTable # create { title: "First Post", body: "Hello", user_id: 1 }
@@ -773,7 +690,7 @@ spec = describe "ClientOm" do
773690 r.test_om_posts.title `shouldEqual` "First Post"
774691 r.test_om_users.name `shouldEqual` "Alice"
775692
776− it "findByIdWith joins by primary key" $ withPostsTable \conn -> do
693+ it "finds a joined record by primary key" $ withPostsTable \conn -> do
777694 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
778695 _ <- conn # run do postsTable # create { title: "My Post", body: "Content", user_id: 1 }
779696 found <- conn # run do findByIdWith 1 postsTable usersTable
@@ -781,7 +698,7 @@ spec = describe "ClientOm" do
781698 r.test_om_posts.title `shouldEqual` "My Post"
782699 r.test_om_users.name `shouldEqual` "Alice"
783700
784− it "findWithWhere joins with conditions" $ withPostsTable \conn -> do
701+ it "joins with a filter" $ withPostsTable \conn -> do
785702 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
786703 _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
787704 _ <- conn # run do postsTable # create { title: "Post A", body: "A", user_id: 1 }
@@ -794,7 +711,7 @@ spec = describe "ClientOm" do
794711 Nothing -> { test_om_posts: { id: 0, title: "", body: "", user_id: 0 }, test_om_users: { id: 0, name: "", email: "", active: false, role: "" } }
795712 r.test_om_posts.title `shouldEqual` "Post A"
796713
797− it "findIncluding nests children under parent" $ withPostsTable \conn -> do
714+ it "nests children under their parent" $ withPostsTable \conn -> do
798715 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
799716 _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
800717 _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
@@ -808,7 +725,7 @@ spec = describe "ClientOm" do
808725 alice.name `shouldEqual` "Alice"
809726 Array.length alice.posts `shouldEqual` 2
810727
811− it "findByIdIncluding nests children for one parent" $ withPostsTable \conn -> do
728+ it "nests children for a single parent by id" $ withPostsTable \conn -> do
812729 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
813730 _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
814731 _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
@@ -817,32 +734,7 @@ spec = describe "ClientOm" do
817734 r.name `shouldEqual` "Alice"
818735 Array.length r.posts `shouldEqual` 2
819736
820− it "findIncludingWhere nests children with conditions" $ withPostsTable \conn -> do
821− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
822− _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
823− _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
824− _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 2 }
825− rows <- conn # run do findIncludingWhere @"posts" { role: "admin" } usersTable postsTable
826− Array.length rows `shouldEqual` 1
827− let
828− r = case Array.head rows of
829− Just x -> x
830− Nothing -> { id: 0, name: "", email: "", active: false, role: "", posts: [] }
831− r.name `shouldEqual` "Alice"
832− Array.length r.posts `shouldEqual` 1
833−
834− describe "join combinator" do
835− it "findAll on plain table" $ withUsersTable \conn -> do
836− _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
837− rows <- conn # run do usersTable # findAll
838− Array.length rows `shouldEqual` 1
839− let
840− r = case Array.head rows of
841− Just x -> x
842− Nothing -> { id: 0, name: "", email: "", active: false, role: "" }
843− r.name `shouldEqual` "Alice"
844−
845− it "usersTable # join postsTable # findAll" $ withPostsTable \conn -> do
737+ it "uses the join combinator to compose tables" $ withPostsTable \conn -> do
846738 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
847739 _ <- conn # run do usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
848740 _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
@@ -857,7 +749,7 @@ spec = describe "ClientOm" do
857749 alice.name `shouldEqual` "Alice"
858750 Array.length alice.test_om_posts `shouldEqual` 2
859751
860− it "usersTable # join postsTable # join commentsTable" $ withPostsAndCommentsTable \conn -> do
752+ it "joins multiple child tables at once" $ withPostsAndCommentsTable \conn -> do
861753 _ <- conn # run do usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
862754 _ <- conn # run do postsTable # create { title: "Post 1", body: "A", user_id: 1 }
863755 _ <- conn # run do postsTable # create { title: "Post 2", body: "B", user_id: 1 }
@@ -871,3 +763,29 @@ spec = describe "ClientOm" do
871763 r.name `shouldEqual` "Alice"
872764 Array.length r.test_om_posts `shouldEqual` 2
873765 Array.length r.test_om_comments `shouldEqual` 1
766+
767+ describe "Transactions" do
768+ it "commits multiple inserts atomically" $ withUsersTable \conn -> do
769+ conn # run do
770+ withTransaction do
771+ _ <- usersTable # create { name: "Alice", email: "a@t.com", role: "admin" }
772+ _ <- usersTable # create { name: "Bob", email: "b@t.com", role: "user" }
773+ pure unit
774+ n <- conn # runNoErr do usersTable # count
775+ n `shouldEqual` 2
776+
777+ describe "UUIDs and newtypes" do
778+ it "round-trips a UUID primary key through create/find/exists" $ withUuidTable \conn -> do
779+ created <- conn # run do uuidTable # create { label: "first" }
780+ row <- shouldBeJust created
781+ row.label `shouldEqual` "first"
782+ found <- conn # run do uuidTable # findById row.id
783+ r2 <- shouldBeJust found
784+ r2.label `shouldEqual` "first"
785+ byCol <- conn # run do uuidTable # findBy @"label" "first"
786+ Array.length byCol `shouldEqual` 1
787+ byId <- conn # run do uuidTable # findOneBy @"id" row.id
788+ r3 <- shouldBeJust byId
789+ r3.label `shouldEqual` "first"
790+ yes <- conn # runNoErr do uuidTable # exists row.id
791+ yes `shouldEqual` true