From ccaf55bf9a5fe52bf5f5e7c6c1b42a58cbbbc88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20P=2E=20Ren=C3=A9=20de=20Cotret?= Date: Mon, 27 Apr 2026 09:16:26 -0400 Subject: [PATCH 1/3] Added mention of `NoMonorphismRestriction` in tutorial2 --- docs/tutorials/tutorial2.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/tutorials/tutorial2.md b/docs/tutorials/tutorial2.md index af565da60..c2dd4fb88 100644 --- a/docs/tutorials/tutorial2.md +++ b/docs/tutorials/tutorial2.md @@ -246,6 +246,18 @@ runBeamSqliteDebug putStrLn conn $ runInsert $ insertValues [ james, betty, sam ] ``` +!!! tip "Tip" + The `let` bindings for `james`, `betty`, and `sam` above are reused at two + different types: as concrete rows (`UserT Identity`) inside + `insertValues`, and as expression-level values (here as `pk james`, of + type `PrimaryKey UserT (QExpr Sqlite s)`) inside `insertExpressions`. + For the same binding to be usable at both types, GHC must infer a + polymorphic type for it. Inside GHCi this happens by default, but in a + compiled module you may need to enable `NoMonomorphismRestriction` (or + give each binding an explicit polymorphic signature). Otherwise the + monomorphism restriction will pin `james` to whichever type it is first + used at, and the second use will fail to typecheck. + Now that we have some `User` objects, we can create associated addresses. Notice that above, we used `insertValues` to insert concrete `User` rows. This worked because we could determine every field of `User` before insertion. `Address`es From a7966c0b3a86a01e8a0d8d3cbb5a8fec10ca1794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20P=2E=20Ren=C3=A9=20de=20Cotret?= Date: Mon, 27 Apr 2026 09:32:02 -0400 Subject: [PATCH 2/3] Fixed counting NULL rows in `shippingInformationByUser` --- docs/tutorials/tutorial3.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/tutorial3.md b/docs/tutorials/tutorial3.md index aa1acb95c..8c69b231d 100644 --- a/docs/tutorials/tutorial3.md +++ b/docs/tutorials/tutorial3.md @@ -589,7 +589,7 @@ shippingInformationByUser <- do user <- all_ (shoppingCartDb ^. shoppingCartUsers) (userEmail, unshippedCount) <- - aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 countAll_)) $ + aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 (count_ (_orderId order)))) $ do user <- all_ (shoppingCartDb ^. shoppingCartUsers) order <- leftJoin_ (all_ (shoppingCartDb ^. shoppingCartOrders)) (\order -> _orderForUser order `references_` user &&. isNothing_ (_orderShippingInfo order)) @@ -598,7 +598,7 @@ shippingInformationByUser <- guard_ (userEmail `references_` user) (userEmail, shippedCount) <- - aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 countAll_)) $ + aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 (count_ (_orderId order)))) $ do user <- all_ (shoppingCartDb ^. shoppingCartUsers) order <- leftJoin_ (all_ (shoppingCartDb ^. shoppingCartOrders)) (\order -> _orderForUser order `references_` user &&. isJust_ (_orderShippingInfo order)) @@ -629,7 +629,7 @@ shippingInformationByUser <- (userEmail, unshippedCount) <- subselect_ $ - aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 countAll_)) $ + aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 (count_ (_orderId order)))) $ do user <- all_ (shoppingCartDb ^. shoppingCartUsers) order <- leftJoin_ (all_ (shoppingCartDb ^. shoppingCartOrders)) (\order -> _orderForUser order `references_` user &&. isNothing_ (_orderShippingInfo order)) @@ -639,7 +639,7 @@ shippingInformationByUser <- (userEmail, shippedCount) <- subselect_ $ - aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 countAll_)) $ + aggregate_ (\(userEmail, order) -> (group_ userEmail, as_ @Int32 (count_ (_orderId order)))) $ do user <- all_ (shoppingCartDb ^. shoppingCartUsers) order <- leftJoin_ (all_ (shoppingCartDb ^. shoppingCartOrders)) (\order -> _orderForUser order `references_` user &&. isJust_ (_orderShippingInfo order)) From 93a4c08b8d4909839c4f3ad812ac938835922889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20P=2E=20Ren=C3=A9=20de=20Cotret?= Date: Mon, 27 Apr 2026 09:35:56 -0400 Subject: [PATCH 3/3] Fixed an issue where `isJust_` would return False for a table resulting of a left Join, which by definition has nullable columns --- docs/tutorials/tutorial3.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/tutorials/tutorial3.md b/docs/tutorials/tutorial3.md index 8c69b231d..64a1cff4f 100644 --- a/docs/tutorials/tutorial3.md +++ b/docs/tutorials/tutorial3.md @@ -483,7 +483,25 @@ Some RDBMSs, like Postgres, given such a query will be unable to utilize availab to perform join operations - this translates to *extremely* poor perfomance for even moderately sized data. +!!! warning "Nullable columns and `maybe_` on a left-joined table" + There is also a *correctness* gotcha here whenever the left-joined table + contains a schema-level nullable column. `maybe_` (and `isJust_`) on a + nullable table is implemented as "every column of the row is non-`NULL`", + so a real, matched row whose nullable column happens to be `NULL` is + treated as `Nothing`. In our schema `OrderT` has the nullable + `_orderShippingInfo` column, so for any of James's unshipped orders + `maybe_ (val_ False) ... order` evaluates to `False`, and the chained + `lineItem` and `product` left joins return no rows for him. The query + above will compute `Just 0` for James instead of `Just 27000`. + + The next variant uses `leftJoin_'` and avoids `maybe_` over a nullable + table entirely, so it is both faster *and* correct in this case. As a + rule of thumb, when a left-joined table contains nullable columns, + prefer `leftJoin_'` with `==?.` (and other `SqlBool` operators) over + `leftJoin_` with `maybe_`. + Luckily, Beam also provides an alternate way to phrase things that directly maps to SQL semantics +(and avoids the gotcha above) !beam-query ```haskell