beam-duckdb-0.2.0.0: tests/Database/Beam/DuckDB/Test/Query.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
module Database.Beam.DuckDB.Test.Query (tests) where
import Control.Monad (void)
import Data.Int (Int32)
import Data.List (nub, nubBy, sort, sortOn)
import Data.Text (Text)
import Database.Beam
( Beamable,
Columnar,
Database,
DatabaseSettings,
Generic,
Identity,
SqlEq ((==.)),
SqlOrd ((>.)),
SqlValable (val_),
Table (..),
TableEntity,
aggregate_,
allInGroup_,
all_,
anyOver_,
as_,
asc_,
avgOver_,
bounds_,
cast_,
concat_,
countAll_,
dbModification,
defaultDbSettings,
desc_,
double,
everyOver_,
filterWhere_,
filter_,
firstValue_,
frame_,
fromMaybe_,
group_,
guard_,
insert,
insertValues,
isTrue_,
lagWithDefault_,
lastValue_,
leftJoin_,
modifyTableFields,
noBounds_,
noOrder_,
noPartition_,
nrows_,
orderBy_,
orderPartitionBy_,
over_,
partitionBy_,
references_,
related_,
reuse,
rowNumber_,
runInsert,
runSelectReturningList,
select,
selectWith,
selecting,
someOver_,
sqlBool_,
sumOver_,
sum_,
tableModification,
unbounded_,
union_,
varchar,
withDbModification,
withWindow_,
(<.),
(>=.),
)
import Database.Beam.DuckDB (DuckDB, runBeamDuckDB)
import Database.DuckDB.Simple (Connection, execute_, withConnection)
import Hedgehog (Gen, annotate, evalIO, forAll, property, (===))
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.Hedgehog (testProperty)
tests :: TestTree
tests =
testGroup
"Queries"
[ testGroup
"SQL92 featureset"
[ testGroup
"Selection"
[ testSelectAll,
testSelectWithFilter,
testSelectEquality
],
testGroup
"Projection"
[ testProjectSingleColumn,
testProjectMultipleColumns,
testProjectWithExpression
],
testGroup
"Join"
[ testInnerJoin,
testMultiInnerJoin,
testMixingJoinsWithFilters,
testLeftJoin
]
],
testGroup
"SQL99 featureset"
[ testGroup "CONCAT" [testConcat],
testGroup
"CTE"
[ testCommonTableExpression,
testMultipleCommonTableExpressions,
testRecursiveCommonTableExpression
],
testGroup
"SOME/ANY/EVERY"
[ testSomeOver,
testAnyOver,
testEveryOver
]
],
testGroup
"SQL2003 featureset"
[ testGroup
"Windowing"
[ testRowNumberOverWholeResult,
testRowNumberPartitionedByColumn,
testWindowingWithBounds
],
testGroup
"LAG/LEAD"
[testLag, testLead],
testGroup
"FILTER"
[ testFilterWhereCountAll,
testFilterWithWindow
],
testGroup
"FIRST_VALUE/LAST_VALUE"
[ testFirstValue,
testLastValue
]
]
]
testSelectAll :: TestTree
testSelectAll = testProperty "selecting all users should return the users initially inserted" $ property $ do
users <- forAll genUsers
results <- evalIO $
withTestDb users [] [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select (all_ (_dbUsers testDb))
sortOn _userId results === sortOn _userId users
testSelectWithFilter :: TestTree
testSelectWithFilter = testProperty "selecting users satisfying a condition works as expected" $ property $ do
users <- forAll genUsers
ageThreshold <- forAll genAge
results <- evalIO $
withTestDb users [] [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
u <- all_ (_dbUsers testDb)
guard_ (_userAge u >. val_ ageThreshold)
pure u
let expected = filter (\u -> _userAge u > ageThreshold) users
sortOn _userId results === sortOn _userId expected
testSelectEquality :: TestTree
testSelectEquality = testProperty "" $ property $ do
users <- forAll genUsers
target <- forAll (Gen.element users)
results <- evalIO $
withTestDb users [] [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
u <- all_ (_dbUsers testDb)
guard_ (_userId u ==. val_ (_userId target))
pure u
results === [target]
testProjectSingleColumn :: TestTree
testProjectSingleColumn = testProperty "Single column projection works as expected" $ property $ do
users <- forAll genUsers
results <- evalIO $
withTestDb users [] [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
u <- all_ (_dbUsers testDb)
pure (_userName u)
sort results === sort (map _userName users)
testProjectMultipleColumns :: TestTree
testProjectMultipleColumns = testProperty "Multiple column projection works as expected" $ property $ do
users <- forAll genUsers
results <- evalIO $
withTestDb users [] [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
u <- all_ (_dbUsers testDb)
pure (_userName u, _userAge u)
let expected = map (\u -> (_userName u, _userAge u)) users
sort results === sort expected
testProjectWithExpression :: TestTree
testProjectWithExpression = testProperty "Projection using an expression works as expected" $ property $ do
products <- forAll genProducts
results <- evalIO $
withTestDb [] products [] $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
p <- all_ (_dbProducts testDb)
pure (_productId p, _productPrice p * val_ 2)
let expected = map (\p -> (_productId p, _productPrice p * 2)) products
sortOn fst results === sortOn fst expected
testInnerJoin :: TestTree
testInnerJoin = testProperty "Inner joins work as expected" $ property $ do
users <- forAll genUsers
products <- forAll genProducts
orders <- forAll (genOrders users products)
results <- evalIO $
withTestDb users products orders $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
o <- all_ (_dbOrders testDb)
u <- related_ (_dbUsers testDb) (_orderUserId o)
pure (_userName u, _orderQuantity o)
let expected = do
o <- orders
let UserId uid = _orderUserId o
u <- filter (\u -> _userId u == uid) users
pure (_userName u, _orderQuantity o)
sort results === sort expected
testMultiInnerJoin :: TestTree
testMultiInnerJoin = testProperty "Multi-inner joins work as expected" $ property $ do
users <- forAll genUsers
products <- forAll genProducts
orders <- forAll (genOrders users products)
results <- evalIO $
withTestDb users products orders $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
o <- all_ (_dbOrders testDb)
u <- related_ (_dbUsers testDb) (_orderUserId o)
p <- related_ (_dbProducts testDb) (_orderProductId o)
pure (_userName u, _productName p, _orderQuantity o)
let expected = do
o <- orders
let UserId uid = _orderUserId o
ProductId pid = _orderProductId o
u <- filter (\u -> _userId u == uid) users
p <- filter (\p -> _productId p == pid) products
pure (_userName u, _productName p, _orderQuantity o)
sort results === sort expected
testMixingJoinsWithFilters :: TestTree
testMixingJoinsWithFilters = testProperty "Mixing joins and filters works as expected" $ property $ do
users <- forAll genUsers
products <- forAll genProducts
orders <- forAll (genOrders users products)
minQty <- forAll (Gen.int32 (Range.linear 1 50))
results <- evalIO $
withTestDb users products orders $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
o <- all_ (_dbOrders testDb)
guard_ (_orderQuantity o >=. val_ minQty)
u <- related_ (_dbUsers testDb) (_orderUserId o)
pure (_userName u, _orderQuantity o)
let expected = do
o <- filter (\o -> _orderQuantity o >= minQty) orders
let UserId uid = _orderUserId o
u <- filter (\u -> _userId u == uid) users
pure (_userName u, _orderQuantity o)
sort results === sort expected
testLeftJoin :: TestTree
testLeftJoin = testProperty "Left joins work as expected" $ property $ do
users <- forAll genUsers
products <- forAll genProducts
-- Generate orders for only a subset of users
let halfUsers = take (length users `div` 2) users
orders <- forAll (genOrders halfUsers products)
results <- evalIO $
withTestDb users products orders $ \conn ->
runBeamDuckDB conn $
runSelectReturningList $
select $ do
u <- all_ (_dbUsers testDb)
o <-
leftJoin_
(all_ (_dbOrders testDb))
(\o -> _orderUserId o ==. primaryKey u)
pure (_userId u, _orderQuantity o)
let resultUserIds = nubBy (\a b -> fst a == fst b) (sortOn fst results)
length resultUserIds === length users
let usersWithoutOrders = filter (\u -> _userId u `notElem` map _userId halfUsers) users
nothingRows = filter (\(uid, _) -> uid `elem` map _userId usersWithoutOrders) results
annotate "Users without orders should have Nothing quantity"
mapM_ (\(_, mq) -> mq === Nothing) nothingRows
-- TODO: this generates an invalid query
-- testCountDistinct :: TestTree
-- testCountDistinct = testCase "counts distinct users who placed orders" $ do
-- let users = [User 1 "Alice" 30]
-- withTestDb users [] [] $ \conn -> do
-- result <-
-- runBeamDuckDB conn $
-- runSelectReturningList $
-- select $ do
-- order <- all_ (_dbOrders testDb)
-- guard_ (distinct_ (pure $ _orderUserId order))
-- pure order
-- result @?= []
testConcat :: TestTree
testConcat = testCase "CONCAT two columns" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
withTestDb users [] [] $ \conn -> do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
orderBy_ (asc_ . fst) $
do
user <- all_ (_dbUsers testDb)
pure
( _userId user,
concat_
[ _userName user,
val_ " (age: ",
cast_ (_userAge user) (varchar Nothing),
val_ ")"
]
)
map snd rows
@?= [ "Alice (age: 30)",
"Bob (age: 25)",
"Charlie (age: 35)"
]
testCommonTableExpression :: TestTree
testCommonTableExpression = testCase "Non-recursive common table expression" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
selectWith $ do
userTotals <-
selecting
$ aggregate_
( \o ->
( group_ (_orderUserId o),
fromMaybe_ (val_ 0) $ sum_ (_orderQuantity o)
)
)
$ all_ (_dbOrders testDb)
pure $ do
(uid, total) <- reuse userTotals
u <- filter_ (\u -> UserId (_userId u) ==. uid) $ all_ (_dbUsers testDb)
pure (_userName u, total)
rows @?= [("Alice", 3), ("Bob", 1)]
testMultipleCommonTableExpressions :: TestTree
testMultipleCommonTableExpressions = testCase "Multiple common table expressions" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn -> do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
selectWith $ do
-- CTE 1: expensive products (price > 1000 cents)
expensiveProducts <-
selecting $
filter_ (\p -> _productPrice p >. val_ 1000) $
all_ (_dbProducts testDb)
-- CTE 2: orders for expensive products
expensiveOrders <-
selecting $ do
p <- reuse expensiveProducts
filter_ (\o -> _orderProductId o `references_` p) $
all_ (_dbOrders testDb)
-- Main query: distinct users who ordered expensive products
pure $ do
o <- reuse expensiveOrders
u <-
filter_ (\u -> _orderUserId o `references_` u) $
all_ (_dbUsers testDb)
pure (_userName u)
-- Gadget costs 2999; Alice ordered it
nub rows @?= ["Alice"]
testRecursiveCommonTableExpression :: TestTree
testRecursiveCommonTableExpression = testCase "Recursive common table expression" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
selectWith $ do
rec cte <-
selecting
-- Base case: SELECT 1
( pure (as_ @Int32 (val_ 1))
`union_`
-- Recursive step: SELECT n + 1 FROM cte WHERE n < 5
( do
n <- reuse cte
guard_ (n <. val_ 5)
pure (n + val_ 1)
)
)
pure (reuse cte)
sort rows @?= [1, 2, 3, 4, 5 :: Int32]
testSomeOver :: TestTree
testSomeOver = testCase "SOME" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
aggregate_
( \o ->
-- Expecting that at least one order per user has a quantity larger than 1
-- This is only true of a single user, UserId 1
( group_ (_orderUserId o),
isTrue_ $ someOver_ allInGroup_ (sqlBool_ (_orderQuantity o >. val_ 1))
)
)
(all_ (_dbOrders testDb))
rows
@?= [ (UserId 1, True),
(UserId 2, False)
]
testAnyOver :: TestTree
testAnyOver = testCase "ANY" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
aggregate_
( \o ->
-- Expecting that at least one order per user has a quantity larger than 1
-- This is only true of a single user, UserId 1
( group_ (_orderUserId o),
isTrue_ $ anyOver_ allInGroup_ (sqlBool_ (_orderQuantity o >. val_ 1))
)
)
(all_ (_dbOrders testDb))
rows
@?= [ (UserId 1, True),
(UserId 2, False)
]
testEveryOver :: TestTree
testEveryOver = testCase "EVERY" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
aggregate_
( \o ->
-- Expecting that all orders per user have a quantity larger than 1
-- This is not true for any user
( group_ (_orderUserId o),
isTrue_ $ everyOver_ allInGroup_ (sqlBool_ (_orderQuantity o >. val_ 1))
)
)
(all_ (_dbOrders testDb))
rows
@?= [ (UserId 1, False),
(UserId 2, False)
]
testRowNumberOverWholeResult :: TestTree
testRowNumberOverWholeResult = testCase "ROW_NUMBERS() over entire result set" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
withTestDb users [] [] $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
withWindow_
(\_ -> frame_ noPartition_ noOrder_ noBounds_)
(\u w -> (_userName u, rowNumber_ `over_` w))
(all_ (_dbUsers testDb))
rows
@?= [ ("Alice", 1 :: Int32),
("Bob", 2),
("Charlie", 3)
]
testRowNumberPartitionedByColumn :: TestTree
testRowNumberPartitionedByColumn = testCase "ROW_NUMBERS() partitioned over column" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 3
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
orderBy_ (\(oid, _, _, _) -> asc_ oid) $
withWindow_
( \o ->
frame_
(partitionBy_ (_orderProductId o))
(orderPartitionBy_ (desc_ (_orderQuantity o)))
noBounds_
)
(\o w -> (_orderId o, _orderProductId o, _orderUserId o, as_ @Int32 (rowNumber_ `over_` w)))
(all_ (_dbOrders testDb))
-- We expect two groups of rows.
-- One group of rows for product ID 1, one group of rows for product ID 2.
-- For each group, the data for orders is considered in descending number of quantity
--
-- the order of each group is not deterministic.
rows @?= [(1, ProductId 1, UserId 1, 1), (2, ProductId 1, UserId 2, 2), (3, ProductId 2, UserId 1, 1)]
testWindowingWithBounds :: TestTree
testWindowingWithBounds = testCase "3-row moving average" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25
]
products =
[Product 1 "Widget" 999]
orders =
[ Order 1 (UserId 1) (ProductId 1) 10,
Order 2 (UserId 1) (ProductId 1) 5,
Order 3 (UserId 2) (ProductId 1) 8,
Order 4 (UserId 1) (ProductId 1) 3,
Order 5 (UserId 2) (ProductId 1) 7
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
withWindow_
( \o ->
frame_
noPartition_
(orderPartitionBy_ (asc_ (_orderId o)))
(bounds_ (nrows_ 1) (Just (nrows_ 1)))
)
( \o w ->
( _orderId o,
avgOver_ allInGroup_ (cast_ (_orderQuantity o) double) `over_` w
)
)
(all_ (_dbOrders testDb))
rows
@?= [ (1, Just 7.5),
(2, Just 7.666666666666667),
(3, Just 5.333333333333333),
(4, Just 6.0),
(5, Just 5.0)
]
testLag :: TestTree
testLag = testCase "LAG" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 3
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
withWindow_
(\o -> frame_ noPartition_ (orderPartitionBy_ (asc_ (_orderId o))) noBounds_)
( \o w ->
lagWithDefault_ (_orderQuantity o) (val_ (1 :: Int32)) (val_ 0) `over_` w
)
(all_ (_dbOrders testDb))
-- First row has no predecessor → default 0
-- Second row lags to first row's qty (2)
-- Third row lags to second row's qty (1)
rows @?= [0, 2, 1]
testLead :: TestTree
testLead = testCase "LEAD" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 3
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
withWindow_
(\o -> frame_ noPartition_ (orderPartitionBy_ (asc_ (_orderId o))) noBounds_)
( \o w ->
lagWithDefault_ (_orderQuantity o) (val_ (1 :: Int32)) (val_ 0) `over_` w
)
(all_ (_dbOrders testDb))
-- First leads to second's qty (1)
-- Second leads to third's qty (1)
-- Third has no successor → default 0
rows @?= [0, 2, 1]
testFilterWhereCountAll :: TestTree
testFilterWhereCountAll = testCase "COUNT(*) FILTER (WHERE ... )" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 3
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn
$ runSelectReturningList
$ select
$ aggregate_
( \o ->
( group_ (_orderUserId o),
as_ @Int32 $ countAll_,
as_ @Int32 $ countAll_ `filterWhere_` (_orderQuantity o >. val_ 1)
)
)
$ all_ (_dbOrders testDb)
-- Alice (uid 1): 2 total orders, 2 with qty > 1
-- Bob (uid 2): 1 total order, 0 with qty > 1
rows @?= [(UserId 1, 2, 2), (UserId 2, 1, 0)]
testFilterWithWindow :: TestTree
testFilterWithWindow = testCase "FILTER with window function" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
withWindow_
(\o -> frame_ noPartition_ (orderPartitionBy_ (asc_ (_orderId o))) noBounds_)
( \o w ->
( _orderId o,
fromMaybe_
(val_ 0)
( ( sumOver_ allInGroup_ (_orderQuantity o)
`filterWhere_` (_orderQuantity o >. val_ 1)
)
`over_` w
)
)
)
(all_ (_dbOrders testDb))
-- Only order 1 (qty=2) passes the filter
-- Running filtered sum: 2, 2, 2
let sorted = sort rows
map snd sorted @?= [2, 2, 2]
testFirstValue :: TestTree
testFirstValue = testCase "FIRST_VALUE" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
orderBy_ (\(oid, _, _) -> desc_ oid) $
withWindow_
( \o ->
frame_
(partitionBy_ (_orderProductId o))
(orderPartitionBy_ (asc_ (_orderQuantity o)))
(bounds_ unbounded_ (Just unbounded_))
)
( \o w ->
( _orderId o,
_orderProductId o,
firstValue_ (_orderQuantity o) `over_` w
)
)
(all_ (_dbOrders testDb))
-- Widget (pid 1): quantities 1, 2 → first_value is 1 for both
-- Gadget (pid 2): quantity 1 → first_value is 1
rows @?= [(3, ProductId 2, 1), (2, ProductId 1, 1), (1, ProductId 1, 1)]
testLastValue :: TestTree
testLastValue = testCase "LAST_VALUE" $ do
let users =
[ User 1 "Alice" 30,
User 2 "Bob" 25,
User 3 "Charlie" 35
]
products =
[ Product 1 "Widget" 999,
Product 2 "Gadget" 2999
]
orders =
[ Order 1 (UserId 1) (ProductId 1) 2,
Order 2 (UserId 2) (ProductId 1) 1,
Order 3 (UserId 1) (ProductId 2) 1
]
withTestDb users products orders $ \conn ->
do
rows <-
runBeamDuckDB conn $
runSelectReturningList $
select $
orderBy_ (\(oid, _, _) -> asc_ oid) $
withWindow_
( \o ->
frame_
(partitionBy_ (_orderProductId o))
(orderPartitionBy_ (asc_ (_orderQuantity o)))
-- Must use UNBOUNDED FOLLOWING to see the actual last value,
-- otherwise the default frame ends at CURRENT ROW.
(bounds_ unbounded_ (Just unbounded_))
)
( \o w ->
( _orderId o,
_orderProductId o,
lastValue_ (_orderQuantity o) `over_` w
)
)
(all_ (_dbOrders testDb))
-- Widget (pid 1): quantities 1, 2 → last_value is 2 for both
rows @?= [(1, ProductId 1, 2), (2, ProductId 1, 2), (3, ProductId 2, 1)]
data UserT f = User
{ _userId :: Columnar f Int32,
_userName :: Columnar f Text,
_userAge :: Columnar f Int32
}
deriving (Generic)
type User = UserT Identity
type UserId = PrimaryKey UserT Identity
deriving instance Show UserId
deriving instance Eq UserId
deriving instance Ord UserId
deriving instance Show User
deriving instance Eq User
deriving instance Ord User
instance Beamable UserT
instance Table UserT where
data PrimaryKey UserT f = UserId (Columnar f Int32)
deriving (Generic)
primaryKey = UserId . _userId
instance Beamable (PrimaryKey UserT)
data ProductT f = Product
{ _productId :: Columnar f Int32,
_productName :: Columnar f Text,
_productPrice :: Columnar f Int32 -- cents, to avoid floating point
}
deriving (Generic)
type Product = ProductT Identity
type ProductId = PrimaryKey ProductT Identity
deriving instance Show ProductId
deriving instance Eq ProductId
deriving instance Ord ProductId
deriving instance Show Product
deriving instance Eq Product
deriving instance Ord Product
instance Beamable ProductT
instance Table ProductT where
data PrimaryKey ProductT f = ProductId (Columnar f Int32)
deriving (Generic)
primaryKey = ProductId . _productId
instance Beamable (PrimaryKey ProductT)
data OrderT f = Order
{ _orderId :: Columnar f Int32,
_orderUserId :: PrimaryKey UserT f,
_orderProductId :: PrimaryKey ProductT f,
_orderQuantity :: Columnar f Int32
}
deriving (Generic)
type Order = OrderT Identity
type OrderId = PrimaryKey OrderT Identity
deriving instance Show OrderId
deriving instance Eq OrderId
deriving instance Ord OrderId
deriving instance Show Order
deriving instance Eq Order
deriving instance Ord Order
instance Beamable OrderT
instance Table OrderT where
data PrimaryKey OrderT f = OrderId (Columnar f Int32)
deriving (Generic)
primaryKey = OrderId . _orderId
instance Beamable (PrimaryKey OrderT)
data TestDB f = TestDB
{ _dbUsers :: f (TableEntity UserT),
_dbProducts :: f (TableEntity ProductT),
_dbOrders :: f (TableEntity OrderT)
}
deriving (Generic, Database be)
testDb :: DatabaseSettings DuckDB TestDB
testDb =
defaultDbSettings
`withDbModification` dbModification
{ _dbUsers =
modifyTableFields
tableModification
{ _userId = "id",
_userName = "name",
_userAge = "age"
},
_dbProducts =
modifyTableFields
tableModification
{ _productId = "id",
_productName = "name",
_productPrice = "price"
},
_dbOrders =
modifyTableFields
tableModification
{ _orderId = "id",
_orderUserId = UserId "user_id",
_orderProductId = ProductId "product_id",
_orderQuantity = "quantity"
}
}
genName :: Gen Text
genName = Gen.text (Range.linear 1 50) Gen.alphaNum
genAge :: Gen Int32
genAge = Gen.int32 (Range.linear 1 120)
genPrice :: Gen Int32
genPrice = Gen.int32 (Range.linear 100 100000)
genQuantity :: Gen Int32
genQuantity = Gen.int32 (Range.linear 1 100)
genUsers :: Gen [User]
genUsers = do
n <- Gen.int (Range.linear 3 20)
traverse (\i -> User (fromIntegral i) <$> genName <*> genAge) [1 .. n]
genProducts :: Gen [Product]
genProducts = do
n <- Gen.int (Range.linear 2 10)
traverse (\i -> Product (fromIntegral i) <$> genName <*> genPrice) [1 .. n]
genOrders :: [User] -> [Product] -> Gen [Order]
genOrders users products = do
n <- Gen.int (Range.linear 1 (length users * length products))
traverse
( \i -> do
uid <- Gen.element (map _userId users)
pid <- Gen.element (map _productId products)
Order (fromIntegral i) (UserId uid) (ProductId pid) <$> genQuantity
)
[1 .. n]
createTables :: Connection -> IO ()
createTables conn = do
void $
execute_
conn
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL)"
void $
execute_
conn
"CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL)"
void $
execute_
conn
"CREATE TABLE orders (\
\ id INTEGER PRIMARY KEY, \
\ user_id INTEGER NOT NULL REFERENCES users(id), \
\ product_id INTEGER NOT NULL REFERENCES products(id), \
\ quantity INTEGER NOT NULL)"
seedData :: Connection -> [User] -> [Product] -> [Order] -> IO ()
seedData conn users products orders = runBeamDuckDB conn $ do
runInsert $ insert (_dbUsers testDb) $ insertValues users
runInsert $ insert (_dbProducts testDb) $ insertValues products
runInsert $ insert (_dbOrders testDb) $ insertValues orders
-- Run a test with a fresh in-memory DB populated with the given data
withTestDb ::
[User] ->
[Product] ->
[Order] ->
(Connection -> IO a) ->
IO a
withTestDb users products orders action =
withConnection ":memory:" $ \conn -> do
createTables conn
seedData conn users products orders
action conn