packages feed

dataframe-3.0.0.0: tests/Operations/Provenance.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Operations.Provenance where

import qualified Data.Map as M
import qualified DataFrame as D
import qualified DataFrame.Functions as F
import qualified DataFrame.Internal.Column as DI
import qualified DataFrame.Internal.DataFrame as DI
import DataFrame.Operations.Merge ((|||))

import Test.HUnit

-- Base frame with no derived columns.
base :: D.DataFrame
base =
    D.fromNamedColumns
        [ ("x", DI.fromList [1 .. 5 :: Int])
        , ("y", DI.fromList [2 .. 6 :: Int])
        ]

-- A frame with one derived column "z".
withZ :: D.DataFrame
withZ = D.derive "z" (F.col @Int "x" + F.col "y") base

-- ── insertColumn ──────────────────────────────────────────────────────────────

-- Inserting a new column must not wipe provenance of existing derived columns.
insertPreservesProvenance :: Test
insertPreservesProvenance =
    TestCase
        ( assertBool
            "insertColumn should preserve existing derivingExpressions"
            ( M.member
                "z"
                (DI.derivingExpressions (D.insertColumn "w" (DI.fromList [0 :: Int]) withZ))
            )
        )

-- Overwriting a derived column removes only *that* expression, not others.
insertOverwriteDropsOwnExpr :: Test
insertOverwriteDropsOwnExpr =
    let
        df2 = D.derive "w" (F.col @Int "x") withZ
        df3 = D.insertColumn "z" (DI.fromList [99 :: Int]) df2
     in
        TestCase $ do
            assertBool
                "overwritten column z should be removed from derivingExpressions"
                (not $ M.member "z" (DI.derivingExpressions df3))
            assertBool
                "sibling column w expression should be preserved"
                (M.member "w" (DI.derivingExpressions df3))

-- ── derive ────────────────────────────────────────────────────────────────────

-- derive adds the expression to derivingExpressions.
deriveTracksExpression :: Test
deriveTracksExpression =
    TestCase
        ( assertBool
            "derive should record z in derivingExpressions"
            (M.member "z" (DI.derivingExpressions withZ))
        )

-- Multiple derives accumulate.
deriveManyTracksAll :: Test
deriveManyTracksAll =
    let df = D.derive "w" (F.col @Int "x") withZ
     in TestCase
            ( assertEqual
                "two derive calls should leave two expressions"
                2
                (M.size (DI.derivingExpressions df))
            )

-- Re-deriving a column replaces its expression and keeps the count stable.
deriveOverwriteReplacesExpression :: Test
deriveOverwriteReplacesExpression =
    let df = D.derive "z" (F.col @Int "y") withZ -- overwrite z
     in TestCase
            ( assertEqual
                "re-deriving z should not duplicate the entry"
                1
                (M.size (DI.derivingExpressions df))
            )

-- ── deriveWithExpr ────────────────────────────────────────────────────────────

-- deriveWithExpr should also track the expression.
deriveWithExprTracksExpression :: Test
deriveWithExprTracksExpression =
    let (_, df) = D.deriveWithExpr @Int "z" (F.col @Int "x" + F.col "y") base
     in TestCase
            ( assertBool
                "deriveWithExpr should record z in derivingExpressions"
                (M.member "z" (DI.derivingExpressions df))
            )

-- ── showDerivedExpressions ────────────────────────────────────────────────────

-- A frame with no derived columns returns identity provenance for each column.
showDerivedEmpty :: Test
showDerivedEmpty =
    TestCase
        ( assertEqual
            "raw-column frame should have identity provenance for each column"
            ["x", "y"]
            (map fst (D.showDerivedExpressions base))
        )

-- Frame with one derived column has an entry for the derived column.
showDerivedContainsName :: Test
showDerivedContainsName =
    TestCase
        ( assertBool
            "showDerivedExpressions should include the derived column name"
            ("z" `elem` map fst (D.showDerivedExpressions withZ))
        )

-- ── Semigroup (<>) provenance propagation ─────────────────────────────────────

-- Vertical merge preserves expressions from both sides.
semiGroupPreservesLeft :: Test
semiGroupPreservesLeft =
    let merged = withZ <> base
     in TestCase
            ( assertBool
                "<> should preserve derivingExpressions from the left frame"
                (M.member "z" (DI.derivingExpressions merged))
            )

