dataframe-1.1.0.0: src/DataFrame/Typed/Freeze.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module DataFrame.Typed.Freeze (
-- * Safe boundary
freeze,
freezeWithError,
-- * Escape hatches
thaw,
unsafeFreeze,
) where
import qualified Data.Text as T
import Type.Reflection (SomeTypeRep)
import Data.List (stripPrefix)
import qualified DataFrame.Internal.Column as C
import qualified DataFrame.Internal.DataFrame as D
import DataFrame.Operations.Core (columnNames)
import DataFrame.Typed.Schema (KnownSchema (..))
import DataFrame.Typed.Types (TypedDataFrame (..))
{- | Validate that an untyped 'DataFrame' matches the expected schema @cols@,
then wrap it. Returns 'Nothing' on mismatch.
-}
freeze ::
forall cols. (KnownSchema cols) => D.DataFrame -> Maybe (TypedDataFrame cols)
freeze df = case validateSchema @cols df of
Left _ -> Nothing
Right _ -> Just (TDF df)
-- | Like 'freeze' but returns a descriptive error message on failure.
freezeWithError ::
forall cols.
(KnownSchema cols) =>
D.DataFrame -> Either T.Text (TypedDataFrame cols)
freezeWithError df = case validateSchema @cols df of
Left err -> Left err
Right _ -> Right (TDF df)
{- | Unwrap a typed DataFrame back to the untyped representation.
Always safe; discards type information.
-}
thaw :: TypedDataFrame cols -> D.DataFrame
thaw (TDF df) = df
{- | Wrap an untyped DataFrame without any validation.
Used internally after delegation where the library guarantees schema correctness.
-}
unsafeFreeze :: D.DataFrame -> TypedDataFrame cols
unsafeFreeze = TDF
validateSchema ::
forall cols.
(KnownSchema cols) =>
D.DataFrame -> Either T.Text ()
validateSchema df = mapM_ checkCol (schemaEvidence @cols)
where
checkCol :: (T.Text, SomeTypeRep) -> Either T.Text ()
checkCol (name, expectedRep) = case D.getColumn name df of
Nothing ->
Left $
"Column '"
<> name
<> "' not found in DataFrame. "
<> "Available columns: "
<> T.pack (show (columnNames df))
Just col ->
if matchesType expectedRep col
then Right ()
else
Left $
"Type mismatch on column '"
<> name
<> "': expected "
<> T.pack (show expectedRep)
<> ", got "
<> T.pack (C.columnTypeString col)
{- | Check if a Column's element type matches the expected SomeTypeRep.
For nullable columns (those with a bitmap), @Maybe a@ in the schema matches
a column whose inner type is @a@, since we store nullable data as
@BoxedColumn (Just bm) a@ or @UnboxedColumn (Just bm) a@ rather than
@Column (Maybe a)@.
-}
matchesType :: SomeTypeRep -> C.Column -> Bool
matchesType expected col =
let expectedStr = show expected
colTypeStr = C.columnTypeString col
in expectedStr == colTypeStr
|| ( C.hasMissing col -- nullable column: schema says "Maybe X", column stores "X" with a bitmap
&& Just colTypeStr == stripPrefix "Maybe " expectedStr
)