packages feed

beam-duckdb-0.3.1.0: tests/Database/Beam/DuckDB/Test/Extensions/DataSource.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Database.Beam.DuckDB.Test.Extensions.DataSource (tests) where

import Data.Int (Int32)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text (Text)
import Data.Time (Day, fromGregorian)
import Database.Beam
  ( Beamable,
    Columnar,
    Database,
    DatabaseSettings,
    Generic,
    Identity,
    Table (..),
    aggregate_,
    as_,
    countAll_,
    dbModification,
    defaultDbSettings,
    guard_,
    min_,
    runSelectReturningList,
    runSelectReturningOne,
    select,
    tableModification,
    val_,
    withDbModification,
    (==.),
  )
import Database.Beam.DuckDB
  ( CSVOptions (..),
    DataSourceEntity,
    DuckDB,
    allFromDataSource_,
    allowMovedPaths,
    csvWith,
    dataSource,
    defaultCSVOptions,
    defaultIcebergTableOptions,
    icebergTableWith,
    modifyDataSourceFields,
    parquet,
    runBeamDuckDB,
  )
import Database.DuckDB.Simple (Connection, withConnection)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "DataSource"
    [ testGroup
        "Parquet"
        [ testCountFromParquet,
          testQueryFromParquet,
          testCountFromMultipleParquetFiles,
          testQueryFromMultipleParquetFiles
        ],
      testGroup
        "Apache Iceberg"
        [ testCountFromIceberg,
          testParseDateFromIceberg
        ],
      testGroup
        "CSV"
        [ testCountFromCSV,
          testQueryFromCSV
        ]
    ]

testCountFromParquet :: TestTree
testCountFromParquet = testCase "Counting records from a Parquet file" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningList $
        select $ do
          exam <- allFromDataSource_ (_dbExams testDb)
          pure (_examName exam)
  results @?= ["alice", "bob", "carol", "dave"]

testQueryFromParquet :: TestTree
testQueryFromParquet = testCase "Query from a Parquet file" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $ do
          exam <- allFromDataSource_ (_dbExams testDb)
          guard_ (_examId exam ==. 1)
          pure (_examName exam)
  results @?= Just "alice"

testCountFromMultipleParquetFiles :: TestTree
testCountFromMultipleParquetFiles = testCase "Counting records from multiple Parquet file" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningList $
        select $ do
          exam <- allFromDataSource_ (_dbExamsMulti testDb)
          pure (_examName exam)
  results @?= ["alice", "bob", "carol", "dave", "erika", "francis", "genevieve", "hugo"]

testQueryFromMultipleParquetFiles :: TestTree
testQueryFromMultipleParquetFiles = testCase "Query from multiple Parquet files" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $ do
          exam <- allFromDataSource_ (_dbExamsMulti testDb)
          guard_ (_examId exam ==. 5)
          pure (_examName exam)
  results @?= Just "erika"

testCountFromIceberg :: TestTree
testCountFromIceberg = testCase "Counting records from an Apache Iceberg table" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $
          aggregate_ (\_ -> as_ @Int32 countAll_) (allFromDataSource_ (_dbLineItems testDb))
  results @?= Just 51793 -- From DuckDB's documentation

testParseDateFromIceberg :: TestTree
testParseDateFromIceberg = testCase "Test parsing date columns from Apache Iceberg table" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $
          aggregate_ (min_ . _lineitemShipdate) (allFromDataSource_ (_dbLineItems testDb))

  results @?= Just (Just (fromGregorian 1992 01 04))

testCountFromCSV :: TestTree
testCountFromCSV = testCase "Counting records from a CSV file" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $
          aggregate_ (\_ -> as_ @Int32 countAll_) (allFromDataSource_ (_dbFlights testDb))
  results @?= Just 3

testQueryFromCSV :: TestTree
testQueryFromCSV = testCase "Query from a CSV file" $ do
  results <- withTestDb $ \conn ->
    runBeamDuckDB conn $
      runSelectReturningOne $
        select $ do
          flight <- allFromDataSource_ (_dbFlights testDb)
          guard_ (_flightDate flight ==. val_ (fromGregorian 1988 01 01))
          pure flight
  results
    @?= Just
      ( Flight
          { _flightDate = fromGregorian 1988 01 01,
            _flightUniqueCarrier = "AA",
            _flightOriginCity = "New York, NY",
            _flightDestCity = "Los Angeles, CA"
          }
      )

data ExamT f = Exam
  { _examId :: Columnar f Int32,
    _examName :: Columnar f Text,
    _examScore :: Columnar f Double,
    _examDate :: Columnar f Day
  }
  deriving (Generic)

type Exam = ExamT Identity

type ExamId = PrimaryKey ExamT Identity

deriving instance Show ExamId

deriving instance Eq ExamId

deriving instance Ord ExamId

deriving instance Show Exam

deriving instance Eq Exam

deriving instance Ord Exam

instance Beamable ExamT

instance Table ExamT where
  data PrimaryKey ExamT f = ExamId (Columnar f Int32)
    deriving (Generic)
  primaryKey = ExamId . _examId

instance Beamable (PrimaryKey ExamT)