semiGroupPreservesBoth :: Test
semiGroupPreservesBoth =
    let dfW = D.derive "w" (F.col @Int "y") base
        merged = withZ <> dfW
     in TestCase
            ( assertEqual
                "<> should union derivingExpressions from both frames"
                2
                (M.size (DI.derivingExpressions merged))
            )

-- Left frame wins when both sides have an expression for the same column.
semiGroupLeftBias :: Test
semiGroupLeftBias =
    let dfLeft = D.derive "z" (F.col @Int "x") base
        dfRight = D.derive "z" (F.col @Int "y") base
        merged = dfLeft <> dfRight
     in TestCase
            ( assertEqual
                "<> should retain exactly one entry for a shared column name"
                1
                (M.size (DI.derivingExpressions merged))
            )

-- Merging with an empty frame must not lose provenance.
semiGroupWithEmpty :: Test
semiGroupWithEmpty =
    TestCase
        ( assertBool
            "withZ <> empty should keep z's expression"
            (M.member "z" (DI.derivingExpressions (withZ <> D.empty)))
        )

emptyWithSemiGroup :: Test
emptyWithSemiGroup =
    TestCase
        ( assertBool
            "empty <> withZ should keep z's expression"
            (M.member "z" (DI.derivingExpressions (D.empty <> withZ)))
        )

-- ── Horizontal merge (|||) provenance propagation ────────────────────────────

horizontalMergePreservesLeft :: Test
horizontalMergePreservesLeft =
    let dfW = D.derive "w" (F.col @Int "y") base
        extra = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]
        merged = dfW ||| extra
     in TestCase
            ( assertBool
                "||| should preserve derivingExpressions from the left frame"
                (M.member "w" (DI.derivingExpressions merged))
            )

horizontalMergePreservesRight :: Test
horizontalMergePreservesRight =
    let extra = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]
        dfW =
            D.derive
                "w"
                (F.col @Int "y")
                (D.fromNamedColumns [("y", DI.fromList [2 .. 6 :: Int])])
        merged = extra ||| dfW
     in TestCase
            ( assertBool
                "||| should bring in derivingExpressions from the right frame"
                (M.member "w" (DI.derivingExpressions merged))
            )

horizontalMergePreservesBoth :: Test
horizontalMergePreservesBoth =
    let dfZ = withZ
        dfW = D.fromNamedColumns [("q", DI.fromList [0 :: Int, 0, 0, 0, 0])]
        -- give dfW a derived column on a separate base
        dfWD = D.derive "w" (F.col @Int "q") dfW
        merged = dfZ ||| dfWD
     in TestCase
            ( assertEqual
                "||| should union derivingExpressions"
                2
                (M.size (DI.derivingExpressions merged))
            )

tests :: [Test]
tests =
    [ TestLabel "insertPreservesProvenance" insertPreservesProvenance
    , TestLabel "insertOverwriteDropsOwnExpr" insertOverwriteDropsOwnExpr
    , TestLabel "deriveTracksExpression" deriveTracksExpression
    , TestLabel "deriveManyTracksAll" deriveManyTracksAll
    , TestLabel "deriveOverwriteReplacesExpression" deriveOverwriteReplacesExpression
    , TestLabel "deriveWithExprTracksExpression" deriveWithExprTracksExpression
    , TestLabel "showDerivedEmpty" showDerivedEmpty
    , TestLabel "showDerivedContainsName" showDerivedContainsName
    , TestLabel "semiGroupPreservesLeft" semiGroupPreservesLeft
    , TestLabel "semiGroupPreservesBoth" semiGroupPreservesBoth
    , TestLabel "semiGroupLeftBias" semiGroupLeftBias
    , TestLabel "semiGroupWithEmpty" semiGroupWithEmpty
    , TestLabel "emptyWithSemiGroup" emptyWithSemiGroup
    , TestLabel "horizontalMergePreservesLeft" horizontalMergePreservesLeft
    , TestLabel "horizontalMergePreservesRight" horizontalMergePreservesRight
    , TestLabel "horizontalMergePreservesBoth" horizontalMergePreservesBoth
    ]