packages feed

dataframe 0.7.0.0 → 1.0.0.0

raw patch · 70 files changed

+6527/−1079 lines, 70 filesdep +http-conduitdep +streamly-bytestringdep +streamly-coredep ~aesondep ~dataframenew-component:flib:dataframe-arrowbinary-addedPVP ok

version bump matches the API change (PVP)

Dependencies added: http-conduit, streamly-bytestring, streamly-core

Dependency ranges changed: aeson, dataframe

API changes (from Hackage documentation)

- DataFrame.Functions: lift2Decorated :: (Columnable c, Columnable b, Columnable a) => (c -> b -> a) -> Text -> Maybe Text -> Bool -> Int -> Expr c -> Expr b -> Expr a
- DataFrame.Functions: liftDecorated :: (Columnable a, Columnable b) => (a -> b) -> Text -> Maybe Text -> Expr a -> Expr b
- DataFrame.IO.Parquet.Binary: littleEndianInt32 :: ByteString -> Int32
- DataFrame.IO.Parquet.Binary: littleEndianWord32 :: ByteString -> Word32
- DataFrame.IO.Parquet.Binary: littleEndianWord64 :: ByteString -> Word64
- DataFrame.IO.Parquet.Binary: word32ToLittleEndian :: Word32 -> ByteString
- DataFrame.IO.Parquet.Binary: word64ToLittleEndian :: Word64 -> ByteString
- DataFrame.Internal.Column: atIndices :: Set Int -> Column -> Column
- DataFrame.Internal.Column: atIndicesWithNulls :: Vector (Maybe Int) -> Column -> Column
- DataFrame.Internal.Column: foldDirectGroups :: Columnable b => (acc -> b -> acc) -> acc -> Column -> Vector Int -> Vector Int -> Either DataFrameException (Vector acc)
- DataFrame.Internal.Column: foldlColumnWith :: Columnable a => (b -> a -> b) -> b -> Column -> Either DataFrameException b
- DataFrame.Lazy.Internal.DataFrame: limit :: Int -> LazyDataFrame -> LazyDataFrame
+ DataFrame: makeSchema :: [(Text, SchemaType)] -> Schema
+ DataFrame: showDerivedExpressions :: DataFrame -> [NamedExpr]
+ DataFrame: stratifiedSample :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
+ DataFrame: stratifiedSplit :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
+ DataFrame.DecisionTree: NDouble :: !Expr Double -> NumExpr
+ DataFrame.DecisionTree: NMaybeDouble :: !Expr (Maybe Double) -> NumExpr
+ DataFrame.DecisionTree: buildProbTree :: Columnable a => Tree a -> Text -> DataFrame -> Vector Int -> ProbTree a
+ DataFrame.DecisionTree: combineNumExprs :: NumExpr -> NumExpr -> [NumExpr]
+ DataFrame.DecisionTree: data NumExpr
+ DataFrame.DecisionTree: fitProbTree :: Columnable a => TreeConfig -> Expr a -> DataFrame -> Map a (Expr Double)
+ DataFrame.DecisionTree: numExprCols :: NumExpr -> [Text]
+ DataFrame.DecisionTree: numExprEq :: NumExpr -> NumExpr -> Bool
+ DataFrame.DecisionTree: probExprs :: Columnable a => ProbTree a -> Map a (Expr Double)
+ DataFrame.DecisionTree: probsFromIndices :: Columnable a => Text -> DataFrame -> Vector Int -> Map a Double
+ DataFrame.DecisionTree: type ProbTree a = Tree Map a Double
+ DataFrame.Functions: cast :: Columnable a => Text -> Expr (Maybe a)
+ DataFrame.Functions: castEither :: Columnable a => Text -> Expr (Either Text a)
+ DataFrame.Functions: castExpr :: (Columnable b, Columnable src) => Expr src -> Expr (Maybe b)
+ DataFrame.Functions: castExprEither :: (Columnable b, Columnable src) => Expr src -> Expr (Either Text b)
+ DataFrame.Functions: castExprWithDefault :: (Columnable b, Columnable src) => b -> Expr src -> Expr b
+ DataFrame.Functions: castWithDefault :: Columnable a => a -> Text -> Expr a
+ DataFrame.Functions: emptyNullableColumnForType :: TType -> Column
+ DataFrame.Functions: nullLift :: (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) => (BaseType a -> r) -> Expr a -> Expr (NullLift1Result a r)
+ DataFrame.Functions: nullLift2 :: (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) => (BaseType a -> BaseType b -> r) -> Expr a -> Expr b -> Expr (NullLift2Result a b r)
+ DataFrame.Functions: unsafeCast :: Columnable a => Text -> Expr a
+ DataFrame.Functions: unsafeCastExpr :: (Columnable b, Columnable src) => Expr src -> Expr b
+ DataFrame.IO.Parquet: HFParquetFile :: Text -> Text -> Text -> Text -> HFParquetFile
+ DataFrame.IO.Parquet: HFParquetResponse :: [HFParquetFile] -> HFParquetResponse
+ DataFrame.IO.Parquet: HFRef :: Text -> Text -> Text -> HFRef
+ DataFrame.IO.Parquet: [hfDataset] :: HFRef -> Text
+ DataFrame.IO.Parquet: [hfGlob] :: HFRef -> Text
+ DataFrame.IO.Parquet: [hfOwner] :: HFRef -> Text
+ DataFrame.IO.Parquet: [hfParquetFiles] :: HFParquetResponse -> [HFParquetFile]
+ DataFrame.IO.Parquet: [hfpConfig] :: HFParquetFile -> Text
+ DataFrame.IO.Parquet: [hfpFilename] :: HFParquetFile -> Text
+ DataFrame.IO.Parquet: [hfpSplit] :: HFParquetFile -> Text
+ DataFrame.IO.Parquet: [hfpUrl] :: HFParquetFile -> Text
+ DataFrame.IO.Parquet: _readParquetWithOpts :: ForceNonSeekable -> ParquetReadOptions -> FilePath -> IO DataFrame
+ DataFrame.IO.Parquet: data HFParquetFile
+ DataFrame.IO.Parquet: data HFRef
+ DataFrame.IO.Parquet: directHFUrl :: HFRef -> Text
+ DataFrame.IO.Parquet: downloadHFFiles :: Maybe ByteString -> [HFParquetFile] -> IO [FilePath]
+ DataFrame.IO.Parquet: fetchHFParquetFiles :: FilePath -> IO [FilePath]
+ DataFrame.IO.Parquet: getHFToken :: IO (Maybe ByteString)
+ DataFrame.IO.Parquet: hasGlob :: Text -> Bool
+ DataFrame.IO.Parquet: hfUrlRepoPath :: HFParquetFile -> String
+ DataFrame.IO.Parquet: instance Data.Aeson.Types.FromJSON.FromJSON DataFrame.IO.Parquet.HFParquetFile
+ DataFrame.IO.Parquet: instance Data.Aeson.Types.FromJSON.FromJSON DataFrame.IO.Parquet.HFParquetResponse
+ DataFrame.IO.Parquet: instance GHC.Show.Show DataFrame.IO.Parquet.HFParquetFile
+ DataFrame.IO.Parquet: isHFUri :: FilePath -> Bool
+ DataFrame.IO.Parquet: matchesGlob :: Text -> HFParquetFile -> Bool
+ DataFrame.IO.Parquet: newtype HFParquetResponse
+ DataFrame.IO.Parquet: parseHFUri :: FilePath -> Either String HFRef
+ DataFrame.IO.Parquet: readMetadataFromHandle :: FileBufferedOrSeekable -> IO FileMetadata
+ DataFrame.IO.Parquet: readMetadataSizeFromFooterSlice :: ByteString -> (Int, ByteString)
+ DataFrame.IO.Parquet: resolveHFUrls :: Maybe ByteString -> HFRef -> IO [HFParquetFile]
+ DataFrame.IO.Parquet.Seeking: AbsoluteSeek :: SeekMode
+ DataFrame.IO.Parquet.Seeking: FileBuffered :: !IORef Int64 -> !ByteString -> FileBufferedOrSeekable
+ DataFrame.IO.Parquet.Seeking: FileSeekable :: !SeekableHandle -> FileBufferedOrSeekable
+ DataFrame.IO.Parquet.Seeking: RelativeSeek :: SeekMode
+ DataFrame.IO.Parquet.Seeking: SeekFromEnd :: SeekMode
+ DataFrame.IO.Parquet.Seeking: advanceBytes :: Int -> FileBufferedOrSeekable -> IO ByteString
+ DataFrame.IO.Parquet.Seeking: data FileBufferedOrSeekable
+ DataFrame.IO.Parquet.Seeking: data SeekMode
+ DataFrame.IO.Parquet.Seeking: data SeekableHandle
+ DataFrame.IO.Parquet.Seeking: mkFileBufferedOrSeekable :: ForceNonSeekable -> Handle -> IO FileBufferedOrSeekable
+ DataFrame.IO.Parquet.Seeking: mkSeekableHandle :: Handle -> IO (Maybe SeekableHandle)
+ DataFrame.IO.Parquet.Seeking: readLastBytes :: Integer -> FileBufferedOrSeekable -> IO ByteString
+ DataFrame.IO.Parquet.Seeking: seekAndReadBytes :: Maybe (SeekMode, Integer) -> Int -> FileBufferedOrSeekable -> IO ByteString
+ DataFrame.IO.Parquet.Seeking: seekAndStreamBytes :: MonadIO m => Maybe (SeekMode, Integer) -> Int -> FileBufferedOrSeekable -> m (Stream m Word8)
+ DataFrame.IO.Parquet.Seeking: type ForceNonSeekable = Maybe Bool
+ DataFrame.IO.Parquet.Seeking: withFileBufferedOrSeekable :: ForceNonSeekable -> FilePath -> IOMode -> (FileBufferedOrSeekable -> IO a) -> IO a
+ DataFrame.IO.Parquet.Thrift: readMetadataByHandleMetaSize :: FileBufferedOrSeekable -> Int -> IO FileMetadata
+ DataFrame.Internal.Binary: assembleWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
+ DataFrame.Internal.Binary: assembleWord64 :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word64
+ DataFrame.Internal.Binary: byteAtOrZero :: Int -> ByteString -> Int -> Word8
+ DataFrame.Internal.Binary: littleEndianInt32 :: ByteString -> Int32
+ DataFrame.Internal.Binary: littleEndianWord32 :: ByteString -> Word32
+ DataFrame.Internal.Binary: littleEndianWord64 :: ByteString -> Word64
+ DataFrame.Internal.Binary: word32ToLittleEndian :: Word32 -> ByteString
+ DataFrame.Internal.Binary: word64ToLittleEndian :: Word64 -> ByteString
+ DataFrame.Internal.Expression: [CastExprWith] :: forall a1 a src. (Columnable a1, Columnable a, Columnable src) => Text -> (Either String a1 -> a) -> Expr src -> Expr a
+ DataFrame.Internal.Expression: [CastWith] :: forall a1 a. (Columnable a1, Columnable a) => Text -> Text -> (Either String a1 -> a) -> Expr a
+ DataFrame.Internal.Nullable: applyNull1 :: NullLift1Op a r c => (BaseType a -> r) -> a -> c
+ DataFrame.Internal.Nullable: applyNull2 :: NullLift2Op a b r c => (BaseType a -> BaseType b -> r) -> a -> b -> c
+ DataFrame.Internal.Nullable: class Columnable PromoteDiv a b => DivWidenOp a b
+ DataFrame.Internal.Nullable: class (Columnable a, Columnable r, Columnable c) => NullLift1Op a r c
+ DataFrame.Internal.Nullable: class (Columnable a, Columnable b, Columnable r, Columnable c) => NullLift2Op a b r c
+ DataFrame.Internal.Nullable: class (Columnable a, Columnable b, Columnable c) => NullableArithOp a b c | a b -> c
+ DataFrame.Internal.Nullable: class (Columnable a, Columnable b, Columnable e) => NullableCmpOp a b e
+ DataFrame.Internal.Nullable: class Columnable Promote a b => NumericWidenOp a b
+ DataFrame.Internal.Nullable: divArithOp :: DivWidenOp a b => (PromoteDiv a b -> PromoteDiv a b -> PromoteDiv a b) -> a -> b -> PromoteDiv a b
+ DataFrame.Internal.Nullable: divWiden1 :: DivWidenOp a b => a -> PromoteDiv a b
+ DataFrame.Internal.Nullable: divWiden2 :: DivWidenOp a b => b -> PromoteDiv a b
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a)) => DataFrame.Internal.Nullable.NullableArithOp (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe a)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a)) => DataFrame.Internal.Nullable.NullableArithOp (GHC.Maybe.Maybe a) a (GHC.Maybe.Maybe a)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a), DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe GHC.Types.Bool)) => DataFrame.Internal.Nullable.NullableCmpOp (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe GHC.Types.Bool)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a), DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe GHC.Types.Bool)) => DataFrame.Internal.Nullable.NullableCmpOp (GHC.Maybe.Maybe a) a (GHC.Maybe.Maybe GHC.Types.Bool)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a), DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe GHC.Types.Bool), a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullableCmpOp a (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe GHC.Types.Bool)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe a), a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullableArithOp a (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe a)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable GHC.Types.Bool, a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullableCmpOp a a GHC.Types.Bool
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable b, DataFrame.Internal.Column.Columnable r, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe r)) => DataFrame.Internal.Nullable.NullLift2Op (GHC.Maybe.Maybe a) (GHC.Maybe.Maybe b) r (GHC.Maybe.Maybe r)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable b, DataFrame.Internal.Column.Columnable r, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe r), a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullLift2Op a (GHC.Maybe.Maybe b) r (GHC.Maybe.Maybe r)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable b, DataFrame.Internal.Column.Columnable r, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe r), b GHC.Types.~ DataFrame.Internal.Nullable.BaseType b) => DataFrame.Internal.Nullable.NullLift2Op (GHC.Maybe.Maybe a) b r (GHC.Maybe.Maybe r)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable b, DataFrame.Internal.Column.Columnable r, a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a, b GHC.Types.~ DataFrame.Internal.Nullable.BaseType b) => DataFrame.Internal.Nullable.NullLift2Op a b r r
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable r, DataFrame.Internal.Column.Columnable (GHC.Maybe.Maybe r)) => DataFrame.Internal.Nullable.NullLift1Op (GHC.Maybe.Maybe a) r (GHC.Maybe.Maybe r)
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, DataFrame.Internal.Column.Columnable r, a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullLift1Op a r r
+ DataFrame.Internal.Nullable: instance (DataFrame.Internal.Column.Columnable a, a GHC.Types.~ DataFrame.Internal.Nullable.BaseType a) => DataFrame.Internal.Nullable.NullableArithOp a a a
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Column.Columnable a => DataFrame.Internal.Nullable.NumericWidenOp a a
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int32 GHC.Int.Int32
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int32 GHC.Int.Int64
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int32 GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int32 GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int32 GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int64 GHC.Int.Int32
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int64 GHC.Int.Int64
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int64 GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int64 GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Int.Int64 GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Double GHC.Int.Int32
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Double GHC.Int.Int64
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Double GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Double GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Double GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Float GHC.Int.Int32
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Float GHC.Int.Int64
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Float GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Float GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Float GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Int GHC.Int.Int32
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Int GHC.Int.Int64
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Int GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Int GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.DivWidenOp GHC.Types.Int GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Double GHC.Types.Float
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Double GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Float GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Float GHC.Types.Int
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Int GHC.Types.Double
+ DataFrame.Internal.Nullable: instance DataFrame.Internal.Nullable.NumericWidenOp GHC.Types.Int GHC.Types.Float
+ DataFrame.Internal.Nullable: nullArithOp :: NullableArithOp a b c => (BaseType a -> BaseType a -> BaseType a) -> a -> b -> c
+ DataFrame.Internal.Nullable: nullCmpOp :: NullableCmpOp a b e => (BaseType a -> BaseType a -> Bool) -> a -> b -> e
+ DataFrame.Internal.Nullable: type WidenResult a b = NullLift2Result a b Promote BaseType a BaseType b
+ DataFrame.Internal.Nullable: type WidenResultDiv a b = NullLift2Result a b PromoteDiv BaseType a BaseType b
+ DataFrame.Internal.Nullable: type family NullCmpResult a b
+ DataFrame.Internal.Nullable: widen1 :: NumericWidenOp a b => a -> Promote a b
+ DataFrame.Internal.Nullable: widen2 :: NumericWidenOp a b => b -> Promote a b
+ DataFrame.Internal.Nullable: widenArithOp :: NumericWidenOp a b => (Promote a b -> Promote a b -> Promote a b) -> a -> b -> Promote a b
+ DataFrame.Internal.Schema: makeSchema :: [(Text, SchemaType)] -> Schema
+ DataFrame.Lazy.Internal.DataFrame: take :: Int -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Operations.Core: showDerivedExpressions :: DataFrame -> [NamedExpr]
+ DataFrame.Operations.Statistics: instance DataFrame.Internal.Column.Columnable b => DataFrame.Operations.Transformations.ImputeOp (GHC.Maybe.Maybe b)
+ DataFrame.Operations.Subset: columnToTextVec :: Column -> Vector Text
+ DataFrame.Operations.Subset: groupByIndices :: Column -> Map Text (Vector Int)
+ DataFrame.Operations.Subset: rowsAtIndices :: Vector Int -> DataFrame -> DataFrame
+ DataFrame.Operations.Subset: stratifiedSample :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> DataFrame
+ DataFrame.Operations.Subset: stratifiedSplit :: forall a g. (SplitGen g, RandomGen g, Columnable a) => g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)
+ DataFrame.Operations.Transformations: class Columnable a => ImputeOp a
+ DataFrame.Operations.Transformations: imputeCore :: Columnable b => Expr (Maybe b) -> b -> DataFrame -> DataFrame
+ DataFrame.Operations.Transformations: instance DataFrame.Internal.Column.Columnable a => DataFrame.Operations.Transformations.ImputeOp a
+ DataFrame.Operations.Transformations: runImpute :: ImputeOp a => Expr a -> BaseType a -> DataFrame -> DataFrame
+ DataFrame.Operations.Transformations: runImputeWith :: ImputeOp a => (Expr (BaseType a) -> Expr (BaseType a)) -> Expr a -> DataFrame -> DataFrame
+ DataFrame.Operations.Typing: ParseOptions :: [Text] -> Int -> Bool -> DateFormat -> ParseOptions
+ DataFrame.Operations.Typing: [missingValues] :: ParseOptions -> [Text]
+ DataFrame.Operations.Typing: [parseDateFormat] :: ParseOptions -> DateFormat
+ DataFrame.Operations.Typing: [parseSafe] :: ParseOptions -> Bool
+ DataFrame.Operations.Typing: [sampleSize] :: ParseOptions -> Int
+ DataFrame.Operations.Typing: data ParseOptions
+ DataFrame.Operations.Typing: defaultParseOptions :: ParseOptions
+ DataFrame.Operations.Typing: readAsEither :: Read a => String -> a
+ DataFrame.Operations.Typing: readAsMaybe :: Read a => String -> Maybe a
+ DataFrame.Operators: (.&&.) :: Expr Bool -> Expr Bool -> Expr Bool
+ DataFrame.Operators: (.*) :: (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr (WidenResult a b)
+ DataFrame.Operators: (.*.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a
+ DataFrame.Operators: (.+) :: (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr (WidenResult a b)
+ DataFrame.Operators: (.+.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a
+ DataFrame.Operators: (.-) :: (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr (WidenResult a b)
+ DataFrame.Operators: (.-.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a
+ DataFrame.Operators: (./) :: (DivWidenOp (BaseType a) (BaseType b), NullLift2Op a b (PromoteDiv (BaseType a) (BaseType b)) (WidenResultDiv a b), Fractional (PromoteDiv (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr (WidenResultDiv a b)
+ DataFrame.Operators: (./.) :: (Columnable a, Fractional a) => Expr a -> Expr a -> Expr a
+ DataFrame.Operators: (./=.) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.<.) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.<=.) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.==.) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.>.) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.>=.) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.^) :: (Columnable (BaseType a), Columnable (BaseType b), Num (BaseType a), Integral (BaseType b), NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (BaseType a) a, Num (Promote (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr a
+ DataFrame.Operators: (.^.) :: (Columnable a, Columnable b, Num a, Integral b) => Expr a -> Expr b -> Expr a
+ DataFrame.Operators: (.^^.) :: (Columnable a, Columnable b, Fractional a, Integral b) => Expr a -> Expr b -> Expr a
+ DataFrame.Operators: (.||.) :: Expr Bool -> Expr Bool -> Expr Bool
+ DataFrame.Operators: infix 6 .-
+ DataFrame.Operators: infix 7 ./
+ DataFrame.Operators: lift2Decorated :: (Columnable c, Columnable b, Columnable a) => (c -> b -> a) -> Text -> Maybe Text -> Bool -> Int -> Expr c -> Expr b -> Expr a
+ DataFrame.Operators: liftDecorated :: (Columnable a, Columnable b) => (a -> b) -> Text -> Maybe Text -> Expr a -> Expr b
+ DataFrame.Typed: (.*) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed: (.+) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed: (.-) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed: (./) :: forall a b (cols :: [Type]). (DivWidenOp (BaseType a) (BaseType b), NullLift2Op a b (PromoteDiv (BaseType a) (BaseType b)) (WidenResultDiv a b), Fractional (PromoteDiv (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResultDiv a b)
+ DataFrame.Typed: (./=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: (.<) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: (.<=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: (.==) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: (.>) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: (.>=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed: infix 4 .>
+ DataFrame.Typed: infixl 6 .-
+ DataFrame.Typed: infixl 7 ./
+ DataFrame.Typed: nullLift :: forall a r (cols :: [Type]). (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) => (BaseType a -> r) -> TExpr cols a -> TExpr cols (NullLift1Result a r)
+ DataFrame.Typed: nullLift2 :: forall a b r (cols :: [Type]). (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) => (BaseType a -> BaseType b -> r) -> TExpr cols a -> TExpr cols b -> TExpr cols (NullLift2Result a b r)
+ DataFrame.Typed.Expr: (.&&) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.*) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed.Expr: (.*.) :: forall a (cols :: [Type]). (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a
+ DataFrame.Typed.Expr: (.+) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed.Expr: (.+.) :: forall a (cols :: [Type]). (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a
+ DataFrame.Typed.Expr: (.-) :: forall a b (cols :: [Type]). (NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b), Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResult a b)
+ DataFrame.Typed.Expr: (.-.) :: forall a (cols :: [Type]). (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a
+ DataFrame.Typed.Expr: (./) :: forall a b (cols :: [Type]). (DivWidenOp (BaseType a) (BaseType b), NullLift2Op a b (PromoteDiv (BaseType a) (BaseType b)) (WidenResultDiv a b), Fractional (PromoteDiv (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols (WidenResultDiv a b)
+ DataFrame.Typed.Expr: (./.) :: forall a (cols :: [Type]). (Columnable a, Fractional a) => TExpr cols a -> TExpr cols a -> TExpr cols a
+ DataFrame.Typed.Expr: (./=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.<) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.<=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.==) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.>) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.>=) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: (.^) :: forall a b (cols :: [Type]). (Columnable (BaseType a), Columnable (BaseType b), Num (BaseType a), Integral (BaseType b), NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (BaseType a) a, Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols a
+ DataFrame.Typed.Expr: (.^.) :: forall a b (cols :: [Type]). (Columnable a, Columnable b, Num a, Integral b) => TExpr cols a -> TExpr cols b -> TExpr cols a
+ DataFrame.Typed.Expr: (.^^) :: forall a b (cols :: [Type]). (Columnable (BaseType a), Columnable (BaseType b), Fractional (BaseType a), Integral (BaseType b), NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (BaseType a) a, Num (Promote (BaseType a) (BaseType b))) => TExpr cols a -> TExpr cols b -> TExpr cols a
+ DataFrame.Typed.Expr: (.^^.) :: forall a b (cols :: [Type]). (Columnable a, Columnable b, Fractional a, Integral b) => TExpr cols a -> TExpr cols b -> TExpr cols a
+ DataFrame.Typed.Expr: (.||) :: forall a b (cols :: [Type]). (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => TExpr cols a -> TExpr cols b -> TExpr cols (NullCmpResult a b)
+ DataFrame.Typed.Expr: castExpr :: forall b (cols :: [Type]) src. (Columnable b, Columnable src) => TExpr cols src -> TExpr cols (Maybe b)
+ DataFrame.Typed.Expr: castExprEither :: forall b (cols :: [Type]) src. (Columnable b, Columnable src) => TExpr cols src -> TExpr cols (Either Text b)
+ DataFrame.Typed.Expr: castExprWithDefault :: forall b (cols :: [Type]) src. (Columnable b, Columnable src) => b -> TExpr cols src -> TExpr cols b
+ DataFrame.Typed.Expr: infix 4 .>
+ DataFrame.Typed.Expr: infix 8 .^
+ DataFrame.Typed.Expr: infixl 6 .-
+ DataFrame.Typed.Expr: infixl 7 ./
+ DataFrame.Typed.Expr: nullLift :: forall a r (cols :: [Type]). (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) => (BaseType a -> r) -> TExpr cols a -> TExpr cols (NullLift1Result a r)
+ DataFrame.Typed.Expr: nullLift2 :: forall a b r (cols :: [Type]). (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) => (BaseType a -> BaseType b -> r) -> TExpr cols a -> TExpr cols b -> TExpr cols (NullLift2Result a b r)
+ DataFrame.Typed.Expr: unsafeCastExpr :: forall b (cols :: [Type]) src. (Columnable b, Columnable src) => TExpr cols src -> TExpr cols b
- DataFrame: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec
+ DataFrame: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec -> TypeSpec
- DataFrame: imputeWith :: Columnable b => (Expr b -> Expr b) -> Expr (Maybe b) -> DataFrame -> DataFrame
+ DataFrame: imputeWith :: (ImputeOp a, Columnable (BaseType a)) => (Expr (BaseType a) -> Expr (BaseType a)) -> Expr a -> DataFrame -> DataFrame
- DataFrame.DecisionTree: numericCols :: DataFrame -> [Expr Double]
+ DataFrame.DecisionTree: numericCols :: DataFrame -> [NumExpr]
- DataFrame.DecisionTree: numericExprs :: SynthConfig -> DataFrame -> [Expr Double] -> Int -> Int -> [Expr Double]
+ DataFrame.DecisionTree: numericExprs :: SynthConfig -> DataFrame -> [NumExpr] -> Int -> Int -> [NumExpr]
- DataFrame.DecisionTree: numericExprsWithTerms :: SynthConfig -> DataFrame -> [Expr Double]
+ DataFrame.DecisionTree: numericExprsWithTerms :: SynthConfig -> DataFrame -> [NumExpr]
- DataFrame.Functions: eq :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Functions: eq :: (Columnable a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool
- DataFrame.Functions: geq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Functions: geq :: (Columnable a, Ord a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool
- DataFrame.Functions: gt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Functions: gt :: (Columnable a, Ord a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool
- DataFrame.Functions: leq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Functions: leq :: (Columnable a, Ord a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool
- DataFrame.Functions: lt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Functions: lt :: (Columnable a, Ord a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool
- DataFrame.Functions: schemaElemToColumn :: SchemaElement -> (Text, Column)
+ DataFrame.Functions: schemaElemToColumn :: Set Text -> SchemaElement -> (Text, Column)
- DataFrame.Functions: schemaToEmptyDataFrame :: [SchemaElement] -> DataFrame
+ DataFrame.Functions: schemaToEmptyDataFrame :: Set Text -> [SchemaElement] -> DataFrame
- DataFrame.IO.CSV: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec
+ DataFrame.IO.CSV: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec -> TypeSpec
- DataFrame.Internal.Column: foldl1DirectGroups :: Columnable a => (a -> a -> a) -> Column -> Vector Int -> Vector Int -> Either DataFrameException (Vector a)
+ DataFrame.Internal.Column: foldl1DirectGroups :: Columnable a => (a -> a -> a) -> Column -> Vector Int -> Vector Int -> Either DataFrameException Column
- DataFrame.Internal.Types: type family FloatingTypes a :: Bool
+ DataFrame.Internal.Types: type family PromoteDiv a b
- DataFrame.Monad: imputeM :: Columnable a => Expr (Maybe a) -> a -> FrameM (Expr a)
+ DataFrame.Monad: imputeM :: (ImputeOp a, Columnable (BaseType a)) => Expr a -> BaseType a -> FrameM (Expr (BaseType a))
- DataFrame.Operations.Statistics: imputeWith :: Columnable b => (Expr b -> Expr b) -> Expr (Maybe b) -> DataFrame -> DataFrame
+ DataFrame.Operations.Statistics: imputeWith :: (ImputeOp a, Columnable (BaseType a)) => (Expr (BaseType a) -> Expr (BaseType a)) -> Expr a -> DataFrame -> DataFrame
- DataFrame.Operations.Transformations: impute :: Columnable b => Expr (Maybe b) -> b -> DataFrame -> DataFrame
+ DataFrame.Operations.Transformations: impute :: ImputeOp a => Expr a -> BaseType a -> DataFrame -> DataFrame
- DataFrame.Operations.Typing: parseDefault :: [Text] -> Int -> Bool -> DateFormat -> Column -> Column
+ DataFrame.Operations.Typing: parseDefault :: ParseOptions -> Column -> Column
- DataFrame.Operations.Typing: parseDefaults :: [Text] -> Int -> Bool -> DateFormat -> DataFrame -> DataFrame
+ DataFrame.Operations.Typing: parseDefaults :: ParseOptions -> DataFrame -> DataFrame
- DataFrame.Operations.Typing: parseFromExamples :: [Text] -> Int -> Bool -> DateFormat -> Vector Text -> Column
+ DataFrame.Operations.Typing: parseFromExamples :: ParseOptions -> Vector Text -> Column
- DataFrame.Operations.Typing: parseWithTypes :: [SchemaType] -> DataFrame -> DataFrame
+ DataFrame.Operations.Typing: parseWithTypes :: Bool -> Map Text SchemaType -> DataFrame -> DataFrame
- DataFrame.Operators: (.&&) :: Expr Bool -> Expr Bool -> Expr Bool
+ DataFrame.Operators: (.&&) :: (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (./=) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (./=) :: (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.<) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.<) :: (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.<=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.<=) :: (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.==) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.==) :: (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.>) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.>) :: (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.>=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
+ DataFrame.Operators: (.>=) :: (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: (.^^) :: (Columnable a, Num a) => Expr a -> Int -> Expr a
+ DataFrame.Operators: (.^^) :: (Columnable (BaseType a), Columnable (BaseType b), Fractional (BaseType a), Integral (BaseType b), NumericWidenOp (BaseType a) (BaseType b), NullLift2Op a b (BaseType a) a, Num (Promote (BaseType a) (BaseType b))) => Expr a -> Expr b -> Expr a
- DataFrame.Operators: (.||) :: Expr Bool -> Expr Bool -> Expr Bool
+ DataFrame.Operators: (.||) :: (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) => Expr a -> Expr b -> Expr (NullCmpResult a b)
- DataFrame.Operators: infix 8 .^^
+ DataFrame.Operators: infix 8 .^.
- DataFrame.Typed.Expr: infixr 2 .||.
+ DataFrame.Typed.Expr: infixr 2 .||
- DataFrame.Typed.Expr: infixr 3 .&&.
+ DataFrame.Typed.Expr: infixr 3 .&&

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Revision history for dataframe +## 1.0.0.0+* Fix mappend to respect schema of empty columns.+* Add cast operators that force column schema+* Add null aware operators so some operations are easier.+* Add arrow shim with python example.+* Add numeric promotion for numeric operations.+* Add column provenance tracking+* Add stratified sampling+* Read files from hugging face+ ## 0.7.0.0 * This release adds A LOT of AI code to the repo (which we'll now pause in favour of refactoring, testing, and completeness for 1.0) * The lazy reader now has a custom binary format that it spills to. This almost halved the time it takes to run the 1 billion row challenge. The lazy evaluation now also supports Parquet.
app/LazyBenchmark.hs view
@@ -224,7 +224,7 @@     -- Demonstrates that the executor reads only the first batch.     runQuery "Q1 — preview first 20 rows (no filter)" $         L.runDataFrame $-            L.limit 20 $+            L.take 20 $                 L.scanCsv schema pathT      -- Q2: Filter + limit.@@ -232,8 +232,8 @@     -- ~512 matches in the first batch and stops — reads only one batch.     runQuery "Q2 — filter (x > 0.999), limit 20" $         L.runDataFrame $-            L.limit 20 $-                L.filter (col @Double "x" .> lit 0.999) $+            L.take 20 $+                L.filter (col @Double "x" .> lit (0.999 :: Double)) $                     L.scanCsv schema pathT      -- Q3: Filter + derive + select + limit.@@ -241,10 +241,10 @@     -- Predicate pushdown moves the filter into the scan batch loop.     runQuery "Q3 — filter (x > 0.999), derive z = x*y, select [id,z], limit 20" $         L.runDataFrame $-            L.limit 20 $+            L.take 20 $                 L.select ["id", "z"] $                     L.derive "z" (col @Double "x" * col @Double "y") $-                        L.filter (col @Double "x" .> lit 0.999) $+                        L.filter (col @Double "x" .> lit (0.999 :: Double)) $                             L.scanCsv schema pathT      -- Q4: Filter fusion demo.@@ -253,9 +253,9 @@     -- We limit to keep result size manageable.     runQuery "Q4 — filter fusion: (x > 0.5) . (y > 0.5), limit 20" $         L.runDataFrame $-            L.limit 20 $-                L.filter (col @Double "y" .> lit 0.5) $-                    L.filter (col @Double "x" .> lit 0.5) $+            L.take 20 $+                L.filter (col @Double "y" .> lit (0.5 :: Double)) $+                    L.filter (col @Double "x" .> lit (0.5 :: Double)) $                         L.scanCsv schema pathT      -- Q5: Full scan, heavy filter, count results.@@ -269,7 +269,7 @@         )         $ L.runDataFrame         $ L.select ["id", "x"]-        $ L.filter (col @Double "x" .> lit 0.999)+        $ L.filter (col @Double "x" .> lit (0.999 :: Double))         $ L.scanCsv schema pathT      putStrLn "\nDone."
app/Synthesis.hs view
@@ -53,13 +53,13 @@             fitDecisionTree                 ( defaultTreeConfig                     { maxTreeDepth = 5-                    , minSamplesSplit = 10+                    , minSamplesSplit = 5                     , minLeafSize = 3                     , taoIterations = 100                     , synthConfig =                         defaultSynthConfig-                            { complexityPenalty = 0.00-                            , maxExprDepth = 2+                            { complexityPenalty = 0.1+                            , maxExprDepth = 3                             , disallowedCombinations =                                 [ (F.name age, F.name fare)                                 , ("passenger_class", "number_of_siblings_and_spouses")@@ -99,12 +99,32 @@ computeAccuracy df =     let         tp =-            fromIntegral $ D.nRows (D.filterWhere (survived .== 1 .&& prediction .== 1) df)+            fromIntegral $+                D.nRows+                    ( D.filterWhere+                        (survived .== F.lit (1 :: Int) .&& prediction .== F.lit (1 :: Int))+                        df+                    )         tn =-            fromIntegral $ D.nRows (D.filterWhere (survived .== 0 .&& prediction .== 0) df)+            fromIntegral $+                D.nRows+                    ( D.filterWhere+                        (survived .== F.lit (0 :: Int) .&& prediction .== F.lit (0 :: Int))+                        df+                    )         fp =-            fromIntegral $ D.nRows (D.filterWhere (survived .== 0 .&& prediction .== 1) df)+            fromIntegral $+                D.nRows+                    ( D.filterWhere+                        (survived .== F.lit (0 :: Int) .&& prediction .== F.lit (1 :: Int))+                        df+                    )         fn =-            fromIntegral $ D.nRows (D.filterWhere (survived .== 1 .&& prediction .== 0) df)+            fromIntegral $+                D.nRows+                    ( D.filterWhere+                        (survived .== F.lit (1 :: Int) .&& prediction .== F.lit (0 :: Int))+                        df+                    )      in         (tp + tn) / (tp + tn + fp + fn)
+ cbits/arrow_abi.h view
@@ -0,0 +1,44 @@+/* Arrow C Data Interface structs (verbatim from specification).+   See https://arrow.apache.org/docs/format/CDataInterface.html */++#pragma once++#include <stdint.h>++#ifdef __cplusplus+extern "C" {+#endif++struct ArrowSchema {+    /* Array type description */+    const char *format;+    const char *name;+    const char *metadata;+    int64_t     flags;+    int64_t     n_children;+    struct ArrowSchema **children;+    struct ArrowSchema  *dictionary;++    void (*release)(struct ArrowSchema *);+    void *private_data;+};++struct ArrowArray {+    /* Array data description */+    int64_t length;+    int64_t null_count;+    int64_t offset;+    int64_t n_buffers;+    int64_t n_children;+    const void **buffers;+    struct ArrowArray **children;+    struct ArrowArray  *dictionary;++    void (*release)(struct ArrowArray *);+    /* Opaque producer-specific data */+    void *private_data;+};++#ifdef __cplusplus+}+#endif
+ cbits/dataframe_arrow.h view
@@ -0,0 +1,30 @@+#pragma once+#include <stdint.h>++#ifdef __cplusplus+extern "C" {+#endif++/**+ * Execute a JSON-encoded query plan and return Arrow C Data Interface pointers.+ *+ * plan_json  – null-terminated UTF-8 JSON (see DataFrame.IR for schema)+ * schema_out – receives ArrowSchema* cast to uint64_t+ * array_out  – receives ArrowArray*  cast to uint64_t+ *+ * Returns 0 on success, -1 on error (message written to stderr).+ *+ * Plan ops: ReadCsv, ReadTsv, FromArrow, Select, GroupBy, Sort, Limit+ *+ * Example (GroupBy):+ *   {"op":"GroupBy","keys":["Sex"],+ *    "aggregations":[{"name":"n","agg":"count","col":"Sex"}],+ *    "input":{"op":"ReadCsv","path":"data/titanic.csv"}}+ */+int dfExecutePlan(const char* plan_json,+                  uint64_t*   schema_out,+                  uint64_t*   array_out);++#ifdef __cplusplus+}+#endif
+ cbits/rts_init.c view
@@ -0,0 +1,18 @@+/* Auto-initialise / finalise the GHC RTS when the shared library is+   loaded / unloaded.  Required because cabal's native-shared libraries+   do not run hs_init automatically on macOS. */++#include <stddef.h>+#include "HsFFI.h"++__attribute__((constructor))+static void df_lib_init(void)+{+    hs_init(0, NULL);+}++__attribute__((destructor))+static void df_lib_fini(void)+{+    hs_exit();+}
+ data/sharded/part-0.parquet view

binary file changed (absent → 2684 bytes)

+ data/sharded/part-1.parquet view

binary file changed (absent → 2462 bytes)

dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               dataframe-version:            0.7.0.0+version:            1.0.0.0  synopsis: A fast, safe, and intuitive DataFrame library. @@ -17,7 +17,11 @@ tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2 extra-doc-files: CHANGELOG.md README.md extra-source-files: cbits/process_csv.h+                    cbits/arrow_abi.h+                    cbits/dataframe_arrow.h+                    cbits/rts_init.c                     data/titanic/*.csv+                    data/sharded/*.parquet                     tests/data/*.csv                     tests/data/*.tsv                     tests/data/*.parquet@@ -53,8 +57,10 @@                     DataFrame.Internal.Types,                     DataFrame.Internal.Expression,                     DataFrame.Internal.Interpreter,+                    DataFrame.Internal.Nullable,                     DataFrame.Internal.Parsing,                     DataFrame.Internal.Column,+                    DataFrame.Internal.Binary,                     DataFrame.Internal.Statistics,                     DataFrame.Display.Terminal.PrettyPrint,                     DataFrame.Display.Terminal.Colours,@@ -86,6 +92,7 @@                     DataFrame.IO.Parquet.Compression,                     DataFrame.IO.Parquet.Encoding,                     DataFrame.IO.Parquet.Page,+                    DataFrame.IO.Parquet.Seeking,                     DataFrame.IO.Parquet.Time,                     DataFrame.IO.Parquet.Types,                     DataFrame.Lazy.IO.CSV,@@ -138,6 +145,9 @@                       stm >= 2.5 && < 3,                       filepath >= 1.4 && < 2,                       Glob >= 0.10 && < 1,+                      http-conduit    >= 2.3 && < 3,+                      streamly-core,+                      streamly-bytestring,      hs-source-dirs:   src     c-sources:        cbits/process_csv.c@@ -146,11 +156,32 @@     install-includes: process_csv.h     default-language: Haskell2010 +foreign-library dataframe-arrow+    type:             native-shared+    hs-source-dirs:   ffi+    other-modules:    DataFrame.FFI+                      DataFrame.IO.Arrow+                      DataFrame.IR+    build-depends:+        base        >= 4   && < 5,+        dataframe   ^>= 1,+        text        >= 2.0 && < 3,+        aeson       >= 0.11 && < 3,+        bytestring  >= 0.11 && < 0.13,+        containers  >= 0.6.7 && < 0.9,+        vector      ^>= 0.13+    c-sources:        cbits/rts_init.c+    include-dirs:     cbits+    includes:         arrow_abi.h dataframe_arrow.h+    install-includes: arrow_abi.h dataframe_arrow.h+    ghc-options:      -threaded+    default-language: Haskell2010+ executable dataframe-benchmark-example     import: warnings     main-is: Benchmark.hs     build-depends:    base >= 4 && < 5,-                      dataframe >= 0.5 && < 1,+                      dataframe ^>= 1,                       random >= 1 && < 2,                       time >= 1.12 && < 2,                       vector ^>= 0.13,@@ -162,7 +193,7 @@     import: warnings     main-is: Synthesis.hs     build-depends:    base >= 4 && < 5,-                      dataframe >= 0.5 && < 1,+                      dataframe ^>= 1,                       random >= 1 && < 2,                       text >= 2.0 && < 3     hs-source-dirs:   app@@ -189,7 +220,7 @@     build-depends:    base >= 4 && < 5,                       bytestring >= 0.11 && < 0.13,                       containers >= 0.6.7 && < 0.9,-                      dataframe >= 0.5 && < 1,+                      dataframe ^>= 1,                       directory >= 1.3.0.0 && < 2,                       random >= 1 && < 2,                       text >= 2.0 && < 3,@@ -206,7 +237,7 @@     build-depends: base >= 4 && < 5,                    criterion >= 1 && < 2,                    process >= 1.6 && < 2,-                   dataframe >= 0.5 && < 1,+                   dataframe ^>= 1,                    random >= 1 && < 2,     default-language: Haskell2010     ghc-options:@@ -219,6 +250,7 @@     type: exitcode-stdio-1.0     main-is: Main.hs     other-modules: Assertions,+                   DecisionTree,                    Functions,                    GenDataFrame,                    Internal.Parsing,@@ -232,6 +264,8 @@                    Operations.InsertColumn,                    Operations.Join,                    Operations.Merge,+                   Operations.Nullable,+                   Operations.Provenance,                    Operations.ReadCsv,                    Operations.Shuffle,                    Operations.Sort,@@ -245,7 +279,8 @@                    Monad     build-depends:  base >= 4 && < 5,                     bytestring >= 0.11 && < 0.13,-                    dataframe >= 0.5 && < 1,+                    dataframe ^>= 1,+                    bytestring >= 0.11 && < 0.13,                     directory >= 1.3.0.0 && < 2,                     HUnit ^>= 1.6,                     QuickCheck >= 2 && < 3,@@ -258,4 +293,3 @@                     containers >= 0.6.7 && < 0.9     hs-source-dirs: tests     default-language: Haskell2010-
+ ffi/DataFrame/FFI.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++-- | C-exported entry points for the Python bindings.+module DataFrame.FFI where++import Control.Exception (SomeException, try)+import Data.Word (Word64)+import Foreign (Ptr, castPtr, poke, ptrToWordPtr)+import Foreign.C.String (CString)+import Foreign.C.Types (CInt (..))+import System.IO (hPrint, hPutStrLn, stderr)++import qualified Data.Aeson as Aeson+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL++import DataFrame.IO.Arrow (dataframeToArrow)+import DataFrame.IR (executePlan)++foreign export ccall "dfExecutePlan"+    dfExecutePlan :: CString -> Ptr Word64 -> Ptr Word64 -> IO CInt++{- | Execute a JSON-encoded query plan and return Arrow C Data Interface+  pointers to Python via output parameters.++  @planCS@    – JSON plan string (see DataFrame.IR for schema)+  @schemaOut@ – receives the ArrowSchema* address as a Word64+  @arrayOut@  – receives the ArrowArray*  address as a Word64++  Returns 0 on success, -1 on error (error message written to stderr).+-}+dfExecutePlan :: CString -> Ptr Word64 -> Ptr Word64 -> IO CInt+dfExecutePlan planCS schemaOut arrayOut = do+    planBytes <- BS.packCString planCS+    result <- try @SomeException $ do+        node <-+            either+                fail+                return+                (Aeson.eitherDecode (BL.fromStrict planBytes))+        df <- executePlan node+        (sPtr, aPtr) <- dataframeToArrow df+        poke schemaOut (fromIntegral (ptrToWordPtr (castPtr sPtr)))+        poke arrayOut (fromIntegral (ptrToWordPtr (castPtr aPtr)))+    case result of+        Left ex -> hPrint stderr ex >> return (CInt (-1))+        Right _ -> return (CInt 0)
+ ffi/DataFrame/IO/Arrow.hs view
@@ -0,0 +1,606 @@+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Convert a 'DataFrame' to Arrow C Data Interface structs for zero-copy+  transfer to Python (or any other Arrow consumer).+-}+module DataFrame.IO.Arrow (+    dataframeToArrow,+    columnToArrow,+    arrowToDataframe,+    -- exported only to force linking of release-callback symbols+    releaseSchemaImpl,+    releaseArrayImpl,+) where++import qualified Data.ByteString as BS+import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame.Internal.Column as DI++import Control.Monad (foldM_, forM, join, void, when, zipWithM_)+import Data.Bits (setBit, testBit)+import Data.Int (Int32, Int64)+import Data.Maybe (fromMaybe, isNothing)+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Data.Word (Word8)+import Foreign hiding (void)+import Foreign.C.String (CString, newCString, peekCString)+import Type.Reflection (typeRep)++import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Operations.Core (fromNamedColumns)++-- ---------------------------------------------------------------------------+-- Opaque phantom types for the Arrow structs+-- ---------------------------------------------------------------------------++data ArrowSchema+data ArrowArray++arrowSchemaSize :: Int+arrowSchemaSize = 72 -- 9 × 8 bytes++arrowArraySize :: Int+arrowArraySize = 80 -- 10 × 8 bytes++-- ArrowSchema field byte offsets+_schemaFormat+    , _schemaName+    , _schemaMetadata+    , _schemaFlags+    , _schemaNChildren+    , _schemaChildren+    , _schemaDictionary+    , _schemaRelease+    , _schemaPrivateData ::+        Int+_schemaFormat = 0+_schemaName = 8+_schemaMetadata = 16+_schemaFlags = 24+_schemaNChildren = 32+_schemaChildren = 40+_schemaDictionary = 48+_schemaRelease = 56+_schemaPrivateData = 64++-- ArrowArray field byte offsets+_arrayLength+    , _arrayNullCount+    , _arrayOffset+    , _arrayNBuffers+    , _arrayNChildren+    , _arrayBuffers+    , _arrayChildren+    , _arrayDictionary+    , _arrayRelease+    , _arrayPrivateData ::+        Int+_arrayLength = 0+_arrayNullCount = 8+_arrayOffset = 16+_arrayNBuffers = 24+_arrayNChildren = 32+_arrayBuffers = 40+_arrayChildren = 48+_arrayDictionary = 56+_arrayRelease = 64+_arrayPrivateData = 72++-- ---------------------------------------------------------------------------+-- Helpers+-- ---------------------------------------------------------------------------++-- Write a Storable value at a byte offset from a base pointer.+at :: (Storable a) => Ptr b -> Int -> a -> IO ()+at p off = poke (castPtr (p `plusPtr` off))++-- Read a Storable value at a byte offset from a base pointer.+readAt :: (Storable a) => Ptr b -> Int -> IO a+readAt p off = peek (castPtr (p `plusPtr` off))++-- ---------------------------------------------------------------------------+-- Release callbacks (self-import trick for compile-time-constant FunPtr)+-- ---------------------------------------------------------------------------++foreign export ccall "df_release_schema"+    releaseSchemaImpl :: Ptr ArrowSchema -> IO ()++foreign import ccall "&df_release_schema"+    pReleaseSchema :: FunPtr (Ptr ArrowSchema -> IO ())++foreign export ccall "df_release_array"+    releaseArrayImpl :: Ptr ArrowArray -> IO ()++foreign import ccall "&df_release_array"+    pReleaseArray :: FunPtr (Ptr ArrowArray -> IO ())++-- Dynamic wrappers to call producer's release callbacks after copying.+foreign import ccall "dynamic"+    callRelSchema :: FunPtr (Ptr ArrowSchema -> IO ()) -> Ptr ArrowSchema -> IO ()++foreign import ccall "dynamic"+    callRelArray :: FunPtr (Ptr ArrowArray -> IO ()) -> Ptr ArrowArray -> IO ()++releaseSchemaImpl :: Ptr ArrowSchema -> IO ()+releaseSchemaImpl p = do+    rawPriv <- peek (castPtr (p `plusPtr` _schemaPrivateData) :: Ptr (Ptr ()))+    let sp = castPtrToStablePtr rawPriv :: StablePtr (IO ())+    join (deRefStablePtr sp)+    freeStablePtr sp+    -- Arrow spec: release callback must set release to NULL to signal completion.+    -- p here is Arrow C++'s internal copy of the struct (not our mallocBytes+    -- allocation); our original allocation is freed inside the cleanup closure.+    p `at` _schemaRelease $ (nullFunPtr :: FunPtr (Ptr ArrowSchema -> IO ()))++releaseArrayImpl :: Ptr ArrowArray -> IO ()+releaseArrayImpl p = do+    rawPriv <- peek (castPtr (p `plusPtr` _arrayPrivateData) :: Ptr (Ptr ()))+    let sp = castPtrToStablePtr rawPriv :: StablePtr (IO ())+    join (deRefStablePtr sp)+    freeStablePtr sp+    -- Same reasoning as releaseSchemaImpl.+    p `at` _arrayRelease $ (nullFunPtr :: FunPtr (Ptr ArrowArray -> IO ()))++makeLeafSchema :: String -> T.Text -> IO (Ptr ArrowSchema)+makeLeafSchema fmt colName = do+    p <- mallocBytes arrowSchemaSize+    fmtStr <- newCString fmt+    nameStr <- newCString (T.unpack colName)+    p `at` _schemaFormat $ fmtStr+    p `at` _schemaName $ nameStr+    p `at` _schemaMetadata $ (nullPtr :: Ptr ())+    p `at` _schemaFlags $ (0 :: Int64)+    p `at` _schemaNChildren $ (0 :: Int64)+    p `at` _schemaChildren $ (nullPtr :: Ptr ())+    p `at` _schemaDictionary $ (nullPtr :: Ptr ())+    p `at` _schemaRelease $ pReleaseSchema+    -- Capture p so our original mallocBytes allocation is freed when release runs.+    cleanup <- newStablePtr (free fmtStr >> free nameStr >> free p)+    p `at` _schemaPrivateData $ castStablePtrToPtr cleanup+    return p++makeLeafArray :: Int -> Int64 -> [Ptr ()] -> IO () -> IO (Ptr ArrowArray)+makeLeafArray nRows nullCnt bufPtrs extraCleanup = do+    p <- mallocBytes arrowArraySize+    let nb = length bufPtrs+    bufArr <- mallocArray nb :: IO (Ptr (Ptr ()))+    zipWithM_ (pokeElemOff bufArr) [0 ..] bufPtrs+    p `at` _arrayLength $ (fromIntegral nRows :: Int64)+    p `at` _arrayNullCount $ nullCnt+    p `at` _arrayOffset $ (0 :: Int64)+    p `at` _arrayNBuffers $ (fromIntegral nb :: Int64)+    p `at` _arrayNChildren $ (0 :: Int64)+    p `at` _arrayBuffers $ bufArr+    p `at` _arrayChildren $ (nullPtr :: Ptr ())+    p `at` _arrayDictionary $ (nullPtr :: Ptr ())+    p `at` _arrayRelease $ pReleaseArray+    -- Capture p so our original mallocBytes allocation is freed when release runs.+    cleanup <- newStablePtr (free bufArr >> extraCleanup >> free p)+    p `at` _arrayPrivateData $ castStablePtrToPtr cleanup+    return p++buildBitmap :: V.Vector (Maybe a) -> IO (Ptr Word8, Int)+buildBitmap vec = do+    let n = V.length vec+        bitmapBytes = max 1 ((n + 7) `div` 8)+        nullCount = V.length (V.filter isNothing vec)+    bitmapPtr <- mallocBytes bitmapBytes :: IO (Ptr Word8)+    mapM_ (\i -> pokeElemOff bitmapPtr i (0 :: Word8)) [0 .. bitmapBytes - 1]+    V.iforM_ vec $ \i mv ->+        case mv of+            Nothing -> return ()+            Just _ -> do+                let byteIdx = i `div` 8+                    bitIdx = i `mod` 8+                b <- peekElemOff bitmapPtr byteIdx+                pokeElemOff bitmapPtr byteIdx (setBit b bitIdx)+    return (bitmapPtr, nullCount)++columnToArrow :: T.Text -> Column -> IO (Ptr ArrowSchema, Ptr ArrowArray)+columnToArrow colName (UnboxedColumn (vec :: VU.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+        let n = VU.length vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+        VU.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v)) vec+        sPtr <- makeLeafSchema "l" colName+        aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (UnboxedColumn (vec :: VU.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+        let n = VU.length vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+        VU.imapM_ (pokeElemOff dataPtr) vec+        sPtr <- makeLeafSchema "g" colName+        aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (BoxedColumn (vec :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do+        let n = V.length vec+            bss = map TE.encodeUtf8 (V.toList vec)+            cumOff = scanl (+) 0 (map BS.length bss)+            total = last cumOff+        offPtr <- mallocArray (n + 1) :: IO (Ptr Int32)+        zipWithM_+            (\i o -> pokeElemOff offPtr i (fromIntegral o :: Int32))+            [0 ..]+            cumOff+        charsPtr <- mallocBytes (max 1 total) :: IO (Ptr Word8)+        foldM_+            ( \pos bs -> do+                BS.useAsCStringLen bs $ \(src, len) ->+                    copyBytes (charsPtr `plusPtr` pos) (castPtr src) len+                return (pos + BS.length bs)+            )+            0+            bss+        sPtr <- makeLeafSchema "u" colName+        aPtr <-+            makeLeafArray+                n+                0+                [nullPtr, castPtr offPtr, castPtr charsPtr]+                (free offPtr >> free charsPtr)+        return (sPtr, aPtr)+columnToArrow colName (BoxedColumn (vec :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+        let n = V.length vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+        V.imapM_ (pokeElemOff dataPtr) vec+        sPtr <- makeLeafSchema "g" colName+        aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (BoxedColumn (vec :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+        let n = V.length vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+        V.imapM_ (\i v -> pokeElemOff dataPtr i (fromIntegral v)) vec+        sPtr <- makeLeafSchema "l" colName+        aPtr <- makeLeafArray n 0 [nullPtr, castPtr dataPtr] (free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) = do+        let n = V.length vec+        (bitmapPtr, nullCount) <- buildBitmap vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Int64)+        V.imapM_+            ( \i mv ->+                pokeElemOff dataPtr i (fromIntegral (fromMaybe 0 mv) :: Int64)+            )+            vec+        sPtr <- makeLeafSchema "l" colName+        aPtr <-+            makeLeafArray+                n+                (fromIntegral nullCount)+                [castPtr bitmapPtr, castPtr dataPtr]+                (free bitmapPtr >> free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) = do+        let n = V.length vec+        (bitmapPtr, nullCount) <- buildBitmap vec+        dataPtr <- mallocArray (max 1 n) :: IO (Ptr Double)+        V.imapM_+            ( \i mv ->+                pokeElemOff dataPtr i (maybe 0.0 realToFrac mv :: Double)+            )+            vec+        sPtr <- makeLeafSchema "g" colName+        aPtr <-+            makeLeafArray+                n+                (fromIntegral nullCount)+                [castPtr bitmapPtr, castPtr dataPtr]+                (free bitmapPtr >> free dataPtr)+        return (sPtr, aPtr)+columnToArrow colName (OptionalColumn (vec :: V.Vector (Maybe a)))+    | Just Refl <- testEquality (typeRep @a) (typeRep @T.Text) = do+        let n = V.length vec+            texts = V.toList vec+            bss = map (maybe BS.empty TE.encodeUtf8) texts+            cumOff = scanl (+) 0 (map BS.length bss)+            total = last cumOff+        (bitmapPtr, nullCount) <- buildBitmap vec+        offPtr <- mallocArray (n + 1) :: IO (Ptr Int32)+        zipWithM_+            (\i o -> pokeElemOff offPtr i (fromIntegral o :: Int32))+            [0 ..]+            cumOff+        charsPtr <- mallocBytes (max 1 total) :: IO (Ptr Word8)+        foldM_+            ( \pos bs -> do+                BS.useAsCStringLen bs $ \(src, len) ->+                    copyBytes (charsPtr `plusPtr` pos) (castPtr src) len+                return (pos + BS.length bs)+            )+            0+            bss+        sPtr <- makeLeafSchema "u" colName+        aPtr <-+            makeLeafArray+                n+                (fromIntegral nullCount)+                [castPtr bitmapPtr, castPtr offPtr, castPtr charsPtr]+                (free bitmapPtr >> free offPtr >> free charsPtr)+        return (sPtr, aPtr)+columnToArrow colName _ =+    error $+        "DataFrame.IO.Arrow.columnToArrow: unsupported column type for '"+            ++ T.unpack colName+            ++ "'"++dataframeToArrow :: DataFrame -> IO (Ptr ArrowSchema, Ptr ArrowArray)+dataframeToArrow df = do+    let idxToName = M.fromList [(v, k) | (k, v) <- M.toList (columnIndices df)]+        ncols = M.size (columnIndices df)+        colsInOrder =+            [ (idxToName M.! i, columns df V.! i)+            | i <- [0 .. ncols - 1]+            ]++    childPairs <- forM colsInOrder (uncurry columnToArrow)+    let childSPtrs = map fst childPairs+        childAPtrs = map snd childPairs++    let nRows = case colsInOrder of+            [] -> 0+            _ -> DI.columnLength (snd (head colsInOrder))+    topSchema <- mallocBytes arrowSchemaSize+    fmtStr <- newCString "+s"+    nameStr <- newCString ""+    childSArr <- mallocArray ncols :: IO (Ptr (Ptr ArrowSchema))+    zipWithM_ (pokeElemOff childSArr) [0 ..] childSPtrs+    topSchema `at` _schemaFormat $ fmtStr+    topSchema `at` _schemaName $ nameStr+    topSchema `at` _schemaMetadata $ (nullPtr :: Ptr ())+    topSchema `at` _schemaFlags $ (0 :: Int64)+    topSchema `at` _schemaNChildren $ (fromIntegral ncols :: Int64)+    topSchema `at` _schemaChildren $ childSArr+    topSchema `at` _schemaDictionary $ (nullPtr :: Ptr ())+    topSchema `at` _schemaRelease $ pReleaseSchema+    -- Do NOT loop over children here: Arrow C++ zeroes children[i]->release+    -- during import, so reading it would yield a null function pointer.+    -- Children are released independently by Arrow C++; their own cleanup+    -- closures free their buffers and struct memory.+    cleanupS <- newStablePtr $ do+        free childSArr+        free fmtStr+        free nameStr+        free topSchema -- free our original mallocBytes allocation+    topSchema `at` _schemaPrivateData $ castStablePtrToPtr cleanupS++    -- ── Top-level struct array ──────────────────────────────────────────────+    topArray <- mallocBytes arrowArraySize+    childAArr <- mallocArray ncols :: IO (Ptr (Ptr ArrowArray))+    zipWithM_ (pokeElemOff childAArr) [0 ..] childAPtrs+    topBufArr <- mallocArray 1 :: IO (Ptr (Ptr ()))+    pokeElemOff topBufArr 0 nullPtr+    topArray `at` _arrayLength $ (fromIntegral nRows :: Int64)+    topArray `at` _arrayNullCount $ (0 :: Int64)+    topArray `at` _arrayOffset $ (0 :: Int64)+    topArray `at` _arrayNBuffers $ (1 :: Int64)+    topArray `at` _arrayNChildren $ (fromIntegral ncols :: Int64)+    topArray `at` _arrayBuffers $ topBufArr+    topArray `at` _arrayChildren $ childAArr+    topArray `at` _arrayDictionary $ (nullPtr :: Ptr ())+    topArray `at` _arrayRelease $ pReleaseArray+    -- Same reasoning as cleanupS: Arrow C++ manages children independently.+    cleanupA <- newStablePtr $ do+        free childAArr+        free topBufArr+        free topArray -- free our original mallocBytes allocation+    topArray `at` _arrayPrivateData $ castStablePtrToPtr cleanupA++    return (topSchema, topArray)++-- | Test whether bit i is set in a validity bitmap.+bitmapIsSet :: Ptr Word8 -> Int -> IO Bool+bitmapIsSet bitmapPtr i =+    testBit <$> peekElemOff bitmapPtr (i `div` 8) <*> pure (i `mod` 8)++{- | Import an Arrow RecordBatch from raw C Data Interface pointers.+  Copies all data into GC-managed Haskell vectors, then calls the+  producer's release callbacks.+-}+arrowToDataframe :: Ptr () -> Ptr () -> IO DataFrame+arrowToDataframe rawSchema rawArray = do+    let schemaPtr = castPtr rawSchema :: Ptr ArrowSchema+        arrayPtr = castPtr rawArray :: Ptr ArrowArray+    nCols <- readAt schemaPtr _schemaNChildren :: IO Int64+    childSArr <- readAt schemaPtr _schemaChildren :: IO (Ptr (Ptr ArrowSchema))+    childAArr <- readAt arrayPtr _arrayChildren :: IO (Ptr (Ptr ArrowArray))+    cols <- forM [0 .. fromIntegral nCols - 1] $ \i -> do+        cs <- peekElemOff childSArr i+        ca <- peekElemOff childAArr i+        readArrowColumn cs ca+    -- Call producer's release callbacks after all data has been copied.+    relA <- readAt arrayPtr _arrayRelease :: IO (FunPtr (Ptr ArrowArray -> IO ()))+    when (relA /= nullFunPtr) $ callRelArray relA arrayPtr+    relS <-+        readAt schemaPtr _schemaRelease :: IO (FunPtr (Ptr ArrowSchema -> IO ()))+    when (relS /= nullFunPtr) $ callRelSchema relS schemaPtr+    return $ fromNamedColumns cols++readArrowColumn :: Ptr ArrowSchema -> Ptr ArrowArray -> IO (T.Text, Column)+readArrowColumn schemaPtr arrayPtr = do+    fmtStr <- (readAt schemaPtr _schemaFormat :: IO CString) >>= peekCString+    nameStr <- (readAt schemaPtr _schemaName :: IO CString) >>= peekCString+    let name = T.pack nameStr+    len <- readAt arrayPtr _arrayLength :: IO Int64+    nullCnt <- readAt arrayPtr _arrayNullCount :: IO Int64+    bufArr <- readAt arrayPtr _arrayBuffers :: IO (Ptr (Ptr ()))+    let n = fromIntegral len+    col <- case fmtStr of+        "l" -> readInt64Col n nullCnt bufArr+        "i" -> readInt32Col n nullCnt bufArr+        "g" -> readFloat64Col n nullCnt bufArr+        "f" -> readFloat32Col n nullCnt bufArr+        "U" -> readLargeUtf8Col n nullCnt bufArr+        "u" -> readUtf8Col n nullCnt bufArr+        _ ->+            error $+                "DataFrame.IO.Arrow.readArrowColumn: unsupported format '"+                    ++ fmtStr+                    ++ "' for column '"+                    ++ nameStr+                    ++ "'"+    return (name, col)++readInt64Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readInt64Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    dataVoid <- peekElemOff bufArr 1+    let dataPtr = castPtr dataVoid :: Ptr Int64+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then do+                        v <- peekElemOff dataPtr i+                        return (Just (fromIntegral v :: Int))+                    else return Nothing+            return $ OptionalColumn (vec :: V.Vector (Maybe Int))+        else do+            vec <- VU.generateM n $ \i -> do+                v <- peekElemOff dataPtr i+                return (fromIntegral v :: Int)+            return $ UnboxedColumn (vec :: VU.Vector Int)++readInt32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readInt32Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    dataVoid <- peekElemOff bufArr 1+    let dataPtr = castPtr dataVoid :: Ptr Int32+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then do+                        v <- peekElemOff dataPtr i+                        return (Just (fromIntegral v :: Int))+                    else return Nothing+            return $ OptionalColumn (vec :: V.Vector (Maybe Int))+        else do+            vec <- VU.generateM n $ \i -> do+                v <- peekElemOff dataPtr i+                return (fromIntegral v :: Int)+            return $ UnboxedColumn (vec :: VU.Vector Int)++readFloat64Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readFloat64Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    dataVoid <- peekElemOff bufArr 1+    let dataPtr = castPtr dataVoid :: Ptr Double+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then Just <$> (peekElemOff dataPtr i :: IO Double)+                    else return Nothing+            return $ OptionalColumn (vec :: V.Vector (Maybe Double))+        else do+            vec <- VU.generateM n (peekElemOff dataPtr)+            return $ UnboxedColumn (vec :: VU.Vector Double)++readFloat32Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readFloat32Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    dataVoid <- peekElemOff bufArr 1+    let dataPtr = castPtr dataVoid :: Ptr Float+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then do+                        v <- peekElemOff dataPtr i+                        return (Just (realToFrac v :: Double))+                    else return Nothing+            return $ OptionalColumn (vec :: V.Vector (Maybe Double))+        else do+            vec <- VU.generateM n $ \i -> do+                v <- peekElemOff dataPtr i+                return (realToFrac v :: Double)+            return $ UnboxedColumn (vec :: VU.Vector Double)++-- | Read a large_string (format "U") column with int64 offsets.+readLargeUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readLargeUtf8Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    offsetVoid <- peekElemOff bufArr 1+    charVoid <- peekElemOff bufArr 2+    let offsetPtr = castPtr offsetVoid :: Ptr Int64+        charPtr = castPtr charVoid :: Ptr Word8+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then do+                        start <- fromIntegral <$> peekElemOff offsetPtr i+                        end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+                        bs <-+                            BS.packCStringLen+                                (castPtr (charPtr `plusPtr` start), end - start)+                        return $ Just (TE.decodeUtf8 bs)+                    else return Nothing+            return $ OptionalColumn vec+        else do+            vec <- V.generateM n $ \i -> do+                start <- fromIntegral <$> peekElemOff offsetPtr i+                end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+                bs <-+                    BS.packCStringLen+                        (castPtr (charPtr `plusPtr` start), end - start)+                return $ TE.decodeUtf8 bs+            return $ BoxedColumn vec++-- | Read a utf8 (format "u") column with int32 offsets.+readUtf8Col :: Int -> Int64 -> Ptr (Ptr ()) -> IO Column+readUtf8Col n nullCnt bufArr = do+    bitmapVoid <- peekElemOff bufArr 0+    offsetVoid <- peekElemOff bufArr 1+    charVoid <- peekElemOff bufArr 2+    let offsetPtr = castPtr offsetVoid :: Ptr Int32+        charPtr = castPtr charVoid :: Ptr Word8+    if nullCnt > 0+        then do+            let bitmapPtr = castPtr bitmapVoid :: Ptr Word8+            vec <- V.generateM n $ \i -> do+                valid <- bitmapIsSet bitmapPtr i+                if valid+                    then do+                        start <- fromIntegral <$> peekElemOff offsetPtr i+                        end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+                        bs <-+                            BS.packCStringLen+                                (castPtr (charPtr `plusPtr` start), end - start)+                        return $ Just (TE.decodeUtf8 bs)+                    else return Nothing+            return $ OptionalColumn vec+        else do+            vec <- V.generateM n $ \i -> do+                start <- fromIntegral <$> peekElemOff offsetPtr i+                end <- fromIntegral <$> peekElemOff offsetPtr (i + 1)+                bs <-+                    BS.packCStringLen+                        (castPtr (charPtr `plusPtr` start), end - start)+                return $ TE.decodeUtf8 bs+            return $ BoxedColumn vec
+ ffi/DataFrame/IR.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Intermediate Representation for DataFrame query plans.+  JSON-decodable plan tree + interpreter.+-}+module DataFrame.IR (+    PlanNode (..),+    AggSpec (..),+    executePlan,+) where++import Data.Aeson (FromJSON (..), withObject, (.:))+import Data.Aeson.Types (Parser)+import qualified Data.Text as T+import Data.Type.Equality (+    TestEquality (testEquality),+    type (:~:) (Refl),+ )+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import Data.Word (Word64)+import Foreign (wordPtrToPtr)+import Type.Reflection (typeRep)++import DataFrame.Functions (count, mean, meanMaybe, sumMaybe)+import qualified DataFrame.Functions as Functions+import DataFrame.IO.Arrow (arrowToDataframe)+import DataFrame.IO.CSV (+    defaultReadOptions,+    readSeparated,+    readTsv,+ )+import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)+import DataFrame.Internal.Expression (Expr (..), NamedExpr)+import DataFrame.Operations.Aggregation (aggregate, groupBy)+import DataFrame.Operations.Permutation (SortOrder (..), sortBy)+import DataFrame.Operations.Subset (select)+import qualified DataFrame.Operations.Subset as Subset+import DataFrame.Operators ((.=))++-- ---------------------------------------------------------------------------+-- IR types+-- ---------------------------------------------------------------------------++data AggSpec = AggSpec+    { aggName :: T.Text+    , aggFn :: T.Text+    , aggCol :: T.Text+    }+    deriving (Show)++data PlanNode+    = ReadCsv FilePath+    | ReadTsv FilePath+    | -- | schema_addr array_addr+      FromArrow Word64 Word64+    | Select [T.Text] PlanNode+    | GroupBy [T.Text] [AggSpec] PlanNode+    | Sort [T.Text] Bool PlanNode+    | Limit Int PlanNode+    deriving (Show)++-- ---------------------------------------------------------------------------+-- JSON decoding+-- ---------------------------------------------------------------------------++instance FromJSON AggSpec where+    parseJSON = withObject "AggSpec" $ \o ->+        AggSpec+            <$> o .: "name"+            <*> o .: "agg"+            <*> o .: "col"++instance FromJSON PlanNode where+    parseJSON = withObject "PlanNode" $ \o -> do+        op <- o .: "op" :: Parser T.Text+        case op of+            "ReadCsv" -> ReadCsv <$> o .: "path"+            "ReadTsv" -> ReadTsv <$> o .: "path"+            "FromArrow" -> FromArrow <$> o .: "schema" <*> o .: "array"+            "Select" -> Select <$> o .: "cols" <*> o .: "input"+            "GroupBy" -> GroupBy <$> o .: "keys" <*> o .: "aggregations" <*> o .: "input"+            "Sort" -> Sort <$> o .: "cols" <*> o .: "ascending" <*> o .: "input"+            "Limit" -> Limit <$> o .: "n" <*> o .: "input"+            _ -> fail $ "DataFrame.IR: unknown op: " ++ T.unpack op++executePlan :: PlanNode -> IO DataFrame+executePlan (ReadCsv path) =+    readSeparated defaultReadOptions path+executePlan (ReadTsv path) =+    readTsv path+executePlan (FromArrow schemaAddr arrayAddr) =+    arrowToDataframe+        (wordPtrToPtr (fromIntegral schemaAddr))+        (wordPtrToPtr (fromIntegral arrayAddr))+executePlan (Select cols node) =+    select cols <$> executePlan node+executePlan (GroupBy keys aggs node) = do+    df <- executePlan node+    nes <- mapM (buildNamedExpr df) aggs+    return $ aggregate nes (groupBy keys df)+executePlan (Sort cols ascending node) = do+    df <- executePlan node+    let orders = map (\c -> mkSortOrder ascending c (unsafeGetColumn c df)) cols+    return $ sortBy orders df+executePlan (Limit k node) =+    Subset.take k <$> executePlan node++-- | Build a SortOrder from a column's runtime type.+mkSortOrder :: Bool -> T.Text -> Column -> SortOrder+mkSortOrder True n (UnboxedColumn (_ :: VU.Vector a)) = Asc (Col @a n)+mkSortOrder False n (UnboxedColumn (_ :: VU.Vector a)) = Desc (Col @a n)+mkSortOrder True n (BoxedColumn (_ :: V.Vector a)) = Asc (Col @a n)+mkSortOrder False n (BoxedColumn (_ :: V.Vector a)) = Desc (Col @a n)+mkSortOrder True n (OptionalColumn (_ :: V.Vector (Maybe a))) = Asc (Col @(Maybe a) n)+mkSortOrder False n (OptionalColumn (_ :: V.Vector (Maybe a))) = Desc (Col @(Maybe a) n)++-- | Dispatch aggregation by fn name and runtime column type.+buildNamedExpr :: DataFrame -> AggSpec -> IO NamedExpr+buildNamedExpr df (AggSpec name fn colName) =+    case fn of+        "count" -> countExpr name colName (unsafeGetColumn colName df)+        "sum" -> sumExpr name colName (unsafeGetColumn colName df)+        "mean" -> meanExpr name colName (unsafeGetColumn colName df)+        other ->+            ioError $+                userError $+                    "DataFrame.IR: unknown aggregation '" ++ T.unpack other ++ "'"++countExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+countExpr name colName (UnboxedColumn (_ :: VU.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (BoxedColumn (_ :: V.Vector a)) = return $ name .= count (Col @a colName)+countExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a))) = return $ name .= count (Col @(Maybe a) colName)++sumExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+sumExpr name colName (UnboxedColumn (_ :: VU.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= Functions.sum (Col @Int colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= Functions.sum (Col @Double colName)+sumExpr name colName (BoxedColumn (_ :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= Functions.sum (Col @Int colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= Functions.sum (Col @Double colName)+sumExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a)))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= sumMaybe (Col @(Maybe Int) colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= sumMaybe (Col @(Maybe Double) colName)+sumExpr name colName _ =+    ioError $+        userError $+            "DataFrame.IR: sum: unsupported column type for '" ++ T.unpack colName ++ "'"++meanExpr :: T.Text -> T.Text -> Column -> IO NamedExpr+meanExpr name colName (UnboxedColumn (_ :: VU.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= mean (Col @Int colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= mean (Col @Double colName)+meanExpr name colName (BoxedColumn (_ :: V.Vector a))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= mean (Col @Double colName)+meanExpr name colName (OptionalColumn (_ :: V.Vector (Maybe a)))+    | Just Refl <- testEquality (typeRep @a) (typeRep @Double) =+        return $ name .= meanMaybe (Col @(Maybe Double) colName)+    | Just Refl <- testEquality (typeRep @a) (typeRep @Int) =+        return $ name .= meanMaybe (Col @(Maybe Int) colName)+meanExpr name colName _ =+    ioError $+        userError $+            "DataFrame.IR: mean: unsupported column type for '" ++ T.unpack colName ++ "'"
src/DataFrame.hs view
@@ -219,6 +219,9 @@     module UnstableCSV,     module Parquet, +    -- * Type conversion+    module Typing,+     -- * Operations     module Subset,     module Transformations,@@ -298,6 +301,7 @@     toRowVector,  ) import DataFrame.Internal.Schema as Schema (+    makeSchema,     schemaType,  ) import DataFrame.Operations.Aggregation as Aggregation (@@ -359,8 +363,11 @@     sample,     select,     selectBy,+    stratifiedSample,+    stratifiedSplit,     take,     takeLast,  ) import DataFrame.Operations.Transformations as Transformations+import DataFrame.Operations.Typing as Typing import DataFrame.Operators as Operators
src/DataFrame/DecisionTree.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -12,7 +13,7 @@ import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn) import DataFrame.Internal.Expression (Expr (..), eSize, getColumns) import DataFrame.Internal.Interpreter (interpret)-import DataFrame.Internal.Statistics (percentile', percentileOrd')+import DataFrame.Internal.Statistics (percentileOrd') import DataFrame.Internal.Types import DataFrame.Operations.Core (columnNames, nRows) import DataFrame.Operations.Subset (exclude, filterWhere)@@ -21,9 +22,12 @@ import Control.Monad (guard) import Data.Containers.ListUtils (nubOrd) import Data.Function (on)+#if MIN_VERSION_base(4,20,0)+import Data.List (maximumBy, minimumBy, sort, sortBy)+#else import Data.List (foldl', maximumBy, minimumBy, sort, sortBy)+#endif import qualified Data.Map.Strict as M-import Data.Maybe import qualified Data.Text as T import Data.Type.Equality import qualified Data.Vector as V@@ -548,38 +552,108 @@                             (boolExpansion (synthConfig cfg))                         ) +-- | Unifies non-nullable and nullable Double expressions for feature generation.+data NumExpr+    = NDouble !(Expr Double)+    | NMaybeDouble !(Expr (Maybe Double))++numExprCols :: NumExpr -> [T.Text]+numExprCols (NDouble e) = getColumns e+numExprCols (NMaybeDouble e) = getColumns e++numExprEq :: NumExpr -> NumExpr -> Bool+numExprEq (NDouble e1) (NDouble e2) = e1 == e2+numExprEq (NMaybeDouble e1) (NMaybeDouble e2) = e1 == e2+numExprEq _ _ = False++combineNumExprs :: NumExpr -> NumExpr -> [NumExpr]+combineNumExprs (NDouble e1) (NDouble e2) =+    [ NDouble (e1 .+ e2)+    , NDouble (e1 .- e2)+    , NDouble (e1 .* e2)+    , NDouble+        (F.ifThenElse (e2 ./= F.lit (0 :: Double)) (e1 ./ e2) (F.lit (0 :: Double)))+    ]+combineNumExprs (NDouble e1) (NMaybeDouble e2) =+    [ NMaybeDouble (e1 .+ e2)+    , NMaybeDouble (e1 .- e2)+    , NMaybeDouble (e1 .* e2)+    , NMaybeDouble+        ( F.ifThenElse+            (F.fromMaybe False (e2 ./= F.lit (0 :: Double)))+            (e1 ./ e2)+            (F.lit (Nothing :: Maybe Double))+        )+    ]+combineNumExprs (NMaybeDouble e1) (NDouble e2) =+    [ NMaybeDouble (e1 .+ e2)+    , NMaybeDouble (e1 .- e2)+    , NMaybeDouble (e1 .* e2)+    , NMaybeDouble+        ( F.ifThenElse+            (e2 ./= F.lit (0 :: Double))+            (e1 ./ e2)+            (F.lit (Nothing :: Maybe Double))+        )+    ]+combineNumExprs (NMaybeDouble e1) (NMaybeDouble e2) =+    [ NMaybeDouble (e1 .+ e2)+    , NMaybeDouble (e1 .- e2)+    , NMaybeDouble (e1 .* e2)+    , NMaybeDouble+        ( F.ifThenElse+            (F.fromMaybe False (e2 ./= F.lit (0 :: Double)))+            (e1 ./ e2)+            (F.lit (Nothing :: Maybe Double))+        )+    ]+ numericConditions :: TreeConfig -> DataFrame -> [Expr Bool] numericConditions = generateNumericConds  generateNumericConds :: TreeConfig -> DataFrame -> [Expr Bool] generateNumericConds cfg df = do     expr <- numericExprsWithTerms (synthConfig cfg) df-    let thresholds = map (\p -> percentile p expr df) (percentiles cfg)+    let thresholds = numericThresholds expr     threshold <- thresholds-    [ expr .<= F.lit threshold-        , expr .>= F.lit threshold-        , expr .< F.lit threshold-        , expr .> F.lit threshold+    numericCondsFromExpr expr threshold+  where+    numericThresholds (NDouble e) = map (\p -> percentile p e df) (percentiles cfg)+    numericThresholds (NMaybeDouble e) = map (\p -> percentile p (F.fromMaybe 0 e) df) (percentiles cfg)++    numericCondsFromExpr (NDouble e) t =+        [e .<= F.lit t, e .>= F.lit t, e .< F.lit t, e .> F.lit t]+    numericCondsFromExpr (NMaybeDouble e) t =+        [ F.fromMaybe False (e .<= F.lit t)+        , F.fromMaybe False (e .>= F.lit t)+        , F.fromMaybe False (e .< F.lit t)+        , F.fromMaybe False (e .> F.lit t)         ] -numericExprsWithTerms :: SynthConfig -> DataFrame -> [Expr Double]+numericExprsWithTerms :: SynthConfig -> DataFrame -> [NumExpr] numericExprsWithTerms cfg df =     concatMap (numericExprs cfg df [] 0) [0 .. maxExprDepth cfg] -numericCols :: DataFrame -> [Expr Double]+numericCols :: DataFrame -> [NumExpr] numericCols df = concatMap extract (columnNames df)   where     extract col = case unsafeGetColumn col df of         UnboxedColumn (_ :: VU.Vector b) ->             case testEquality (typeRep @b) (typeRep @Double) of-                Just Refl -> [Col col]+                Just Refl -> [NDouble (Col col)]                 Nothing -> case sIntegral @b of-                    STrue -> [F.toDouble (Col @b col)]+                    STrue -> [NDouble (F.toDouble (Col @b col))]                     SFalse -> []+        OptionalColumn (_ :: V.Vector (Maybe b)) ->+            case testEquality (typeRep @b) (typeRep @Double) of+                Just Refl -> [NMaybeDouble (Col @(Maybe b) col)]+                Nothing -> case sIntegral @b of+                    STrue -> [NMaybeDouble (F.whenPresent (realToFrac @b @Double) (Col @(Maybe b) col))]+                    SFalse -> []         _ -> []  numericExprs ::-    SynthConfig -> DataFrame -> [Expr Double] -> Int -> Int -> [Expr Double]+    SynthConfig -> DataFrame -> [NumExpr] -> Int -> Int -> [NumExpr] numericExprs cfg df prevExprs depth maxDepth     | depth == 0 = baseExprs ++ numericExprs cfg df baseExprs (depth + 1) maxDepth     | depth >= maxDepth = []@@ -592,16 +666,16 @@         | otherwise = do             e1 <- prevExprs             e2 <- baseExprs-            let cols = getColumns e1 <> getColumns e2+            let cols = numExprCols e1 <> numExprCols e2             guard-                ( e1 /= e2+                ( not (numExprEq e1 e2)                     && not                         ( any                             (\(l, r) -> l `elem` cols && r `elem` cols)                             (disallowedCombinations cfg)                         )                 )-            [e1 + e2, e1 - e2, e1 * e2, F.ifThenElse (e2 ./= 0) (e1 / e2) 0]+            combineNumExprs e1 e2  boolExprs ::     DataFrame -> [Expr Bool] -> [Expr Bool] -> Int -> Int -> [Expr Bool]@@ -625,42 +699,14 @@         genConds colName = case unsafeGetColumn colName df of             (BoxedColumn (col :: V.Vector a)) ->                 let ps = map (Lit . (`percentileOrd'` col)) [1, 25, 75, 99]-                 in map (Col @a colName .==) ps+                 in map (F.lift2 (==) (Col @a colName)) ps             (OptionalColumn (col :: V.Vector (Maybe a))) -> case sFloating @a of-                STrue ->-                    let doubleCol =-                            VU.convert-                                (V.map fromJust (V.filter isJust (V.map (fmap (realToFrac @a @Double)) col)))-                     in zipWith-                            ($)-                            [ (Col @(Maybe a) colName .==)-                            , (Col @(Maybe a) colName .<=)-                            , (Col @(Maybe a) colName .>=)-                            ]-                            ( Lit Nothing-                                : map-                                    (Lit . Just . realToFrac . (`percentile'` doubleCol))-                                    (percentiles cfg)-                            )+                STrue -> [] -- handled by numericCols / numericExprs                 SFalse -> case sIntegral @a of-                    STrue ->-                        let doubleCol =-                                VU.convert-                                    (V.map fromJust (V.filter isJust (V.map (fmap (fromIntegral @a @Double)) col)))-                         in zipWith-                                ($)-                                [ (Col @(Maybe a) colName .==)-                                , (Col @(Maybe a) colName .<=)-                                , (Col @(Maybe a) colName .>=)-                                ]-                                ( Lit Nothing-                                    : map-                                        (Lit . Just . round . (`percentile'` doubleCol))-                                        (percentiles cfg)-                                )+                    STrue -> [] -- handled by numericCols / numericExprs                     SFalse ->                         map-                            ((Col @(Maybe a) colName .==) . Lit . (`percentileOrd'` col))+                            (F.lift2 (==) (Col @(Maybe a) colName) . Lit . (`percentileOrd'` col))                             [1, 25, 75, 99]             (UnboxedColumn (_ :: VU.Vector a)) -> [] @@ -681,15 +727,18 @@                 (BoxedColumn (col1 :: V.Vector a), BoxedColumn (_ :: V.Vector b)) ->                     case testEquality (typeRep @a) (typeRep @b) of                         Nothing -> []-                        Just Refl -> [Col @a l .== Col @a r]+                        Just Refl -> [F.lift2 (==) (Col @a l) (Col @a r)]                 (UnboxedColumn (_ :: VU.Vector a), UnboxedColumn (_ :: VU.Vector b)) -> []                 ( OptionalColumn (_ :: V.Vector (Maybe a))                     , OptionalColumn (_ :: V.Vector (Maybe b))                     ) -> case testEquality (typeRep @a) (typeRep @b) of                         Nothing -> []                         Just Refl -> case testEquality (typeRep @a) (typeRep @T.Text) of-                            Nothing -> [Col @(Maybe a) l .<= Col r, Col @(Maybe a) l .== Col r]-                            Just Refl -> [Col @(Maybe a) l .== Col r]+                            Nothing ->+                                [ F.lift2 (<=) (Col @(Maybe a) l) (Col r)+                                , F.lift2 (==) (Col @(Maybe a) l) (Col r)+                                ]+                            Just Refl -> [F.lift2 (==) (Col @(Maybe a) l) (Col r)]                 _ -> []      in         concatMap genConds (columnNames df) ++ columnConds@@ -759,3 +808,117 @@  pruneTree :: forall a. (Columnable a, Eq a) => Expr a -> Expr a pruneTree = pruneExpr++-- | A tree where each leaf stores a class-probability distribution.+type ProbTree a = Tree (M.Map a Double)++-- | Compute normalised class probabilities from a subset of training rows.+probsFromIndices ::+    forall a.+    (Columnable a) =>+    T.Text ->+    DataFrame ->+    V.Vector Int ->+    M.Map a Double+probsFromIndices target df indices =+    case interpret @a df (Col target) of+        Left _ -> M.empty+        Right (TColumn col) ->+            case toVector @a col of+                Left _ -> M.empty+                Right vals ->+                    let counts =+                            V.foldl'+                                (\acc i -> M.insertWith (+) (vals V.! i) 1 acc)+                                M.empty+                                indices+                        total = fromIntegral (V.length indices) :: Double+                     in M.map (\c -> fromIntegral c / total) counts++{- | Annotate a fitted 'Tree a' with class distributions by routing the+  training data through it.  The split conditions are preserved; only the+  leaf values change from a majority label to a probability map.+-}+buildProbTree ::+    forall a.+    (Columnable a) =>+    Tree a ->+    T.Text ->+    DataFrame ->+    V.Vector Int ->+    ProbTree a+buildProbTree (Leaf _) target df indices =+    Leaf (probsFromIndices @a target df indices)+buildProbTree (Branch cond left right) target df indices =+    let (indicesL, indicesR) = partitionIndices cond df indices+     in Branch+            cond+            (buildProbTree @a left target df indicesL)+            (buildProbTree @a right target df indicesR)++{- | Fit a TAO decision tree and return one @Expr Double@ per class.++  Each @(c, e)@ pair in the result map means: evaluate @e@ on a 'DataFrame'+  row to get the predicted probability of class @c@.  You can insert these+  as new columns with 'derive' or evaluate them with 'interpret'.++  Example:+  @+  let pes = fitProbTree \@T.Text cfg (Col \"species\") trainDf+  -- pes M.! \"setosa\" :: Expr Double+  df' = M.foldlWithKey' (\\d cls e -> D.derive (cls <> \"_prob\") e d) testDf pes+  @+-}+fitProbTree ::+    forall a.+    (Columnable a) =>+    TreeConfig ->+    Expr a -> -- target column, e.g. @Col \"label\"@+    DataFrame ->+    M.Map a (Expr Double)+fitProbTree cfg (Col target) df =+    let+        conds =+            nubOrd $+                numericConditions cfg (exclude [target] df)+                    ++ generateConditionsOld cfg (exclude [target] df)+        initialTree = buildGreedyTree @a cfg (maxTreeDepth cfg) target conds df+        indices = V.enumFromN 0 (nRows df)+        optimizedTree = taoOptimize @a cfg target conds df indices initialTree+        pruned = pruneDead optimizedTree+     in+        probExprs (buildProbTree @a pruned target df indices)+fitProbTree _ expr _ =+    error $ "Cannot create prob tree for compound expression: " ++ show expr++{- | Convert a 'ProbTree' into one 'Expr Double' per class.++  Each @(c, e)@ pair means: evaluate @e@ on a 'DataFrame' row to get the+  predicted probability of class @c@.  You can insert these as new columns+  with 'derive' or evaluate them with 'interpret'.++  Example:+  @+  let pt  = fitProbTree \@T.Text cfg (Col \"species\") trainDf+      pes = probExprs pt+  -- pes M.! \"setosa\" :: Expr Double+  df' = M.foldlWithKey' (\\d cls e -> D.derive (cls <> \"_prob\") e d) testDf pes+  @+-}+probExprs ::+    forall a.+    (Columnable a) =>+    ProbTree a ->+    M.Map a (Expr Double)+probExprs tree =+    let classes = nubOrd (allClasses tree)+     in M.fromList [(c, classExpr c tree) | c <- classes]+  where+    allClasses :: ProbTree a -> [a]+    allClasses (Leaf m) = M.keys m+    allClasses (Branch _ l r) = allClasses l ++ allClasses r++    classExpr :: a -> ProbTree a -> Expr Double+    classExpr c (Leaf m) = Lit (M.findWithDefault 0.0 c m)+    classExpr c (Branch cond l r) =+        F.ifThenElse cond (classExpr c l) (classExpr c r)
src/DataFrame/Functions.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-}@@ -11,6 +10,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}  module DataFrame.Functions (module DataFrame.Functions, module DataFrame.Operators) where@@ -29,12 +29,14 @@ import Control.Monad import Control.Monad.IO.Class import qualified Data.Char as Char+import Data.Either import Data.Function import Data.Functor import Data.Int import qualified Data.List as L import qualified Data.Map as M import qualified Data.Maybe as Maybe+import qualified Data.Set as S import qualified Data.Text as T import Data.Time import qualified Data.Vector as V@@ -43,6 +45,14 @@ import qualified DataFrame.IO.CSV as CSV import qualified DataFrame.IO.Parquet as Parquet import DataFrame.IO.Parquet.Thrift+import DataFrame.IO.Parquet.Types (columnNullCount)+import DataFrame.Internal.Nullable (+    BaseType,+    NullLift1Op (applyNull1),+    NullLift1Result,+    NullLift2Op (applyNull2),+    NullLift2Result,+ ) import DataFrame.Operators import Debug.Trace (trace) import Language.Haskell.TH@@ -72,32 +82,107 @@             }         ) -liftDecorated ::-    (Columnable a, Columnable b) =>-    (a -> b) -> T.Text -> Maybe T.Text -> Expr a -> Expr b-liftDecorated f name rep = Unary (MkUnaryOp{unaryFn = f, unaryName = name, unarySymbol = rep})+{- | Lift a unary function over a nullable or non-nullable column expression.+When the input is @Maybe a@, 'Nothing' short-circuits (like 'fmap').+When the input is plain @a@, the function is applied directly. -lift2Decorated ::-    (Columnable c, Columnable b, Columnable a) =>-    (c -> b -> a) ->-    T.Text ->-    Maybe T.Text ->-    Bool ->-    Int ->-    Expr c ->+The return type is inferred via 'NullLift1Result': no annotation needed.+-}+nullLift ::+    (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) =>+    (BaseType a -> r) ->+    Expr a ->+    Expr (NullLift1Result a r)+nullLift f =+    Unary+        (MkUnaryOp{unaryFn = applyNull1 f, unaryName = "nullLift", unarySymbol = Nothing})++{- | Lift a binary function over nullable or non-nullable column expressions.+Any 'Nothing' operand short-circuits to 'Nothing' in the result.++The return type is inferred via 'NullLift2Result': no annotation needed.+-}+nullLift2 ::+    (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) =>+    (BaseType a -> BaseType b -> r) ->+    Expr a ->     Expr b ->-    Expr a-lift2Decorated f name rep comm prec =+    Expr (NullLift2Result a b r)+nullLift2 f =     Binary         ( MkBinaryOp-            { binaryFn = f-            , binaryName = name-            , binarySymbol = rep-            , binaryCommutative = comm-            , binaryPrecedence = prec+            { binaryFn = applyNull2 f+            , binaryName = "nullLift2"+            , binarySymbol = Nothing+            , binaryCommutative = False+            , binaryPrecedence = 0             }         ) +{- | Lenient numeric \/ text coercion returning @Maybe a@.  Looks up column+@name@ and coerces its values to @a@.  Values that cannot be converted+(parse failures, type mismatches) become 'Nothing'; successfully converted+values are wrapped in 'Just'.  Existing 'Nothing' in optional source columns+stays as 'Nothing'.+-}+cast :: forall a. (Columnable a) => T.Text -> Expr (Maybe a)+cast name = CastWith name "cast" (either (const Nothing) Just)++{- | Lenient coercion that substitutes a default for unconvertible values.+Looks up column @name@, coerces its values to @a@, and uses @def@ wherever+conversion fails or the source value is 'Nothing'.+-}+castWithDefault :: forall a. (Columnable a) => a -> T.Text -> Expr a+castWithDefault def name =+    CastWith name ("castWithDefault:" <> T.pack (show def)) (fromRight def)++{- | Lenient coercion returning @Either T.Text a@.  Successfully converted+values are 'Right'; values that cannot be parsed are kept as 'Left' with+their original string representation, so the caller can inspect or handle+them downstream.  Existing 'Nothing' in optional source columns becomes+@Left \"null\"@.+-}+castEither :: forall a. (Columnable a) => T.Text -> Expr (Either T.Text a)+castEither name = CastWith name "castEither" (either (Left . T.pack) Right)++{- | Lenient coercion for assertedly non-nullable columns.+Substitutes @error@ for @Nothing@, so it will crash at evaluation time if+any @Nothing@ is actually encountered.  For non-nullable and+fully-populated nullable columns no cost is paid.+-}+unsafeCast :: forall a. (Columnable a) => T.Text -> Expr a+unsafeCast name =+    CastWith+        name+        "unsafeCast"+        (fromRight (error "unsafeCast: unexpected Nothing in column"))++castExpr ::+    forall b src. (Columnable b, Columnable src) => Expr src -> Expr (Maybe b)+castExpr = CastExprWith @b @(Maybe b) @src "castExpr" (either (const Nothing) Just)++castExprWithDefault ::+    forall b src. (Columnable b, Columnable src) => b -> Expr src -> Expr b+castExprWithDefault def =+    CastExprWith @b @b @src+        ("castExprWithDefault:" <> T.pack (show def))+        (fromRight def)++castExprEither ::+    forall b src.+    (Columnable b, Columnable src) => Expr src -> Expr (Either T.Text b)+castExprEither =+    CastExprWith @b @(Either T.Text b) @src+        "castExprEither"+        (either (Left . T.pack) Right)++unsafeCastExpr ::+    forall b src. (Columnable b, Columnable src) => Expr src -> Expr b+unsafeCastExpr =+    CastExprWith @b @b @src+        "unsafeCastExpr"+        (fromRight (error "unsafeCastExpr: unexpected Nothing in column"))+ toDouble :: (Columnable a, Real a) => Expr a -> Expr Double toDouble =     Unary@@ -115,20 +200,22 @@ mod :: (Integral a, Columnable a) => Expr a -> Expr a -> Expr a mod = lift2Decorated Prelude.mod "mod" Nothing False 7 -eq :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-eq = (.==)+eq :: (Columnable a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool+eq = lift2Decorated (==) "eq" (Just "==") True 4 -lt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-lt = (.<)+lt :: (Columnable a, Ord a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool+lt = lift2Decorated (<) "lt" (Just "<") False 4 -gt :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-gt = (.>)+gt :: (Columnable a, Ord a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool+gt = lift2Decorated (>) "gt" (Just ">") False 4 -leq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-leq = (.<=)+leq ::+    (Columnable a, Ord a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool+leq = lift2Decorated (<=) "leq" (Just "<=") False 4 -geq :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-geq = (.>=)+geq ::+    (Columnable a, Ord a, Eq a, a ~ BaseType a) => Expr a -> Expr a -> Expr Bool+geq = lift2Decorated (>=) "geq" (Just ">=") False 4  and :: Expr Bool -> Expr Bool -> Expr Bool and = (.&&)@@ -222,7 +309,7 @@ zScore c = (c - mean c) / stddev c  pow :: (Columnable a, Num a) => Expr a -> Int -> Expr a-pow = (.^^)+pow expr i = lift2Decorated (^) "max" (Just "^") True 8 expr (Lit i)  relu :: (Columnable a, Num a, Ord a) => Expr a -> Expr a relu = liftDecorated (Prelude.max 0) "relu" Nothing@@ -256,13 +343,13 @@ whenPresent ::     forall a b.     (Columnable a, Columnable b) => (a -> b) -> Expr (Maybe a) -> Expr (Maybe b)-whenPresent f = lift (fmap f)+whenPresent f = liftDecorated (fmap f) "whenPresent" Nothing  whenBothPresent ::     forall a b c.     (Columnable a, Columnable b, Columnable c) =>     (a -> b -> c) -> Expr (Maybe a) -> Expr (Maybe b) -> Expr (Maybe c)-whenBothPresent f = lift2 (\l r -> f <$> l <*> r)+whenBothPresent f = lift2Decorated (\l r -> f <$> l <*> r) "whenBothPresent" Nothing False 0  recode ::     forall a b.@@ -340,7 +427,7 @@     forall a b m.     (Columnable a, Columnable (m a), Monad m, Columnable b, Columnable (m b)) =>     (a -> m b) -> Expr (m a) -> Expr (m b)-bind f = lift (>>= f)+bind f = liftDecorated (>>= f) "bind" Nothing  -- See Section 2.4 of the Haskell Report https://www.haskell.org/definition/haskell2010.pdf isReservedId :: T.Text -> Bool@@ -452,33 +539,43 @@ declareColumnsFromParquetFile :: String -> DecsQ declareColumnsFromParquetFile path = do     isDir <- liftIO $ doesDirectoryExist path-     let pat = if isDir then path </> "*.parquet" else path-     matches <- liftIO $ glob pat-     files <- liftIO $ filterM (fmap Prelude.not . doesDirectoryExist) matches-    df <--        liftIO $-            foldM-                ( \acc p -> do-                    (metadata, _) <- liftIO (Parquet.readMetadataFromPath path)-                    let d = schemaToEmptyDataFrame (schema metadata)-                    pure $ acc <> d-                )+    metas <- liftIO $ mapM (fmap fst . Parquet.readMetadataFromPath) files+    let nullableCols :: S.Set T.Text+        nullableCols =+            S.fromList+                [ T.pack (last colPath)+                | meta <- metas+                , rg <- rowGroups meta+                , cc <- rowGroupColumns rg+                , let cm = columnMetaData cc+                      colPath = columnPathInSchema cm+                , Prelude.not (null colPath)+                , columnNullCount (columnStatistics cm) > 0+                ]+    let df =+            foldl+                (\acc meta -> acc <> schemaToEmptyDataFrame nullableCols (schema meta))                 DataFrame.Internal.DataFrame.empty-                files+                metas     declareColumns df -schemaToEmptyDataFrame :: [SchemaElement] -> DataFrame-schemaToEmptyDataFrame elems =+schemaToEmptyDataFrame :: S.Set T.Text -> [SchemaElement] -> DataFrame+schemaToEmptyDataFrame nullableCols elems =     let leafElems = filter (\e -> numChildren e == 0) elems-     in fromNamedColumns (map schemaElemToColumn leafElems)+     in fromNamedColumns (map (schemaElemToColumn nullableCols) leafElems) -schemaElemToColumn :: SchemaElement -> (T.Text, Column)-schemaElemToColumn elem =+schemaElemToColumn :: S.Set T.Text -> SchemaElement -> (T.Text, Column)+schemaElemToColumn nullableCols elem =     let name = elementName elem-     in (name, emptyColumnForType (elementType elem))+        isNull = name `S.member` nullableCols+        col =+            if isNull+                then emptyNullableColumnForType (elementType elem)+                else emptyColumnForType (elementType elem)+     in (name, col)  emptyColumnForType :: TType -> Column emptyColumnForType = \case@@ -491,6 +588,19 @@     FLOAT -> fromList @Float []     DOUBLE -> fromList @Double []     STRING -> fromList @T.Text []+    other -> error $ "Unsupported parquet type for column: " <> show other++emptyNullableColumnForType :: TType -> Column+emptyNullableColumnForType = \case+    BOOL -> fromList @(Maybe Bool) []+    BYTE -> fromList @(Maybe Word8) []+    I16 -> fromList @(Maybe Int16) []+    I32 -> fromList @(Maybe Int32) []+    I64 -> fromList @(Maybe Int64) []+    I96 -> fromList @(Maybe Int64) []+    FLOAT -> fromList @(Maybe Float) []+    DOUBLE -> fromList @(Maybe Double) []+    STRING -> fromList @(Maybe T.Text) []     other -> error $ "Unsupported parquet type for column: " <> show other  declareColumnsFromCsvWithOpts :: CSV.ReadOptions -> String -> DecsQ
src/DataFrame/IO/CSV.hs view
@@ -169,7 +169,7 @@  data TypeSpec     = InferFromSample Int-    | SpecifyTypes [(T.Text, SchemaType)]+    | SpecifyTypes [(T.Text, SchemaType)] TypeSpec     | NoInference  -- | CSV read parameters.@@ -202,14 +202,16 @@  shouldInferFromSample :: TypeSpec -> Bool shouldInferFromSample (InferFromSample _) = True+shouldInferFromSample (SpecifyTypes _ fallback) = shouldInferFromSample fallback shouldInferFromSample _ = False  schemaTypeMap :: TypeSpec -> M.Map T.Text SchemaType-schemaTypeMap (SpecifyTypes xs) = M.fromList xs+schemaTypeMap (SpecifyTypes xs _) = M.fromList xs schemaTypeMap _ = M.empty  typeInferenceSampleSize :: TypeSpec -> Int typeInferenceSampleSize (InferFromSample n) = n+typeInferenceSampleSize (SpecifyTypes _ fallback) = typeInferenceSampleSize fallback typeInferenceSampleSize _ = 0  defaultReadOptions :: ReadOptions@@ -221,7 +223,8 @@         , dateFormat = "%Y-%m-%d"         , columnSeparator = ','         , numColumns = Nothing-        , missingIndicators = []+        , missingIndicators =+            ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"]         }  {- | Read CSV file from path and load it into a dataframe.@@ -310,12 +313,13 @@     frozenCols <- V.mapM (finalizeBuilderColumn opts) builderColsV     let numRows = maybe 0 columnLength (frozenCols V.!? 0) -    return $-        DataFrame-            frozenCols-            (M.fromList (zip columnNames [0 ..]))-            (numRows, V.length frozenCols)-            M.empty -- TODO give typed column references+    let df =+            DataFrame+                frozenCols+                (M.fromList (zip columnNames [0 ..]))+                (numRows, V.length frozenCols)+                M.empty -- TODO give typed column references+    pure $ parseWithTypes (safeRead opts) (schemaTypeMap (typeSpec opts)) df  initializeColumns ::     [T.Text] -> [BL.ByteString] -> ReadOptions -> IO [BuilderColumn]@@ -325,7 +329,7 @@     -- Return Nothing for columns that should be inferred from BS     shouldInfer = case typeSpec opts of         InferFromSample _ -> True-        SpecifyTypes _ -> True+        SpecifyTypes _ fallback -> shouldInferFromSample fallback         NoInference -> False     lookupType name = M.lookup name typeMap     initColumn :: T.Text -> Maybe SchemaType -> IO BuilderColumn@@ -370,12 +374,12 @@                 Nothing -> appendPagedUnboxedVector gv 0.0 >> appendPagedUnboxedVector valid 0             BuilderText gv valid -> do                 let !val = T.strip (TE.decodeUtf8Lenient bs')-                if isNullish val || val `elem` missing+                if val `elem` missing                     then appendPagedVector gv T.empty >> appendPagedUnboxedVector valid 0                     else appendPagedVector gv val >> appendPagedUnboxedVector valid 1             BuilderBS gv valid -> do                 let !bs'' = C.strip bs'-                if isNullishBS bs'' || TE.decodeUtf8Lenient bs'' `elem` missing+                if TE.decodeUtf8Lenient bs'' `elem` missing                     then appendPagedVector gv BS.empty >> appendPagedUnboxedVector valid 0                     else appendPagedVector gv bs'' >> appendPagedUnboxedVector valid 1 
src/DataFrame/IO/Parquet.hs view
@@ -6,39 +6,53 @@  module DataFrame.IO.Parquet where -import Control.Exception (throw)+import Control.Exception (throw, try) import Control.Monad-import Data.Bits import qualified Data.ByteString as BSO import Data.Either import Data.IORef import Data.Int import qualified Data.List as L-import qualified Data.Map as M+import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import Data.Text.Encoding import Data.Time import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Word import DataFrame.Errors (DataFrameException (ColumnNotFoundException))+import DataFrame.Internal.Binary (littleEndianWord32) import qualified DataFrame.Internal.Column as DI import DataFrame.Internal.DataFrame (DataFrame) import DataFrame.Internal.Expression (Expr, getColumns) import qualified DataFrame.Operations.Core as DI import DataFrame.Operations.Merge () import qualified DataFrame.Operations.Subset as DS-import System.FilePath.Glob (glob)+import System.FilePath.Glob (compile, glob, match) +import Data.Aeson (FromJSON (..), eitherDecodeStrict, withObject, (.:)) import DataFrame.IO.Parquet.Dictionary import DataFrame.IO.Parquet.Levels import DataFrame.IO.Parquet.Page import DataFrame.IO.Parquet.Thrift import DataFrame.IO.Parquet.Types-import System.Directory (doesDirectoryExist)+import Network.HTTP.Simple (+    getResponseBody,+    getResponseStatusCode,+    httpBS,+    parseRequest,+    setRequestHeader,+ )+import System.Directory (+    doesDirectoryExist,+    getHomeDirectory,+    getTemporaryDirectory,+ )+import System.Environment (lookupEnv)  import qualified Data.Vector.Unboxed as VU+import DataFrame.IO.Parquet.Seeking import System.FilePath ((</>))+import System.IO (IOMode (ReadMode))  -- Options ----------------------------------------------------------------- @@ -131,8 +145,19 @@                     p : go (sChildren n) ps False  readParquetWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetWithOpts opts path = do-    (fileMetadata, contents) <- readMetadataFromPath path+readParquetWithOpts opts path+    | isHFUri path = do+        paths <- fetchHFParquetFiles path+        let optsNoRange = opts{rowRange = Nothing}+        dfs <- mapM (_readParquetWithOpts Nothing optsNoRange) paths+        pure (applyRowRange opts (mconcat dfs))+    | otherwise = _readParquetWithOpts Nothing opts path++-- | Internal function to pass testing parameters+_readParquetWithOpts ::+    ForceNonSeekable -> ParquetReadOptions -> FilePath -> IO DataFrame+_readParquetWithOpts extraConfig opts path = withFileBufferedOrSeekable extraConfig path ReadMode $ \file -> do+    fileMetadata <- readMetadataFromHandle file     let columnPaths = getColumnPaths (drop 1 $ schema fileMetadata)     let columnNames = map fst columnPaths     let leafNames = map (last . T.splitOn ".") columnNames@@ -205,7 +230,11 @@                             else colDataPageOffset                 let colLength = columnTotalCompressedSize metadata -                let columnBytes = BSO.take (fromIntegral colLength) (BSO.drop (fromIntegral colStart) contents)+                columnBytes <-+                    seekAndReadBytes+                        (Just (AbsoluteSeek, fromIntegral colStart))+                        (fromIntegral colLength)+                        file                  pages <- readAllPages (columnCodec metadata) columnBytes @@ -237,13 +266,13 @@                     Nothing -> do                         mc <- DI.newMutableColumn totalRows column                         DI.copyIntoMutableColumn mc 0 column-                        modifyIORef colMutMap (M.insert colFullName mc)-                        modifyIORef colOffMap (M.insert colFullName (DI.columnLength column))+                        modifyIORef' colMutMap (M.insert colFullName mc)+                        modifyIORef' colOffMap (M.insert colFullName (DI.columnLength column))                     Just mc -> do                         off <- (M.! colFullName) <$> readIORef colOffMap                         DI.copyIntoMutableColumn mc off column-                        modifyIORef colOffMap (M.adjust (+ DI.columnLength column) colFullName)-                modifyIORef lTypeMap (M.insert colFullName lType)+                        modifyIORef' colOffMap (M.adjust (+ DI.columnLength column) colFullName)+                modifyIORef' lTypeMap (M.insert colFullName lType)      finalMutMap <- readIORef colMutMap     finalColMap <-@@ -282,23 +311,29 @@ @ -} readParquetFilesWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame-readParquetFilesWithOpts opts path = do-    isDir <- doesDirectoryExist path+readParquetFilesWithOpts opts path+    | isHFUri path = do+        files <- fetchHFParquetFiles path+        let optsWithoutRowRange = opts{rowRange = Nothing}+        dfs <- mapM (_readParquetWithOpts Nothing optsWithoutRowRange) files+        pure (applyRowRange opts (mconcat dfs))+    | otherwise = do+        isDir <- doesDirectoryExist path -    let pat = if isDir then path </> "*.parquet" else path+        let pat = if isDir then path </> "*.parquet" else path -    matches <- glob pat+        matches <- glob pat -    files <- filterM (fmap not . doesDirectoryExist) matches+        files <- filterM (fmap not . doesDirectoryExist) matches -    case files of-        [] ->-            error $-                "readParquetFiles: no parquet files found for " ++ path-        _ -> do-            let optsWithoutRowRange = opts{rowRange = Nothing}-            dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files-            pure (applyRowRange opts (mconcat dfs))+        case files of+            [] ->+                error $+                    "readParquetFiles: no parquet files found for " ++ path+            _ -> do+                let optsWithoutRowRange = opts{rowRange = Nothing}+                dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files+                pure (applyRowRange opts (mconcat dfs))  -- Options application ----------------------------------------------------- @@ -322,28 +357,35 @@  -- File and metadata parsing ----------------------------------------------- +-- | read the file in memory at once, parse magicString and return the entire file ByteString readMetadataFromPath :: FilePath -> IO (FileMetadata, BSO.ByteString) readMetadataFromPath path = do     contents <- BSO.readFile path-    let (size, magicString) = contents `seq` readMetadataSizeFromFooter contents+    let (size, magicString) = readMetadataSizeFromFooter contents     when (magicString /= "PAR1") $ error "Invalid Parquet file"     meta <- readMetadata contents size     pure (meta, contents) -readMetadataSizeFromFooter :: BSO.ByteString -> (Int, BSO.ByteString)-readMetadataSizeFromFooter contents =+-- | read from the end of the file, parse magicString and return the entire file ByteString+readMetadataFromHandle :: FileBufferedOrSeekable -> IO FileMetadata+readMetadataFromHandle sh = do+    footerBs <- readLastBytes (fromIntegral footerSize) sh+    let (size, magicString) = readMetadataSizeFromFooterSlice footerBs+    when (magicString /= "PAR1") $ error "Invalid Parquet file"+    readMetadataByHandleMetaSize sh size++-- | Takes the last 8 bit of the file to parse metadata size and magic string+readMetadataSizeFromFooterSlice :: BSO.ByteString -> (Int, BSO.ByteString)+readMetadataSizeFromFooterSlice contents =     let-        footerOffSet = BSO.length contents - 8-        sizeBytes =-            map-                (fromIntegral @Word8 @Int32 . BSO.index contents)-                [footerOffSet .. footerOffSet + 3]-        size = fromIntegral $ L.foldl' (.|.) 0 $ zipWith shift sizeBytes [0, 8, 16, 24]-        magicStringBytes = map (BSO.index contents) [footerOffSet + 4 .. footerOffSet + 7]-        magicString = BSO.pack magicStringBytes+        size = fromIntegral (littleEndianWord32 contents)+        magicString = BSO.take 4 (BSO.drop 4 contents)      in         (size, magicString) +readMetadataSizeFromFooter :: BSO.ByteString -> (Int, BSO.ByteString)+readMetadataSizeFromFooter = readMetadataSizeFromFooterSlice . BSO.takeEnd 8+ -- Schema navigation -------------------------------------------------------  getColumnPaths :: [SchemaElement] -> [(T.Text, Int)]@@ -582,3 +624,155 @@ applyScale :: Int32 -> Int32 -> Double applyScale scale rawValue =     fromIntegral rawValue / (10 ^ scale)++-- HuggingFace support -----------------------------------------------------++data HFRef = HFRef+    { hfOwner :: T.Text+    , hfDataset :: T.Text+    , hfGlob :: T.Text+    }++data HFParquetFile = HFParquetFile+    { hfpUrl :: T.Text+    , hfpConfig :: T.Text+    , hfpSplit :: T.Text+    , hfpFilename :: T.Text+    }+    deriving (Show)++instance FromJSON HFParquetFile where+    parseJSON = withObject "HFParquetFile" $ \o ->+        HFParquetFile+            <$> o .: "url"+            <*> o .: "config"+            <*> o .: "split"+            <*> o .: "filename"++newtype HFParquetResponse = HFParquetResponse {hfParquetFiles :: [HFParquetFile]}++instance FromJSON HFParquetResponse where+    parseJSON = withObject "HFParquetResponse" $ \o ->+        HFParquetResponse <$> o .: "parquet_files"++isHFUri :: FilePath -> Bool+isHFUri = L.isPrefixOf "hf://"++parseHFUri :: FilePath -> Either String HFRef+parseHFUri path =+    let stripped = drop (length ("hf://datasets/" :: String)) path+     in case T.splitOn "/" (T.pack stripped) of+            (owner : dataset : rest)+                | not (null rest) ->+                    Right $ HFRef owner dataset (T.intercalate "/" rest)+            _ ->+                Left $ "Invalid hf:// URI (expected hf://datasets/owner/dataset/glob): " ++ path++getHFToken :: IO (Maybe BSO.ByteString)+getHFToken = do+    envToken <- lookupEnv "HF_TOKEN"+    case envToken of+        Just t -> pure (Just (encodeUtf8 (T.pack t)))+        Nothing -> do+            home <- getHomeDirectory+            let tokenPath = home </> ".cache" </> "huggingface" </> "token"+            result <- try (BSO.readFile tokenPath) :: IO (Either IOError BSO.ByteString)+            case result of+                Right bs -> pure (Just (BSO.takeWhile (/= 10) bs))+                Left _ -> pure Nothing++{- | Extract the repo-relative path from a HuggingFace download URL.+URL format: https://huggingface.co/datasets/{owner}/{dataset}/resolve/{ref}/{path}+Returns the {path} portion (e.g. "data/train-00000-of-00001.parquet").+-}+hfUrlRepoPath :: HFParquetFile -> String+hfUrlRepoPath f =+    case T.breakOn "/resolve/" (hfpUrl f) of+        (_, rest)+            | not (T.null rest) ->+                -- Drop "/resolve/", then drop the ref component (up to and including "/")+                T.unpack $ T.drop 1 $ T.dropWhile (/= '/') $ T.drop (T.length "/resolve/") rest+        _ ->+            T.unpack (hfpConfig f) </> T.unpack (hfpSplit f) </> T.unpack (hfpFilename f)++matchesGlob :: T.Text -> HFParquetFile -> Bool+matchesGlob g f = match (compile (T.unpack g)) (hfUrlRepoPath f)++resolveHFUrls :: Maybe BSO.ByteString -> HFRef -> IO [HFParquetFile]+resolveHFUrls mToken ref = do+    let dataset = hfOwner ref <> "/" <> hfDataset ref+    let apiUrl = "https://datasets-server.huggingface.co/parquet?dataset=" ++ T.unpack dataset+    req0 <- parseRequest apiUrl+    let req = case mToken of+            Nothing -> req0+            Just tok -> setRequestHeader "Authorization" ["Bearer " <> tok] req0+    resp <- httpBS req+    let status = getResponseStatusCode resp+    when (status /= 200) $+        ioError $+            userError $+                "HuggingFace API returned status "+                    ++ show status+                    ++ " for dataset "+                    ++ T.unpack dataset+    case eitherDecodeStrict (getResponseBody resp) of+        Left err -> ioError $ userError $ "Failed to parse HF API response: " ++ err+        Right hfResp -> pure $ filter (matchesGlob (hfGlob ref)) (hfParquetFiles hfResp)++downloadHFFiles :: Maybe BSO.ByteString -> [HFParquetFile] -> IO [FilePath]+downloadHFFiles mToken files = do+    tmpDir <- getTemporaryDirectory+    forM files $ \f -> do+        -- Derive a collision-resistant temp name from the URL path components+        let fname = case (hfpConfig f, hfpSplit f) of+                (c, s) | T.null c && T.null s -> T.unpack (hfpFilename f)+                (c, s) -> T.unpack c <> "_" <> T.unpack s <> "_" <> T.unpack (hfpFilename f)+        let destPath = tmpDir </> fname+        req0 <- parseRequest (T.unpack (hfpUrl f))+        let req = case mToken of+                Nothing -> req0+                Just tok -> setRequestHeader "Authorization" ["Bearer " <> tok] req0+        resp <- httpBS req+        let status = getResponseStatusCode resp+        when (status /= 200) $+            ioError $+                userError $+                    "Failed to download " ++ T.unpack (hfpUrl f) ++ " (HTTP " ++ show status ++ ")"+        BSO.writeFile destPath (getResponseBody resp)+        pure destPath++-- | True when the path contains glob wildcard characters.+hasGlob :: T.Text -> Bool+hasGlob = T.any (\c -> c == '*' || c == '?' || c == '[')++{- | Build the direct HF repo download URL for a path with no wildcards.+Format: https://huggingface.co/datasets/{owner}/{dataset}/resolve/main/{path}+-}+directHFUrl :: HFRef -> T.Text+directHFUrl ref =+    "https://huggingface.co/datasets/"+        <> hfOwner ref+        <> "/"+        <> hfDataset ref+        <> "/resolve/main/"+        <> hfGlob ref++fetchHFParquetFiles :: FilePath -> IO [FilePath]+fetchHFParquetFiles uri = do+    ref <- case parseHFUri uri of+        Left err -> ioError (userError err)+        Right r -> pure r+    mToken <- getHFToken+    if hasGlob (hfGlob ref)+        then do+            hfFiles <- resolveHFUrls mToken ref+            when (null hfFiles) $+                ioError $+                    userError $+                        "No parquet files found for " ++ uri+            downloadHFFiles mToken hfFiles+        else do+            -- Direct repo file download — no datasets-server needed+            let url = directHFUrl ref+            let filename = last $ T.splitOn "/" (hfGlob ref)+            downloadHFFiles mToken [HFParquetFile url "" "" filename]
src/DataFrame/IO/Parquet/Binary.hs view
@@ -15,44 +15,6 @@ import qualified Foreign.Ptr as Foreign import qualified Foreign.Storable as Foreign -littleEndianWord32 :: BS.ByteString -> Word32-littleEndianWord32 bytes-    | BS.length bytes >= 4 =-        foldr-            (.|.)-            0-            ( zipWith-                (\b i -> fromIntegral b `shiftL` i)-                (BS.unpack $ BS.take 4 bytes)-                [0, 8, 16, 24]-            )-    | otherwise =-        littleEndianWord32 (BS.take 4 $ bytes `BS.append` BS.pack [0, 0, 0, 0])--littleEndianWord64 :: BS.ByteString -> Word64-littleEndianWord64 bytes =-    foldr-        (.|.)-        0-        ( zipWith-            (\b i -> fromIntegral b `shiftL` i)-            (BS.unpack $ BS.take 8 bytes)-            [0, 8 ..]-        )--littleEndianInt32 :: BS.ByteString -> Int32-littleEndianInt32 = fromIntegral . littleEndianWord32--word64ToLittleEndian :: Word64 -> BS.ByteString-word64ToLittleEndian w =-    BS.map-        (\i -> fromIntegral (w `shiftR` fromIntegral i))-        (BS.pack [0, 8, 16, 24, 32, 40, 48, 56])--word32ToLittleEndian :: Word32 -> BS.ByteString-word32ToLittleEndian w =-    BS.map (\i -> fromIntegral (w `shiftR` fromIntegral i)) (BS.pack [0, 8, 16, 24])- readUVarInt :: BS.ByteString -> (Word64, BS.ByteString) readUVarInt xs = loop xs 0 0 0   where@@ -100,7 +62,7 @@ readAndAdvance bufferPos buffer = do     pos <- readIORef bufferPos     let b = BS.index buffer pos-    modifyIORef bufferPos (+ 1)+    modifyIORef' bufferPos (+ 1)     return b  readVarIntFromBuffer :: (Integral a) => BS.ByteString -> IORef Int -> IO a
src/DataFrame/IO/Parquet/Dictionary.hs view
@@ -17,11 +17,15 @@ import qualified Data.Vector as V import qualified Data.Vector.Mutable as VM import qualified Data.Vector.Unboxed as VU-import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Encoding import DataFrame.IO.Parquet.Levels import DataFrame.IO.Parquet.Time import DataFrame.IO.Parquet.Types+import DataFrame.Internal.Binary (+    littleEndianInt32,+    littleEndianWord32,+    littleEndianWord64,+ ) import qualified DataFrame.Internal.Column as DI import GHC.Float 
src/DataFrame/IO/Parquet/Encoding.hs view
@@ -1,14 +1,18 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}  module DataFrame.IO.Parquet.Encoding where  import Data.Bits import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BSU+#if !MIN_VERSION_base(4,20,0) import Data.List (foldl')+#endif import qualified Data.Vector.Unboxed as VU import Data.Word-import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Binary (readUVarInt)+import DataFrame.Internal.Binary (littleEndianWord32)  ceilLog2 :: Int -> Int ceilLog2 x
src/DataFrame/IO/Parquet/Levels.hs view
@@ -5,10 +5,10 @@ import Data.List import qualified Data.Text as T -import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Encoding import DataFrame.IO.Parquet.Thrift import DataFrame.IO.Parquet.Types+import DataFrame.Internal.Binary (littleEndianWord32)  readLevelsV1 ::     Int -> Int -> Int -> BS.ByteString -> ([Int], [Int], BS.ByteString)
src/DataFrame/IO/Parquet/Page.hs view
@@ -14,6 +14,11 @@ import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Thrift import DataFrame.IO.Parquet.Types+import DataFrame.Internal.Binary (+    littleEndianInt32,+    littleEndianWord32,+    littleEndianWord64,+ ) import GHC.Float import qualified Snappy 
+ src/DataFrame/IO/Parquet/Seeking.hs view
@@ -0,0 +1,145 @@+{- | This module contains low-level utilities around file seeking++potentially also contains all Streamly related low-level utilities.++later this module can be renamed / moved to an internal module.+-}+module DataFrame.IO.Parquet.Seeking (+    SeekableHandle (getSeekableHandle),+    SeekMode (..),+    FileBufferedOrSeekable (..),+    ForceNonSeekable,+    advanceBytes,+    mkFileBufferedOrSeekable,+    mkSeekableHandle,+    readLastBytes,+    seekAndReadBytes,+    seekAndStreamBytes,+    withFileBufferedOrSeekable,+) where++import Control.Monad+import Control.Monad.IO.Class+import qualified Data.ByteString as BS+import Data.IORef+import Data.Int+import Data.Word+import Streamly.Data.Stream (Stream)+import qualified Streamly.Data.Stream as S+import qualified Streamly.External.ByteString as SBS+import qualified Streamly.FileSystem.Handle as SHandle+import System.IO++{- | This handle carries a proof that it must be seekable.+Note: Handle and SeekableHandle are not thread safe, should not be+shared across threads, beaware when running parallel/concurrent code.++Not seekable:+  - stdin / stdout+  - pipes / FIFOs++But regular files are always seekable. Parquet fundamentally wants random+access, a non-seekable source will not support effecient access without+buffering the entire file.+-}+newtype SeekableHandle = SeekableHandle {getSeekableHandle :: Handle}++{- | If we truely want to support non-seekable files, we need to also consider the case+to buffer the entire file in memory.++Not thread safe, contains mutable reference (as Handle already is).++If we need concurrent / parallel parsing or something, we need to read into ByteString+first, not sharing the same handle.+-}+data FileBufferedOrSeekable+    = FileBuffered !(IORef Int64) !BS.ByteString+    | FileSeekable !SeekableHandle++-- | Smart constructor for SeekableHandle+mkSeekableHandle :: Handle -> IO (Maybe SeekableHandle)+mkSeekableHandle h = do+    seekable <- hIsSeekable h+    pure $ if seekable then Just (SeekableHandle h) else Nothing++-- | For testing only+type ForceNonSeekable = Maybe Bool++{- | Smart constructor for FileBufferedOrSeekable, tries to keep in the seekable case+if possible.+-}+mkFileBufferedOrSeekable ::+    ForceNonSeekable -> Handle -> IO FileBufferedOrSeekable+mkFileBufferedOrSeekable forceNonSeek h = do+    seekable <- hIsSeekable h+    if not seekable || forceNonSeek == Just True+        then FileBuffered <$> newIORef 0 <*> BS.hGetContents h+        else pure $ FileSeekable $ SeekableHandle h++{- | With / bracket pattern for FileBufferedOrSeekable++Warning: do not return the FileBufferedOrSeekable outside the scope of the action as+it will be closed.+-}+withFileBufferedOrSeekable ::+    ForceNonSeekable ->+    FilePath ->+    IOMode ->+    (FileBufferedOrSeekable -> IO a) ->+    IO a+withFileBufferedOrSeekable forceNonSeek path ioMode action = withFile path ioMode $ \h -> do+    fbos <- mkFileBufferedOrSeekable forceNonSeek h+    action fbos++-- | Read from the end, useful for reading metadata without loading entire file+readLastBytes :: Integer -> FileBufferedOrSeekable -> IO BS.ByteString+readLastBytes n (FileSeekable sh) = do+    let h = getSeekableHandle sh+    hSeek h SeekFromEnd (negate n)+    S.fold SBS.write (SHandle.read h)+readLastBytes n (FileBuffered i bs) = do+    writeIORef i (fromIntegral $ BS.length bs)+    when (n > fromIntegral (BS.length bs)) $ error "lastBytes: n > length bs"+    pure $ BS.drop (BS.length bs - fromIntegral n) bs++-- | Note: this does not guarantee n bytes (if it ends early)+advanceBytes :: Int -> FileBufferedOrSeekable -> IO BS.ByteString+advanceBytes = seekAndReadBytes Nothing++-- | Note: this does not guarantee n bytes (if it ends early)+seekAndReadBytes ::+    Maybe (SeekMode, Integer) -> Int -> FileBufferedOrSeekable -> IO BS.ByteString+seekAndReadBytes mSeek len f = seekAndStreamBytes mSeek len f >>= S.fold SBS.write++{- | Warning: the stream produced from this function accesses to the mutable handler.+if multiple streams are pulled from the same handler at the same time, chaos happen.+Make sure there is only one stream running at one time for each SeekableHandle,+and streams are not read again when they are not used anymore.+-}+seekAndStreamBytes ::+    (MonadIO m) =>+    Maybe (SeekMode, Integer) -> Int -> FileBufferedOrSeekable -> m (Stream m Word8)+seekAndStreamBytes mSeek len f = do+    liftIO $+        case mSeek of+            Nothing -> pure ()+            Just (seekMode, seekTo) -> fSeek f seekMode seekTo+    pure $ S.take len $ fRead f++fSeek :: FileBufferedOrSeekable -> SeekMode -> Integer -> IO ()+fSeek (FileSeekable (SeekableHandle h)) seekMode seekTo = hSeek h seekMode seekTo+fSeek (FileBuffered i bs) AbsoluteSeek seekTo = writeIORef i (fromIntegral seekTo)+fSeek (FileBuffered i bs) RelativeSeek seekTo = modifyIORef' i (+ fromIntegral seekTo)+fSeek (FileBuffered i bs) SeekFromEnd seekTo = writeIORef i (fromIntegral $ BS.length bs + fromIntegral seekTo)++fRead :: (MonadIO m) => FileBufferedOrSeekable -> Stream m Word8+fRead (FileSeekable (SeekableHandle h)) = SHandle.read h+fRead (FileBuffered i bs) = S.concatEffect $ do+    pos <- liftIO $ readIORef i+    pure $+        S.mapM+            ( \x -> do+                liftIO (modifyIORef' i (+ 1))+                pure x+            )+            (S.unfold SBS.reader (BS.drop (fromIntegral pos) bs))
src/DataFrame/IO/Parquet/Thrift.hs view
@@ -21,6 +21,7 @@ import qualified Data.Vector.Unboxed as VU import Data.Word import DataFrame.IO.Parquet.Binary+import DataFrame.IO.Parquet.Seeking import DataFrame.IO.Parquet.Types import qualified DataFrame.Internal.Column as DI import DataFrame.Internal.DataFrame (DataFrame, unsafeGetColumn)@@ -329,6 +330,17 @@     let elemType = toTType sizeAndType     replicateM_ sizeOnly (skipFieldData elemType buf pos) +{- | This avoids reading entire bytestring at once: it uses the seekable handle+     seeks it to the end of the file to read the metadata+-}+readMetadataByHandleMetaSize :: FileBufferedOrSeekable -> Int -> IO FileMetadata+readMetadataByHandleMetaSize sh metaSize = do+    let lastFieldId = 0+    bs <- readLastBytes (fromIntegral $ metaSize + footerSize) sh+    bufferPos <- newIORef 0+    readFileMetaData defaultMetadata bs bufferPos lastFieldId++-- | metadata starts from (L - 8 - meta_size) to L - 8 - 1. readMetadata :: BS.ByteString -> Int -> IO FileMetadata readMetadata contents size = do     let metadataStartPos = BS.length contents - footerSize - size
src/DataFrame/IO/Parquet/Time.hs view
@@ -6,7 +6,12 @@ import Data.Time import Data.Word -import DataFrame.IO.Parquet.Binary+import DataFrame.Internal.Binary (+    littleEndianWord32,+    littleEndianWord64,+    word32ToLittleEndian,+    word64ToLittleEndian,+ )  int96ToUTCTime :: BS.ByteString -> UTCTime int96ToUTCTime bytes
src/DataFrame/IO/Unstable/CSV.hs view
@@ -53,7 +53,7 @@     typeInferenceSampleSize,  ) import DataFrame.Internal.DataFrame (DataFrame (..))-import DataFrame.Operations.Typing (parseFromExamples)+import DataFrame.Operations.Typing (ParseOptions (..), parseFromExamples)  readSeparatedDefaultFast :: Word8 -> FilePath -> IO DataFrame readSeparatedDefaultFast separator =@@ -124,12 +124,14 @@                     if shouldInferFromSample (typeSpec opts)                         then typeInferenceSampleSize (typeSpec opts)                         else 0-             in parseFromExamples-                    (missingIndicators opts)-                    n-                    (safeRead opts)-                    (dateFormat opts)-                    col+                parseOpts =+                    ParseOptions+                        { missingValues = missingIndicators opts+                        , sampleSize = n+                        , parseSafe = safeRead opts+                        , parseDateFormat = dateFormat opts+                        }+             in parseFromExamples parseOpts col         generateColumn col =             parseTypes $                 Vector.fromListN
+ src/DataFrame/Internal/Binary.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE BangPatterns #-}++module DataFrame.Internal.Binary where++import Data.Bits (Bits (unsafeShiftL, (.|.)))+import Data.ByteString (toStrict)+import qualified Data.ByteString as BS+import Data.ByteString.Builder (toLazyByteString, word32LE, word64LE)+import qualified Data.ByteString.Unsafe as BS+import Data.Int (Int32)+import Data.Word (Word32, Word64, Word8)++littleEndianWord32 :: BS.ByteString -> Word32+littleEndianWord32 bytes+    | len >= 4 =+        assembleWord32+            (BS.unsafeIndex bytes 0)+            (BS.unsafeIndex bytes 1)+            (BS.unsafeIndex bytes 2)+            (BS.unsafeIndex bytes 3)+    | otherwise =+        assembleWord32+            (byteAtOrZero len bytes 0)+            (byteAtOrZero len bytes 1)+            (byteAtOrZero len bytes 2)+            (byteAtOrZero len bytes 3)+  where+    len = BS.length bytes+{-# INLINE littleEndianWord32 #-}++littleEndianWord64 :: BS.ByteString -> Word64+littleEndianWord64 bytes+    | len >= 8 =+        assembleWord64+            (BS.index bytes 0)+            (BS.index bytes 1)+            (BS.index bytes 2)+            (BS.index bytes 3)+            (BS.index bytes 4)+            (BS.index bytes 5)+            (BS.index bytes 6)+            (BS.index bytes 7)+    | otherwise =+        assembleWord64+            (byteAtOrZero len bytes 0)+            (byteAtOrZero len bytes 1)+            (byteAtOrZero len bytes 2)+            (byteAtOrZero len bytes 3)+            (byteAtOrZero len bytes 4)+            (byteAtOrZero len bytes 5)+            (byteAtOrZero len bytes 6)+            (byteAtOrZero len bytes 7)+  where+    len = BS.length bytes+{-# INLINE littleEndianWord64 #-}++littleEndianInt32 :: BS.ByteString -> Int32+littleEndianInt32 = fromIntegral . littleEndianWord32+{-# INLINE littleEndianInt32 #-}++word64ToLittleEndian :: Word64 -> BS.ByteString+word64ToLittleEndian = toStrict . toLazyByteString . word64LE+{-# INLINE word64ToLittleEndian #-}++word32ToLittleEndian :: Word32 -> BS.ByteString+word32ToLittleEndian = toStrict . toLazyByteString . word32LE+{-# INLINE word32ToLittleEndian #-}++byteAtOrZero :: Int -> BS.ByteString -> Int -> Word8+byteAtOrZero len bytes i+    | i >= 0 && i < len = BS.unsafeIndex bytes i+    | otherwise = 0+{-# INLINE byteAtOrZero #-}++assembleWord32 :: Word8 -> Word8 -> Word8 -> Word8 -> Word32+assembleWord32 !b0 !b1 !b2 !b3 =+    fromIntegral b0+        .|. (fromIntegral b1 `unsafeShiftL` 8)+        .|. (fromIntegral b2 `unsafeShiftL` 16)+        .|. (fromIntegral b3 `unsafeShiftL` 24)+{-# INLINE assembleWord32 #-}++assembleWord64 ::+    Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word64+assembleWord64 !b0 !b1 !b2 !b3 !b4 !b5 !b6 !b7 =+    fromIntegral b0+        .|. (fromIntegral b1 `unsafeShiftL` 8)+        .|. (fromIntegral b2 `unsafeShiftL` 16)+        .|. (fromIntegral b3 `unsafeShiftL` 24)+        .|. (fromIntegral b4 `unsafeShiftL` 32)+        .|. (fromIntegral b5 `unsafeShiftL` 40)+        .|. (fromIntegral b6 `unsafeShiftL` 48)+        .|. (fromIntegral b7 `unsafeShiftL` 56)+{-# INLINE assembleWord64 #-}
src/DataFrame/Internal/Column.hs view
@@ -18,7 +18,6 @@  module DataFrame.Internal.Column where -import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Vector as VB import qualified Data.Vector.Algorithms.Merge as VA@@ -50,6 +49,10 @@     UnboxedColumn :: (Columnable a, VU.Unbox a) => VU.Vector a -> Column     OptionalColumn :: (Columnable a) => VB.Vector (Maybe a) -> Column +{- | A mutable companion struct to dataframe columns.++Used mostly as an intermediate structure for I/O.+-} data MutableColumn where     MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn     MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn@@ -57,6 +60,9 @@  {- | A TypedColumn is a wrapper around our type-erased column. It is used to type check expressions on columns.++Note: there is no guarantee that the Phanton type is the+same as the underlying vector type. -} data TypedColumn a where     TColumn :: (Columnable a) => Column -> TypedColumn a@@ -99,15 +105,13 @@  -- | Checks if a column is of a given type values. hasElemType :: forall a. (Columnable a) => Column -> Bool-hasElemType (BoxedColumn (column :: VB.Vector b)) = fromMaybe False $ do-    Refl <- testEquality (typeRep @a) (typeRep @b)-    pure True-hasElemType (UnboxedColumn (column :: VU.Vector b)) = fromMaybe False $ do-    Refl <- testEquality (typeRep @a) (typeRep @b)-    pure True-hasElemType (OptionalColumn (column :: VB.Vector b)) = fromMaybe False $ do-    Refl <- testEquality (typeRep @a) (typeRep @b)-    pure True+hasElemType = \case+    BoxedColumn (column :: VB.Vector b) -> check (typeRep @b)+    UnboxedColumn (column :: VU.Vector b) -> check (typeRep @b)+    OptionalColumn (column :: VB.Vector b) -> check (typeRep @b)+  where+    check :: forall (b :: Type). TypeRep b -> Bool+    check = isJust . testEquality (typeRep @a)  -- | An internal/debugging function to get the column type of a column. columnVersionString :: Column -> String@@ -254,6 +258,7 @@     [a] -> Column fromList = toColumnRep @(KindOf a) . VB.fromList +-- An internal helper for type errors throwTypeMismatch ::     forall (a :: Type) (b :: Type).     (Typeable a, Typeable b) => Either DataFrameException Column@@ -263,7 +268,7 @@             MkTypeErrorContext                 { userType = Right (typeRep @b)                 , expectedType = Right (typeRep @a)-                , callingFunctionName = Just "mapColumn"+                , callingFunctionName = Nothing                 , errorColumnName = Nothing                 } @@ -274,7 +279,7 @@     (b -> c) -> Column -> Either DataFrameException Column mapColumn f = \case     BoxedColumn (col :: VB.Vector a) -> run col-    OptionalColumn (col :: VB.Vector a) -> run col+    OptionalColumn (col :: VB.Vector (Maybe a)) -> runOpt col     UnboxedColumn (col :: VU.Vector a) -> runUnboxed col   where     run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column@@ -282,6 +287,17 @@         Just Refl -> Right (fromVector @c (VB.map f col))         Nothing -> throwTypeMismatch @a @b +    -- For OptionalColumn: first try exact match (b = Maybe a, user maps over Maybe+    -- values directly), then try lenient fmap fallback (b = a, auto-fmap over inner).+    runOpt ::+        forall a.+        (Columnable a) => VB.Vector (Maybe a) -> Either DataFrameException Column+    runOpt col = case testEquality (typeRep @(Maybe a)) (typeRep @b) of+        Just Refl -> Right (fromVector @c (VB.map f col))+        Nothing -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> Right (OptionalColumn (VB.map (fmap f) col))+            Nothing -> throwTypeMismatch @(Maybe a) @b+     runUnboxed ::         forall a.         (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column@@ -299,7 +315,7 @@     (Int -> b -> c) -> Column -> Either DataFrameException Column imapColumn f = \case     BoxedColumn (col :: VB.Vector a) -> run col-    OptionalColumn (col :: VB.Vector a) -> run col+    OptionalColumn (col :: VB.Vector (Maybe a)) -> runOpt col     UnboxedColumn (col :: VU.Vector a) -> runUnboxed col   where     run :: forall a. (Typeable a) => VB.Vector a -> Either DataFrameException Column@@ -307,6 +323,15 @@         Just Refl -> Right (fromVector @c (VB.imap f col))         Nothing -> throwTypeMismatch @a @b +    runOpt ::+        forall a.+        (Columnable a) => VB.Vector (Maybe a) -> Either DataFrameException Column+    runOpt col = case testEquality (typeRep @(Maybe a)) (typeRep @b) of+        Just Refl -> Right (fromVector @c (VB.imap f col))+        Nothing -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> Right (OptionalColumn (VB.imap (fmap . f) col))+            Nothing -> throwTypeMismatch @(Maybe a) @b+     runUnboxed ::         forall a.         (Typeable a, VU.Unbox a) => VU.Vector a -> Either DataFrameException Column@@ -318,16 +343,16 @@  -- | O(1) Gets the number of elements in the column. columnLength :: Column -> Int-columnLength (BoxedColumn xs) = VG.length xs-columnLength (UnboxedColumn xs) = VG.length xs-columnLength (OptionalColumn xs) = VG.length xs+columnLength (BoxedColumn xs) = VB.length xs+columnLength (UnboxedColumn xs) = VU.length xs+columnLength (OptionalColumn xs) = VB.length xs {-# INLINE columnLength #-}  -- | O(n) Gets the number of elements in the column. numElements :: Column -> Int-numElements (BoxedColumn xs) = VG.length xs-numElements (UnboxedColumn xs) = VG.length xs-numElements (OptionalColumn xs) = VG.foldl' (\acc x -> acc + fromEnum (isJust x)) 0 xs+numElements (BoxedColumn xs) = VB.length xs+numElements (UnboxedColumn xs) = VU.length xs+numElements (OptionalColumn xs) = VB.foldl' (\acc x -> acc + fromEnum (isJust x)) 0 xs {-# INLINE numElements #-}  -- | O(n) Takes the first n values of a column.@@ -349,13 +374,6 @@ sliceColumn start n (OptionalColumn xs) = OptionalColumn $ VG.slice start n xs {-# INLINE sliceColumn #-} --- | O(n) Selects the elements at a given set of indices. May change the order.-atIndices :: S.Set Int -> Column -> Column-atIndices indexes (BoxedColumn column) = BoxedColumn $ VG.ifilter (\i _ -> i `S.member` indexes) column-atIndices indexes (OptionalColumn column) = OptionalColumn $ VG.ifilter (\i _ -> i `S.member` indexes) column-atIndices indexes (UnboxedColumn column) = UnboxedColumn $ VU.ifilter (\i _ -> i `S.member` indexes) column-{-# INLINE atIndices #-}- -- | O(n) Selects the elements at a given set of indices. Does not change the order. atIndicesStable :: VU.Vector Int -> Column -> Column atIndicesStable indexes (BoxedColumn column) =@@ -396,16 +414,6 @@                          in if idx < 0 then Nothing else v `VB.unsafeIndex` idx {-# INLINE gatherWithSentinel #-} -atIndicesWithNulls :: VB.Vector (Maybe Int) -> Column -> Column-atIndicesWithNulls indices column =-    case column of-        BoxedColumn col ->-            OptionalColumn $ VB.map (fmap (col VB.!)) indices-        UnboxedColumn col ->-            OptionalColumn $ VB.map (fmap (col VU.!)) indices-        OptionalColumn col ->-            OptionalColumn $ VB.map (\ix -> ix >>= (col VB.!)) indices- -- | Internal helper to get indices in a boxed vector. getIndices :: VU.Vector Int -> VB.Vector a -> VB.Vector a getIndices indices xs = VB.generate (VU.length indices) (\i -> xs VB.! (indices VU.! i))@@ -448,19 +456,19 @@ -- | An internal function that returns a vector of how indexes change after a column is sorted. sortedIndexes :: Bool -> Column -> VU.Vector Int sortedIndexes asc = \case-    BoxedColumn column -> sortWorker column-    UnboxedColumn column -> sortWorker column-    OptionalColumn column -> sortWorker column+    BoxedColumn column -> sortWorker VG.convert column+    UnboxedColumn column -> sortWorker id column+    OptionalColumn column -> sortWorker VG.convert column   where     sortWorker ::         (VG.Vector v a, Ord a, VG.Vector v (Int, a), VG.Vector v Int) =>-        v a -> VU.Vector Int-    sortWorker column = runST $ do+        (v Int -> VU.Vector Int) -> v a -> VU.Vector Int+    sortWorker finalize column = runST $ do         withIndexes <- VG.thaw $ VG.indexed column         let cmp = if asc then compare else flip compare         VA.sortBy (\(_, b) (_, b') -> cmp b b') withIndexes         sorted <- VG.unsafeFreeze withIndexes-        return $ VG.convert $ VG.map fst sorted+        return $ finalize $ VG.map fst sorted {-# INLINE sortedIndexes #-}  -- | Fold (right) column with index.@@ -468,262 +476,83 @@     forall a b.     (Columnable a, Columnable b) =>     (Int -> a -> b -> b) -> b -> Column -> Either DataFrameException b-ifoldrColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldr f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldrColumn"-                    , errorColumnName = Nothing-                    }-                )-ifoldrColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldr f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldrColumn"-                    , errorColumnName = Nothing-                    }-                )-ifoldrColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.ifoldr f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "ifoldrColumn"-                    , errorColumnName = Nothing-                    }-                )--foldlColumn ::-    forall a b.-    (Columnable a, Columnable b) =>-    (b -> a -> b) -> b -> Column -> Either DataFrameException b-foldlColumn f acc c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldlColumn"-                    , errorColumnName = Nothing-                    }-                )-foldlColumn f acc c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldlColumn"-                    , errorColumnName = Nothing-                    }-                )-foldlColumn f acc c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl' f acc column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldlColumn"-                    , errorColumnName = Nothing-                    }-                )--foldlColumnWith ::-    forall a b.-    (Columnable a) =>-    (b -> a -> b) -> b -> Column -> Either DataFrameException b-foldlColumnWith f acc (BoxedColumn (column :: VB.Vector d)) =-    case testEquality (typeRep @a) (typeRep @d) of-        Just Refl -> pure $ VG.foldl' f acc column+ifoldrColumn f acc = \case+    BoxedColumn column -> foldrWorker column+    UnboxedColumn column -> foldrWorker column+    OptionalColumn column -> foldrWorker column+  where+    foldrWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException b+    foldrWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.ifoldr f acc vec         Nothing ->             Left $                 TypeMismatchException                     ( MkTypeErrorContext                         { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @d)-                        , callingFunctionName = Just "foldlColumnWith"+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "ifoldrColumn"                         , errorColumnName = Nothing                         }                     )-foldlColumnWith f acc (OptionalColumn (column :: VB.Vector d)) =-    case testEquality (typeRep @a) (typeRep @d) of-        Just Refl -> pure $ VG.foldl' f acc column++foldlColumn ::+    forall a b.+    (Columnable a, Columnable b) =>+    (b -> a -> b) -> b -> Column -> Either DataFrameException b+foldlColumn f acc = \case+    BoxedColumn column -> foldlWorker column+    UnboxedColumn column -> foldlWorker column+    OptionalColumn column -> foldlWorker column+  where+    foldlWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException b+    foldlWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.foldl' f acc vec         Nothing ->             Left $                 TypeMismatchException                     ( MkTypeErrorContext                         { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @d)-                        , callingFunctionName = Just "foldlColumnWith"+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "ifoldrColumn"                         , errorColumnName = Nothing                         }                     )-foldlColumnWith f acc (UnboxedColumn (column :: VU.Vector d)) =-    case testEquality (typeRep @a) (typeRep @d) of-        Just Refl -> pure $ VG.foldl' f acc column++foldl1Column ::+    forall a.+    (Columnable a) =>+    (a -> a -> a) -> Column -> Either DataFrameException a+foldl1Column f = \case+    BoxedColumn column -> foldl1Worker column+    UnboxedColumn column -> foldl1Worker column+    OptionalColumn column -> foldl1Worker column+  where+    foldl1Worker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException a+    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> pure $ VG.foldl1' f vec         Nothing ->             Left $                 TypeMismatchException                     ( MkTypeErrorContext                         { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @d)-                        , callingFunctionName = Just "foldlColumnWith"+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldl1Column"                         , errorColumnName = Nothing                         }                     ) -foldl1Column ::-    forall a.-    (Columnable a) =>-    (a -> a -> a) -> Column -> Either DataFrameException a-foldl1Column f c@(BoxedColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl1' f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldl1Column"-                    , errorColumnName = Nothing-                    }-                )-foldl1Column f c@(OptionalColumn (column :: VB.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl1' f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldl1Column"-                    , errorColumnName = Nothing-                    }-                )-foldl1Column f c@(UnboxedColumn (column :: VU.Vector d)) = case testEquality (typeRep @a) (typeRep @d) of-    Just Refl -> pure $ VG.foldl1' f column-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @d)-                    , callingFunctionName = Just "foldl1Column"-                    , errorColumnName = Nothing-                    }-                )--{- | O(n) Fold a column over groups without materialising a sorted copy.-Instead of backpermuting the column (expensive: O(n) allocation + random reads),-this iterates @valueIndices@ sequentially and accesses the column at each index.-Avoids one 10M-element allocation per column per aggregation expression.--}-foldDirectGroups ::-    forall b acc.-    (Columnable b) =>-    (acc -> b -> acc) ->-    acc ->-    Column ->-    VU.Vector Int -> -- valueIndices (sorted row order)-    VU.Vector Int -> -- offsets (group boundaries)-    Either DataFrameException (VB.Vector acc)-foldDirectGroups f seed col valueIndices offsets-    | VU.length offsets <= 1 = Right VB.empty-    | otherwise =-        let !nGroups = VU.length offsets - 1-         in case col of-                UnboxedColumn (vec :: VU.Vector d) ->-                    case testEquality (typeRep @b) (typeRep @d) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                 in go s e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VU.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @b)-                                        , expectedType = Right (typeRep @d)-                                        , callingFunctionName = Just "foldDirectGroups"-                                        , errorColumnName = Nothing-                                        }-                BoxedColumn (vec :: VB.Vector d) ->-                    case testEquality (typeRep @b) (typeRep @d) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                 in go s e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @b)-                                        , expectedType = Right (typeRep @d)-                                        , callingFunctionName = Just "foldDirectGroups"-                                        , errorColumnName = Nothing-                                        }-                OptionalColumn (vec :: VB.Vector (Maybe d)) ->-                    case testEquality (typeRep @b) (typeRep @(Maybe d)) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                 in go s e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @b)-                                        , expectedType = Right (typeRep @(Maybe d))-                                        , callingFunctionName = Just "foldDirectGroups"-                                        , errorColumnName = Nothing-                                        }-{-# INLINEABLE foldDirectGroups #-}- {- | O(n) Seedless fold over groups using the first element of each group as seed. Like 'foldDirectGroups' but for the case where no initial accumulator is available. -}@@ -734,87 +563,43 @@     Column ->     VU.Vector Int ->     VU.Vector Int ->-    Either DataFrameException (VB.Vector a)+    Either DataFrameException Column foldl1DirectGroups f col valueIndices offsets-    | VU.length offsets <= 1 = Right VB.empty-    | otherwise =-        let !nGroups = VU.length offsets - 1-         in case col of-                UnboxedColumn (vec :: VU.Vector d) ->-                    case testEquality (typeRep @a) (typeRep @d) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                    !seed = VU.unsafeIndex vec (VU.unsafeIndex valueIndices s)-                                 in go (s + 1) e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VU.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @a)-                                        , expectedType = Right (typeRep @d)-                                        , callingFunctionName = Just "foldl1DirectGroups"-                                        , errorColumnName = Nothing-                                        }-                BoxedColumn (vec :: VB.Vector d) ->-                    case testEquality (typeRep @a) (typeRep @d) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                    !seed = VB.unsafeIndex vec (VU.unsafeIndex valueIndices s)-                                 in go (s + 1) e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @a)-                                        , expectedType = Right (typeRep @d)-                                        , callingFunctionName = Just "foldl1DirectGroups"-                                        , errorColumnName = Nothing-                                        }-                OptionalColumn (vec :: VB.Vector (Maybe d)) ->-                    case testEquality (typeRep @a) (typeRep @(Maybe d)) of-                        Just Refl ->-                            Right $-                                VB.generate nGroups foldGroup-                          where-                            foldGroup k =-                                let !s = VU.unsafeIndex offsets k-                                    !e = VU.unsafeIndex offsets (k + 1)-                                    !seed = VB.unsafeIndex vec (VU.unsafeIndex valueIndices s)-                                 in go (s + 1) e seed-                            go !i !e !acc-                                | i >= e = acc-                                | otherwise =-                                    go (i + 1) e $!-                                        f acc (VB.unsafeIndex vec (VU.unsafeIndex valueIndices i))-                        Nothing ->-                            Left $-                                TypeMismatchException-                                    MkTypeErrorContext-                                        { userType = Right (typeRep @a)-                                        , expectedType = Right (typeRep @(Maybe d))-                                        , callingFunctionName = Just "foldl1DirectGroups"-                                        , errorColumnName = Nothing-                                        }+    | VU.length offsets <= 1 = pure $ fromVector @a VB.empty+    | otherwise = case col of+        UnboxedColumn (vec :: VU.Vector d) -> UnboxedColumn <$> foldl1Worker vec+        BoxedColumn (vec :: VB.Vector d) -> BoxedColumn <$> foldl1Worker vec+        OptionalColumn (vec :: VB.Vector (Maybe d)) -> OptionalColumn <$> foldl1Worker vec+  where+    foldl1Worker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException (v c)+    foldl1Worker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl ->+            Right $+                VG.generate (VU.length offsets - 1) foldGroup+          where+            foldGroup k =+                let !s = VU.unsafeIndex offsets k+                    !e = VU.unsafeIndex offsets (k + 1)+                    !seed = VG.unsafeIndex vec (VU.unsafeIndex valueIndices s)+                 in go (s + 1) e seed+            go !i !e !acc+                | i >= e = acc+                | otherwise =+                    go (i + 1) e $!+                        f acc (VG.unsafeIndex vec (VU.unsafeIndex valueIndices i))+        Nothing ->+            Left $+                TypeMismatchException+                    MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldl1DirectGroups"+                        , errorColumnName = Nothing+                        } {-# INLINEABLE foldl1DirectGroups #-}  {- | O(n) fold over groups by scanning the column LINEARLY.@@ -836,70 +621,36 @@ foldLinearGroups f seed col rowToGroup nGroups     | nGroups == 0 = Right (fromVector @acc VB.empty)     | otherwise = case col of-        UnboxedColumn (vec :: VU.Vector d) ->-            case testEquality (typeRep @b) (typeRep @d) of-                Just Refl ->-                    Right $-                        unsafePerformIO $-                            runWith-                                ( \readAt writeAt ->-                                    VU.iforM_ vec $ \row x -> do-                                        let !k = VU.unsafeIndex rowToGroup row-                                        cur <- readAt k-                                        writeAt k $! f cur x-                                )-                Nothing ->-                    Left $-                        TypeMismatchException-                            MkTypeErrorContext-                                { userType = Right (typeRep @b)-                                , expectedType = Right (typeRep @d)-                                , callingFunctionName = Just "foldLinearGroups"-                                , errorColumnName = Nothing-                                }-        BoxedColumn (vec :: VB.Vector d) ->-            case testEquality (typeRep @b) (typeRep @d) of-                Just Refl ->-                    Right $-                        unsafePerformIO $-                            runWith-                                ( \readAt writeAt ->-                                    VB.iforM_ vec $ \row x -> do-                                        let !k = VU.unsafeIndex rowToGroup row-                                        cur <- readAt k-                                        writeAt k $! f cur x-                                )-                Nothing ->-                    Left $-                        TypeMismatchException-                            MkTypeErrorContext-                                { userType = Right (typeRep @b)-                                , expectedType = Right (typeRep @d)-                                , callingFunctionName = Just "foldLinearGroups"-                                , errorColumnName = Nothing-                                }-        OptionalColumn (vec :: VB.Vector (Maybe d)) ->-            case testEquality (typeRep @b) (typeRep @(Maybe d)) of-                Just Refl ->-                    Right $-                        unsafePerformIO $-                            runWith-                                ( \readAt writeAt ->-                                    VB.iforM_ vec $ \row x -> do-                                        let !k = VU.unsafeIndex rowToGroup row-                                        cur <- readAt k-                                        writeAt k $! f cur x-                                )-                Nothing ->-                    Left $-                        TypeMismatchException-                            MkTypeErrorContext-                                { userType = Right (typeRep @b)-                                , expectedType = Right (typeRep @(Maybe d))-                                , callingFunctionName = Just "foldLinearGroups"-                                , errorColumnName = Nothing-                                }+        UnboxedColumn (vec :: VU.Vector d) -> foldLinearWorker vec+        BoxedColumn (vec :: VB.Vector d) -> foldLinearWorker vec+        OptionalColumn (vec :: VB.Vector (Maybe d)) -> foldLinearWorker vec   where+    foldLinearWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException Column+    foldLinearWorker vec = case testEquality (typeRep @b) (typeRep @c) of+        Just Refl ->+            Right $+                unsafePerformIO $+                    runWith+                        ( \readAt writeAt ->+                            VG.iforM_ vec $ \row x -> do+                                let !k = VG.unsafeIndex rowToGroup row+                                cur <- readAt k+                                writeAt k $! f cur x+                        )+        Nothing ->+            Left $+                TypeMismatchException+                    MkTypeErrorContext+                        { userType = Right (typeRep @b)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "foldLinearGroups"+                        , errorColumnName = Nothing+                        }+     -- \| Allocate accumulators, run the traversal, return a frozen Column.     -- When @acc@ is unboxable, uses an unboxed mutable vector (no pointer     -- indirection per read/write) and returns UnboxedColumn directly —@@ -918,51 +669,31 @@ {-# INLINEABLE foldLinearGroups #-}  headColumn :: forall a. (Columnable a) => Column -> Either DataFrameException a-headColumn (BoxedColumn (col :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl ->-        if VG.null col-            then Left (EmptyDataSetException "headColumn")-            else pure (VG.head col)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "headColumn"-                    , errorColumnName = Nothing-                    }-                )-headColumn (UnboxedColumn (col :: VU.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl ->-        if VG.null col-            then Left (EmptyDataSetException "headColumn")-            else pure (VG.head col)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "headColumn"-                    , errorColumnName = Nothing-                    }-                )-headColumn (OptionalColumn (col :: VB.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-    Just Refl ->-        if VG.null col-            then Left (EmptyDataSetException "headColumn")-            else pure (VG.head col)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeRep @a)-                    , expectedType = Right (typeRep @b)-                    , callingFunctionName = Just "headColumn"-                    , errorColumnName = Nothing-                    }-                )+headColumn = \case+    BoxedColumn col -> headWorker col+    UnboxedColumn col -> headWorker col+    OptionalColumn col -> headWorker col+  where+    headWorker ::+        forall c v.+        (Typeable c, VG.Vector v c) =>+        v c ->+        Either DataFrameException a+    headWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl ->+            if VG.null vec+                then Left (EmptyDataSetException "headColumn")+                else pure (VG.head vec)+        Nothing ->+            Left $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right (typeRep @a)+                        , expectedType = Right (typeRep @c)+                        , callingFunctionName = Just "headColumn"+                        , errorColumnName = Nothing+                        }+                    )  -- | An internal, column version of zip. zipColumns :: Column -> Column -> Column@@ -1033,7 +764,7 @@ zipWithColumns f (UnboxedColumn (column :: VU.Vector d)) (UnboxedColumn (other :: VU.Vector e)) = case testEquality (typeRep @a) (typeRep @d) of     Just Refl -> case testEquality (typeRep @b) (typeRep @e) of         Just Refl -> pure $ case sUnbox @c of-            STrue -> fromUnboxedVector (VU.zipWith f column other)+            STrue -> UnboxedColumn (VU.zipWith f column other)             SFalse -> fromVector $ VB.zipWith f (VG.convert column) (VG.convert other)         Nothing ->             Left $@@ -1055,6 +786,8 @@                     , errorColumnName = Nothing                     }                 )+-- TODO: mchavinda - reuse pattern from interpret where we augment the+-- error at the end. zipWithColumns f left right = case toVector @a left of     Left (TypeMismatchException context) ->         Left $@@ -1172,52 +905,31 @@ Returns Nothing if the columns are of different types. -} concatColumns :: Column -> Column -> Either DataFrameException Column-concatColumns (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of-    Just Refl -> pure (OptionalColumn $ left <> right)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeOf right)-                    , expectedType = Right (typeOf left)-                    , callingFunctionName = Just "concatColumns"-                    , errorColumnName = Nothing-                    }-                )-concatColumns (BoxedColumn left) (BoxedColumn right) = case testEquality (typeOf left) (typeOf right) of-    Just Refl -> pure (BoxedColumn $ left <> right)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeOf right)-                    , expectedType = Right (typeOf left)-                    , callingFunctionName = Just "concatColumns"-                    , errorColumnName = Nothing-                    }-                )-concatColumns (UnboxedColumn left) (UnboxedColumn right) = case testEquality (typeOf left) (typeOf right) of-    Just Refl -> pure (UnboxedColumn $ left <> right)-    Nothing ->-        Left $-            TypeMismatchException-                ( MkTypeErrorContext-                    { userType = Right (typeOf right)-                    , expectedType = Right (typeOf left)-                    , callingFunctionName = Just "concatColumns"-                    , errorColumnName = Nothing-                    }-                )-concatColumns left right =-    Left $-        TypeMismatchException-            ( MkTypeErrorContext-                { userType = Right (typeOf right)-                , expectedType = Right (typeOf left)-                , callingFunctionName = Just "concatColumns"-                , errorColumnName = Nothing-                }-            )+concatColumns left right = case (left, right) of+    (OptionalColumn l, OptionalColumn r) -> case testEquality (typeOf l) (typeOf r) of+        Just Refl -> pure (OptionalColumn $ l <> r)+        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))+    (BoxedColumn l, BoxedColumn r) -> case testEquality (typeOf l) (typeOf r) of+        Just Refl -> pure (BoxedColumn $ l <> r)+        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))+    (UnboxedColumn l, UnboxedColumn r) -> case testEquality (typeOf l) (typeOf r) of+        Just Refl -> pure (UnboxedColumn $ l <> r)+        Nothing -> Left (mismatchErr (typeOf r) (typeOf l))+    _ -> Left (mismatchErr (typeOf right) (typeOf left))+  where+    mismatchErr ::+        forall (x :: Type) (y :: Type). TypeRep x -> TypeRep y -> DataFrameException+    mismatchErr ta tb =+        withTypeable ta $+            withTypeable tb $+                TypeMismatchException+                    ( MkTypeErrorContext+                        { userType = Right ta+                        , expectedType = Right tb+                        , callingFunctionName = Just "concatColumns"+                        , errorColumnName = Nothing+                        }+                    )  {- | Concatenates two columns. @@ -1375,41 +1087,24 @@ toVector ::     forall a v.     (VG.Vector v a, Columnable a) => Column -> Either DataFrameException (v a)-toVector column@(OptionalColumn (col :: VB.Vector b)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ VG.convert col-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @b)-                        , callingFunctionName = Just "toVector"-                        , errorColumnName = Nothing-                        }-                    )-toVector (BoxedColumn (col :: VB.Vector b)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ VG.convert col-        Nothing ->-            Left $-                TypeMismatchException-                    ( MkTypeErrorContext-                        { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @b)-                        , callingFunctionName = Just "toVector"-                        , errorColumnName = Nothing-                        }-                    )-toVector (UnboxedColumn (col :: VU.Vector b)) =-    case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> Right $ VG.convert col+toVector = \case+    OptionalColumn col -> toVectorWorker col+    BoxedColumn col -> toVectorWorker col+    UnboxedColumn col -> toVectorWorker col+  where+    toVectorWorker ::+        forall c w.+        (Typeable c, VG.Vector w c) =>+        w c ->+        Either DataFrameException (v a)+    toVectorWorker vec = case testEquality (typeRep @a) (typeRep @c) of+        Just Refl -> Right $ VG.convert vec         Nothing ->             Left $                 TypeMismatchException                     ( MkTypeErrorContext                         { userType = Right (typeRep @a)-                        , expectedType = Right (typeRep @b)+                        , expectedType = Right (typeRep @c)                         , callingFunctionName = Just "toVector"                         , errorColumnName = Nothing                         }
src/DataFrame/Internal/Expression.hs view
@@ -56,6 +56,18 @@  data Expr a where     Col :: (Columnable a) => T.Text -> Expr a+    CastWith ::+        (Columnable a, Columnable b) =>+        T.Text ->+        T.Text ->+        (Either String a -> b) ->+        Expr b+    CastExprWith ::+        (Columnable a, Columnable b, Columnable src) =>+        T.Text ->+        (Either String a -> b) ->+        Expr src ->+        Expr b     Lit :: (Columnable a) => a -> Expr a     Unary ::         (Columnable a, Columnable b) => UnaryOp b a -> Expr b -> Expr a@@ -228,6 +240,8 @@ instance (Show a) => Show (Expr a) where     show :: forall a. (Show a) => Expr a -> String     show (Col name) = "(col @" ++ show (typeRep @a) ++ " " ++ show name ++ ")"+    show (CastWith name tag _) = "(castWith " ++ show tag ++ " " ++ show name ++ ")"+    show (CastExprWith tag _ inner) = "(castExprWith " ++ show tag ++ " " ++ show inner ++ ")"     show (Lit value) = "(lit (" ++ show value ++ "))"     show (If cond l r) = "(ifThenElse " ++ show cond ++ " " ++ show l ++ " " ++ show r ++ ")"     show (Unary op value) = "(" ++ T.unpack (unaryName op) ++ " " ++ show value ++ ")"@@ -239,6 +253,8 @@ normalize :: (Eq a, Ord a, Show a, Typeable a) => Expr a -> Expr a normalize expr = case expr of     Col name -> Col name+    CastWith n t f -> CastWith n t f+    CastExprWith t f e -> CastExprWith t f (normalize e)     Lit val -> Lit val     If cond th el -> If (normalize cond) (normalize th) (normalize el)     Unary op e -> Unary op (normalize e)@@ -261,6 +277,8 @@   where     exprKey :: Expr a -> String     exprKey (Col name) = "0:" ++ T.unpack name+    exprKey (CastWith name tag _) = "0CW:" ++ T.unpack name ++ ":" ++ T.unpack tag+    exprKey (CastExprWith tag _ _) = "0CE:" ++ T.unpack tag     exprKey (Lit val) = "1:" ++ show val     exprKey (If c t e) = "2:" ++ exprKey c ++ exprKey t ++ exprKey e     exprKey (Unary op e) = "3:" ++ T.unpack (unaryName op) ++ exprKey e@@ -278,6 +296,8 @@             Nothing -> False         eqNormalized :: Expr a -> Expr a -> Bool         eqNormalized (Col n1) (Col n2) = n1 == n2+        eqNormalized (CastWith n1 t1 _) (CastWith n2 t2 _) = n1 == n2 && t1 == t2+        eqNormalized (CastExprWith t1 _ e1) (CastExprWith t2 _ e2) = t1 == t2 && e1 `exprEq` e2         eqNormalized (Lit v1) (Lit v2) = v1 == v2         eqNormalized (If c1 t1 e1) (If c2 t2 e2) =             c1 == c2 && t1 `exprEq` t2 && e1 `exprEq` e2@@ -295,6 +315,8 @@     compare :: Expr a -> Expr a -> Ordering     compare e1 e2 = case (e1, e2) of         (Col n1, Col n2) -> compare n1 n2+        (CastWith n1 t1 _, CastWith n2 t2 _) -> compare n1 n2 <> compare t1 t2+        (CastExprWith t1 _ _, CastExprWith t2 _ _) -> compare t1 t2         (Lit v1, Lit v2) -> compare v1 v2         (If c1 t1 e1', If c2 t2 e2') ->             compare c1 c2 <> exprComp t1 t2 <> exprComp e1' e2'@@ -307,6 +329,10 @@         -- Different constructors - compare by priority         (Col _, _) -> LT         (_, Col _) -> GT+        (CastWith{}, _) -> LT+        (_, CastWith{}) -> GT+        (CastExprWith{}, _) -> LT+        (_, CastExprWith{}) -> GT         (Lit _, _) -> LT         (_, Lit _) -> GT         (Unary{}, _) -> LT@@ -334,6 +360,8 @@   where     replace' = case expr of         (Col _) -> expr+        (CastWith{}) -> expr+        (CastExprWith t f e) -> CastExprWith t f (replaceExpr new old e)         (Lit _) -> expr         (If cond l r) ->             If (replaceExpr new old cond) (replaceExpr new old l) (replaceExpr new old r)@@ -343,6 +371,8 @@  eSize :: Expr a -> Int eSize (Col _) = 1+eSize (CastWith{}) = 1+eSize (CastExprWith _ _ e) = 1 + eSize e eSize (Lit _) = 1 eSize (If c l r) = 1 + eSize c + eSize l + eSize r eSize (Unary _ e) = 1 + eSize e@@ -351,6 +381,8 @@  getColumns :: Expr a -> [T.Text] getColumns (Col cName) = [cName]+getColumns (CastWith name _ _) = [name]+getColumns (CastExprWith _ _ e) = getColumns e getColumns expr@(Lit _) = [] getColumns (If cond l r) = getColumns cond <> getColumns l <> getColumns r getColumns (Unary op value) = getColumns value@@ -366,6 +398,8 @@     go :: Int -> Int -> Expr a -> String     go depth prec expr = case expr of         Col name -> T.unpack name+        CastWith name _ _ -> T.unpack name+        CastExprWith tag _ inner -> T.unpack tag ++ "(" ++ go depth 0 inner ++ ")"         Lit value -> show value         If cond t e ->             let inner =
src/DataFrame/Internal/Interpreter.hs view
@@ -36,7 +36,10 @@ import DataFrame.Internal.DataFrame import DataFrame.Internal.Expression import DataFrame.Internal.Types-import Type.Reflection (Typeable, typeRep)+import Type.Reflection (+    Typeable,+    typeRep,+ )  ------------------------------------------------------------------------------- -- Value: the unified result type@@ -231,6 +234,237 @@ numGroups gdf = VU.length (offsets gdf) - 1  -------------------------------------------------------------------------------+-- promoteColumnWith: unified numeric / text coercion for CastWith+-------------------------------------------------------------------------------++{- | Apply a result-handler @onResult@ to each element of a column after+coercing it to type @a@.  Covers three modes in one:++* @onResult = either (const Nothing) Just@  → like @cast@   (returns @Maybe a@)+* @onResult = either (const def) id@         → like @castWithDefault@ (returns @a@)+* @onResult = either (Left . T.pack) Right@  → like @castEither@       (returns @Either T.Text a@)++Numeric coercion handles Double, Float, and Int targets.  Text columns+(String / T.Text) are parsed via 'reads'.  Any other mismatch returns+'Left TypeMismatchException'.+-}+promoteColumnWith ::+    forall a b.+    (Columnable a, Columnable b) =>+    (Either String a -> b) -> Column -> Either DataFrameException Column+promoteColumnWith onResult col+    | hasElemType @b col = Right col+    | hasElemType @a col = mapColumn @a (onResult . Right) col+    | Just result <- tryMaybeWrap @a @b onResult col = result+    | otherwise =+        case testEquality (typeRep @a) (typeRep @Double) of+            Just Refl -> promoteToDoubleWith onResult col+            Nothing ->+                case testEquality (typeRep @a) (typeRep @Float) of+                    Just Refl -> promoteToFloatWith onResult col+                    Nothing ->+                        case testEquality (typeRep @a) (typeRep @Int) of+                            Just Refl -> promoteToIntWith onResult col+                            Nothing -> tryParseWith @a onResult col++promoteToDoubleWith ::+    forall b.+    (Columnable b) =>+    (Either String Double -> b) -> Column -> Either DataFrameException Column+promoteToDoubleWith onResult col = case col of+    UnboxedColumn (v :: VU.Vector c) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        (V.map (onResult . Right . (realToFrac :: c -> Double)) (VG.convert v))+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            (V.map (onResult . Right . (fromIntegral :: c -> Double)) (VG.convert v))+                SFalse -> castMismatch @c @b+    OptionalColumn (v :: V.Vector (Maybe c)) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        ( V.map+                            (maybe (onResult (Left "null")) (onResult . Right . (realToFrac :: c -> Double)))+                            v+                        )+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            ( V.map+                                ( maybe+                                    (onResult (Left "null"))+                                    (onResult . Right . (fromIntegral :: c -> Double))+                                )+                                v+                            )+                SFalse -> tryParseWith @Double onResult col+    BoxedColumn _ -> tryParseWith @Double onResult col++promoteToFloatWith ::+    forall b.+    (Columnable b) =>+    (Either String Float -> b) -> Column -> Either DataFrameException Column+promoteToFloatWith onResult col = case col of+    UnboxedColumn (v :: VU.Vector c) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        (V.map (onResult . Right . (realToFrac :: c -> Float)) (VG.convert v))+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            (V.map (onResult . Right . (fromIntegral :: c -> Float)) (VG.convert v))+                SFalse -> castMismatch @c @b+    OptionalColumn (v :: V.Vector (Maybe c)) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        ( V.map+                            (maybe (onResult (Left "null")) (onResult . Right . (realToFrac :: c -> Float)))+                            v+                        )+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            ( V.map+                                (maybe (onResult (Left "null")) (onResult . Right . (fromIntegral :: c -> Float)))+                                v+                            )+                SFalse -> tryParseWith @Float onResult col+    BoxedColumn _ -> tryParseWith @Float onResult col++promoteToIntWith ::+    forall b.+    (Columnable b) =>+    (Either String Int -> b) -> Column -> Either DataFrameException Column+promoteToIntWith onResult col = case col of+    UnboxedColumn (v :: VU.Vector c) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        (V.map (onResult . Right . (round . (realToFrac :: c -> Double))) (VG.convert v))+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            (V.map (onResult . Right . (fromIntegral :: c -> Int)) (VG.convert v))+                SFalse -> castMismatch @c @b+    OptionalColumn (v :: V.Vector (Maybe c)) ->+        case sFloating @c of+            STrue ->+                Right $+                    fromVector @b+                        ( V.map+                            ( maybe+                                (onResult (Left "null"))+                                (onResult . Right . (round . (realToFrac :: c -> Double)))+                            )+                            v+                        )+            SFalse -> case sIntegral @c of+                STrue ->+                    Right $+                        fromVector @b+                            ( V.map+                                (maybe (onResult (Left "null")) (onResult . Right . (fromIntegral :: c -> Int)))+                                v+                            )+                SFalse -> tryParseWith @Int onResult col+    BoxedColumn _ -> tryParseWith @Int onResult col++-- | Single parse primitive: apply @onResult@ to the result of 'reads'.+parseWith :: (Read a) => (Either String a -> b) -> String -> b+parseWith f s = case reads s of+    [(x, "")] -> f (Right x)+    _ -> f (Left s)++tryParseWith ::+    forall a b.+    (Columnable a, Columnable b) =>+    (Either String a -> b) -> Column -> Either DataFrameException Column+tryParseWith onResult col = case col of+    BoxedColumn (v :: V.Vector c) ->+        case testEquality (typeRep @c) (typeRep @String) of+            Just Refl -> Right $ fromVector @b $ V.map (parseWith onResult) v+            Nothing ->+                case testEquality (typeRep @c) (typeRep @T.Text) of+                    Just Refl -> Right $ fromVector @b $ V.map (parseWith onResult . T.unpack) v+                    Nothing -> castMismatch @c @b+    OptionalColumn (v :: V.Vector (Maybe c)) ->+        case testEquality (typeRep @c) (typeRep @String) of+            Just Refl ->+                Right $+                    fromVector @b $+                        V.map (maybe (onResult (Left "null")) (parseWith onResult)) v+            Nothing ->+                case testEquality (typeRep @c) (typeRep @T.Text) of+                    Just Refl ->+                        Right $+                            fromVector @b $+                                V.map (maybe (onResult (Left "null")) (parseWith onResult . T.unpack)) v+                    Nothing -> castMismatch @c @b+    UnboxedColumn (_ :: VU.Vector c) -> castMismatch @c @b++{- | When the output type @b@ is @Maybe c@ (or @Maybe (Maybe c)@) and the+column stores plain @c@ values, wrap each element in 'Just'.+The @Maybe (Maybe c)@ case applies join semantics: instead of producing+a double-wrapped column, a @Maybe c@ column is returned, so+@castExpr \@(Maybe Double)@ on a @Double@ column yields @Maybe Double@+rather than @Maybe (Maybe Double)@.+Returns 'Nothing' when neither condition holds.+-}+tryMaybeWrap ::+    forall a b.+    (Columnable a, Columnable b) =>+    (Either String a -> b) -> Column -> Maybe (Either DataFrameException Column)+tryMaybeWrap _onResult col = case col of+    UnboxedColumn (v :: VU.Vector c) ->+        let wrapped = V.map Just (VG.convert v) :: V.Vector (Maybe c)+         in case testEquality (typeRep @b) (typeRep @(Maybe c)) of+                Just Refl -> Just $ Right $ fromVector @b wrapped+                Nothing ->+                    -- join: b = Maybe (Maybe c) → produce Maybe c column+                    case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of+                        Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped+                        Nothing -> Nothing+    BoxedColumn (v :: V.Vector c) ->+        let wrapped = V.map Just v :: V.Vector (Maybe c)+         in case testEquality (typeRep @b) (typeRep @(Maybe c)) of+                Just Refl -> Just $ Right $ fromVector @b wrapped+                Nothing ->+                    case testEquality (typeRep @b) (typeRep @(Maybe (Maybe c))) of+                        Just _ -> Just $ Right $ fromVector @(Maybe c) wrapped+                        Nothing -> Nothing+    -- OptionalColumn and NullableColumn are already handled by the hasElemType guards above.+    _ -> Nothing++castMismatch ::+    forall src tgt.+    (Typeable src, Typeable tgt) =>+    Either DataFrameException Column+castMismatch =+    Left $+        TypeMismatchException+            MkTypeErrorContext+                { userType = Right (typeRep @tgt)+                , expectedType = Right (typeRep @src)+                , callingFunctionName = Just "cast"+                , errorColumnName = Nothing+                }++------------------------------------------------------------------------------- -- eval: the unified interpreter ------------------------------------------------------------------------------- @@ -248,10 +482,51 @@ eval (FlatCtx df) (Col name) =     case getColumn name df of         Nothing ->+            Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)+        Just c+            | hasElemType @a c -> Right (Flat c)+            | otherwise ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @a)+                            , expectedType = Left (columnTypeString c)+                            , errorColumnName = Just (T.unpack name)+                            , callingFunctionName = Just "col"+                            } ::+                            TypeErrorContext a ()+                        )+eval (GroupCtx gdf) (Col name) =+    case getColumn name (fullDataframe gdf) of+        Nothing ->             Left $+                ColumnNotFoundException+                    name+                    ""+                    (M.keys $ columnIndices $ fullDataframe gdf)+        Just c+            | hasElemType @a c ->+                Right (Group (sliceGroups c (offsets gdf) (valueIndices gdf)))+            | otherwise ->+                Left $+                    TypeMismatchException+                        ( MkTypeErrorContext+                            { userType = Right (typeRep @a)+                            , expectedType = Left (columnTypeString c)+                            , errorColumnName = Just (T.unpack name)+                            , callingFunctionName = Just "col"+                            } ::+                            TypeErrorContext a ()+                        )+-- CastWith ---------------------------------------------------------------++eval (FlatCtx df) (CastWith name _tag onResult) =+    case getColumn name df of+        Nothing ->+            Left $                 ColumnNotFoundException name "" (M.keys $ columnIndices df)-        Just c -> Right (Flat c)-eval (GroupCtx gdf) (Col name) =+        Just c -> Flat <$> promoteColumnWith onResult c+eval (GroupCtx gdf) (CastWith name _tag onResult) =     case getColumn name (fullDataframe gdf) of         Nothing ->             Left $@@ -259,11 +534,20 @@                     name                     ""                     (M.keys $ columnIndices $ fullDataframe gdf)-        Just c ->-            Right-                ( Group-                    (sliceGroups c (offsets gdf) (valueIndices gdf))-                )+        Just c -> do+            promoted <- promoteColumnWith onResult c+            Right $ Group (sliceGroups promoted (offsets gdf) (valueIndices gdf))+-- CastExprWith -----------------------------------------------------------++eval ctx (CastExprWith _tag onResult (inner :: Expr src)) = do+    v <- eval @src ctx inner+    case v of+        Scalar s ->+            Flat <$> promoteColumnWith onResult (fromList @src [s])+        Flat col ->+            Flat <$> promoteColumnWith onResult col+        Group gs ->+            Group <$> V.mapM (promoteColumnWith onResult) gs -- Unary ------------------------------------------------------------------  eval ctx expr@(Unary (op :: UnaryOp b a) inner) = addContext expr $ do@@ -320,8 +604,7 @@                                 ""                                 (M.keys $ columnIndices $ fullDataframe gdf)                     Just col ->-                        Flat . fromVector-                            <$> foldl1DirectGroups @b f col (valueIndices gdf) (offsets gdf)+                        Flat <$> foldl1DirectGroups @b f col (valueIndices gdf) (offsets gdf) -- Fast path: MergeAgg on a bare Col in GroupCtx.  eval@@ -391,10 +674,10 @@                         InternalException                             "Cannot apply a merge aggregation to a scalar"                 Flat col ->-                    Scalar . finalize <$> foldlColumnWith @b step seed col+                    Scalar . finalize <$> foldlColumn @b step seed col                 Group gs ->                     Flat . fromVector-                        <$> V.mapM (fmap finalize . foldlColumnWith @b step seed) gs+                        <$> V.mapM (fmap finalize . foldlColumn @b step seed) gs  -- Aggregation: FoldAgg without seed (fold1) ------------------------------ 
+ src/DataFrame/Internal/Nullable.hs view
@@ -0,0 +1,477 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}++{- | Nullable-aware binary operations for expressions.++This module provides two type classes, 'NullableArithOp' and 'NullableCmpOp',+which enable operators like '.+', '.-', '.*', './', '.==' etc. to work+transparently across combinations of nullable (@Maybe a@) and non-nullable+(@a@) column types.++The partial functional dependencies uniquely determine the result type from+the operand types, so GHC infers it without annotations.++The four combinations covered for each class:++* @(a, a)@               — non-nullable × non-nullable+* @(Maybe a, a)@         — nullable × non-nullable+* @(a, Maybe a)@         — non-nullable × nullable+* @(Maybe a, Maybe a)@   — both nullable++== Usage++@+-- Mixing nullable and non-nullable columns:+F.col \@Int \"x\" '.+' F.col \@(Maybe Int) \"y\"  -- :: Expr (Maybe Int)++-- Both non-nullable (existing behaviour preserved):+F.col \@Int \"x\" '.+' F.col \@Int \"y\"           -- :: Expr Int++-- Comparison with three-valued logic:+F.col \@(Maybe Int) \"x\" '.==' F.col \@Int \"y\"  -- :: Expr (Maybe Bool)+@+-}+module DataFrame.Internal.Nullable (+    -- * Type family+    BaseType,++    -- * Arithmetic class+    NullableArithOp (..),++    -- * Comparison class+    NullableCmpOp (..),++    -- * Generalized nullable lift classes+    NullLift1Op (..),+    NullLift2Op (..),++    -- * Result-type type families (drive inference in nullLift / nullLift2)+    NullLift1Result,+    NullLift2Result,++    -- * Result-type type family for comparison operators+    NullCmpResult,++    -- * Numeric widening+    NumericWidenOp (..),+    widenArithOp,+    WidenResult,++    -- * Division widening (integral × integral → Double)+    DivWidenOp (..),+    divArithOp,+    WidenResultDiv,+) where++import Data.Int (Int32, Int64)+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.Types (Promote, PromoteDiv)++{- | Strip one layer of 'Maybe'.++@+BaseType (Maybe a) = a+BaseType a         = a   -- for any non-Maybe type+@+-}+type family BaseType a where+    BaseType (Maybe a) = a+    BaseType a = a++{- | Class for arithmetic binary operations that work transparently over+nullable and non-nullable column types.++The functional dependency @a b -> c@ ensures GHC can infer the result type @c@+from the operand types. The 'OVERLAPPABLE' pragma on the non-nullable instance+ensures the more specific @(Maybe a, Maybe a)@ instance wins when both operands+are nullable.+-}+class+    ( Columnable a+    , Columnable b+    , Columnable c+    ) =>+    NullableArithOp a b c+        | a b -> c+    where+    {- | Lift an arithmetic function over the inner (non-Maybe) values.+    'Nothing' short-circuits: any 'Nothing' operand produces 'Nothing'.+    -}+    nullArithOp ::+        (BaseType a -> BaseType a -> BaseType a) ->+        a ->+        b ->+        c++{- | Compute the result type of a nullable comparison.++@+NullCmpResult (Maybe a) b = Maybe Bool+NullCmpResult a (Maybe b) = Maybe Bool   -- when a is apart from Maybe+NullCmpResult a b         = Bool+@++Used by the comparison operators ('.==', '.<', etc.) so GHC infers the+return type without an explicit annotation.+-}+type family NullCmpResult a b where+    NullCmpResult (Maybe a) b = Maybe Bool+    NullCmpResult a (Maybe b) = Maybe Bool+    NullCmpResult a b = Bool++{- | Class for comparison binary operations that work transparently over+nullable and non-nullable column types.++No functional dependency on @e@: the 'OVERLAPPING'\/'OVERLAPPABLE' pragmas on+instances disambiguate at call sites without a FundDep (which would conflict+when both operands are @Maybe@). GHC selects the unique most-specific instance+from the concrete operand types.+-}+class+    ( Columnable a+    , Columnable b+    , Columnable e+    ) =>+    NullableCmpOp a b e+    where+    {- | Lift a comparison function over the inner values (three-valued logic).+    Returns 'Nothing' when either operand is 'Nothing'.+    -}+    nullCmpOp ::+        (BaseType a -> BaseType a -> Bool) ->+        a ->+        b ->+        e++{- | Non-nullable × Non-nullable: apply directly, no wrapping.+Arithmetic result is @a@; comparison result is @Bool@.+-}+instance+    {-# OVERLAPPABLE #-}+    (Columnable a, a ~ BaseType a) =>+    NullableArithOp a a a+    where+    nullArithOp f = f++instance+    {-# OVERLAPPABLE #-}+    (Columnable a, Columnable Bool, a ~ BaseType a) =>+    NullableCmpOp a a Bool+    where+    nullCmpOp f = f++-- | Nullable × Non-nullable: 'Nothing' short-circuits.+instance+    (Columnable a, Columnable (Maybe a)) =>+    NullableArithOp (Maybe a) a (Maybe a)+    where+    nullArithOp f Nothing _ = Nothing+    nullArithOp f (Just x) y = Just (f x y)++instance+    (Columnable a, Columnable (Maybe a), Columnable (Maybe Bool)) =>+    NullableCmpOp (Maybe a) a (Maybe Bool)+    where+    nullCmpOp f Nothing _ = Nothing+    nullCmpOp f (Just x) y = Just (f x y)++-- | Non-nullable × Nullable: 'Nothing' short-circuits.+instance+    ( Columnable a+    , Columnable (Maybe a)+    , a ~ BaseType a+    ) =>+    NullableArithOp a (Maybe a) (Maybe a)+    where+    nullArithOp f _ Nothing = Nothing+    nullArithOp f x (Just y) = Just (f x y)++instance+    ( Columnable a+    , Columnable (Maybe a)+    , Columnable (Maybe Bool)+    , a ~ BaseType a+    ) =>+    NullableCmpOp a (Maybe a) (Maybe Bool)+    where+    nullCmpOp f _ Nothing = Nothing+    nullCmpOp f x (Just y) = Just (f x y)++-- | Nullable × Nullable: either 'Nothing' short-circuits.+instance+    {-# OVERLAPPING #-}+    (Columnable a, Columnable (Maybe a)) =>+    NullableArithOp (Maybe a) (Maybe a) (Maybe a)+    where+    nullArithOp f Nothing _ = Nothing+    nullArithOp f _ Nothing = Nothing+    nullArithOp f (Just x) (Just y) = Just (f x y)++instance+    {-# OVERLAPPING #-}+    (Columnable a, Columnable (Maybe a), Columnable (Maybe Bool)) =>+    NullableCmpOp (Maybe a) (Maybe a) (Maybe Bool)+    where+    nullCmpOp f Nothing _ = Nothing+    nullCmpOp f _ Nothing = Nothing+    nullCmpOp f (Just x) (Just y) = Just (f x y)++-- ---------------------------------------------------------------------------+-- Generalized nullable lift (unary)+-- ---------------------------------------------------------------------------++{- | Lift a unary function over a column expression, propagating 'Nothing'.++When @a@ is non-nullable the function is applied directly; when @a = Maybe x@+the function is applied under the 'Just' and 'Nothing' short-circuits.++Use via 'DataFrame.Functions.nullLift'.+-}++{- | Compute the result type of a nullable unary lift.++@+NullLift1Result (Maybe a) r = Maybe r+NullLift1Result a         r = r        -- for any non-Maybe a+@++Used by 'DataFrame.Functions.nullLift' so GHC can infer the return type+without an explicit annotation.+-}+type family NullLift1Result a r where+    NullLift1Result (Maybe a) r = Maybe r+    NullLift1Result a r = r++class+    ( Columnable a+    , Columnable r+    , Columnable c+    ) =>+    NullLift1Op a r c+    where+    applyNull1 :: (BaseType a -> r) -> a -> c++-- | Non-nullable: apply directly.+instance+    {-# OVERLAPPABLE #-}+    (Columnable a, Columnable r, a ~ BaseType a) =>+    NullLift1Op a r r+    where+    applyNull1 f = f++-- | Nullable: propagate 'Nothing'.+instance+    {-# OVERLAPPING #-}+    (Columnable a, Columnable r, Columnable (Maybe r)) =>+    NullLift1Op (Maybe a) r (Maybe r)+    where+    applyNull1 _ Nothing = Nothing+    applyNull1 f (Just x) = Just (f x)++-- ---------------------------------------------------------------------------+-- Generalized nullable lift (binary)+-- ---------------------------------------------------------------------------++{- | Lift a binary function over two column expressions, propagating 'Nothing'.++The four combinations:++* @(a, b)@               — both non-nullable: result is @r@+* @(Maybe a, b)@         — left nullable: result is @Maybe r@+* @(a, Maybe b)@         — right nullable: result is @Maybe r@+* @(Maybe a, Maybe b)@   — both nullable: result is @Maybe r@++Use via 'DataFrame.Functions.nullLift2'.+-}++{- | Compute the result type of a nullable binary lift.++@+NullLift2Result (Maybe a) b         r = Maybe r+NullLift2Result a         (Maybe b) r = Maybe r   -- when a is apart from Maybe+NullLift2Result a         b         r = r+@++Used by 'DataFrame.Functions.nullLift2' so GHC can infer the return type.+-}+type family NullLift2Result a b r where+    NullLift2Result (Maybe a) b r = Maybe r+    NullLift2Result a (Maybe b) r = Maybe r+    NullLift2Result a b r = r++class+    ( Columnable a+    , Columnable b+    , Columnable r+    , Columnable c+    ) =>+    NullLift2Op a b r c+    where+    applyNull2 :: (BaseType a -> BaseType b -> r) -> a -> b -> c++-- | Both non-nullable: apply directly.+instance+    {-# OVERLAPPABLE #-}+    (Columnable a, Columnable b, Columnable r, a ~ BaseType a, b ~ BaseType b) =>+    NullLift2Op a b r r+    where+    applyNull2 f = f++-- | Left nullable: 'Nothing' short-circuits.+instance+    {-# OVERLAPPABLE #-}+    (Columnable a, Columnable b, Columnable r, Columnable (Maybe r), b ~ BaseType b) =>+    NullLift2Op (Maybe a) b r (Maybe r)+    where+    applyNull2 _ Nothing _ = Nothing+    applyNull2 f (Just x) y = Just (f x y)++-- | Right nullable: 'Nothing' short-circuits.+instance+    {-# OVERLAPPABLE #-}+    (Columnable a, Columnable b, Columnable r, Columnable (Maybe r), a ~ BaseType a) =>+    NullLift2Op a (Maybe b) r (Maybe r)+    where+    applyNull2 _ _ Nothing = Nothing+    applyNull2 f x (Just y) = Just (f x y)++-- | Both nullable: either 'Nothing' short-circuits.+instance+    {-# OVERLAPPING #-}+    (Columnable a, Columnable b, Columnable r, Columnable (Maybe r)) =>+    NullLift2Op (Maybe a) (Maybe b) r (Maybe r)+    where+    applyNull2 _ Nothing _ = Nothing+    applyNull2 _ _ Nothing = Nothing+    applyNull2 f (Just x) (Just y) = Just (f x y)++-- ---------------------------------------------------------------------------+-- Numeric widening+-- ---------------------------------------------------------------------------++{- | Widen two numeric base types to their promoted common type.++When @a ~ b@ the coercions are identity; otherwise one operand is widened+(e.g. 'Int' → 'Double').+-}+class (Columnable (Promote a b)) => NumericWidenOp a b where+    widen1 :: a -> Promote a b+    widen2 :: b -> Promote a b++-- | Same type: identity coercions.+instance {-# OVERLAPPING #-} (Columnable a) => NumericWidenOp a a where+    widen1 = id+    widen2 = id++instance NumericWidenOp Int Double where widen1 = fromIntegral; widen2 = id+instance NumericWidenOp Double Int where+    widen1 = id+    widen2 = fromIntegral+instance NumericWidenOp Float Double where widen1 = realToFrac; widen2 = id+instance NumericWidenOp Double Float where+    widen1 = id+    widen2 = realToFrac+instance NumericWidenOp Int Float where widen1 = fromIntegral; widen2 = id+instance NumericWidenOp Float Int where+    widen1 = id+    widen2 = fromIntegral++-- | Apply an arithmetic function after widening both operands to their common type.+widenArithOp ::+    forall a b.+    (NumericWidenOp a b) =>+    (Promote a b -> Promote a b -> Promote a b) ->+    a ->+    b ->+    Promote a b+widenArithOp f x y = f (widen1 @a @b x) (widen2 @a @b y)++-- | Result type of a widening binary operator, accounting for nullable wrappers.+type WidenResult a b = NullLift2Result a b (Promote (BaseType a) (BaseType b))++-- ---------------------------------------------------------------------------+-- Division widening (integral × integral → Double)+-- ---------------------------------------------------------------------------++{- | Like 'NumericWidenOp' but uses 'PromoteDiv': integral×integral → Double.+Floating types still dominate (Double > Float), and any two integral types+(same or mixed) are both widened to Double.+-}+class (Columnable (PromoteDiv a b)) => DivWidenOp a b where+    divWiden1 :: a -> PromoteDiv a b+    divWiden2 :: b -> PromoteDiv a b++-- Floating same-type (identity)+instance DivWidenOp Double Double where divWiden1 = id; divWiden2 = id+instance DivWidenOp Float Float where divWiden1 = id; divWiden2 = id++-- Mixed Double/Float+instance DivWidenOp Double Float where divWiden1 = id; divWiden2 = realToFrac+instance DivWidenOp Float Double where divWiden1 = realToFrac; divWiden2 = id++-- Double beats integral+instance DivWidenOp Double Int where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int Double where divWiden1 = fromIntegral; divWiden2 = id+instance DivWidenOp Double Int32 where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int32 Double where divWiden1 = fromIntegral; divWiden2 = id+instance DivWidenOp Double Int64 where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int64 Double where divWiden1 = fromIntegral; divWiden2 = id++-- Float beats integral+instance DivWidenOp Float Int where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int Float where divWiden1 = fromIntegral; divWiden2 = id+instance DivWidenOp Float Int32 where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int32 Float where divWiden1 = fromIntegral; divWiden2 = id+instance DivWidenOp Float Int64 where divWiden1 = id; divWiden2 = fromIntegral+instance DivWidenOp Int64 Float where divWiden1 = fromIntegral; divWiden2 = id++-- Integral × integral → Double+instance DivWidenOp Int Int where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int32 Int32 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int64 Int64 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int Int32 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int32 Int where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int Int64 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int64 Int where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int32 Int64 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral+instance DivWidenOp Int64 Int32 where+    divWiden1 = fromIntegral+    divWiden2 = fromIntegral++-- | Apply an arithmetic function after widening both operands via 'PromoteDiv'.+divArithOp ::+    forall a b.+    (DivWidenOp a b) =>+    (PromoteDiv a b -> PromoteDiv a b -> PromoteDiv a b) ->+    a ->+    b ->+    PromoteDiv a b+divArithOp f x y = f (divWiden1 @a @b x) (divWiden2 @a @b y)++-- | Result type of a division-widening binary operator, accounting for nullable wrappers.+type WidenResultDiv a b =+    NullLift2Result a b (PromoteDiv (BaseType a) (BaseType b))
src/DataFrame/Internal/Schema.hs view
@@ -96,3 +96,15 @@     -}     }     deriving (Show, Eq)++{- | Construct a 'Schema' from a list of @(columnName, schemaType)@ pairs.++==== __Example__+>>> :set -XTypeApplications+>>> import qualified Data.Text as T+>>> let s = makeSchema [("name", schemaType @T.Text), ("age", schemaType @Int)]+>>> M.member "age" (elements s)+True+-}+makeSchema :: [(T.Text, SchemaType)] -> Schema+makeSchema = Schema . M.fromList
src/DataFrame/Internal/Types.hs view
@@ -127,3 +127,28 @@ sFloating = sbool @(FloatingTypes a)  type FloatingIf a = When (FloatingTypes a) (Real a, Fractional a)++{- | Numeric type promotion: resolves the common type for mixed arithmetic.+Double dominates over Float/Int; Float dominates over Int; same types stay unchanged.+-}+type family Promote (a :: Type) (b :: Type) :: Type where+    Promote a a = a+    Promote Double _ = Double+    Promote _ Double = Double+    Promote Float _ = Float+    Promote _ Float = Float+    Promote Int64 _ = Int64+    Promote _ Int64 = Int64+    Promote Int32 _ = Int32+    Promote _ Int32 = Int32+    Promote a _ = a++{- | Like 'Promote', but integral × integral → Double for use with './' .+Double\/Float still dominate; any two integral types (same or mixed) become Double.+-}+type family PromoteDiv (a :: Type) (b :: Type) :: Type where+    PromoteDiv Double _ = Double+    PromoteDiv _ Double = Double+    PromoteDiv Float _ = Float+    PromoteDiv _ Float = Float+    PromoteDiv _ _ = Double -- Int/Int32/Int64 in any combination
src/DataFrame/Lazy/IO/Binary.hs view
@@ -54,6 +54,7 @@ import Data.Maybe (fromMaybe, isJust) import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl)) import Data.Word (Word16, Word32, Word64, Word8)+import qualified DataFrame.Internal.Binary as Binary import DataFrame.Internal.Column (Column (..)) import DataFrame.Internal.DataFrame (DataFrame (..)) import Foreign (ForeignPtr, castForeignPtr, plusForeignPtr, sizeOf)@@ -362,37 +363,12 @@ readWord32LE :: BS.ByteString -> Int -> ParseResult Word32 readWord32LE bs off     | off + 4 > BS.length bs = Left "unexpected end of input"-    | otherwise =-        let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word32-            b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word32-            b2 = fromIntegral (BSU.unsafeIndex bs (off + 2)) :: Word32-            b3 = fromIntegral (BSU.unsafeIndex bs (off + 3)) :: Word32-         in Right-                (off + 4, b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24))+    | otherwise = Right (off + 4, Binary.littleEndianWord32 (BS.drop off bs))  readWord64LE :: BS.ByteString -> Int -> ParseResult Word64 readWord64LE bs off     | off + 8 > BS.length bs = Left "unexpected end of input"-    | otherwise =-        let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word64-            b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word64-            b2 = fromIntegral (BSU.unsafeIndex bs (off + 2)) :: Word64-            b3 = fromIntegral (BSU.unsafeIndex bs (off + 3)) :: Word64-            b4 = fromIntegral (BSU.unsafeIndex bs (off + 4)) :: Word64-            b5 = fromIntegral (BSU.unsafeIndex bs (off + 5)) :: Word64-            b6 = fromIntegral (BSU.unsafeIndex bs (off + 6)) :: Word64-            b7 = fromIntegral (BSU.unsafeIndex bs (off + 7)) :: Word64-         in Right-                ( off + 8-                , b0-                    .|. (b1 `shiftL` 8)-                    .|. (b2 `shiftL` 16)-                    .|. (b3 `shiftL` 24)-                    .|. (b4 `shiftL` 32)-                    .|. (b5 `shiftL` 40)-                    .|. (b6 `shiftL` 48)-                    .|. (b7 `shiftL` 56)-                )+    | otherwise = Right (off + 8, Binary.littleEndianWord64 (BS.drop off bs))  -- | Read @n@ consecutive Word32LE values starting at offset @off@. readWord32Array :: BS.ByteString -> Int -> Int -> Either String [Word32]@@ -401,11 +377,7 @@     | otherwise =         Right             [ let i = off + k * 4-                  b0 = fromIntegral (BSU.unsafeIndex bs i) :: Word32-                  b1 = fromIntegral (BSU.unsafeIndex bs (i + 1)) :: Word32-                  b2 = fromIntegral (BSU.unsafeIndex bs (i + 2)) :: Word32-                  b3 = fromIntegral (BSU.unsafeIndex bs (i + 3)) :: Word32-               in b0 .|. (b1 `shiftL` 8) .|. (b2 `shiftL` 16) .|. (b3 `shiftL` 24)+               in Binary.littleEndianWord32 (BS.drop i bs)             | k <- [0 .. n - 1]             ] 
src/DataFrame/Lazy/IO/CSV.hs view
@@ -339,17 +339,13 @@     case testEquality (typeRep @a) (typeRep @T.Text) of         Just Refl ->             let val = TextEncoding.decodeUtf8Lenient bs-             in if isNullish val-                    then VM.unsafeWrite col i T.empty >> return (Left val)-                    else VM.unsafeWrite col i val >> return (Right True)+             in VM.unsafeWrite col i val >> return (Right True)         Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs)) writeColumnBs i bs (MOptionalColumn (col :: VM.IOVector (Maybe a))) =     case testEquality (typeRep @a) (typeRep @T.Text) of         Just Refl ->             let val = TextEncoding.decodeUtf8Lenient bs-             in if isNullish val-                    then VM.unsafeWrite col i Nothing >> return (Left val)-                    else VM.unsafeWrite col i (Just val) >> return (Right True)+             in VM.unsafeWrite col i (Just val) >> return (Right True)         Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs)) writeColumnBs i bs (MUnboxedColumn (col :: VUM.IOVector a)) =     case testEquality (typeRep @a) (typeRep @Double) of
src/DataFrame/Lazy/Internal/DataFrame.hs view
@@ -134,5 +134,5 @@ sortBy cols ldf = ldf{plan = Sort cols (plan ldf)}  -- | Retain at most @n@ rows.-limit :: Int -> LazyDataFrame -> LazyDataFrame-limit n ldf = ldf{plan = Limit n (plan ldf)}+take :: Int -> LazyDataFrame -> LazyDataFrame+take n ldf = ldf{plan = Limit n (plan ldf)}
src/DataFrame/Lazy/Internal/Executor.hs view
@@ -459,26 +459,65 @@ via 'ParquetReadOptions'. -} executeParquetScan :: FilePath -> ScanConfig -> IO Stream-executeParquetScan path cfg = do-    isDir <- doesDirectoryExist path-    let pat = if isDir then path </> "*" else path-    matches <- glob pat-    files <- filterM (fmap not . doesDirectoryExist) matches-    when (null files) $-        error ("executeParquetScan: no parquet files found for " ++ path)+executeParquetScan path cfg+    | Parquet.isHFUri path = executeHFParquetScan path cfg+    | otherwise = do+        isDir <- doesDirectoryExist path+        let pat = if isDir then path </> "*" else path+        matches <- glob pat+        files <- filterM (fmap not . doesDirectoryExist) matches+        when (null files) $+            error ("executeParquetScan: no parquet files found for " ++ path)+        let opts =+                Parquet.defaultParquetReadOptions+                    { Parquet.selectedColumns = Just (M.keys (elements (scanSchema cfg)))+                    , Parquet.predicate = scanPushdownPredicate cfg+                    }+        ref <- newIORef files+        return . Stream $ do+            fs <- readIORef ref+            case fs of+                [] -> return Nothing+                (f : rest) -> do+                    writeIORef ref rest+                    Just <$> Parquet.readParquetWithOpts opts f++{- | HuggingFace Parquet scan.  Files are resolved once (API call or direct URL)+then downloaded one at a time as the stream is pulled — so only one file's worth+of data is in memory at a time, regardless of dataset size.+-}++-- TODO: mchavinda - this should be a more general online file scanner.+executeHFParquetScan :: FilePath -> ScanConfig -> IO Stream+executeHFParquetScan path cfg = do+    ref <- case Parquet.parseHFUri path of+        Left err -> error err+        Right r -> pure r+    mToken <- Parquet.getHFToken+    hfFiles <-+        if Parquet.hasGlob (Parquet.hfGlob ref)+            then Parquet.resolveHFUrls mToken ref+            else do+                let url = Parquet.directHFUrl ref+                    filename = last $ T.splitOn "/" (Parquet.hfGlob ref)+                pure [Parquet.HFParquetFile url "" "" filename]+    when (null hfFiles) $+        error ("executeParquetScan: no HF parquet files found for " ++ path)     let opts =             Parquet.defaultParquetReadOptions                 { Parquet.selectedColumns = Just (M.keys (elements (scanSchema cfg)))                 , Parquet.predicate = scanPushdownPredicate cfg                 }-    ref <- newIORef files+    filesRef <- newIORef hfFiles     return . Stream $ do-        fs <- readIORef ref+        fs <- readIORef filesRef         case fs of             [] -> return Nothing             (f : rest) -> do-                writeIORef ref rest-                Just <$> Parquet.readParquetWithOpts opts f+                writeIORef filesRef rest+                -- Download a single file, read it, then return the batch.+                [localPath] <- Parquet.downloadHFFiles mToken [f]+                Just <$> Parquet.readParquetWithOpts opts localPath  -- --------------------------------------------------------------------------- -- CSV scan implementation
src/DataFrame/Monad.hs view
@@ -12,6 +12,8 @@ import qualified DataFrame as D import DataFrame.Internal.Column (Columnable) import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Internal.Nullable (BaseType)+import DataFrame.Operations.Transformations (ImputeOp)  import qualified Data.Text as T import System.Random@@ -75,7 +77,11 @@ filterJustM expr =     error $ "Cannot filter on compound expression: " ++ show expr -imputeM :: (Columnable a) => Expr (Maybe a) -> a -> FrameM (Expr a)+imputeM ::+    (ImputeOp a, Columnable (BaseType a)) =>+    Expr a ->+    BaseType a ->+    FrameM (Expr (BaseType a)) imputeM expr@(Col name) value = FrameM $ \df ->     let df' = D.impute expr value df      in (df', Col name)
src/DataFrame/Operations/Core.hs view
@@ -41,6 +41,7 @@ import DataFrame.Internal.DataFrame (     DataFrame (..),     columnIndices,+    derivingExpressions,     empty,     getColumn,  )@@ -344,6 +345,7 @@     let         (r, c) = dataframeDimensions d         n = max (columnLength column) r+        exprs = M.delete name (derivingExpressions d)      in         case M.lookup name (columnIndices d) of             Just i ->@@ -351,13 +353,13 @@                     (V.map (expandColumn n) (columns d V.// [(i, column)]))                     (columnIndices d)                     (n, c)-                    M.empty+                    exprs             Nothing ->                 DataFrame                     (V.map (expandColumn n) (columns d `V.snoc` column))                     (M.insert name c (columnIndices d))                     (n, c + 1)-                    M.empty+                    exprs  {- | /O(n)/ Clones a column and places it under a new name in the dataframe. @@ -945,3 +947,22 @@ -} columnAsList :: forall a. (Columnable a) => Expr a -> DataFrame -> [a] columnAsList expr df = either throw V.toList (columnAsVector expr df)++{- | Returns the provenance of all columns in the DataFrame as a list of+@(name, expression)@ pairs. Derived columns show their expression;+raw columns show an identity @col \@type name@ expression.+-}+showDerivedExpressions :: DataFrame -> [NamedExpr]+showDerivedExpressions df =+    let exprs = derivingExpressions df+        names = columnNames df+        toNamedExpr name = case M.lookup name exprs of+            Just uexpr -> (name, uexpr)+            Nothing -> (name, identityUExpr name)+     in map toNamedExpr names+  where+    identityUExpr name = case getColumn name df of+        Just (BoxedColumn (_ :: V.Vector a)) -> UExpr (Col @a name)+        Just (UnboxedColumn (_ :: VU.Vector a)) -> UExpr (Col @a name)+        Just (OptionalColumn (_ :: V.Vector (Maybe a))) -> UExpr (Col @(Maybe a) name)+        Nothing -> error $ "showDerivedExpressions: column not found: " ++ T.unpack name
src/DataFrame/Operations/Join.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE NumericUnderscores #-}@@ -13,7 +14,9 @@ import Control.Monad (forM_, when) import Control.Monad.ST (ST, runST) import qualified Data.HashMap.Strict as HM+#if !MIN_VERSION_base(4,20,0) import Data.List (foldl')+#endif import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) import Data.STRef (newSTRef, readSTRef, writeSTRef)
src/DataFrame/Operations/Merge.hs view
@@ -18,11 +18,11 @@     (<>) a b =         let             addColumns a' b' df name-                | fst (D.dimensions a') == 0 && fst (D.dimensions b') == 0 = df-                | fst (D.dimensions a') == 0 = fromMaybe df $ do+                | snd (D.dimensions a') == 0 && snd (D.dimensions b') == 0 = df+                | snd (D.dimensions a') == 0 = fromMaybe df $ do                     col <- D.getColumn name b'                     pure $ D.insertColumn name col df-                | fst (D.dimensions b') == 0 = fromMaybe df $ do+                | snd (D.dimensions b') == 0 = fromMaybe df $ do                     col <- D.getColumn name a'                     pure $ D.insertColumn name col df                 | otherwise =@@ -49,8 +49,11 @@                                 Just a'' ->                                     let concatedColumns = D.concatColumnsEither a'' b''                                      in D.insertColumn name concatedColumns df+            result = L.foldl' (addColumns a b) D.empty (D.columnNames a `L.union` D.columnNames b)          in-            L.foldl' (addColumns a b) D.empty (D.columnNames a `L.union` D.columnNames b)+            result+                { D.derivingExpressions = D.derivingExpressions a <> D.derivingExpressions b+                }  instance Monoid D.DataFrame where     mempty = D.empty@@ -58,7 +61,12 @@ -- | Add two dataframes side by side/horizontally. (|||) :: D.DataFrame -> D.DataFrame -> D.DataFrame (|||) a b =-    D.fold-        (\name acc -> D.insertColumn name (D.unsafeGetColumn name b) acc)-        (D.columnNames b)-        a+    let result =+            D.fold+                (\name acc -> D.insertColumn name (D.unsafeGetColumn name b) acc)+                (D.columnNames b)+                a+     in result+            { D.derivingExpressions =+                D.derivingExpressions result <> D.derivingExpressions b+            }
src/DataFrame/Operations/Statistics.hs view
@@ -1,10 +1,12 @@ {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}  module DataFrame.Operations.Statistics where @@ -30,12 +32,13 @@  ) import DataFrame.Internal.Expression import DataFrame.Internal.Interpreter+import DataFrame.Internal.Nullable (BaseType) import DataFrame.Internal.Row (showValue, toAny) import DataFrame.Internal.Statistics import DataFrame.Internal.Types import DataFrame.Operations.Core import DataFrame.Operations.Subset (filterJust)-import DataFrame.Operations.Transformations (impute)+import DataFrame.Operations.Transformations (ImputeOp (..), imputeCore) import Text.Printf (printf) import Type.Reflection (typeRep) @@ -287,22 +290,28 @@ -- 20 @ -}+instance {-# OVERLAPPING #-} (Columnable b) => ImputeOp (Maybe b) where+    runImpute = imputeCore++    runImputeWith f col@(Col columnName) df =+        case interpret @b (filterJust columnName df) (f (Col @b columnName)) of+            Left e -> throw e+            Right (TColumn value) -> case headColumn @b value of+                Left e -> throw e+                Right h ->+                    if all (== h) (toList @b value)+                        then imputeCore col h df+                        else error "Impute expression returned more than one value"+    runImputeWith _ _ df = df+ imputeWith ::-    forall b.-    (Columnable b) =>-    (Expr b -> Expr b) ->-    Expr (Maybe b) ->+    forall a.+    (ImputeOp a, Columnable (BaseType a)) =>+    (Expr (BaseType a) -> Expr (BaseType a)) ->+    Expr a ->     DataFrame ->     DataFrame-imputeWith f col@(Col columnName) df = case interpret @b (filterJust columnName df) (f (Col @b columnName)) of-    Left e -> throw e-    Right (TColumn value) -> case headColumn @b value of-        Left e -> throw e-        Right h ->-            if all (== h) (toList @b value)-                then impute col h df-                else error "Impute expression returned more than one value"-imputeWith _ _ df = df+imputeWith = runImputeWith  applyStatistic ::     (VU.Vector Double -> Double) -> T.Text -> DataFrame -> Maybe Double
src/DataFrame/Operations/Subset.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}@@ -33,12 +34,15 @@ import DataFrame.Internal.Column import DataFrame.Internal.DataFrame (     DataFrame (..),+    derivingExpressions,     empty,     getColumn,+    unsafeGetColumn,  ) import DataFrame.Internal.Expression import DataFrame.Internal.Interpreter import DataFrame.Operations.Core+import DataFrame.Operations.Merge () import DataFrame.Operations.Transformations (apply) import System.Random import Type.Reflection@@ -242,7 +246,10 @@                 (T.pack $ show $ cs L.\\ columnNames df)                 "select"                 (columnNames df)-    | otherwise = L.foldl' addKeyValue empty cs+    | otherwise =+        let result = L.foldl' addKeyValue empty cs+            filteredExprs = M.filterWithKey (\k _ -> k `L.elem` cs) (derivingExpressions df)+         in result{derivingExpressions = filteredExprs}   where     addKeyValue d k = fromMaybe df $ do         col <- getColumn k df@@ -467,3 +474,81 @@             (v, g') = uniformR (0 :: Double, 1 :: Double) g          in             v : go g' (n - 1)++-- | Convert any Column to a vector of Text labels (one per row).+columnToTextVec :: Column -> V.Vector T.Text+columnToTextVec (BoxedColumn (col :: V.Vector a)) =+    case testEquality (typeRep @a) (typeRep @T.Text) of+        Just Refl -> col+        Nothing -> V.map (T.pack . show) col+columnToTextVec (UnboxedColumn col) = V.map (T.pack . show) (V.convert col)+columnToTextVec (OptionalColumn col) = V.map (T.pack . show) col++-- | Build a map from stringified label to row indices.+groupByIndices :: Column -> M.Map T.Text (VU.Vector Int)+groupByIndices col =+    let textVec = columnToTextVec col+        (grouped, _) =+            V.foldl'+                (\(!m, !i) key -> (M.insertWith (++) key [i] m, i + 1))+                (M.empty, 0)+                textVec+     in M.map (VU.fromList . L.reverse) grouped++-- | Select rows at the given indices from all columns.+rowsAtIndices :: VU.Vector Int -> DataFrame -> DataFrame+rowsAtIndices ixs df =+    df+        { columns = V.map (atIndicesStable ixs) (columns df)+        , dataframeDimensions = (VU.length ixs, snd (dataframeDimensions df))+        }++{- | Sample a dataframe, preserving per-stratum proportions.++==== __Example__+@+ghci> import System.Random+ghci> D.stratifiedSample (mkStdGen 42) 0.8 "label" df+@+-}+stratifiedSample ::+    forall a g.+    (SplitGen g, RandomGen g, Columnable a) =>+    g -> Double -> Expr a -> DataFrame -> DataFrame+stratifiedSample gen p strataCol df =+    let col = case strataCol of+            Col name -> unsafeGetColumn name df+            _ -> unwrapTypedColumn (either throw id (interpret @a df strataCol))+        groups = M.elems (groupByIndices col)+        go _ [] = mempty+        go g (ixs : rest) =+            let stratum = rowsAtIndices ixs df+                (g1, g2) = splitGen g+             in sample g1 p stratum <> go g2 rest+     in go gen groups++{- | Split a dataframe into two, preserving per-stratum proportions.++==== __Example__+@+ghci> import System.Random+ghci> D.stratifiedSplit (mkStdGen 42) 0.8 "label" df+@+-}+stratifiedSplit ::+    forall a g.+    (SplitGen g, RandomGen g, Columnable a) =>+    g -> Double -> Expr a -> DataFrame -> (DataFrame, DataFrame)+stratifiedSplit gen p strataCol df =+    let col = case strataCol of+            Col name -> unsafeGetColumn name df+            _ -> unwrapTypedColumn (either throw id (interpret @a df strataCol))+        groups = M.elems (groupByIndices col)+        go _ [] = (mempty, mempty)+        go g (ixs : rest) =+            let stratum = rowsAtIndices ixs df+                (g1, g2) = splitGen g+                (tr, va) = randomSplit g1 p stratum+                (trAcc, vaAcc) = go g2 rest+             in (tr <> trAcc, va <> vaAcc)+     in go gen groups
src/DataFrame/Operations/Transformations.hs view
@@ -1,10 +1,14 @@+{-# LANGUAGE ConstrainedClassMethods #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE UndecidableSuperClasses #-}  module DataFrame.Operations.Transformations where @@ -27,6 +31,7 @@ import DataFrame.Internal.DataFrame (DataFrame (..), getColumn) import DataFrame.Internal.Expression import DataFrame.Internal.Interpreter+import DataFrame.Internal.Nullable (BaseType) import DataFrame.Operations.Core  -- | O(k) Apply a function to a given column in a dataframe.@@ -86,7 +91,12 @@     forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> (Expr a, DataFrame) deriveWithExpr name expr df = case interpret @a df (normalize expr) of     Left e -> throw e-    Right (TColumn value) -> (Col name, insertColumn name value df)+    Right (TColumn value) ->+        ( Col name+        , (insertColumn name value df)+            { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)+            }+        )  deriveMany :: [NamedExpr] -> DataFrame -> DataFrame deriveMany exprs df =@@ -188,20 +198,45 @@         Left e -> throw e         Right column' -> insertColumn columnName column' df --- | Replace all instances of `Nothing` in a column with the given value.-impute ::+-- | Core impute implementation for nullable columns. Silently no-ops on non-nullable columns.+imputeCore ::     forall b.     (Columnable b) =>     Expr (Maybe b) ->     b ->     DataFrame ->     DataFrame-impute (Col columnName) value df = case getColumn columnName df of+imputeCore (Col columnName) value df = case getColumn columnName df of     Nothing ->         throw $ ColumnNotFoundException columnName "impute" (M.keys $ columnIndices df)     Just (OptionalColumn _) -> case safeApply (fromMaybe value) columnName df of         Left (TypeMismatchException context) -> throw $ TypeMismatchException (context{callingFunctionName = Just "impute"})         Left exception -> throw exception         Right res -> res-    _ -> error $ "Cannot impute to a non-Empty column: " ++ T.unpack columnName-impute _ _ df = df+    _ -> df+imputeCore _ _ df = df++class (Columnable a) => ImputeOp a where+    runImpute :: Expr a -> BaseType a -> DataFrame -> DataFrame+    runImputeWith ::+        (Columnable (BaseType a)) =>+        (Expr (BaseType a) -> Expr (BaseType a)) ->+        Expr a ->+        DataFrame ->+        DataFrame++instance {-# OVERLAPPABLE #-} (Columnable a) => ImputeOp a where+    runImpute _ _ df = df+    runImputeWith _ _ df = df++{- | Replace all instances of `Nothing` in a column with the given value.+When the column is already non-nullable, this is a silent no-op.+-}+impute ::+    forall a.+    (ImputeOp a) =>+    Expr a ->+    BaseType a ->+    DataFrame ->+    DataFrame+impute = runImpute
src/DataFrame/Operations/Typing.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -6,61 +5,87 @@  module DataFrame.Operations.Typing where +import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Vector as V +import Control.Applicative (asum)+import Control.Monad (join) import Data.Maybe (fromMaybe) import qualified Data.Proxy as P import Data.Time-import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))+import Data.Type.Equality (TestEquality (..)) import DataFrame.Internal.Column (Column (..), fromVector)-import DataFrame.Internal.DataFrame (DataFrame (..))+import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn) import DataFrame.Internal.Parsing import DataFrame.Internal.Schema+import DataFrame.Operations.Core import Text.Read-import Type.Reflection (typeRep)+import Type.Reflection  type DateFormat = String -parseDefaults :: [T.Text] -> Int -> Bool -> DateFormat -> DataFrame -> DataFrame-parseDefaults missing n safeRead dateFormat df = df{columns = V.map (parseDefault missing n safeRead dateFormat) (columns df)}+-- | Options controlling how text columns are parsed into typed values.+data ParseOptions = ParseOptions+    { missingValues :: [T.Text]+    -- ^ Values to treat as @Nothing@ when 'parseSafe' is @True@.+    , sampleSize :: Int+    -- ^ Number of rows to inspect when inferring a column's type (0 = all rows).+    , parseSafe :: Bool+    {- ^ When @True@, treat 'missingValues' and nullish strings as @Nothing@.+    When @False@, only empty strings become @Nothing@.+    -}+    , parseDateFormat :: DateFormat+    -- ^ Date format string as accepted by "Data.Time.Format" (e.g. @\"%Y-%m-%d\"@).+    } -parseDefault :: [T.Text] -> Int -> Bool -> DateFormat -> Column -> Column-parseDefault missing n safeRead dateFormat (BoxedColumn (c :: V.Vector a)) =+{- | Sensible out-of-the-box parse options: infer from the first 100 rows,+  treat common nullish strings as missing, and expect ISO 8601 dates.+-}+defaultParseOptions :: ParseOptions+defaultParseOptions =+    ParseOptions+        { missingValues = []+        , sampleSize = 100+        , parseSafe = True+        , parseDateFormat = "%Y-%m-%d"+        }++parseDefaults :: ParseOptions -> DataFrame -> DataFrame+parseDefaults opts df = df{columns = V.map (parseDefault opts) (columns df)}++parseDefault :: ParseOptions -> Column -> Column+parseDefault opts (BoxedColumn (c :: V.Vector a)) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of-            Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map T.pack c)+            Just Refl -> parseFromExamples opts (V.map T.pack c)             Nothing -> BoxedColumn c-        Just Refl -> parseFromExamples missing n safeRead dateFormat c-parseDefault missing n safeRead dateFormat (OptionalColumn (c :: V.Vector (Maybe a))) =+        Just Refl -> parseFromExamples opts c+parseDefault opts (OptionalColumn (c :: V.Vector (Maybe a))) =     case (typeRep @a) `testEquality` (typeRep @T.Text) of         Nothing -> case (typeRep @a) `testEquality` (typeRep @String) of             Just Refl ->-                parseFromExamples-                    missing-                    n-                    safeRead-                    dateFormat-                    (V.map (T.pack . fromMaybe "") c)+                parseFromExamples opts (V.map (T.pack . fromMaybe "") c)             Nothing -> BoxedColumn c-        Just Refl -> parseFromExamples missing n safeRead dateFormat (V.map (fromMaybe "") c)-parseDefault _ _ _ _ column = column+        Just Refl -> parseFromExamples opts (V.map (fromMaybe "") c)+parseDefault _ column = column -parseFromExamples ::-    [T.Text] -> Int -> Bool -> DateFormat -> V.Vector T.Text -> Column-parseFromExamples missing n safeRead dateFormat cols =+parseFromExamples :: ParseOptions -> V.Vector T.Text -> Column+parseFromExamples opts cols =     let-        converter = if safeRead then convertNullish missing else convertOnlyEmpty-        examples = V.map converter (V.take n cols)+        converter =+            if parseSafe opts then convertNullish (missingValues opts) else convertOnlyEmpty+        examples = V.map converter (V.take (sampleSize opts) cols)         asMaybeText = V.map converter cols+        dfmt = parseDateFormat opts      in-        case makeParsingAssumption dateFormat examples of+        case makeParsingAssumption dfmt examples of             BoolAssumption -> handleBoolAssumption asMaybeText             IntAssumption -> handleIntAssumption asMaybeText             DoubleAssumption -> handleDoubleAssumption asMaybeText             TextAssumption -> handleTextAssumption asMaybeText-            DateAssumption -> handleDateAssumption dateFormat asMaybeText-            NoAssumption -> handleNoAssumption dateFormat asMaybeText+            DateAssumption -> handleDateAssumption dfmt asMaybeText+            NoAssumption -> handleNoAssumption dfmt asMaybeText  handleBoolAssumption :: V.Vector (Maybe T.Text) -> Column handleBoolAssumption asMaybeText@@ -197,19 +222,63 @@     | NoAssumption     | TextAssumption -parseWithTypes :: [SchemaType] -> DataFrame -> DataFrame-parseWithTypes ts df = df{columns = go 0 ts (columns df)}+parseWithTypes :: Bool -> M.Map T.Text SchemaType -> DataFrame -> DataFrame+parseWithTypes safe ts df+    | M.null ts = df+    | otherwise =+        M.foldrWithKey+            (\k v d -> insertColumn k (asType v (unsafeGetColumn k d)) d)+            df+            ts   where-    go :: Int -> [SchemaType] -> V.Vector Column -> V.Vector Column-    go n [] xs = xs-    go n (t : rest) xs-        | n >= V.length xs = xs-        | otherwise =-            go (n + 1) rest (V.update xs (V.fromList [(n, asType t (xs V.! n))]))     asType :: SchemaType -> Column -> Column-    asType (SType (_ :: P.Proxy a)) c@(BoxedColumn (col :: V.Vector b)) = case testEquality (typeRep @a) (typeRep @b) of-        Just Refl -> c-        Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of-            Just Refl -> fromVector (V.map ((readMaybe @a) . T.unpack) col)-            Nothing -> fromVector (V.map ((readMaybe @a) . show) col)+    asType (SType (_ :: P.Proxy a)) c@(BoxedColumn (col :: V.Vector b)) = case typeRep @a of+        App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of+            Just HRefl -> case testEquality (typeRep @a) (typeRep @b) of+                Just Refl -> c+                Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of+                    Just Refl -> fromVector (V.map (join . (readAsMaybe @a) . T.unpack) col)+                    Nothing -> fromVector (V.map (join . (readAsMaybe @a) . show) col)+            Nothing -> case t1 of+                App t1' t2' -> case eqTypeRep t1' (typeRep @Either) of+                    Just HRefl -> case testEquality (typeRep @a) (typeRep @b) of+                        Just Refl -> c+                        Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of+                            Just Refl -> fromVector (V.map ((readAsEither @a) . T.unpack) col)+                            Nothing -> fromVector (V.map ((readAsEither @a) . show) col)+                    Nothing -> case testEquality (typeRep @a) (typeRep @b) of+                        Just Refl -> c+                        Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of+                            Just Refl ->+                                if safe+                                    then fromVector (V.map ((readMaybe @a) . T.unpack) col)+                                    else fromVector (V.map ((read @a) . T.unpack) col)+                            Nothing ->+                                if safe+                                    then fromVector (V.map ((readMaybe @a) . show) col)+                                    else fromVector (V.map ((read @a) . show) col)+                _ -> c+        _ -> case testEquality (typeRep @a) (typeRep @b) of+            Just Refl -> c+            Nothing -> case testEquality (typeRep @T.Text) (typeRep @b) of+                Just Refl ->+                    if safe+                        then fromVector (V.map ((readMaybe @a) . T.unpack) col)+                        else fromVector (V.map ((read @a) . T.unpack) col)+                Nothing ->+                    if safe+                        then fromVector (V.map ((readMaybe @a) . show) col)+                        else fromVector (V.map ((read @a) . show) col)     asType _ c = c++readAsMaybe :: (Read a) => String -> Maybe a+readAsMaybe s+    | null s = Nothing+    | otherwise = readMaybe $ "Just " <> s++readAsEither :: (Read a) => String -> a+readAsEither v = case asum [readMaybe $ "Left " <> s, readMaybe $ "Right " <> s] of+    Nothing -> error $ "Couldn't read value: " <> s+    Just v -> v+  where+    s = if null v then "\"\"" else v
src/DataFrame/Operators.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}  module DataFrame.Operators where @@ -15,15 +17,31 @@         binaryPrecedence,         binarySymbol     ),-    Expr (Binary, Col, If, Lit),+    Expr (Binary, Col, If, Lit, Unary),     NamedExpr,     UExpr (UExpr),+    UnaryOp (MkUnaryOp, unaryFn, unaryName, unarySymbol),  )+import DataFrame.Internal.Nullable (+    BaseType,+    DivWidenOp,+    NullCmpResult,+    NullLift2Op (applyNull2),+    NullableCmpOp (nullCmpOp),+    NumericWidenOp,+    WidenResult,+    WidenResultDiv,+    divArithOp,+    widenArithOp,+ )+import DataFrame.Internal.Types (Promote, PromoteDiv) -infix 8 .^^-infix 4 .==, .<, .<=, .>=, .>, ./=-infixr 3 .&&-infixr 2 .||+infix 8 .^^, .^^., .^, .^.+infix 6 .+, .-+infix 7 .*, ./+infix 4 .==, .==., .<, .<., .<=, .<=., .>=, .>=., .>, .>., ./=, ./=.+infixr 3 .&&, .&&.+infixr 2 .||, .||. infixr 0 .=  (|>) :: a -> (a -> b) -> b@@ -50,112 +68,238 @@ (.=) :: (Columnable a) => T.Text -> Expr a -> NamedExpr (.=) = flip as -(.==) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(.==) =-    Binary-        ( MkBinaryOp-            { binaryFn = (==)-            , binaryName = "eq"-            , binarySymbol = Just "=="-            , binaryCommutative = True-            , binaryPrecedence = 4-            }-        )+liftDecorated ::+    (Columnable a, Columnable b) =>+    (a -> b) -> T.Text -> Maybe T.Text -> Expr a -> Expr b+liftDecorated f name rep = Unary (MkUnaryOp{unaryFn = f, unaryName = name, unarySymbol = rep}) -(./=) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool-(./=) =+lift2Decorated ::+    (Columnable c, Columnable b, Columnable a) =>+    (c -> b -> a) ->+    T.Text ->+    Maybe T.Text ->+    Bool ->+    Int ->+    Expr c ->+    Expr b ->+    Expr a+lift2Decorated f name rep comm prec =     Binary         ( MkBinaryOp-            { binaryFn = (/=)-            , binaryName = "neq"-            , binarySymbol = Just "/="-            , binaryCommutative = True-            , binaryPrecedence = 4+            { binaryFn = f+            , binaryName = name+            , binarySymbol = rep+            , binaryCommutative = comm+            , binaryPrecedence = prec             }         ) -(.<) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.<) =-    Binary-        ( MkBinaryOp-            { binaryFn = (<)-            , binaryName = "lt"-            , binarySymbol = Just "<"-            , binaryCommutative = False-            , binaryPrecedence = 4-            }-        )+(.==.) ::+    (Columnable a, Eq a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(.==.) = lift2Decorated (==) "eq" (Just ".==.") True 4 -(.>) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool-(.>) =-    Binary-        ( MkBinaryOp-            { binaryFn = (>)-            , binaryName = "gt"-            , binarySymbol = Just ">"-            , binaryCommutative = False-            , binaryPrecedence = 4-            }-        )+(./=.) ::+    (Columnable a, Eq a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(./=.) = lift2Decorated (/=) "neq" (Just "./=.") True 4 -(.<=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.<=) =-    Binary-        ( MkBinaryOp-            { binaryFn = (<=)-            , binaryName = "leq"-            , binarySymbol = Just "<="-            , binaryCommutative = False-            , binaryPrecedence = 4-            }-        )+(.<.) ::+    (Columnable a, Ord a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(.<.) = lift2Decorated (<) "lt" (Just ".<.") False 4 -(.>=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool-(.>=) =-    Binary-        ( MkBinaryOp-            { binaryFn = (>=)-            , binaryName = "geq"-            , binarySymbol = Just ">="-            , binaryCommutative = False-            , binaryPrecedence = 4-            }-        )+(.>.) ::+    (Columnable a, Ord a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(.>.) = lift2Decorated (>) "gt" (Just ".>.") False 4 -(.&&) :: Expr Bool -> Expr Bool -> Expr Bool-(.&&) =-    Binary-        ( MkBinaryOp-            { binaryFn = (&&)-            , binaryName = "and"-            , binarySymbol = Just "&&"-            , binaryCommutative = True-            , binaryPrecedence = 3-            }-        )+(.<=.) ::+    (Columnable a, Ord a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(.<=.) = lift2Decorated (<=) "leq" (Just ".<=.") False 4 -(.||) :: Expr Bool -> Expr Bool -> Expr Bool-(.||) =-    Binary-        ( MkBinaryOp-            { binaryFn = (||)-            , binaryName = "or"-            , binarySymbol = Just "||"-            , binaryCommutative = True-            , binaryPrecedence = 2-            }-        )+(.>=.) ::+    (Columnable a, Ord a) =>+    Expr a ->+    Expr a ->+    Expr Bool+(.>=.) = lift2Decorated (>=) "geq" (Just ".>=.") False 4 -(.^^) :: (Columnable a, Num a) => Expr a -> Int -> Expr a-(.^^) expr i =-    Binary-        ( MkBinaryOp-            { binaryFn = (^)-            , binaryName = "pow"-            , binarySymbol = Just "^"-            , binaryCommutative = False-            , binaryPrecedence = 8-            }-        )-        expr-        (Lit i)+(.+.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a+(.+.) = (+)++(.-.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a+(.-.) = (-)++(.*.) :: (Columnable a, Num a) => Expr a -> Expr a -> Expr a+(.*.) = (*)++(./.) :: (Columnable a, Fractional a) => Expr a -> Expr a -> Expr a+(./.) = (/)++-- Nullable-aware arithmetic operators++{- | Nullable-aware addition. Works for all combinations of nullable\/non-nullable operands.+@col \@Int "x" .+ col \@(Maybe Int) "y"  -- :: Expr (Maybe Int)@+-}+(.+) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    Expr a ->+    Expr b ->+    Expr (WidenResult a b)+(.+) = lift2Decorated (applyNull2 (widenArithOp (+))) "nulladd" (Just ".+") True 6++-- | Nullable-aware subtraction.+(.-) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    Expr a ->+    Expr b ->+    Expr (WidenResult a b)+(.-) = lift2Decorated (applyNull2 (widenArithOp (-))) "nullsub" (Just ".-") False 6++-- | Nullable-aware multiplication.+(.*) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    Expr a ->+    Expr b ->+    Expr (WidenResult a b)+(.*) = lift2Decorated (applyNull2 (widenArithOp (*))) "nullmul" (Just ".*") True 7++-- | Nullable-aware division. Integral operands are promoted to Double.+(./) ::+    ( DivWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (PromoteDiv (BaseType a) (BaseType b)) (WidenResultDiv a b)+    , Fractional (PromoteDiv (BaseType a) (BaseType b))+    ) =>+    Expr a ->+    Expr b ->+    Expr (WidenResultDiv a b)+(./) = lift2Decorated (applyNull2 (divArithOp (/))) "nulldiv" (Just "./") False 7++-- Nullable-aware comparison operators (three-valued logic: Nothing if either operand is Nothing)++-- | Nullable-aware equality. Returns @Maybe Bool@ when either operand is nullable.+(.==) ::+    (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.==) = lift2Decorated (nullCmpOp (==)) "eq" (Just ".==") True 4++-- | Nullable-aware inequality.+(./=) ::+    (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(./=) = lift2Decorated (nullCmpOp (/=)) "neq" (Just "./=") True 4++-- | Nullable-aware less-than.+(.<) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.<) = lift2Decorated (nullCmpOp (<)) "lt" (Just ".<") False 4++-- | Nullable-aware greater-than.+(.>) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.>) = lift2Decorated (nullCmpOp (>)) "gt" (Just ".>") False 4++-- | Nullable-aware less-than-or-equal.+(.<=) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.<=) = lift2Decorated (nullCmpOp (<=)) "leq" (Just ".<=") False 4++-- | Nullable-aware greater-than-or-equal.+(.>=) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.>=) = lift2Decorated (nullCmpOp (>=)) "geq" (Just ".>=") False 4++(.&&.) :: Expr Bool -> Expr Bool -> Expr Bool+(.&&.) = lift2Decorated (&&) "and" (Just ".&&.") True 3++(.||.) :: Expr Bool -> Expr Bool -> Expr Bool+(.||.) = lift2Decorated (||) "or" (Just ".||.") True 2++-- | Nullable-aware logical AND. Returns @Maybe Bool@ when either operand is nullable.+(.&&) ::+    (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.&&) = lift2Decorated (nullCmpOp (&&)) "nulland" (Just ".&&") True 3++-- | Nullable-aware logical OR. Returns @Maybe Bool@ when either operand is nullable.+(.||) ::+    (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) =>+    Expr a ->+    Expr b ->+    Expr (NullCmpResult a b)+(.||) = lift2Decorated (nullCmpOp (||)) "nullor" (Just ".||") True 2++(.^^) ::+    ( Columnable (BaseType a)+    , Columnable (BaseType b)+    , Fractional (BaseType a)+    , Integral (BaseType b)+    , NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (BaseType a) a+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    Expr a -> Expr b -> Expr a+(.^^) = lift2Decorated (applyNull2 (^^)) "pow" (Just ".^^") False 8++(.^) ::+    ( Columnable (BaseType a)+    , Columnable (BaseType b)+    , Num (BaseType a)+    , Integral (BaseType b)+    , NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (BaseType a) a+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    Expr a -> Expr b -> Expr a+(.^) = lift2Decorated (applyNull2 (^)) "pow" (Just ".^") False 8++-- Same-type (non-nullable) exponentiation operators++(.^^.) ::+    (Columnable a, Columnable b, Fractional a, Integral b) =>+    Expr a -> Expr b -> Expr a+(.^^.) = lift2Decorated (^^) "pow" (Just ".^^.") False 8++(.^.) ::+    (Columnable a, Columnable b, Num a, Integral b) =>+    Expr a -> Expr b -> Expr a+(.^.) = lift2Decorated (^) "pow" (Just ".^.") False 8
src/DataFrame/Synthesis.hs view
@@ -260,7 +260,7 @@             []             [] of             Nothing -> Left "No programs found"-            Just p -> Right (F.ifThenElse (p .> 0) 1 0)+            Just p -> Right (F.ifThenElse (p .> (0 :: Expr Double)) 1 0)  percentiles :: DataFrame -> [Expr Double] percentiles df =
src/DataFrame/Typed.hs view
@@ -70,14 +70,30 @@     ifThenElse,     lift,     lift2,+    nullLift,+    nullLift2, -    -- * Comparison operators+    -- * Same-type comparison operators     (.==.),     (./=.),     (.<.),     (.<=.),     (.>=.),     (.>.),++    -- * Nullable-aware arithmetic operators+    (.+),+    (.-),+    (.*),+    (./),++    -- * Nullable-aware comparison operators (three-valued logic)+    (.==),+    (./=),+    (.<),+    (.<=),+    (.>=),+    (.>),      -- * Logical operators     (.&&.),
src/DataFrame/Typed/Expr.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -53,8 +54,10 @@     -- * Unary / binary lifting     lift,     lift2,+    nullLift,+    nullLift2, -    -- * Comparison operators+    -- * Same-type comparison operators     (.==.),     (./=.),     (.<.),@@ -62,9 +65,39 @@     (.>=.),     (.>.), +    -- * Same-type arithmetic operators+    (.+.),+    (.-.),+    (.*.),+    (./.),++    -- * Same-type exponentiation operators+    (.^^.),+    (.^.),++    -- * Nullable-aware arithmetic operators+    (.+),+    (.-),+    (.*),+    (./),++    -- * Nullable-aware exponentiation operators+    (.^^),+    (.^),++    -- * Nullable-aware comparison operators (three-valued logic)+    (.==),+    (./=),+    (.<),+    (.<=),+    (.>=),+    (.>),+     -- * Logical operators     (.&&.),     (.||.),+    (.&&),+    (.||),     DataFrame.Typed.Expr.not,      -- * Aggregation combinators@@ -75,6 +108,12 @@     maximum,     collect, +    -- * Cast / coercion expressions+    castExpr,+    castExprWithDefault,+    castExprEither,+    unsafeCastExpr,+     -- * Named expression helper     as, @@ -83,6 +122,7 @@     desc, ) where +import Data.Either (fromRight) import Data.Proxy (Proxy (..)) import Data.String (IsString (..)) import qualified Data.Text as T@@ -98,6 +138,22 @@     UExpr (..),     UnaryOp (..),  )+import DataFrame.Internal.Nullable (+    BaseType,+    DivWidenOp,+    NullCmpResult,+    NullLift1Op (applyNull1),+    NullLift1Result,+    NullLift2Op (applyNull2),+    NullLift2Result,+    NullableCmpOp (nullCmpOp),+    NumericWidenOp,+    WidenResult,+    WidenResultDiv,+    divArithOp,+    widenArithOp,+ )+import DataFrame.Internal.Types (Promote, PromoteDiv)  import DataFrame.Typed.Schema (AssertPresent, Lookup) import DataFrame.Typed.Types (TExpr (..), TSortOrder (..))@@ -189,9 +245,36 @@     (a -> b -> c) -> TExpr cols a -> TExpr cols b -> TExpr cols c lift2 f (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp f "binaryUdf" Nothing False 0) a b) +{- | Typed 'nullLift': lift a unary function with nullable propagation.+When the input is @Maybe a@, 'Nothing' short-circuits; when plain @a@, applies directly.+The return type is inferred via 'NullLift1Result': no annotation needed.+-}+nullLift ::+    (NullLift1Op a r (NullLift1Result a r), Columnable (NullLift1Result a r)) =>+    (BaseType a -> r) ->+    TExpr cols a ->+    TExpr cols (NullLift1Result a r)+nullLift f (TExpr e) = TExpr (Unary (MkUnaryOp (applyNull1 f) "nullLift" Nothing) e)++{- | Typed 'nullLift2': lift a binary function with nullable propagation.+Any 'Nothing' operand short-circuits to 'Nothing' in the result.+The return type is inferred via 'NullLift2Result': no annotation needed.+-}+nullLift2 ::+    (NullLift2Op a b r (NullLift2Result a b r), Columnable (NullLift2Result a b r)) =>+    (BaseType a -> BaseType b -> r) ->+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullLift2Result a b r)+nullLift2 f (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (applyNull2 f) "nullLift2" Nothing False 0) a b)+ infixl 4 .==., ./=., .<., .<=., .>=., .>.-infixr 3 .&&.-infixr 2 .||.+infix 4 .==, ./=, .<, .<=, .>=, .>+infixr 3 .&&., .&&+infixr 2 .||., .||+infixl 6 .+., .-.+infixl 7 .*., ./.+infix 8 .^^., .^^, .^., .^  (.==.) ::     (Columnable a, Eq a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool@@ -217,12 +300,220 @@     (Columnable a, Ord a) => TExpr cols a -> TExpr cols a -> TExpr cols Bool (.>.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (>) "gt" (Just ">") False 4) a b) +-- Same-type arithmetic operators++(.+.) :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a+(.+.) = (+)++(.-.) :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a+(.-.) = (-)++(.*.) :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a -> TExpr cols a+(.*.) = (*)++(./.) ::+    (Columnable a, Fractional a) => TExpr cols a -> TExpr cols a -> TExpr cols a+(./.) = (/)++-- Same-type exponentiation operators++(.^^.) ::+    (Columnable a, Columnable b, Fractional a, Integral b) =>+    TExpr cols a -> TExpr cols b -> TExpr cols a+(.^^.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (^^) "pow" (Just ".^^.") False 8) a b)++(.^.) ::+    (Columnable a, Columnable b, Num a, Integral b) =>+    TExpr cols a -> TExpr cols b -> TExpr cols a+(.^.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (^) "pow" (Just ".^.") False 8) a b)+ (.&&.) :: TExpr cols Bool -> TExpr cols Bool -> TExpr cols Bool-(.&&.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (&&) "and" (Just "&&") True 3) a b)+(.&&.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (&&) "and" (Just ".&&.") True 3) a b)  (.||.) :: TExpr cols Bool -> TExpr cols Bool -> TExpr cols Bool-(.||.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (||) "or" (Just "||") True 2) a b)+(.||.) (TExpr a) (TExpr b) = TExpr (Binary (MkBinaryOp (||) "or" (Just ".||.") True 2) a b) +-- | Nullable-aware logical AND. Returns @Maybe Bool@ when either operand is nullable.+(.&&) ::+    (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.&&) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (&&)) "nulland" (Just ".&&") True 3) a b)++-- | Nullable-aware logical OR. Returns @Maybe Bool@ when either operand is nullable.+(.||) ::+    (NullableCmpOp a b (NullCmpResult a b), BaseType a ~ Bool) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.||) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (||)) "nullor" (Just ".||") True 2) a b)++-------------------------------------------------------------------------------+-- Nullable-aware arithmetic operators+-------------------------------------------------------------------------------++infixl 6 .+, .-+infixl 7 .*, ./++{- | Nullable-aware addition. Works for all combinations of nullable\/non-nullable operands.+@col \@\"x\" '.+' col \@\"y\"  -- :: TExpr cols (Maybe Int)  when y :: Maybe Int@+-}+(.+) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (WidenResult a b)+(.+) (TExpr a) (TExpr b) =+    TExpr+        ( Binary+            (MkBinaryOp (applyNull2 (widenArithOp (+))) "nulladd" (Just "+") True 6)+            a+            b+        )++-- | Nullable-aware subtraction.+(.-) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (WidenResult a b)+(.-) (TExpr a) (TExpr b) =+    TExpr+        ( Binary+            (MkBinaryOp (applyNull2 (widenArithOp (-))) "nullsub" (Just "-") False 6)+            a+            b+        )++-- | Nullable-aware multiplication.+(.*) ::+    ( NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (Promote (BaseType a) (BaseType b)) (WidenResult a b)+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (WidenResult a b)+(.*) (TExpr a) (TExpr b) =+    TExpr+        ( Binary+            (MkBinaryOp (applyNull2 (widenArithOp (*))) "nullmul" (Just "*") True 7)+            a+            b+        )++-- | Nullable-aware division. Integral operands are promoted to Double.+(./) ::+    ( DivWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (PromoteDiv (BaseType a) (BaseType b)) (WidenResultDiv a b)+    , Fractional (PromoteDiv (BaseType a) (BaseType b))+    ) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (WidenResultDiv a b)+(./) (TExpr a) (TExpr b) =+    TExpr+        ( Binary+            (MkBinaryOp (applyNull2 (divArithOp (/))) "nulldiv" (Just "/") False 7)+            a+            b+        )++-- | Nullable-aware exponentiation (fractional base, integral exponent).+(.^^) ::+    ( Columnable (BaseType a)+    , Columnable (BaseType b)+    , Fractional (BaseType a)+    , Integral (BaseType b)+    , NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (BaseType a) a+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    TExpr cols a -> TExpr cols b -> TExpr cols a+(.^^) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (applyNull2 (^^)) "pow" (Just ".^^") False 8) a b)++-- | Nullable-aware exponentiation (num base, integral exponent).+(.^) ::+    ( Columnable (BaseType a)+    , Columnable (BaseType b)+    , Num (BaseType a)+    , Integral (BaseType b)+    , NumericWidenOp (BaseType a) (BaseType b)+    , NullLift2Op a b (BaseType a) a+    , Num (Promote (BaseType a) (BaseType b))+    ) =>+    TExpr cols a -> TExpr cols b -> TExpr cols a+(.^) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (applyNull2 (^)) "pow" (Just ".^") False 8) a b)++-------------------------------------------------------------------------------+-- Nullable-aware comparison operators (three-valued logic)+-------------------------------------------------------------------------------++-- | Nullable-aware equality. Returns @Maybe Bool@ when either operand is nullable.+(.==) ::+    (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.==) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (==)) "eq" (Just "==") True 4) a b)++-- | Nullable-aware inequality.+(./=) ::+    (NullableCmpOp a b (NullCmpResult a b), Eq (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(./=) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (/=)) "neq" (Just "/=") True 4) a b)++-- | Nullable-aware less-than.+(.<) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.<) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (<)) "lt" (Just "<") False 4) a b)++-- | Nullable-aware less-than-or-equal.+(.<=) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.<=) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (<=)) "leq" (Just "<=") False 4) a b)++-- | Nullable-aware greater-than-or-equal.+(.>=) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.>=) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (>=)) "geq" (Just ">=") False 4) a b)++-- | Nullable-aware greater-than.+(.>) ::+    (NullableCmpOp a b (NullCmpResult a b), Ord (BaseType a)) =>+    TExpr cols a ->+    TExpr cols b ->+    TExpr cols (NullCmpResult a b)+(.>) (TExpr a) (TExpr b) =+    TExpr (Binary (MkBinaryOp (nullCmpOp (>)) "gt" (Just ">") False 4) a b)+ not :: TExpr cols Bool -> TExpr cols Bool not (TExpr e) = TExpr (Unary (MkUnaryOp Prelude.not "not" (Just "!")) e) @@ -258,6 +549,50 @@  collect :: (Columnable a) => TExpr cols a -> TExpr cols [a] collect (TExpr e) = TExpr (Agg (FoldAgg "collect" (Just []) (flip (:))) e)++-------------------------------------------------------------------------------+-- Cast / coercion expressions+-------------------------------------------------------------------------------++castExpr ::+    forall b cols src.+    (Columnable b, Columnable src) => TExpr cols src -> TExpr cols (Maybe b)+castExpr (TExpr e) =+    TExpr+        (CastExprWith @b @(Maybe b) @src "castExpr" (either (const Nothing) Just) e)++castExprWithDefault ::+    forall b cols src.+    (Columnable b, Columnable src) => b -> TExpr cols src -> TExpr cols b+castExprWithDefault def (TExpr e) =+    TExpr+        ( CastExprWith @b @b @src+            ("castExprWithDefault:" <> T.pack (show def))+            (fromRight def)+            e+        )++castExprEither ::+    forall b cols src.+    (Columnable b, Columnable src) => TExpr cols src -> TExpr cols (Either T.Text b)+castExprEither (TExpr e) =+    TExpr+        ( CastExprWith @b @(Either T.Text b) @src+            "castExprEither"+            (either (Left . T.pack) Right)+            e+        )++unsafeCastExpr ::+    forall b cols src.+    (Columnable b, Columnable src) => TExpr cols src -> TExpr cols b+unsafeCastExpr (TExpr e) =+    TExpr+        ( CastExprWith @b @b @src+            "unsafeCastExpr"+            (fromRight (error "unsafeCastExpr: unexpected Nothing in column"))+            e+        )  ------------------------------------------------------------------------------- -- Named expression helper
+ tests/DecisionTree.hs view
@@ -0,0 +1,764 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module DecisionTree where++import qualified DataFrame as D+import DataFrame.DecisionTree+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Operators++import Data.Function (on)+import Data.List (maximumBy, sort)+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Vector as V+import Test.HUnit++------------------------------------------------------------------------+-- Shared fixtures+------------------------------------------------------------------------++-- 4 rows: label = ["A","B","A","C"], x = [1.0,2.0,3.0,4.0]+fixtureDF :: D.DataFrame+fixtureDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (["A", "B", "A", "C"] :: [T.Text]))+        , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+        ]++allIndices :: V.Vector Int+allIndices = V.fromList [0, 1, 2, 3]++leftTree :: Tree T.Text+leftTree = Leaf "A"++rightTree :: Tree T.Text+rightTree = Leaf "B"++-- x <= 2.5: True for idx 0,1 (→ left); False for idx 2,3 (→ right)+splitCond :: Expr Bool+splitCond = F.col @Double "x" .<= F.lit (2.5 :: Double)++-- Pre-computed care points for the full fixture+carePoints3 :: [CarePoint]+carePoints3 =+    identifyCarePoints @T.Text "label" fixtureDF allIndices leftTree rightTree++------------------------------------------------------------------------+-- Unit tests: identifyCarePoints+------------------------------------------------------------------------++carePointsBothWrong :: Test+carePointsBothWrong =+    TestCase $+        assertBool+            "idx 3 (label=C, neither A nor B) should not be a care point"+            (3 `notElem` map cpIndex carePoints3)++carePointsLeftCorrect :: Test+carePointsLeftCorrect = TestCase $ do+    let cp0 = filter ((== 0) . cpIndex) carePoints3+    assertBool "idx 0 should be a care point" (not (null cp0))+    assertEqual+        "idx 0 (label=A matches left Leaf A) should route GoLeft"+        GoLeft+        (cpCorrectDir (head cp0))++carePointsRightCorrect :: Test+carePointsRightCorrect = TestCase $ do+    let cp1 = filter ((== 1) . cpIndex) carePoints3+    assertBool "idx 1 should be a care point" (not (null cp1))+    assertEqual+        "idx 1 (label=B matches right Leaf B) should route GoRight"+        GoRight+        (cpCorrectDir (head cp1))++carePointsMixed :: Test+carePointsMixed = TestCase $ do+    assertEqual "exactly 3 care points" 3 (length carePoints3)+    let idxs = map cpIndex carePoints3+    assertBool "idx 0 present" (0 `elem` idxs)+    assertBool "idx 1 present" (1 `elem` idxs)+    assertBool "idx 2 present" (2 `elem` idxs)+    assertBool "idx 3 absent" (3 `notElem` idxs)++carePointsBothCorrect :: Test+carePointsBothCorrect = TestCase $ do+    let df2 =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0] :: [Double]))+                ]+        cps =+            identifyCarePoints @T.Text+                "label"+                df2+                (V.fromList [0, 1])+                (Leaf "A")+                (Leaf "A")+    assertEqual "no care points when both subtrees agree" 0 (length cps)++------------------------------------------------------------------------+-- Unit tests: majorityValueFromIndices+------------------------------------------------------------------------++majorityVoteTest :: Test+majorityVoteTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["cat", "dog", "cat", "cat"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+    assertEqual+        "majority is cat (3 votes)"+        "cat"+        (majorityValueFromIndices @T.Text "label" df (V.fromList [0, 1, 2, 3]))++majorityVoteSubset :: Test+majorityVoteSubset = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["cat", "dog", "cat", "cat"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        -- indices [0,1,3]: cat×2, dog×1 → "cat" wins clearly+        result = majorityValueFromIndices @T.Text "label" df (V.fromList [0, 1, 3])+    assertEqual "majority from subset [0,1,3] is cat" "cat" result++------------------------------------------------------------------------+-- Unit tests: computeTreeLoss+------------------------------------------------------------------------++computeLossZero :: Test+computeLossZero = TestCase $ do+    -- target = ["A","A","B","B"]: perfect split on x <= 2.5+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        loss = computeTreeLoss @T.Text "label" df (V.fromList [0, 1, 2, 3]) stump+    assertEqual "perfect stump has zero loss" 0.0 loss++computeLossHalf :: Test+computeLossHalf = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        constTree = Leaf "A" :: Tree T.Text+        loss = computeTreeLoss @T.Text "label" df (V.fromList [0, 1, 2, 3]) constTree+    assertEqual "constant leaf misclassifies half of balanced data" 0.5 loss++------------------------------------------------------------------------+-- Unit tests: partitionIndices+------------------------------------------------------------------------++partitionDisjoint :: Test+partitionDisjoint = TestCase $ do+    let (lft, rgt) = partitionIndices splitCond fixtureDF allIndices+        leftSet = V.toList lft+        rightSet = V.toList rgt+        intersection = filter (`elem` rightSet) leftSet+    assertEqual "left and right partitions are disjoint" [] intersection++partitionUnion :: Test+partitionUnion = TestCase $ do+    let (lft, rgt) = partitionIndices splitCond fixtureDF allIndices+        combined = sort (V.toList lft ++ V.toList rgt)+    assertEqual+        "union of partitions equals the original index set"+        [0, 1, 2, 3]+        combined++------------------------------------------------------------------------+-- Unit tests: countCarePointErrors+------------------------------------------------------------------------++countErrorsAllCorrect :: Test+countErrorsAllCorrect = TestCase $ do+    -- x <= 1.5: idx 0 goes left (True), idx 1 goes right (False)+    -- CarePoint 0 GoLeft  → goesLeft=True,  shouldGoLeft=True  → correct+    -- CarePoint 1 GoRight → goesLeft=False, shouldGoLeft=False → correct+    let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]+        cond = F.col @Double "x" .<= F.lit (1.5 :: Double)+        errs = countCarePointErrors cond fixtureDF cps+    assertEqual "condition routes all care points correctly" 0 errs++countErrorsAllWrong :: Test+countErrorsAllWrong = TestCase $ do+    -- x > 1.5: idx 0 goes right (False), idx 1 goes left (True) — both wrong+    -- CarePoint 0 GoLeft  → goesLeft=False, shouldGoLeft=True  → wrong+    -- CarePoint 1 GoRight → goesLeft=True,  shouldGoLeft=False → wrong+    let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]+        cond = F.col @Double "x" .> F.lit (1.5 :: Double)+        errs = countCarePointErrors cond fixtureDF cps+    assertEqual "reversed condition misroutes all care points" 2 errs++------------------------------------------------------------------------+-- Unit tests: predictWithTree+------------------------------------------------------------------------++predictLeaf :: Test+predictLeaf =+    TestCase $+        assertEqual+            "leaf prediction ignores row index"+            "Z"+            (predictWithTree @T.Text "label" fixtureDF 0 (Leaf "Z"))++predictBranch :: Test+predictBranch = TestCase $ do+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+    assertEqual+        "idx 0 (x=1.0 <= 2.5) routes left -> A"+        "A"+        (predictWithTree @T.Text "label" fixtureDF 0 stump)+    assertEqual+        "idx 3 (x=4.0 > 2.5) routes right -> B"+        "B"+        (predictWithTree @T.Text "label" fixtureDF 3 stump)++------------------------------------------------------------------------+-- Integration tests+------------------------------------------------------------------------++-- 20-row, linearly separable: x in [1..10] -> "pos", x in [11..20] -> "neg"+sepDF :: D.DataFrame+sepDF =+    let xs = map fromIntegral [1 .. 20 :: Int] :: [Double]+        labels = map (\x -> if x <= 10.0 then "pos" else "neg") xs :: [T.Text]+     in D.fromNamedColumns+            [ ("label", DI.fromList labels)+            , ("x", DI.fromList xs)+            ]++-- Candidate conditions that bracket the decision boundary+sepConds :: [Expr Bool]+sepConds =+    [ F.col @Double "x" .<= F.lit (10.5 :: Double)+    , F.col @Double "x" .> F.lit (10.5 :: Double)+    ]++testCfg :: TreeConfig+testCfg =+    defaultTreeConfig+        { taoIterations = 5+        , expressionPairs = 4+        , minLeafSize = 1+        }++-- Initial tree deliberately wrong: routes "pos" rows to the "neg" leaf+wrongStump :: Tree T.Text+wrongStump =+    Branch+        (F.col @Double "x" .> F.lit (10.5 :: Double))+        (Leaf "pos")+        (Leaf "neg")++taoNoDegradation :: Test+taoNoDegradation = TestCase $ do+    let indices = V.enumFromN 0 20+        initialLoss = computeTreeLoss @T.Text "label" sepDF indices wrongStump+        optimized =+            taoOptimize @T.Text testCfg "label" sepConds sepDF indices wrongStump+        finalLoss = computeTreeLoss @T.Text "label" sepDF indices optimized+    assertBool+        "taoOptimize must not increase loss"+        (finalLoss <= initialLoss + 1e-9)++taoMonotone :: Test+taoMonotone = TestCase $ do+    let indices = V.enumFromN 0 20+        initLoss = computeTreeLoss @T.Text "label" sepDF indices wrongStump+        stepTree = taoIteration @T.Text testCfg "label" sepConds sepDF indices+        -- Track (tree, loss) pairs: take 6 snapshots (initial + 5 steps)+        step (tree, _) =+            let tree' = stepTree tree+             in (tree', computeTreeLoss @T.Text "label" sepDF indices tree')+        snapshots = take 6 $ iterate step (wrongStump, initLoss)+        losses = map snd snapshots+        pairs = zip losses (tail losses)+    assertBool+        "loss must be non-increasing across taoIteration steps"+        (all (\(a, b) -> b <= a + 1e-9) pairs)++taoConvergesPureLabels :: Test+taoConvergesPureLabels = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (replicate 10 ("A" :: T.Text)))+                , ("x", DI.fromList ([1.0 .. 10.0] :: [Double]))+                ]+        indices = V.enumFromN 0 10+        initTree = Leaf "A" :: Tree T.Text+        initLoss = computeTreeLoss @T.Text "label" df indices initTree+        result =+            taoOptimize @T.Text testCfg "label" sepConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual "pure-label initial loss must be zero" 0.0 initLoss+    assertEqual "pure-label final loss must still be zero" 0.0 finalLoss++taoDeadBranchNoCrash :: Test+taoDeadBranchNoCrash = TestCase $ do+    -- Threshold below all x values: x <= 0.5 is False for every row+    -- → all indices route to the right child; left partition is always empty+    let badCond = F.col @Double "x" .<= F.lit (0.5 :: Double)+        indices = V.enumFromN 0 20+        initTree = Branch badCond (Leaf "pos") (Leaf "neg") :: Tree T.Text+        result =+            taoOptimize @T.Text testCfg "label" [badCond] sepDF indices initTree+        finalLoss = computeTreeLoss @T.Text "label" sepDF indices result+    assertBool+        "dead-branch tree must produce a valid loss in [0,1]"+        (finalLoss >= 0.0 && finalLoss <= 1.0)++------------------------------------------------------------------------+-- Shared fixtures: 4x4 grid+------------------------------------------------------------------------++gridPairs :: [(Double, Double)]+gridPairs = [(x, y) | y <- [1 .. 4], x <- [1 .. 4]]++gridBaseDF :: D.DataFrame+gridBaseDF =+    D.fromNamedColumns+        [ ("x", DI.fromList (map fst gridPairs))+        , ("y", DI.fromList (map snd gridPairs))+        ]++------------------------------------------------------------------------+-- Oblique recovery tests+------------------------------------------------------------------------++taoRecoversSingleObliqueDerived :: Test+taoRecoversSingleObliqueDerived = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("pos" :: T.Text))+                (F.lit ("neg" :: T.Text))+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (2.5 :: Double))+                (Leaf "pos")+                (Leaf "neg") ::+                Tree T.Text+        conds =+            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)+            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)+            ]+        cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        result = taoOptimize @T.Text cfg "label" conds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "TAO recovers single oblique (x+y) split with zero loss"+        0.0+        finalLoss++taoRecoversNestedObliqueDerived :: Test+taoRecoversNestedObliqueDerived = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("low" :: T.Text))+                ( F.ifThenElse+                    ((F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double))+                    (F.lit "mid")+                    (F.lit "high")+                )+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (1.5 :: Double))+                (Leaf "low")+                ( Branch+                    (F.col @Double "y" .<= F.lit (3.5 :: Double))+                    (Leaf "mid")+                    (Leaf "high")+                ) ::+                Tree T.Text+        conds =+            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)+            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)+            , (F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double)+            , (F.col @Double "x" - F.col @Double "y") .> F.lit (0.5 :: Double)+            ]+        cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        result = taoOptimize @T.Text cfg "label" conds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertEqual+        "TAO recovers nested oblique (x+y)/(x-y) tree with zero loss"+        0.0+        finalLoss++taoAxisAlignedInsufficientForOblique :: Test+taoAxisAlignedInsufficientForOblique = TestCase $ do+    let labelExpr =+            F.ifThenElse+                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))+                (F.lit ("pos" :: T.Text))+                (F.lit ("neg" :: T.Text))+        df = D.derive @T.Text "label" labelExpr gridBaseDF+        indices = V.enumFromN 0 16+        axisConds =+            [F.col @Double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+                ++ [F.col @Double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]+        initTree =+            Branch+                (F.col @Double "x" .<= F.lit (2.5 :: Double))+                (Leaf "pos")+                (Leaf "neg") ::+                Tree T.Text+        cfg = defaultTreeConfig{taoIterations = 10, expressionPairs = 6, minLeafSize = 1}+        result = taoOptimize @T.Text cfg "label" axisConds df indices initTree+        finalLoss = computeTreeLoss @T.Text "label" df indices result+    assertBool+        "axis-aligned stump cannot recover oblique label (loss must remain > 0.1)"+        (finalLoss > 0.1)++------------------------------------------------------------------------+-- Nullable numeric feature tests+------------------------------------------------------------------------++-- Cleanly separable: Just 1..6 -> "pos", Just 7..12 -> "neg", no nulls.+-- Uses OptionalColumn directly to exercise the new nullable numeric path.+nullableSepDF :: D.DataFrame+nullableSepDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (replicate 6 "pos" ++ replicate 6 "neg" :: [T.Text]))+        ,+            ( "x"+            , DI.OptionalColumn+                ( V.fromList $+                    map (Just . fromIntegral) ([1 .. 6] :: [Int])+                        ++ map (Just . fromIntegral) ([7 .. 12] :: [Int]) ::+                    V.Vector (Maybe Double)+                )+            )+        ]++-- DF with genuine nulls interspersed.+nullsMixedDF :: D.DataFrame+nullsMixedDF =+    D.fromNamedColumns+        [ ("label", DI.fromList (["pos", "pos", "pos", "neg", "neg", "neg"] :: [T.Text]))+        ,+            ( "x"+            , DI.OptionalColumn+                ( V.fromList+                    [Just 1.0, Nothing, Just 3.0, Just 7.0, Nothing, Just 9.0] ::+                    V.Vector (Maybe Double)+                )+            )+        ]++-- numericCols picks up OptionalColumn (Maybe Double) as NMaybeDouble.+numericColsNullableDoubleTest :: Test+numericColsNullableDoubleTest = TestCase $ do+    let exprs = numericCols nullableSepDF+        hasMD = any (\case NMaybeDouble _ -> True; _ -> False) exprs+    assertBool+        "numericCols finds NMaybeDouble for OptionalColumn (Maybe Double)"+        hasMD++-- numericCols picks up OptionalColumn (Maybe Int) as NMaybeDouble (via whenPresent).+numericColsNullableIntTest :: Test+numericColsNullableIntTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["pos", "neg"] :: [T.Text]))+                ,+                    ( "n"+                    , DI.OptionalColumn (V.fromList [Just (1 :: Int), Just 2] :: V.Vector (Maybe Int))+                    )+                ]+        hasMD = any (\case NMaybeDouble _ -> True; _ -> False) (numericCols df)+    assertBool "numericCols finds NMaybeDouble for OptionalColumn (Maybe Int)" hasMD++-- generateNumericConds is non-empty for a DF with an OptionalColumn (Maybe Double).+numericCondsNullableNonEmptyTest :: Test+numericCondsNullableNonEmptyTest =+    TestCase $+        assertBool+            "generateNumericConds non-empty for OptionalColumn (Maybe Double)"+            (not (null (generateNumericConds defaultTreeConfig nullableSepDF)))++-- Null values evaluate to False for threshold conditions (null rows route right).+nullValueRoutesFalseTest :: Test+nullValueRoutesFalseTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "B"] :: [T.Text]))+                ,+                    ( "x"+                    , DI.OptionalColumn+                        (V.fromList [Nothing, Just (5.0 :: Double)] :: V.Vector (Maybe Double))+                    )+                ]+        -- Nothing <= 6.0 = Nothing  -> fromMaybe False = False -> right+        -- Just 5.0 <= 6.0 = Just True -> fromMaybe False = True  -> left+        cond = F.fromMaybe False (F.col @(Maybe Double) "x" .<= F.lit (6.0 :: Double))+        (lft, rgt) = partitionIndices cond df (V.fromList [0, 1])+    assertBool "null row (idx 0) routes to right (false) partition" (0 `V.elem` rgt)+    assertBool "Just 5.0 <= 6.0 routes to left (true) partition" (1 `V.elem` lft)++-- fitDecisionTree with an OptionalColumn nullable feature achieves zero loss+-- on cleanly separable data (no actual nulls in the column).+nullableFitZeroLossTest :: Test+nullableFitZeroLossTest = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        featureDf = D.exclude ["label"] nullableSepDF+        conds = generateNumericConds cfg featureDf+        initTree = buildGreedyTree @T.Text cfg (maxTreeDepth cfg) "label" conds nullableSepDF+        indices = V.enumFromN 0 12+        result = taoOptimize @T.Text cfg "label" conds nullableSepDF indices initTree+        loss = computeTreeLoss @T.Text "label" nullableSepDF indices result+    assertEqual "zero loss on cleanly separable OptionalColumn data" 0.0 loss++-- fitDecisionTree with genuine nulls: loss is a valid probability and no crash.+nullableFitWithNullsNoCrashTest :: Test+nullableFitWithNullsNoCrashTest = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 3, expressionPairs = 4, minLeafSize = 1}+        featureDf = D.exclude ["label"] nullsMixedDF+        conds = generateNumericConds cfg featureDf+        initTree = buildGreedyTree @T.Text cfg (maxTreeDepth cfg) "label" conds nullsMixedDF+        indices = V.enumFromN 0 6+        result = taoOptimize @T.Text cfg "label" conds nullsMixedDF indices initTree+        loss = computeTreeLoss @T.Text "label" nullsMixedDF indices result+    assertBool+        "loss is in [0,1] with null values present"+        (loss >= 0.0 && loss <= 1.0)++-- numericExprsWithTerms produces cross-column combinations when one col is+-- OptionalColumn (Maybe Double) and another is a plain UnboxedColumn Double.+numericExprsWithTermsMixedTest :: Test+numericExprsWithTermsMixedTest = TestCase $ do+    let df =+            D.fromNamedColumns+                [+                    ( "x"+                    , DI.OptionalColumn+                        (V.fromList [Just 1.0, Just 2.0, Just 3.0] :: V.Vector (Maybe Double))+                    )+                , ("y", DI.fromList ([4.0, 5.0, 6.0] :: [Double]))+                ]+        cfg = defaultSynthConfig{maxExprDepth = 2, enableArithOps = True}+        exprs = numericExprsWithTerms cfg df+    assertBool+        "more than 2 expressions: base cols + combinations"+        (length exprs > 2)+    assertBool+        "combined exprs include NMaybeDouble (nullable arithmetic)"+        (any (\case NMaybeDouble _ -> True; _ -> False) exprs)++------------------------------------------------------------------------+-- Probability tree tests+------------------------------------------------------------------------++-- probsFromIndices: counts correct on a 3-row slice+probsFromIndicesBasic :: Test+probsFromIndicesBasic = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+                ]+        probs = probsFromIndices @T.Text "label" df (V.fromList [0, 1, 2])+    assertBool "A prob ≈ 2/3" (abs (probs M.! "A" - 2 / 3) < 1e-9)+    assertBool "B prob ≈ 1/3" (abs (probs M.! "B" - 1 / 3) < 1e-9)++-- probsFromIndices: only a subset of rows counted+probsFromIndicesSubset :: Test+probsFromIndicesSubset = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "B", "B"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0, 4.0] :: [Double]))+                ]+        probs = probsFromIndices @T.Text "label" df (V.fromList [0, 1])+    assertEqual "only rows 0,1 → A:1.0" (M.fromList [("A", 1.0)]) probs++-- probsFromIndices: single class → probability 1.0+probsFromIndicesSingleClass :: Test+probsFromIndicesSingleClass = TestCase $ do+    let probs = probsFromIndices @T.Text "label" fixtureDF (V.fromList [0, 2])+    assertEqual "rows 0,2 both A → A:1.0" (M.fromList [("A", 1.0)]) probs++-- buildProbTree: Leaf preserves distribution+buildProbTreeLeaf :: Test+buildProbTreeLeaf = TestCase $ do+    let df =+            D.fromNamedColumns+                [ ("label", DI.fromList (["A", "A", "A"] :: [T.Text]))+                , ("x", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+                ]+        pt = buildProbTree @T.Text (Leaf "A") "label" df (V.fromList [0, 1, 2])+    case pt of+        Leaf m -> assertEqual "pure-A leaf → {A:1.0}" (M.fromList [("A", 1.0)]) m+        _ -> assertFailure "expected Leaf"++-- buildProbTree: Branch distributes rows to left/right leaves correctly+buildProbTreeBranch :: Test+buildProbTreeBranch = TestCase $ do+    -- splitCond: x <= 2.5 → idx 0,1 go left; idx 2,3 go right+    -- left  (idx 0,1): labels ["A","B"] → {A:0.5, B:0.5}+    -- right (idx 2,3): labels ["A","C"] → {A:0.5, C:0.5}+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        pt = buildProbTree @T.Text stump "label" fixtureDF allIndices+    case pt of+        Branch _ (Leaf lm) (Leaf rm) -> do+            assertBool "left leaf has A:0.5" (abs (M.findWithDefault 0 "A" lm - 0.5) < 1e-9)+            assertBool "left leaf has B:0.5" (abs (M.findWithDefault 0 "B" lm - 0.5) < 1e-9)+            assertBool+                "right leaf has A:0.5"+                (abs (M.findWithDefault 0 "A" rm - 0.5) < 1e-9)+            assertBool+                "right leaf has C:0.5"+                (abs (M.findWithDefault 0 "C" rm - 0.5) < 1e-9)+        _ -> assertFailure "expected Branch with two Leaves"++-- probExprs: leaf tree produces Lit values+probExprsLeaf :: Test+probExprsLeaf = TestCase $ do+    let pt = Leaf (M.fromList [("A", 0.75), ("B", 0.25)]) :: ProbTree T.Text+        pe = probExprs pt+    assertEqual "A expr is Lit 0.75" (Lit 0.75) (pe M.! "A")+    assertEqual "B expr is Lit 0.25" (Lit 0.25) (pe M.! "B")++-- probExprs: class absent from one leaf gets Lit 0.0 on that side+probExprsMissingClass :: Test+probExprsMissingClass = TestCase $ do+    let pt =+            Branch+                splitCond+                (Leaf (M.fromList [("A", 1.0)]))+                (Leaf (M.fromList [("B", 1.0)])) ::+                ProbTree T.Text+        pe = probExprs pt+    assertEqual+        "A expr: If cond (Lit 1.0) (Lit 0.0)"+        (F.ifThenElse splitCond (Lit 1.0) (Lit 0.0))+        (pe M.! "A")+    assertEqual+        "B expr: If cond (Lit 0.0) (Lit 1.0)"+        (F.ifThenElse splitCond (Lit 0.0) (Lit 1.0))+        (pe M.! "B")++-- probExprs: keys equal all classes that appear across any leaf+probExprsAllClasses :: Test+probExprsAllClasses = TestCase $ do+    let pt =+            Branch+                splitCond+                (Leaf (M.fromList [("A", 1.0)]))+                (Leaf (M.fromList [("B", 0.6), ("C", 0.4)])) ::+                ProbTree T.Text+        pe = probExprs pt+    assertEqual "three classes in result" (sort ["A", "B", "C"]) (sort (M.keys pe))++-- Probabilities sum to 1.0 at every row after applying probExprs+probsSumToOne :: Test+probsSumToOne = TestCase $ do+    let stump = Branch splitCond (Leaf "A") (Leaf "B") :: Tree T.Text+        pt = buildProbTree @T.Text stump "label" fixtureDF allIndices+        pe = probExprs pt+        sumExpr = foldl1 (.+) (M.elems pe)+    case interpret @Double fixtureDF sumExpr of+        Left e -> assertFailure (show e)+        Right (DI.TColumn col) ->+            case DI.toVector @Double col of+                Left e -> assertFailure (show e)+                Right vals ->+                    mapM_+                        (\v -> assertBool ("sum ≈ 1.0, got " ++ show v) (abs (v - 1.0) < 1e-9))+                        (V.toList vals)++-- argmax of probExprs agrees with fitDecisionTree on sepDF+probArgmaxMatchesClassifier :: Test+probArgmaxMatchesClassifier = TestCase $ do+    let cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}+        hardExpr = fitDecisionTree @T.Text cfg (Col "label") sepDF+        pe = fitProbTree @T.Text cfg (Col "label") sepDF+        indices = [0 .. D.nRows sepDF - 1]+    case interpret @T.Text sepDF hardExpr of+        Left e -> assertFailure (show e)+        Right (DI.TColumn hardCol) ->+            case DI.toVector @T.Text hardCol of+                Left e -> assertFailure (show e)+                Right hardVals -> do+                    probCols <-+                        mapM+                            ( \(cls, expr) -> case interpret @Double sepDF expr of+                                Left e -> assertFailure (show e) >> return (cls, V.empty)+                                Right (DI.TColumn col) -> case DI.toVector @Double col of+                                    Left e -> assertFailure (show e) >> return (cls, V.empty)+                                    Right v -> return (cls, v)+                            )+                            (M.toList pe)+                    mapM_+                        ( \i ->+                            let argmax = fst $ maximumBy (compare `on` (V.! i) . snd) probCols+                                hard = hardVals V.! i+                             in assertEqual ("row " ++ show i) hard argmax+                        )+                        indices++------------------------------------------------------------------------+-- Test list+------------------------------------------------------------------------++tests :: [Test]+tests =+    [ TestLabel "carePointsBothWrong" carePointsBothWrong+    , TestLabel "carePointsLeftCorrect" carePointsLeftCorrect+    , TestLabel "carePointsRightCorrect" carePointsRightCorrect+    , TestLabel "carePointsMixed" carePointsMixed+    , TestLabel "carePointsBothCorrect" carePointsBothCorrect+    , TestLabel "majorityVoteTest" majorityVoteTest+    , TestLabel "majorityVoteSubset" majorityVoteSubset+    , TestLabel "computeLossZero" computeLossZero+    , TestLabel "computeLossHalf" computeLossHalf+    , TestLabel "partitionDisjoint" partitionDisjoint+    , TestLabel "partitionUnion" partitionUnion+    , TestLabel "countErrorsAllCorrect" countErrorsAllCorrect+    , TestLabel "countErrorsAllWrong" countErrorsAllWrong+    , TestLabel "predictLeaf" predictLeaf+    , TestLabel "predictBranch" predictBranch+    , TestLabel "taoNoDegradation" taoNoDegradation+    , TestLabel "taoMonotone" taoMonotone+    , TestLabel "taoConvergesPureLabels" taoConvergesPureLabels+    , TestLabel "taoDeadBranchNoCrash" taoDeadBranchNoCrash+    , TestLabel "taoRecoversSingleObliqueDerived" taoRecoversSingleObliqueDerived+    , TestLabel "taoRecoversNestedObliqueDerived" taoRecoversNestedObliqueDerived+    , TestLabel+        "taoAxisAlignedInsufficientForOblique"+        taoAxisAlignedInsufficientForOblique+    , TestLabel "numericColsNullableDouble" numericColsNullableDoubleTest+    , TestLabel "numericColsNullableInt" numericColsNullableIntTest+    , TestLabel "numericCondsNullableNonEmpty" numericCondsNullableNonEmptyTest+    , TestLabel "nullValueRoutesFalse" nullValueRoutesFalseTest+    , TestLabel "nullableFitZeroLoss" nullableFitZeroLossTest+    , TestLabel "nullableFitWithNullsNoCrash" nullableFitWithNullsNoCrashTest+    , TestLabel "numericExprsWithTermsMixed" numericExprsWithTermsMixedTest+    , TestLabel "probsFromIndicesBasic" probsFromIndicesBasic+    , TestLabel "probsFromIndicesSubset" probsFromIndicesSubset+    , TestLabel "probsFromIndicesSingleClass" probsFromIndicesSingleClass+    , TestLabel "buildProbTreeLeaf" buildProbTreeLeaf+    , TestLabel "buildProbTreeBranch" buildProbTreeBranch+    , TestLabel "probExprsLeaf" probExprsLeaf+    , TestLabel "probExprsMissingClass" probExprsMissingClass+    , TestLabel "probExprsAllClasses" probExprsAllClasses+    , TestLabel "probsSumToOne" probsSumToOne+    , TestLabel "probArgmaxMatchesClassifier" probArgmaxMatchesClassifier+    ]
tests/LazyParquet.hs view
@@ -110,7 +110,7 @@         ( do             actual <-                 L.runDataFrame-                    ( L.limit+                    ( L.take                         3                         ( L.sortBy                             [("id", Ascending)]
tests/Main.hs view
@@ -9,6 +9,7 @@ import Test.HUnit import Test.QuickCheck +import qualified DecisionTree import qualified Functions import qualified IO.JSON import qualified Internal.Parsing@@ -23,6 +24,8 @@ import qualified Operations.InsertColumn import qualified Operations.Join import qualified Operations.Merge+import qualified Operations.Nullable+import qualified Operations.Provenance import qualified Operations.ReadCsv import qualified Operations.Shuffle import qualified Operations.Sort@@ -36,7 +39,8 @@ tests :: Test tests =     TestList $-        Internal.Parsing.tests+        DecisionTree.tests+            ++ Internal.Parsing.tests             ++ Operations.Aggregations.tests             ++ Operations.Apply.tests             ++ Operations.Core.tests@@ -46,10 +50,13 @@             ++ Operations.InsertColumn.tests             ++ Operations.Join.tests             ++ Operations.Merge.tests+            ++ Operations.Nullable.tests+            ++ Operations.Provenance.tests             ++ Operations.ReadCsv.tests             ++ Operations.Shuffle.tests             ++ Operations.Sort.tests             ++ Operations.Statistics.tests+            ++ Operations.Subset.hunitTests             ++ Operations.Take.tests             ++ Operations.Typing.tests             ++ Functions.tests
tests/Monad.hs view
@@ -22,6 +22,8 @@     let finalRowCount = D.nRows finalDf     let realRate = roundToTwoPlaces $ fromIntegral finalRowCount / fromIntegral rowCount     let diff = abs $ expectedRate - realRate-    assert (diff <= 0.11)+    -- calculates the 99.99% confidence interval (quickcheck runs 100 tests, aim for 1/10000)+    let tolerance = 3.89 * sqrt (expectedRate * (1 - expectedRate) / fromIntegral rowCount)+    assert (diff <= tolerance)  tests = [prop_sampleM]
tests/Operations/Apply.hs view
@@ -13,6 +13,7 @@ import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.DataFrame as DI+import DataFrame.Operations.Statistics (imputeWith) import DataFrame.Operations.Transformations (impute)  import Assertions@@ -238,12 +239,30 @@ imputeOnNonOptional :: Test imputeOnNonOptional =     TestCase-        ( assertExpectException-            "[Error Case]"-            "Cannot impute to a non-Empty column: plain"-            (print $ impute (F.col @(Maybe Int) "plain") 0 imputeData)+        ( assertEqual+            "impute is a no-op on a non-nullable column"+            imputeData+            (impute (F.col @(Maybe Int) "plain") 0 imputeData)         ) +imputePlainNoOp :: Test+imputePlainNoOp =+    TestCase+        ( assertEqual+            "impute with non-Maybe expr is always a no-op"+            imputeData+            (impute (F.col @Int "plain") 0 imputeData)+        )++imputeWithPlainNoOp :: Test+imputeWithPlainNoOp =+    TestCase+        ( assertEqual+            "imputeWith with non-Maybe expr is always a no-op"+            imputeData+            (imputeWith id (F.col @Int "plain") imputeData)+        )+ tests :: [Test] tests =     [ TestLabel "applyBoxedToUnboxed" applyBoxedToUnboxed@@ -263,4 +282,6 @@     , TestLabel "imputeHappyPath" imputeHappyPath     , TestLabel "imputeColumnNotFound" imputeColumnNotFound     , TestLabel "imputeOnNonOptional" imputeOnNonOptional+    , TestLabel "imputePlainNoOp" imputePlainNoOp+    , TestLabel "imputeWithPlainNoOp" imputeWithPlainNoOp     ]
+ tests/Operations/Nullable.hs view
@@ -0,0 +1,773 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Nullable where++import qualified Data.Vector as V+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.Internal.Expression (Expr)+import DataFrame.Operators ((.*), (.+), (.-), (./), (.==))+import qualified DataFrame.Typed as DT+import qualified DataFrame.Typed.Expr as TE+import DataFrame.Typed.Types (TExpr (..))++import Test.HUnit++-- ---------------------------------------------------------------------------+-- Untyped Expr layer tests+-- ---------------------------------------------------------------------------++-- DataFrame with one plain Int column and one Maybe Int column+testData :: D.DataFrame+testData =+    D.fromNamedColumns+        [ ("x", DI.fromList ([1, 2, 3] :: [Int]))+        , ("y", DI.OptionalColumn (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+        ]++-- | col @Int .+ col @(Maybe Int)  should give Maybe Int column+addIntMaybeInt :: Test+addIntMaybeInt =+    TestCase+        ( assertEqual+            "Int .+ Maybe Int = Maybe Int"+            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @Int "x" .+ F.col @(Maybe Int) "y")+                    testData+            )+        )++-- | col @(Maybe Int) .+ col @Int  should give Maybe Int column+addMaybeIntInt :: Test+addMaybeIntInt =+    TestCase+        ( assertEqual+            "Maybe Int .+ Int = Maybe Int"+            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @(Maybe Int) "y" .+ F.col @Int "x")+                    testData+            )+        )++-- | col @Int .+ col @Int  (same-type non-nullable) should give Int column+addIntInt :: Test+addIntInt =+    TestCase+        ( assertEqual+            "Int .+ Int = Int (non-nullable preserved)"+            (Just $ DI.fromList [2, 4, 6 :: Int])+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @Int "x" .+ F.col @Int "x")+                    testData+            )+        )++-- | col @(Maybe Int) .+ col @(Maybe Int)  should give Maybe Int column+addMaybeMaybe :: Test+addMaybeMaybe =+    TestCase+        ( assertEqual+            "Maybe Int .+ Maybe Int = Maybe Int"+            (Just $ DI.OptionalColumn (V.fromList [Just 20, Nothing, Just 60 :: Maybe Int]))+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @(Maybe Int) "y" .+ F.col @(Maybe Int) "y")+                    testData+            )+        )++-- | Subtraction: Int .- Maybe Int+subIntMaybeInt :: Test+subIntMaybeInt =+    TestCase+        ( assertEqual+            "Int .- Maybe Int = Maybe Int"+            ( Just $+                DI.OptionalColumn (V.fromList [Just (-9), Nothing, Just (-27) :: Maybe Int])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @Int "x" .- F.col @(Maybe Int) "y")+                    testData+            )+        )++-- | Comparison: Int .== Maybe Int gives Maybe Bool+eqIntMaybeInt :: Test+eqIntMaybeInt =+    TestCase+        ( assertEqual+            "Int .== Maybe Int = Maybe Bool"+            ( Just $+                DI.OptionalColumn (V.fromList [Just False, Nothing, Just False :: Maybe Bool])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @Int "x" .== F.col @(Maybe Int) "y" :: Expr (Maybe Bool))+                    testData+            )+        )++-- | Comparison: Int .== Int  gives plain Bool (backwards compatible)+eqIntInt :: Test+eqIntInt =+    TestCase+        ( assertEqual+            "Int .== Int = Bool (non-nullable, backwards compatible)"+            (Just $ DI.fromList [True, False, False :: Bool])+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @Int "x" .== F.lit (1 :: Int) :: Expr Bool)+                    testData+            )+        )++-- ---------------------------------------------------------------------------+-- nullLift / nullLift2 — untyped layer+-- ---------------------------------------------------------------------------++-- | nullLift negate on Maybe Int propagates Nothing+nullLiftMaybeInt :: Test+nullLiftMaybeInt =+    TestCase+        ( assertEqual+            "nullLift negate (Maybe Int) propagates Nothing"+            ( Just $+                DI.OptionalColumn (V.fromList [Just (-10), Nothing, Just (-30) :: Maybe Int])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.nullLift negate (F.col @(Maybe Int) "y") :: Expr (Maybe Int))+                    testData+            )+        )++-- | nullLift negate on plain Int — no Nothing, plain result+nullLiftInt :: Test+nullLiftInt =+    TestCase+        ( assertEqual+            "nullLift negate (Int) gives Int column"+            (Just $ DI.fromList [-1, -2, -3 :: Int])+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.nullLift negate (F.col @Int "x") :: Expr Int)+                    testData+            )+        )++-- | nullLift2 (+) Int (Maybe Int) → Maybe Int+nullLift2IntMaybeInt :: Test+nullLift2IntMaybeInt =+    TestCase+        ( assertEqual+            "nullLift2 (+) Int (Maybe Int) = Maybe Int"+            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.nullLift2 (+) (F.col @Int "x") (F.col @(Maybe Int) "y") :: Expr (Maybe Int))+                    testData+            )+        )++-- | nullLift2 (+) (Maybe Int) Int → Maybe Int+nullLift2MaybeIntInt :: Test+nullLift2MaybeIntInt =+    TestCase+        ( assertEqual+            "nullLift2 (+) (Maybe Int) Int = Maybe Int"+            (Just $ DI.OptionalColumn (V.fromList [Just 11, Nothing, Just 33 :: Maybe Int]))+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.nullLift2 (+) (F.col @(Maybe Int) "y") (F.col @Int "x") :: Expr (Maybe Int))+                    testData+            )+        )++-- | nullLift2 (+) Int Int → plain Int+nullLift2IntInt :: Test+nullLift2IntInt =+    TestCase+        ( assertEqual+            "nullLift2 (+) Int Int = Int (non-nullable)"+            (Just $ DI.fromList [2, 4, 6 :: Int])+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.nullLift2 (+) (F.col @Int "x") (F.col @Int "x") :: Expr Int)+                    testData+            )+        )++-- ---------------------------------------------------------------------------+-- Cross-type arithmetic (Int × Double promotion)+-- ---------------------------------------------------------------------------++-- DataFrame with Int, Maybe Int, Double, Maybe Double columns+crossData :: D.DataFrame+crossData =+    D.fromNamedColumns+        [ ("x", DI.fromList ([1, 2, 3] :: [Int]))+        , ("y", DI.OptionalColumn (V.fromList [Just 10, Nothing, Just 30 :: Maybe Int]))+        , ("d", DI.fromList ([1.5, 2.5, 3.5] :: [Double]))+        ,+            ( "md"+            , DI.OptionalColumn (V.fromList [Just 10.5, Nothing, Just 30.5 :: Maybe Double])+            )+        ]++-- | col @Int .+ col @Double → Double+addIntDouble :: Test+addIntDouble =+    TestCase+        ( assertEqual+            "Int .+ Double = Double"+            (Just $ DI.fromList [2.5, 4.5, 6.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .+ F.col @Double "d") crossData+            )+        )++-- | col @Double .+ col @Int → Double+addDoubleInt :: Test+addDoubleInt =+    TestCase+        ( assertEqual+            "Double .+ Int = Double"+            (Just $ DI.fromList [2.5, 4.5, 6.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Double "d" .+ F.col @Int "x") crossData+            )+        )++-- | col @(Maybe Int) .+ col @Double → Maybe Double+addMaybeIntDouble :: Test+addMaybeIntDouble =+    TestCase+        ( assertEqual+            "Maybe Int .+ Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @(Maybe Int) "y" .+ F.col @Double "d") crossData+            )+        )++-- | col @Int .+ col @(Maybe Double) → Maybe Double+addIntMaybeDouble :: Test+addIntMaybeDouble =+    TestCase+        ( assertEqual+            "Int .+ Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 11.5, Nothing, Just 33.5 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .+ F.col @(Maybe Double) "md") crossData+            )+        )++-- | col @(Maybe Int) .+ col @(Maybe Double) → Maybe Double+addMaybeIntMaybeDouble :: Test+addMaybeIntMaybeDouble =+    TestCase+        ( assertEqual+            "Maybe Int .+ Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 20.5, Nothing, Just 60.5 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @(Maybe Int) "y" .+ F.col @(Maybe Double) "md")+                    crossData+            )+        )++-- | col @Int .- col @Double → Double+subIntDouble :: Test+subIntDouble =+    TestCase+        ( assertEqual+            "Int .- Double = Double"+            (Just $ DI.fromList [-0.5, -0.5, -0.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .- F.col @Double "d") crossData+            )+        )++-- | col @Int .* col @Double → Double+mulIntDouble :: Test+mulIntDouble =+    TestCase+        ( assertEqual+            "Int .* Double = Double"+            (Just $ DI.fromList [1.5, 5.0, 10.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .* F.col @Double "d") crossData+            )+        )++-- | col @Double .- col @Int → Double+subDoubleInt :: Test+subDoubleInt =+    TestCase+        ( assertEqual+            "Double .- Int = Double"+            (Just $ DI.fromList [0.5, 0.5, 0.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Double "d" .- F.col @Int "x") crossData+            )+        )++-- | col @(Maybe Int) .- col @Double → Maybe Double+subMaybeIntDouble :: Test+subMaybeIntDouble =+    TestCase+        ( assertEqual+            "Maybe Int .- Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 8.5, Nothing, Just 26.5 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @(Maybe Int) "y" .- F.col @Double "d") crossData+            )+        )++-- | col @Int .- col @(Maybe Double) → Maybe Double+subIntMaybeDouble :: Test+subIntMaybeDouble =+    TestCase+        ( assertEqual+            "Int .- Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn+                    (V.fromList [Just (-9.5), Nothing, Just (-27.5) :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .- F.col @(Maybe Double) "md") crossData+            )+        )++-- | col @(Maybe Int) .- col @(Maybe Double) → Maybe Double+subMaybeIntMaybeDouble :: Test+subMaybeIntMaybeDouble =+    TestCase+        ( assertEqual+            "Maybe Int .- Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn+                    (V.fromList [Just (-0.5), Nothing, Just (-0.5) :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @(Maybe Int) "y" .- F.col @(Maybe Double) "md")+                    crossData+            )+        )++-- | col @Double .* col @Int → Double+mulDoubleInt :: Test+mulDoubleInt =+    TestCase+        ( assertEqual+            "Double .* Int = Double"+            (Just $ DI.fromList [1.5, 5.0, 10.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Double "d" .* F.col @Int "x") crossData+            )+        )++-- | col @(Maybe Int) .* col @Double → Maybe Double+mulMaybeIntDouble :: Test+mulMaybeIntDouble =+    TestCase+        ( assertEqual+            "Maybe Int .* Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 15.0, Nothing, Just 105.0 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @(Maybe Int) "y" .* F.col @Double "d") crossData+            )+        )++-- | col @Int .* col @(Maybe Double) → Maybe Double+mulIntMaybeDouble :: Test+mulIntMaybeDouble =+    TestCase+        ( assertEqual+            "Int .* Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 10.5, Nothing, Just 91.5 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" .* F.col @(Maybe Double) "md") crossData+            )+        )++-- | col @(Maybe Int) .* col @(Maybe Double) → Maybe Double+mulMaybeIntMaybeDouble :: Test+mulMaybeIntMaybeDouble =+    TestCase+        ( assertEqual+            "Maybe Int .* Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 105.0, Nothing, Just 915.0 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive+                    "result"+                    (F.col @(Maybe Int) "y" .* F.col @(Maybe Double) "md")+                    crossData+            )+        )++-- Division tests use clean-dividing data: x=[2,4,6], d=[1,2,3]+divData :: D.DataFrame+divData =+    D.fromNamedColumns+        [ ("x", DI.fromList ([2, 4, 6] :: [Int]))+        , ("y", DI.OptionalColumn (V.fromList [Just 4, Nothing, Just 6 :: Maybe Int]))+        , ("d", DI.fromList ([1.0, 2.0, 3.0] :: [Double]))+        ,+            ( "md"+            , DI.OptionalColumn (V.fromList [Just 1.0, Nothing, Just 3.0 :: Maybe Double])+            )+        ]++-- | col @Int ./ col @Double → Double+divIntDouble :: Test+divIntDouble =+    TestCase+        ( assertEqual+            "Int ./ Double = Double"+            (Just $ DI.fromList [2.0, 2.0, 2.0 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" ./ F.col @Double "d") divData+            )+        )++-- | col @Double ./ col @Int → Double+divDoubleInt :: Test+divDoubleInt =+    TestCase+        ( assertEqual+            "Double ./ Int = Double"+            (Just $ DI.fromList [0.5, 0.5, 0.5 :: Double])+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Double "d" ./ F.col @Int "x") divData+            )+        )++-- | col @(Maybe Int) ./ col @Double → Maybe Double+divMaybeIntDouble :: Test+divMaybeIntDouble =+    TestCase+        ( assertEqual+            "Maybe Int ./ Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @Double "d") divData+            )+        )++-- | col @Int ./ col @(Maybe Double) → Maybe Double+divIntMaybeDouble :: Test+divIntMaybeDouble =+    TestCase+        ( assertEqual+            "Int ./ Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 2.0, Nothing, Just 2.0 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @Int "x" ./ F.col @(Maybe Double) "md") divData+            )+        )++-- | col @(Maybe Int) ./ col @(Maybe Double) → Maybe Double+divMaybeIntMaybeDouble :: Test+divMaybeIntMaybeDouble =+    TestCase+        ( assertEqual+            "Maybe Int ./ Maybe Double = Maybe Double"+            ( Just $+                DI.OptionalColumn (V.fromList [Just 4.0, Nothing, Just 2.0 :: Maybe Double])+            )+            ( DI.getColumn "result" $+                D.derive "result" (F.col @(Maybe Int) "y" ./ F.col @(Maybe Double) "md") divData+            )+        )++-- ---------------------------------------------------------------------------+-- Typed TExpr layer — cross-type arithmetic+-- ---------------------------------------------------------------------------++type CrossSchema =+    '[ DT.Column "x" Int+     , DT.Column "y" (Maybe Int)+     , DT.Column "d" Double+     , DT.Column "md" (Maybe Double)+     ]++typedCrossData :: DT.TypedDataFrame CrossSchema+typedCrossData =+    either (error . show) id $+        DT.freezeWithError @CrossSchema crossData++-- | Typed: col @"x" .+ col @"d"  → Double+typedAddIntDouble :: Test+typedAddIntDouble =+    TestCase+        ( assertEqual+            "Typed: Int .+ Double = Double"+            [2.5, 4.5, 6.5 :: Double]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"x" TE..+ TE.col @"d") typedCrossData+            )+        )++-- | Typed: col @"y" .+ col @"d"  → Maybe Double+typedAddMaybeIntDouble :: Test+typedAddMaybeIntDouble =+    TestCase+        ( assertEqual+            "Typed: Maybe Int .+ Double = Maybe Double"+            [Just 11.5, Nothing, Just 33.5]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"y" TE..+ TE.col @"d") typedCrossData+            )+        )++-- | Typed: col @"x" .+ col @"md"  → Maybe Double+typedAddIntMaybeDouble :: Test+typedAddIntMaybeDouble =+    TestCase+        ( assertEqual+            "Typed: Int .+ Maybe Double = Maybe Double"+            [Just 11.5, Nothing, Just 33.5]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"x" TE..+ TE.col @"md") typedCrossData+            )+        )++-- | Typed: col @"y" .+ col @"md"  → Maybe Double+typedAddMaybeIntMaybeDouble :: Test+typedAddMaybeIntMaybeDouble =+    TestCase+        ( assertEqual+            "Typed: Maybe Int .+ Maybe Double = Maybe Double"+            [Just 20.5, Nothing, Just 60.5]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"y" TE..+ TE.col @"md") typedCrossData+            )+        )++-- | Typed: col @"x" .- col @"d"  → Double+typedSubIntDouble :: Test+typedSubIntDouble =+    TestCase+        ( assertEqual+            "Typed: Int .- Double = Double"+            [-0.5, -0.5, -0.5 :: Double]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"x" TE..- TE.col @"d") typedCrossData+            )+        )++-- | Typed: col @"x" .* col @"d"  → Double+typedMulIntDouble :: Test+typedMulIntDouble =+    TestCase+        ( assertEqual+            "Typed: Int .* Double = Double"+            [1.5, 5.0, 10.5 :: Double]+            ( DT.columnAsList @"result" $+                DT.derive @"result" (TE.col @"x" TE..* TE.col @"d") typedCrossData+            )+        )++-- ---------------------------------------------------------------------------+-- Typed TExpr layer tests+-- ---------------------------------------------------------------------------++type TestSchema = '[DT.Column "x" Int, DT.Column "y" (Maybe Int)]++typedTestData :: DT.TypedDataFrame TestSchema+typedTestData =+    either (error . show) id $+        DT.freezeWithError @TestSchema testData++-- | Typed: col @"x" .+ col @"y"  should give Maybe Int column+typedAddIntMaybeInt :: Test+typedAddIntMaybeInt =+    TestCase+        ( assertEqual+            "Typed: Int .+ Maybe Int = Maybe Int"+            [Just 11, Nothing, Just 33]+            ( DT.columnAsList @"result" $+                DT.derive+                    @"result"+                    (TE.col @"x" TE..+ TE.col @"y")+                    typedTestData+            )+        )++-- | Typed: nullLift negate on Maybe Int propagates Nothing+typedNullLiftMaybeInt :: Test+typedNullLiftMaybeInt =+    TestCase+        ( assertEqual+            "Typed nullLift negate (Maybe Int) propagates Nothing"+            [Just (-10), Nothing, Just (-30 :: Int)]+            ( DT.columnAsList @"result" $+                DT.derive+                    @"result"+                    (TE.nullLift negate (TE.col @"y") :: TExpr TestSchema (Maybe Int))+                    typedTestData+            )+        )++-- | Typed: nullLift negate on plain Int+typedNullLiftInt :: Test+typedNullLiftInt =+    TestCase+        ( assertEqual+            "Typed nullLift negate (Int) gives Int column"+            [-1, -2, -3 :: Int]+            ( DT.columnAsList @"result" $+                DT.derive+                    @"result"+                    (TE.nullLift negate (TE.col @"x") :: TExpr TestSchema Int)+                    typedTestData+            )+        )++-- | Typed: nullLift2 (+) col @"x" col @"y" → Maybe Int+typedNullLift2IntMaybeInt :: Test+typedNullLift2IntMaybeInt =+    TestCase+        ( assertEqual+            "Typed nullLift2 (+) Int (Maybe Int) = Maybe Int"+            [Just 11, Nothing, Just 33 :: Maybe Int]+            ( DT.columnAsList @"result" $+                DT.derive+                    @"result"+                    (TE.nullLift2 (+) (TE.col @"x") (TE.col @"y") :: TExpr TestSchema (Maybe Int))+                    typedTestData+            )+        )++-- | Typed: col @"y" .== col @"y"  should give Maybe Bool column+typedEqMaybeMaybe :: Test+typedEqMaybeMaybe =+    TestCase+        ( assertEqual+            "Typed: Maybe Int .== Maybe Int = Maybe Bool"+            [Just True, Nothing, Just True]+            ( DT.columnAsList @"result" $+                DT.derive+                    @"result"+                    (TE.col @"y" TE..== TE.col @"y")+                    typedTestData+            )+        )++-- | apply negate to a Maybe Int column — should fmap over inner values+applyFmapMaybeInt :: Test+applyFmapMaybeInt =+    TestCase+        ( assertEqual+            "apply negate to Maybe Int column fmaps"+            (Just $ DI.OptionalColumn (V.fromList [Just (-10), Nothing, Just (-30 :: Int)]))+            ( DI.getColumn "y" $+                D.apply @Int negate "y" testData+            )+        )++-- | apply to a plain Int column still works+applyPlainInt :: Test+applyPlainInt =+    TestCase+        ( assertEqual+            "apply negate to plain Int column still works"+            (Just $ DI.fromList [-1, -2, -3 :: Int])+            ( DI.getColumn "x" $+                D.apply @Int negate "x" testData+            )+        )++tests :: [Test]+tests =+    [ TestLabel "addIntMaybeInt" addIntMaybeInt+    , TestLabel "addMaybeIntInt" addMaybeIntInt+    , TestLabel "addIntInt" addIntInt+    , TestLabel "addMaybeMaybe" addMaybeMaybe+    , TestLabel "subIntMaybeInt" subIntMaybeInt+    , TestLabel "eqIntMaybeInt" eqIntMaybeInt+    , TestLabel "eqIntInt" eqIntInt+    , TestLabel "nullLiftMaybeInt" nullLiftMaybeInt+    , TestLabel "nullLiftInt" nullLiftInt+    , TestLabel "nullLift2IntMaybeInt" nullLift2IntMaybeInt+    , TestLabel "nullLift2MaybeIntInt" nullLift2MaybeIntInt+    , TestLabel "nullLift2IntInt" nullLift2IntInt+    , TestLabel "typedAddIntMaybeInt" typedAddIntMaybeInt+    , TestLabel "typedEqMaybeMaybe" typedEqMaybeMaybe+    , TestLabel "typedNullLiftMaybeInt" typedNullLiftMaybeInt+    , TestLabel "typedNullLiftInt" typedNullLiftInt+    , TestLabel "typedNullLift2IntMaybeInt" typedNullLift2IntMaybeInt+    , TestLabel "applyFmapMaybeInt" applyFmapMaybeInt+    , TestLabel "applyPlainInt" applyPlainInt+    , TestLabel "addIntDouble" addIntDouble+    , TestLabel "addDoubleInt" addDoubleInt+    , TestLabel "addMaybeIntDouble" addMaybeIntDouble+    , TestLabel "addIntMaybeDouble" addIntMaybeDouble+    , TestLabel "addMaybeIntMaybeDouble" addMaybeIntMaybeDouble+    , TestLabel "subIntDouble" subIntDouble+    , TestLabel "subDoubleInt" subDoubleInt+    , TestLabel "subMaybeIntDouble" subMaybeIntDouble+    , TestLabel "subIntMaybeDouble" subIntMaybeDouble+    , TestLabel "subMaybeIntMaybeDouble" subMaybeIntMaybeDouble+    , TestLabel "mulIntDouble" mulIntDouble+    , TestLabel "mulDoubleInt" mulDoubleInt+    , TestLabel "mulMaybeIntDouble" mulMaybeIntDouble+    , TestLabel "mulIntMaybeDouble" mulIntMaybeDouble+    , TestLabel "mulMaybeIntMaybeDouble" mulMaybeIntMaybeDouble+    , TestLabel "divIntDouble" divIntDouble+    , TestLabel "divDoubleInt" divDoubleInt+    , TestLabel "divMaybeIntDouble" divMaybeIntDouble+    , TestLabel "divIntMaybeDouble" divIntMaybeDouble+    , TestLabel "divMaybeIntMaybeDouble" divMaybeIntMaybeDouble+    , TestLabel "typedAddIntDouble" typedAddIntDouble+    , TestLabel "typedAddMaybeIntDouble" typedAddMaybeIntDouble+    , TestLabel "typedAddIntMaybeDouble" typedAddIntMaybeDouble+    , TestLabel "typedAddMaybeIntMaybeDouble" typedAddMaybeIntMaybeDouble+    , TestLabel "typedSubIntDouble" typedSubIntDouble+    , TestLabel "typedMulIntDouble" typedMulIntDouble+    ]
+ tests/Operations/Provenance.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Operations.Provenance where++import qualified Data.List as L+import qualified Data.Map as M+import qualified Data.Text as T+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+    ]
tests/Operations/ReadCsv.hs view
@@ -18,12 +18,14 @@ import Data.Function (on) import Data.Maybe (fromMaybe) import Data.Type.Equality (testEquality, (:~:) (Refl))-import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.Column (Column (..), columnTypeString)+import qualified DataFrame.Internal.Column as DI import DataFrame.Internal.DataFrame (     DataFrame (..),     columnIndices,     columns,     dataframeDimensions,+    getColumn,  ) import System.Directory (removeFile) import System.IO (IOMode (..), withFile)@@ -36,6 +38,11 @@ tempDir :: FilePath tempDir = "./tests/data/unstable_csv/" +readCsvNoInfer :: FilePath -> IO DataFrame+readCsvNoInfer =+    D.readCsvWithOpts+        D.defaultReadOptions{D.typeSpec = D.NoInference}+ -------------------------------------------------------------------------------- -- Pretty-printer --------------------------------------------------------------------------------@@ -143,6 +150,219 @@ testJsonDataFast :: Test testJsonDataFast = testFastCsv "json_data" (fixtureDir <> "json_data.csv") +arbuthnotPath :: FilePath+arbuthnotPath = "./tests/data/arbuthnot.csv"++-- SpecifyTypes with NoInference fallback: named column is typed, rest stay Text+specifyTypesNoInferenceFallback :: Test+specifyTypesNoInferenceFallback =+    TestLabel "specifyTypes_noInference_fallback" $ TestCase $ do+        df <-+            D.readCsvWithOpts+                D.defaultReadOptions+                    { D.typeSpec =+                        D.SpecifyTypes+                            [("year", D.schemaType @Int)]+                            D.NoInference+                    }+                arbuthnotPath+        -- "year" must be Int+        case getColumn "year" df of+            Just col@(UnboxedColumn _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+            _ -> assertFailure "expected UnboxedColumn for 'year'"+        -- "boys" unspecified + NoInference → stays Text+        case getColumn "boys" df of+            Just col@(BoxedColumn _) -> assertEqual "boys should be Text" "Text" (columnTypeString col)+            _ -> assertFailure "expected BoxedColumn for 'boys' with NoInference fallback"++-- SpecifyTypes with InferFromSample fallback: named column typed, rest inferred+specifyTypesInferFallback :: Test+specifyTypesInferFallback =+    TestLabel "specifyTypes_inferFromSample_fallback" $ TestCase $ do+        df <-+            D.readCsvWithOpts+                D.defaultReadOptions+                    { D.typeSpec =+                        D.SpecifyTypes+                            [("year", D.schemaType @Int)]+                            (D.InferFromSample 100)+                    }+                arbuthnotPath+        -- "year" must be Int (explicitly specified)+        case getColumn "year" df of+            Just col@(UnboxedColumn _) -> assertEqual "year should be Int" "Int" (columnTypeString col)+            _ -> assertFailure "expected UnboxedColumn for 'year'"+        -- "boys" unspecified + InferFromSample → inferred as Int+        case getColumn "boys" df of+            Just col@(UnboxedColumn _) -> assertEqual "boys should be Int" "Int" (columnTypeString col)+            _ ->+                assertFailure "expected UnboxedColumn for 'boys' with InferFromSample fallback"++-- SpecifyTypes: typeInferenceSampleSize delegates to fallback+specifyTypesSampleSize :: Test+specifyTypesSampleSize =+    TestLabel "specifyTypes_sampleSize_from_fallback" $ TestCase $ do+        -- Use a small sample size; all numeric columns should still be inferred+        df <-+            D.readCsvWithOpts+                D.defaultReadOptions+                    { D.typeSpec =+                        D.SpecifyTypes+                            []+                            (D.InferFromSample 10)+                    }+                arbuthnotPath+        case getColumn "girls" df of+            Just col@(UnboxedColumn _) -> assertEqual "girls should be Int" "Int" (columnTypeString col)+            _ ->+                assertFailure+                    "expected UnboxedColumn for 'girls' via fallback InferFromSample 10"++testCrlfCsv :: Test+testCrlfCsv = TestLabel "malformed_crlf_csv" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "crlf.csv")+    let (rows, cols) = dataframeDimensions df+    assertEqual "crlf.csv: 2 data rows" 2 rows+    assertEqual "crlf.csv: 2 columns" 2 cols+    case getColumn "name" df of+        Nothing -> assertFailure "crlf.csv: column 'name' missing"+        Just col ->+            assertEqual+                "crlf.csv: name has no \\r"+                (DI.fromList @T.Text ["Alice", "Bob"])+                col++testCrlfTsv :: Test+testCrlfTsv = TestLabel "malformed_crlf_tsv" $ TestCase $ do+    df <- D.readTsvUnstable (fixtureDir <> "crlf.tsv")+    let (rows, _) = dataframeDimensions df+    assertEqual "crlf.tsv: 1 data row" 1 rows+    case getColumn "name" df of+        Nothing -> assertFailure "crlf.tsv: column 'name' missing"+        Just col ->+            assertEqual+                "crlf.tsv: name has no \\r"+                (DI.fromList @T.Text ["Alice"])+                col++testHeaderOnly :: Test+testHeaderOnly = TestLabel "malformed_header_only" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "header_only.csv")+    let (rows, cols) = dataframeDimensions df+    assertEqual "header_only.csv: 0 data rows" 0 rows+    assertEqual "header_only.csv: 3 columns" 3 cols+    let names = map fst . L.sortBy (compare `on` snd) . M.toList $ columnIndices df+    assertEqual "header_only.csv: column names" ["first", "second", "third"] names++testTrailingBlankLine :: Test+testTrailingBlankLine = TestLabel "malformed_trailing_blank_line" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "trailing_blank_line.csv")+    -- blank line contributes 1 extra delimiter; (3+3+1) div 3 = 2, numRow=1+    assertEqual+        "trailing_blank_line.csv: 1 data row visible"+        1+        (fst (dataframeDimensions df))++testAllEmptyRow :: Test+testAllEmptyRow = TestLabel "malformed_all_empty_row" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "all_empty_row.csv")+    assertEqual "all_empty_row.csv: 1 data row" 1 (fst (dataframeDimensions df))+    let checkEmpty colName =+            case getColumn colName df of+                Nothing -> assertFailure ("column '" <> T.unpack colName <> "' missing")+                Just col ->+                    assertEqual+                        (T.unpack colName <> " is Nothing (empty field → null)")+                        (DI.fromList @(Maybe T.Text) [Nothing])+                        col+    mapM_ checkEmpty ["a", "b", "c"]++testSingleCol :: Test+testSingleCol = TestLabel "malformed_single_col" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "single_col.csv")+    let (rows, cols) = dataframeDimensions df+    assertEqual "single_col.csv: 3 data rows" 3 rows+    assertEqual "single_col.csv: 1 column" 1 cols+    case getColumn "name" df of+        Nothing -> assertFailure "single_col.csv: column 'name' missing"+        Just col ->+            assertEqual+                "single_col.csv: correct values"+                (DI.fromList @T.Text ["Alice", "Bob", "Carol"])+                col++testWhitespaceFields :: Test+testWhitespaceFields = TestLabel "malformed_whitespace_fields" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "whitespace_fields.csv")+    assertEqual+        "whitespace_fields.csv: 2 data rows"+        2+        (fst (dataframeDimensions df))+    case getColumn "name" df of+        Nothing -> assertFailure "whitespace_fields.csv: 'name' missing"+        Just col -> assertEqual "name stripped" (DI.fromList @T.Text ["Alice", "Bob"]) col+    case getColumn "city" df of+        Nothing -> assertFailure "whitespace_fields.csv: 'city' missing"+        Just col ->+            assertEqual+                "city stripped"+                (DI.fromList @T.Text ["New York", "Los Angeles"])+                col++-- File: a,b,c header; row "1,2" (short); row "X,Y,Z" (full)+-- Total delimiters: 3 (header) + 2 (short) + 3 (full) = 8+-- numCol=3, totalRows=8 div 3=2, numRow=1+-- Row-1 stride offsets 3,4,5 → fields "1","2","X"  (X bleeds in from next row)+testMissingFields :: Test+testMissingFields = TestLabel "malformed_missing_fields" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "missing_fields.csv")+    assertEqual+        "missing_fields.csv: integer-division gives 1 visible row"+        1+        (fst (dataframeDimensions df))+    case getColumn "c" df of+        Nothing -> assertFailure "missing_fields.csv: column 'c' missing"+        Just col ->+            -- "X" bleeds from the start of the next row into the missing slot+            assertEqual+                "missing_fields.csv: col 'c' bleeds 'X' from next row"+                (DI.fromList @T.Text ["X"])+                col++-- File: a,b,c header; row "1,2,3,EXTRA"+-- Total delimiters: 3 + 4 = 7; 7 div 3 = 2, numRow=1+-- Row-1 strides 3,4,5 → "1","2","3" — EXTRA (stride 6) is never accessed+testExtraFields :: Test+testExtraFields = TestLabel "malformed_extra_fields" $ TestCase $ do+    df <- readCsvNoInfer (fixtureDir <> "extra_fields.csv")+    assertEqual "extra_fields.csv: 1 data row" 1 (fst (dataframeDimensions df))+    assertEqual "extra_fields.csv: 3 columns" 3 (snd (dataframeDimensions df))+    case getColumn "c" df of+        Nothing -> assertFailure "extra_fields.csv: column 'c' missing"+        Just col ->+            assertEqual+                "extra_fields.csv: 'c' = '3' (EXTRA ignored)"+                (DI.fromList @T.Text ["3"])+                col++testNoTrailingNewline :: Test+testNoTrailingNewline = TestLabel "malformed_no_trailing_newline" $ TestCase $ do+    df <- D.readCsvUnstable (fixtureDir <> "no_trailing_newline.csv")+    assertEqual+        "no_trailing_newline.csv: 1 data row"+        1+        (fst (dataframeDimensions df))+    case getColumn "name" df of+        Nothing -> assertFailure "no_trailing_newline.csv: 'name' missing"+        Just col -> assertEqual "name = Alice" (DI.fromList @T.Text ["Alice"]) col+    case getColumn "city" df of+        Nothing -> assertFailure "no_trailing_newline.csv: 'city' missing"+        Just col ->+            assertEqual+                "city = London (synthetic delimiter worked)"+                (DI.fromList @T.Text ["London"])+                col+ tests :: [Test] tests =     [ testSimpleFast@@ -154,4 +374,17 @@     , testQuotesAndNewlinesFast     , testEmptyValuesFast     , testJsonDataFast+    , specifyTypesNoInferenceFallback+    , specifyTypesInferFallback+    , specifyTypesSampleSize+    , testCrlfCsv+    , testCrlfTsv+    , testHeaderOnly+    , testTrailingBlankLine+    , testAllEmptyRow+    , testSingleCol+    , testWhitespaceFields+    , testMissingFields+    , testExtraFields+    , testNoTrailingNewline     ]
tests/Operations/Subset.hs view
@@ -1,11 +1,16 @@ {-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}  module Operations.Subset where +import qualified Data.Text as T+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as Col import DataFrame.Internal.DataFrame-import qualified DataFrame.Operations.Core as D-import qualified DataFrame.Operations.Subset as D+import DataFrame.Operations.Merge () import System.Random+import Test.HUnit  prop_dropZero :: DataFrame -> Bool prop_dropZero df = D.drop 0 df == df@@ -75,6 +80,113 @@         sampled = D.sample gen 0.0 df      in fst (dataframeDimensions sampled) == 0 +prop_stratifiedSplit_deterministic :: DataFrame -> Bool+prop_stratifiedSplit_deterministic _ =+    let df =+            D.fromNamedColumns+                [ ("label", Col.fromList (replicate 50 ("A" :: T.Text) ++ replicate 50 "B"))+                , ("val", Col.fromList ([1 .. 100] :: [Int]))+                ]+        (tr, va) = D.stratifiedSplit (mkStdGen 314) 0.7 (D.col @T.Text "label") df+     in fst (dataframeDimensions tr) + fst (dataframeDimensions va) == 100++strataDf :: DataFrame+strataDf =+    D.fromNamedColumns+        [ ("label", Col.fromList (replicate 5 ("A" :: T.Text) ++ replicate 5 "B"))+        , ("val", Col.fromList ([1 .. 10] :: [Int]))+        ]++unit_stratifiedSample_full :: Test+unit_stratifiedSample_full =+    TestCase $+        let sampled = D.stratifiedSample (mkStdGen 42) 1.0 (D.col @T.Text "label") strataDf+         in assertEqual+                "p=1.0 preserves row count"+                (fst $ dataframeDimensions strataDf)+                (fst $ dataframeDimensions sampled)++unit_stratifiedSplit_rowCount :: Test+unit_stratifiedSplit_rowCount =+    TestCase $+        let (tr, va) = D.stratifiedSplit (mkStdGen 99) 0.8 (D.col @T.Text "label") strataDf+         in assertEqual+                "train+validation == total"+                (fst $ dataframeDimensions strataDf)+                (fst (dataframeDimensions tr) + fst (dataframeDimensions va))++unit_stratifiedSplit_singleRowStratum :: Test+unit_stratifiedSplit_singleRowStratum =+    TestCase $+        let tinyDf =+                D.fromNamedColumns+                    [ ("label", Col.fromList (["A", "A", "A", "A", "A", "B"] :: [T.Text]))+                    , ("val", Col.fromList ([1 .. 6] :: [Int]))+                    ]+            (tr, va) = D.stratifiedSplit (mkStdGen 7) 0.8 (D.col @T.Text "label") tinyDf+         in assertEqual+                "single-row stratum: no rows lost"+                (fst $ dataframeDimensions tinyDf)+                (fst (dataframeDimensions tr) + fst (dataframeDimensions va))++-- | Count occurrences of a label in a column, expressed as a fraction of total rows.+labelProportion :: T.Text -> T.Text -> DataFrame -> Double+labelProportion col label df =+    let total = fst (dataframeDimensions df)+        vals = case getColumn col df of+            Just c -> Col.toList @T.Text c+            Nothing -> []+        n = length (filter (== label) vals)+     in fromIntegral n / fromIntegral total++unit_stratifiedSplit_proportions :: Test+unit_stratifiedSplit_proportions =+    TestCase $+        let aCount = 100+            bCount = 50+            df =+                D.fromNamedColumns+                    [+                        ( "label"+                        , Col.fromList (replicate aCount ("A" :: T.Text) ++ replicate bCount "B")+                        )+                    , ("val", Col.fromList ([1 .. aCount + bCount] :: [Int]))+                    ]+            (tr, va) = D.stratifiedSplit (mkStdGen 42) 0.8 (D.col @T.Text "label") df+            origProp = labelProportion "label" "A" df+            trProp = labelProportion "label" "A" tr+            vaProp = labelProportion "label" "A" va+            tol = 0.05 :: Double+         in do+                assertBool+                    ( "train A-proportion "+                        ++ show trProp+                        ++ " differs from original "+                        ++ show origProp+                        ++ " by more than "+                        ++ show tol+                    )+                    (abs (trProp - origProp) < tol)+                assertBool+                    ( "validation A-proportion "+                        ++ show vaProp+                        ++ " differs from original "+                        ++ show origProp+                        ++ " by more than "+                        ++ show tol+                    )+                    (abs (vaProp - origProp) < tol)++hunitTests :: [Test]+hunitTests =+    [ TestLabel "unit_stratifiedSample_full" unit_stratifiedSample_full+    , TestLabel "unit_stratifiedSplit_rowCount" unit_stratifiedSplit_rowCount+    , TestLabel+        "unit_stratifiedSplit_singleRowStratum"+        unit_stratifiedSplit_singleRowStratum+    , TestLabel "unit_stratifiedSplit_proportions" unit_stratifiedSplit_proportions+    ]+ tests =     [ prop_dropZero     , prop_takeZero@@ -92,4 +204,5 @@     , prop_excludeAll     , prop_cubePreservesSmall     , prop_sampleEmptyApprox+    , prop_stratifiedSplit_deterministic     ]
tests/Operations/Typing.hs view
@@ -42,7 +42,10 @@         beforeParse :: [T.Text]         beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Bools"@@ -57,7 +60,10 @@         beforeParse :: [T.Text]         beforeParse = T.pack . show <$> [1 .. 50]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints"@@ -74,7 +80,10 @@             T.pack . show                 <$> [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"@@ -121,7 +130,10 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days"@@ -206,7 +218,10 @@             , "Meowth, that's right!"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Text without missing values as BoxedColumn of Text"@@ -222,7 +237,10 @@         beforeParse :: [T.Text]         beforeParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses mixture of Bools and Ints as Text"@@ -449,7 +467,10 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"@@ -544,7 +565,10 @@             , "2020-02-20"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"@@ -683,7 +707,10 @@             , "12.22222235049451"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"@@ -736,7 +763,10 @@             , "Meowth"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"@@ -754,7 +784,10 @@         beforeParse = replicate 10 "true" ++ replicate 10 "false"         expected = DI.UnboxedColumn $ VU.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"@@ -872,7 +905,10 @@             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"@@ -1000,7 +1036,10 @@             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"@@ -1048,7 +1087,10 @@             ]         expected = DI.BoxedColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days"@@ -1134,7 +1176,10 @@             ]         expected = DI.BoxedColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Text without missing values as BoxedColumn of Text"@@ -1150,7 +1195,10 @@         beforeParse = replicate 10 "" ++ replicate 10 "true" ++ replicate 10 "false"         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"@@ -1278,7 +1326,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"@@ -1416,7 +1467,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -1474,7 +1528,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"@@ -1574,7 +1631,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -1590,7 +1650,10 @@         beforeParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"         expected = DI.BoxedColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"@@ -1718,7 +1781,10 @@             ]         expected = DI.BoxedColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"@@ -1856,7 +1922,10 @@             ]         expected = DI.BoxedColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -1996,7 +2065,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Texts, when safeRead is off"@@ -2102,7 +2174,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 10, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -2118,7 +2193,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 10 "" ++ replicate 10 "true"         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"@@ -2245,7 +2323,10 @@             , "50"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"@@ -2382,7 +2463,10 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is on"@@ -2439,7 +2523,10 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"@@ -2538,7 +2625,10 @@             , "Meowth, that's right!"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"@@ -2665,7 +2755,10 @@             , "50"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"@@ -2802,7 +2895,10 @@             , "12.03"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses combination of Ints, Doubles and empty strings as OptionalColumn of Doubles, when safeRead is off"@@ -2941,7 +3037,10 @@             , ""             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints with nullish values AND empty strings as OptionalColumn of Ints, when safeRead is on"@@ -3038,7 +3137,10 @@             , "3.14"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints and Doubles with nullish values AND empty strings as OptionalColumn of Doubles, when safeRead is on"@@ -3143,7 +3245,10 @@             , "N/A"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"@@ -3159,7 +3264,10 @@         beforeParse :: [T.Text]         beforeParse = "false" : replicate 50 "true"         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"@@ -3174,7 +3282,10 @@         beforeParse :: [T.Text]         beforeParse = "false" : replicate 50 "true"         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"@@ -3291,7 +3402,10 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"@@ -3408,7 +3522,10 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 25}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"@@ -3525,7 +3642,10 @@             , "50"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 49}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"@@ -3572,7 +3692,10 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"@@ -3619,7 +3742,10 @@             , "2020-02-26"             ]         expected = DI.BoxedColumn $ V.fromList afterParse-        actual = D.parseDefault [] 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 15}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"@@ -3846,7 +3972,10 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4073,7 +4202,10 @@             , "12.22222235049451"             ]         expected = DI.UnboxedColumn $ VU.fromList afterParse-        actual = D.parseDefault [] 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 50}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4320,7 +4452,10 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4569,7 +4704,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"@@ -4737,7 +4875,10 @@             , "12.22222235049451"             ]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 1, D.parseSafe = False}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with just one example, when safeRead is off"@@ -4906,7 +5047,10 @@             ]         expected = DI.OptionalColumn $ V.fromList afterParse         actual =-            D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+            D.parseDefault+                (D.defaultParseOptions{D.sampleSize = 30, D.parseSafe = False})+                $ DI.fromVector+                $ V.fromList beforeParse      in TestCase             ( assertEqual                 "Correctly parses a mixture of Ints, Doubles, empty strings, nullish as OptionalColumn of Text with many examples, when safeRead is off"@@ -4923,7 +5067,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["100000"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4934,7 +5081,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["3.14"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4945,7 +5095,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["2024-12-25"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4956,7 +5109,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN" ++ ["2024-12-25", "2024-12-w6"]         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) @@ -4967,7 +5123,10 @@         beforeParse :: [T.Text]         beforeParse = replicate 100 "NaN"         expected = DI.OptionalColumn $ V.fromList afterParse-        actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+        actual =+            D.parseDefault (D.defaultParseOptions{D.sampleSize = 10}) $+                DI.fromVector $+                    V.fromList beforeParse      in TestCase             (assertEqual "Correctly parses many Nulls followed by one Int" expected actual) 
tests/Parquet.hs view
@@ -6,10 +6,32 @@ import Assertions (assertExpectException) import qualified DataFrame as D import qualified DataFrame.Functions as F+import qualified DataFrame.IO.Parquet as DP +import qualified Data.ByteString as BS import Data.Int+import qualified Data.Set as S import Data.Text (Text)+import qualified Data.Text as T import Data.Time+import Data.Word+import DataFrame.IO.Parquet.Thrift (+    columnMetaData,+    columnPathInSchema,+    columnStatistics,+    rowGroupColumns,+    rowGroups,+    schema,+ )+import DataFrame.IO.Parquet.Types (columnNullCount)+import DataFrame.Internal.Binary (+    littleEndianWord32,+    littleEndianWord64,+    word32ToLittleEndian,+    word64ToLittleEndian,+ )+import DataFrame.Internal.Column (hasMissing)+import DataFrame.Internal.DataFrame (unsafeGetColumn) import GHC.IO (unsafePerformIO) import Test.HUnit @@ -53,13 +75,20 @@             )         ] +testBothReadParquetPaths :: ((FilePath -> IO D.DataFrame) -> Test) -> Test+testBothReadParquetPaths test =+    TestList+        [ test D.readParquet+        , test (DP._readParquetWithOpts (Just True) D.defaultParquetReadOptions)+        ]+ allTypesPlain :: Test-allTypesPlain =+allTypesPlain = testBothReadParquetPaths $ \readParquet ->     TestCase         ( assertEqual             "allTypesPlain"             allTypes-            (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.parquet"))+            (unsafePerformIO (readParquet "./tests/data/alltypes_plain.parquet"))         )  allTypesTinyPagesDimensions :: Test@@ -155,7 +184,7 @@         ]  allTypesTinyPagesLastFew :: Test-allTypesTinyPagesLastFew =+allTypesTinyPagesLastFew = testBothReadParquetPaths $ \readParquet ->     TestCase         ( assertEqual             "allTypesTinyPages dimensions"@@ -164,27 +193,27 @@                 -- Excluding doubles because they are weird to compare.                 ( fmap                     (D.takeLast 10 . D.exclude ["double_col"])-                    (D.readParquet "./tests/data/alltypes_tiny_pages.parquet")+                    (readParquet "./tests/data/alltypes_tiny_pages.parquet")                 )             )         )  allTypesPlainSnappy :: Test-allTypesPlainSnappy =+allTypesPlainSnappy = testBothReadParquetPaths $ \readParquet ->     TestCase         ( assertEqual             "allTypesPlainSnappy"             (D.filter (F.col @Int32 "id") (`elem` [6, 7]) allTypes)-            (unsafePerformIO (D.readParquet "./tests/data/alltypes_plain.snappy.parquet"))+            (unsafePerformIO (readParquet "./tests/data/alltypes_plain.snappy.parquet"))         )  allTypesDictionary :: Test-allTypesDictionary =+allTypesDictionary = testBothReadParquetPaths $ \readParquet ->     TestCase         ( assertEqual             "allTypesPlainSnappy"             (D.filter (F.col @Int32 "id") (`elem` [0, 1]) allTypes)-            (unsafePerformIO (D.readParquet "./tests/data/alltypes_dictionary.parquet"))+            (unsafePerformIO (readParquet "./tests/data/alltypes_dictionary.parquet"))         )  selectedColumnsWithOpts :: Test@@ -460,12 +489,12 @@         ]  transactionsTest :: Test-transactionsTest =+transactionsTest = testBothReadParquetPaths $ \readParquet ->     TestCase         ( assertEqual             "transactions"             transactions-            (unsafePerformIO (D.readParquet "./tests/data/transactions.parquet"))+            (unsafePerformIO (readParquet "./tests/data/transactions.parquet"))         )  mtCarsDataset :: D.DataFrame@@ -926,6 +955,66 @@             (unsafePerformIO (D.readParquet "./tests/data/mtcars.parquet"))         ) +littleEndianWord64KnownPattern :: Test+littleEndianWord64KnownPattern =+    TestCase+        ( assertEqual+            "littleEndianWord64KnownPattern"+            (0x1122334455667788 :: Word64)+            ( littleEndianWord64+                (BS.pack [0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11])+            )+        )++littleEndianWord32KnownPattern :: Test+littleEndianWord32KnownPattern =+    TestCase+        ( assertEqual+            "littleEndianWord32KnownPattern"+            (0x11223344 :: Word32)+            (littleEndianWord32 (BS.pack [0x44, 0x33, 0x22, 0x11]))+        )++littleEndianWord64ShortInputPadsZeroes :: Test+littleEndianWord64ShortInputPadsZeroes =+    TestCase+        ( assertEqual+            "littleEndianWord64ShortInputPadsZeroes"+            (0x00CCBBAA :: Word64)+            (littleEndianWord64 (BS.pack [0xAA, 0xBB, 0xCC]))+        )++littleEndianWord32ShortInputPadsZeroes :: Test+littleEndianWord32ShortInputPadsZeroes =+    TestCase+        ( assertEqual+            "littleEndianWord32ShortInputPadsZeroes"+            (0x0000BEEF :: Word32)+            (littleEndianWord32 (BS.pack [0xEF, 0xBE]))+        )++littleEndianWord64RoundTrip :: Test+littleEndianWord64RoundTrip =+    TestCase+        ( assertEqual+            "littleEndianWord64RoundTrip"+            value+            (littleEndianWord64 (word64ToLittleEndian value))+        )+  where+    value = 0x1122334455667788 :: Word64++littleEndianWord32RoundTrip :: Test+littleEndianWord32RoundTrip =+    TestCase+        ( assertEqual+            "littleEndianWord32RoundTrip"+            value+            (littleEndianWord32 (word32ToLittleEndian value))+        )+  where+    value = 0xA1B2C3D4 :: Word32+ -- --------------------------------------------------------------------------- -- Group 1: Plain variant -- ---------------------------------------------------------------------------@@ -1611,6 +1700,58 @@             (D.readParquet "./tests/data/nation.dict-malformed.parquet")         ) +shardedNullableSchema :: Test+shardedNullableSchema =+    TestCase $ do+        metas <-+            mapM+                (fmap fst . DP.readMetadataFromPath)+                ["data/sharded/part-0.parquet", "data/sharded/part-1.parquet"]+        let nullableCols =+                S.fromList+                    [ last (map T.pack colPath)+                    | meta <- metas+                    , rg <- rowGroups meta+                    , cc <- rowGroupColumns rg+                    , let cm = columnMetaData cc+                          colPath = columnPathInSchema cm+                    , not (null colPath)+                    , columnNullCount (columnStatistics cm) > 0+                    ]+            df =+                foldl+                    (\acc meta -> acc <> F.schemaToEmptyDataFrame nullableCols (schema meta))+                    D.empty+                    metas+        assertBool "id should be nullable" (hasMissing (unsafeGetColumn "id" df))+        assertBool "name should be nullable" (hasMissing (unsafeGetColumn "name" df))+        assertBool "score should be nullable" (hasMissing (unsafeGetColumn "score" df))++singleShardNoNulls :: Test+singleShardNoNulls =+    TestCase $ do+        (meta, _) <- DP.readMetadataFromPath "data/sharded/part-0.parquet"+        let nullableCols =+                S.fromList+                    [ last (map T.pack colPath)+                    | rg <- rowGroups meta+                    , cc <- rowGroupColumns rg+                    , let cm = columnMetaData cc+                          colPath = columnPathInSchema cm+                    , not (null colPath)+                    , columnNullCount (columnStatistics cm) > 0+                    ]+            df = F.schemaToEmptyDataFrame nullableCols (schema meta)+        assertBool+            "id should NOT be nullable"+            (not (hasMissing (unsafeGetColumn "id" df)))+        assertBool+            "name should NOT be nullable"+            (not (hasMissing (unsafeGetColumn "name" df)))+        assertBool+            "score should NOT be nullable"+            (not (hasMissing (unsafeGetColumn "score" df)))+ tests :: [Test] tests =     [ allTypesPlain@@ -1626,6 +1767,12 @@     , allTypesTinyPagesLastFew     , allTypesTinyPagesDimensions     , transactionsTest+    , littleEndianWord64KnownPattern+    , littleEndianWord32KnownPattern+    , littleEndianWord64ShortInputPadsZeroes+    , littleEndianWord32ShortInputPadsZeroes+    , littleEndianWord64RoundTrip+    , littleEndianWord32RoundTrip     , -- Group 1       allTypesTinyPagesPlain     , -- Group 2: compression codecs@@ -1698,4 +1845,7 @@     , unknownLogicalType     , -- Group 12: malformed files       nationDictMalformed+    , -- Group 13: metadata-based null detection+      shardedNullableSchema+    , singleShardNoNulls     ]
+ tests/data/unstable_csv/all_empty_row.csv view
@@ -0,0 +1,2 @@+a,b,c+,,
+ tests/data/unstable_csv/crlf.csv view
@@ -0,0 +1,3 @@+name,city
+Alice,New York
+Bob,Los Angeles
+ tests/data/unstable_csv/crlf.tsv view
@@ -0,0 +1,2 @@+name	city
+Alice	New York
+ tests/data/unstable_csv/extra_fields.csv view
@@ -0,0 +1,2 @@+a,b,c+1,2,3,EXTRA
+ tests/data/unstable_csv/header_only.csv view
@@ -0,0 +1,1 @@+first,second,third
+ tests/data/unstable_csv/missing_fields.csv view
@@ -0,0 +1,3 @@+a,b,c+1,2+X,Y,Z
+ tests/data/unstable_csv/no_trailing_newline.csv view
@@ -0,0 +1,2 @@+name,city+Alice,London
+ tests/data/unstable_csv/single_col.csv view
@@ -0,0 +1,4 @@+name+Alice+Bob+Carol
+ tests/data/unstable_csv/trailing_blank_line.csv view
@@ -0,0 +1,3 @@+a,b,c+1,2,3+
+ tests/data/unstable_csv/whitespace_fields.csv view
@@ -0,0 +1,3 @@+name,city+  Alice  ,  New York+  Bob  ,  Los Angeles