data LineItemT f = Lineitem
  { _lineitemOrderkey :: Columnar f Int32,
    _lineitemPartkey :: Columnar f Int32,
    _lineitemSuppkey :: Columnar f Int32,
    _lineitemLinenumber :: Columnar f Int32,
    _lineitemQuantity :: Columnar f Double,
    _lineitemExtendedprice :: Columnar f Double,
    _lineitemDiscount :: Columnar f Double,
    _lineitemTax :: Columnar f Double,
    _lineitemReturnflag :: Columnar f Text,
    _lineitemLinestatus :: Columnar f Text,
    _lineitemShipdate :: Columnar f Day,
    _lineitemCommitdate :: Columnar f Day,
    _lineitemReceiptdate :: Columnar f Day,
    _lineitemShipinstruct :: Columnar f Text,
    _lineitemShipmode :: Columnar f Text,
    _lineitemComment :: Columnar f Text
  }
  deriving (Generic)

type Lineitem = LineItemT Identity

deriving instance Show Lineitem

deriving instance Eq Lineitem

instance Beamable LineItemT

instance Table LineItemT where
  data PrimaryKey LineItemT f
    = LineItemKey (Columnar f Int32) (Columnar f Int32)
    deriving (Generic)
  primaryKey li =
    LineItemKey (_lineitemOrderkey li) (_lineitemLinenumber li)

instance Beamable (PrimaryKey LineItemT)

deriving instance Show (PrimaryKey LineItemT Identity)

deriving instance Eq (PrimaryKey LineItemT Identity)

data FlightT f = Flight
  { _flightDate :: Columnar f Day,
    _flightUniqueCarrier :: Columnar f Text,
    _flightOriginCity :: Columnar f Text,
    _flightDestCity :: Columnar f Text
  }
  deriving (Generic)

type Flight = FlightT Identity

deriving instance Show Flight

deriving instance Eq Flight

instance Beamable FlightT

instance Table FlightT where
  data PrimaryKey FlightT f
    = FlightKey
        (Columnar f Day)
        (Columnar f Text)
        (Columnar f Text)
        (Columnar f Text)
    deriving (Generic)
  primaryKey fl =
    FlightKey
      (_flightDate fl)
      (_flightUniqueCarrier fl)
      (_flightOriginCity fl)
      (_flightDestCity fl)

instance Beamable (PrimaryKey FlightT)

deriving instance Show (PrimaryKey FlightT Identity)

deriving instance Eq (PrimaryKey FlightT Identity)

data TestDB f = TestDB
  { _dbExams :: f (DataSourceEntity ExamT),
    _dbExamsMulti :: f (DataSourceEntity ExamT), -- set up with multiple parquet files
    _dbLineItems :: f (DataSourceEntity LineItemT),
    _dbFlights :: f (DataSourceEntity FlightT)
  }
  deriving (Generic, Database DuckDB)

testDb :: DatabaseSettings DuckDB TestDB
testDb =
  defaultDbSettings
    `withDbModification` (dbModification @_ @DuckDB)
      { _dbExams =
          dataSource
            (parquet ("tests/data/test1.parquet" :| []))
            <> modifyDataSourceFields
              tableModification
                { _examId = "id",
                  _examName = "name",
                  _examScore = "score",
                  _examDate = "exam_date"
                },
        _dbExamsMulti =
          dataSource
            ( parquet
                ("tests/data/test1.parquet" :| ["tests/data/test2.parquet"])
            )
            <> modifyDataSourceFields
              tableModification
                { _examId = "id",
                  _examName = "name",
                  _examScore = "score",
                  _examDate = "exam_date"
                },
        _dbLineItems =
          dataSource
            ( icebergTableWith
                "tests/data/lineitem_iceberg"
                (defaultIcebergTableOptions {allowMovedPaths = Just True})
            )
            <> modifyDataSourceFields
              tableModification
                { _lineitemOrderkey = "l_orderkey",
                  _lineitemPartkey = "l_partkey",
                  _lineitemSuppkey = "l_suppkey",
                  _lineitemLinenumber = "l_linenumber",
                  _lineitemQuantity = "l_quantity",
                  _lineitemExtendedprice = "l_extendedprice",
                  _lineitemDiscount = "l_discount",
                  _lineitemTax = "l_tax",
                  _lineitemReturnflag = "l_returnflag",
                  _lineitemLinestatus = "l_linestatus",
                  _lineitemShipdate = "l_shipdate",
                  _lineitemCommitdate = "l_commitdate",
                  _lineitemReceiptdate = "l_receiptdate",
                  _lineitemShipinstruct = "l_shipinstruct",
                  _lineitemShipmode = "l_shipmode",
                  _lineitemComment = "l_comment"
                },
        _dbFlights =
          dataSource
            ( csvWith
                ("tests/data/flights.csv" :| [])
                ( defaultCSVOptions
                    { header = Just True,
                      comment = Just '#',
                      delim = Just "|",
                      ignoreErrors = Just False
                    }
                )
            )
            <> modifyDataSourceFields
              tableModification
                { _flightDate = "FlightDate",
                  _flightUniqueCarrier = "UniqueCarrier",
                  _flightOriginCity = "OriginCityName",
                  _flightDestCity = "DestCityName"
                }
      }

withTestDb ::
  (Connection -> IO a) ->
  IO a
withTestDb =
  withConnection ":memory:"