dataframe 0.6.0.0 → 0.7.0.0
raw patch · 46 files changed
+11199/−6769 lines, 46 filesdep +deepseqdep +stmdep ~randomnew-component:exe:lazy-benchPVP ok
version bump matches the API change (PVP)
Dependencies added: deepseq, stm
Dependency ranges changed: random
API changes (from Hackage documentation)
- DataFrame.IO.CSV: schemaTypes :: TypeSpec -> [SchemaType]
- DataFrame.Internal.Interpreter: handleInterpretException :: String -> DataFrameException -> DataFrameException
- DataFrame.Internal.Interpreter: mkAggregatedColumnUnboxed :: (Columnable a, Unbox a, Columnable b, Unbox b) => Vector a -> Vector Int -> Vector Int -> (Vector a -> b) -> Vector b
- DataFrame.Internal.Interpreter: mkReducedColumnBoxed :: Vector a -> Vector Int -> Vector Int -> (a -> a -> a) -> Vector a
- DataFrame.Internal.Interpreter: mkReducedColumnUnboxed :: Unbox a => Vector a -> Vector Int -> Vector Int -> (a -> a -> a) -> Vector a
- DataFrame.Internal.Interpreter: mkTypeMismatchException :: (Typeable a, Typeable b) => Maybe String -> Maybe String -> TypeErrorContext a b -> DataFrameException
- DataFrame.Internal.Interpreter: mkUnaggregatedColumnBoxed :: Columnable a => Vector a -> Vector Int -> Vector Int -> Vector (Vector a)
- DataFrame.Internal.Interpreter: mkUnaggregatedColumnUnboxed :: (Columnable a, Unbox a) => Vector a -> Vector Int -> Vector Int -> Vector (Vector a)
- DataFrame.Internal.Interpreter: nestedTypeException :: (Typeable a, Typeable b) => String -> DataFrameException
- DataFrame.Internal.Interpreter: numRows :: DataFrame -> Int
- DataFrame.Lazy.IO.CSV: countRows :: Char -> FilePath -> IO Int
- DataFrame.Lazy.IO.CSV: field :: Char -> Parser Text
- DataFrame.Lazy.IO.CSV: getRowAsText :: DataFrame -> Int -> [Text]
- DataFrame.Lazy.IO.CSV: inferValueType :: Text -> Text
- DataFrame.Lazy.IO.CSV: lineEnd :: Parser ()
- DataFrame.Lazy.IO.CSV: parseRow :: Char -> Parser [Text]
- DataFrame.Lazy.IO.CSV: parseSep :: Char -> Text -> [Text]
- DataFrame.Lazy.IO.CSV: quotedField :: Parser Text
- DataFrame.Lazy.IO.CSV: readSingleLine :: Char -> Text -> Handle -> IO ([Text], Text)
- DataFrame.Lazy.IO.CSV: record :: Char -> Parser [Text]
- DataFrame.Lazy.IO.CSV: unquotedField :: Char -> Parser Text
- DataFrame.Lazy.IO.CSV: unquotedTerminators :: Char -> Set Char
- DataFrame.Lazy.IO.CSV: writeCsv :: FilePath -> DataFrame -> IO ()
- DataFrame.Lazy.IO.CSV: writeSeparated :: Char -> FilePath -> DataFrame -> IO ()
- DataFrame.Lazy.Internal.DataFrame: ICSV :: InputType
- DataFrame.Lazy.Internal.DataFrame: [Derive] :: forall a. Columnable a => Text -> Expr a -> LazyOperation
- DataFrame.Lazy.Internal.DataFrame: [Filter] :: Expr Bool -> LazyOperation
- DataFrame.Lazy.Internal.DataFrame: [Select] :: [Text] -> LazyOperation
- DataFrame.Lazy.Internal.DataFrame: [inputPath] :: LazyDataFrame -> FilePath
- DataFrame.Lazy.Internal.DataFrame: [inputType] :: LazyDataFrame -> InputType
- DataFrame.Lazy.Internal.DataFrame: [operations] :: LazyDataFrame -> [LazyOperation]
- DataFrame.Lazy.Internal.DataFrame: addOperation :: LazyOperation -> LazyDataFrame -> LazyDataFrame
- DataFrame.Lazy.Internal.DataFrame: batchRanges :: Int -> Int -> [(Int, Int)]
- DataFrame.Lazy.Internal.DataFrame: data InputType
- DataFrame.Lazy.Internal.DataFrame: data LazyOperation
- DataFrame.Lazy.Internal.DataFrame: eval :: LazyOperation -> DataFrame -> DataFrame
- DataFrame.Lazy.Internal.DataFrame: instance GHC.Show.Show DataFrame.Lazy.Internal.DataFrame.InputType
- DataFrame.Lazy.Internal.DataFrame: instance GHC.Show.Show DataFrame.Lazy.Internal.DataFrame.LazyOperation
- DataFrame.Typed: (|>) :: a -> (a -> b) -> b
- DataFrame.Typed: infixl 1 |>
- DataFrame.Typed.Operations: (|>) :: a -> (a -> b) -> b
- DataFrame.Typed.Operations: infixl 1 |>
+ DataFrame: fastReadCsvUnstable :: FilePath -> IO DataFrame
+ DataFrame: fastReadTsvUnstable :: FilePath -> IO DataFrame
+ DataFrame: readCsvUnstable :: FilePath -> IO DataFrame
+ DataFrame: readTsvUnstable :: FilePath -> IO DataFrame
+ DataFrame.IO.CSV: BuilderBS :: !PagedVector ByteString -> !PagedUnboxedVector Word8 -> BuilderColumn
+ DataFrame.IO.CSV: finalizeBuilderColumn :: ReadOptions -> BuilderColumn -> IO Column
+ DataFrame.IO.CSV: handleBSBool :: Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: handleBSDate :: String -> Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: handleBSDouble :: Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: handleBSInt :: String -> Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: handleBSNo :: String -> Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: handleBSText :: Vector (Maybe ByteString) -> Column
+ DataFrame.IO.CSV: inferColumnFromBS :: ReadOptions -> Vector ByteString -> Vector Word8 -> Column
+ DataFrame.IO.CSV: makeParsingAssumptionBS :: String -> Vector (Maybe ByteString) -> ParsingAssumption
+ DataFrame.IO.CSV: schemaTypeMap :: TypeSpec -> Map Text SchemaType
+ DataFrame.IO.Parquet: cleanColPath :: [SNode] -> [String] -> [String]
+ DataFrame.IO.Parquet: decodePageData :: Maybe DictVals -> (Int, Int) -> ParquetType -> Maybe Int32 -> ParquetEncoding -> [Int] -> [Int] -> Int -> ByteString -> String -> IO Column
+ DataFrame.IO.Parquet: findLeafSchema :: [SchemaElement] -> [String] -> Maybe SchemaElement
+ DataFrame.IO.Parquet.Binary: fillByteStringByWord8 :: Int -> (Int -> IO Word8) -> IO ByteString
+ DataFrame.IO.Parquet.Dictionary: applyDictToColumn :: (Columnable a, Columnable (Maybe a)) => Vector a -> Vector Int -> Int -> [Int] -> IO Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepBool :: Int -> Int -> [Int] -> [Int] -> [Bool] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepDouble :: Int -> Int -> [Int] -> [Int] -> [Double] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepFloat :: Int -> Int -> [Int] -> [Int] -> [Float] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepInt32 :: Int -> Int -> [Int] -> [Int] -> [Int32] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepInt64 :: Int -> Int -> [Int] -> [Int] -> [Int64] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepText :: Int -> Int -> [Int] -> [Int] -> [Text] -> Column
+ DataFrame.IO.Parquet.Dictionary: stitchForRepUTCTime :: Int -> Int -> [Int] -> [Int] -> [UTCTime] -> Column
+ DataFrame.IO.Parquet.Encoding: extractBits :: Int -> Int -> ByteString -> [Word32]
+ DataFrame.IO.Parquet.Levels: pairWithVals :: Int -> [(Int, Int)] -> [a] -> [(Int, Int, Maybe a)]
+ DataFrame.IO.Parquet.Levels: splitAtRepBound :: Int -> [(Int, Int, Maybe a)] -> [[(Int, Int, Maybe a)]]
+ DataFrame.IO.Parquet.Levels: stitchList :: Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe a]]
+ DataFrame.IO.Parquet.Levels: stitchList2 :: Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe a]]]
+ DataFrame.IO.Parquet.Levels: stitchList3 :: Int -> Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe [Maybe a]]]]
+ DataFrame.IO.Parquet.Page: readNDoubleVec :: Int -> ByteString -> Vector Double
+ DataFrame.IO.Parquet.Page: readNFloatVec :: Int -> ByteString -> Vector Float
+ DataFrame.IO.Parquet.Page: readNInt32Vec :: Int -> ByteString -> Vector Int32
+ DataFrame.IO.Parquet.Page: readNInt64Vec :: Int -> ByteString -> Vector Int64
+ DataFrame.IO.Unstable.CSV: getDelimiterIndices :: Word8 -> Int -> Vector Word8 -> IO (Vector CSize)
+ DataFrame.Internal.Column: [MOptionalColumn] :: forall a. Columnable a => IOVector (Maybe a) -> MutableColumn
+ DataFrame.Internal.Column: concatManyColumns :: [Column] -> Column
+ DataFrame.Internal.Column: copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()
+ DataFrame.Internal.Column: foldDirectGroups :: Columnable b => (acc -> b -> acc) -> acc -> Column -> Vector Int -> Vector Int -> Either DataFrameException (Vector acc)
+ DataFrame.Internal.Column: foldLinearGroups :: (Columnable b, Columnable acc) => (acc -> b -> acc) -> acc -> Column -> Vector Int -> Int -> Either DataFrameException Column
+ DataFrame.Internal.Column: foldl1DirectGroups :: Columnable a => (a -> a -> a) -> Column -> Vector Int -> Vector Int -> Either DataFrameException (Vector a)
+ DataFrame.Internal.Column: foldlColumnWith :: Columnable a => (b -> a -> b) -> b -> Column -> Either DataFrameException b
+ DataFrame.Internal.Column: freezeMutableColumn :: MutableColumn -> IO Column
+ DataFrame.Internal.Column: instance Control.DeepSeq.NFData DataFrame.Internal.Column.Column
+ DataFrame.Internal.Column: newMutableColumn :: Int -> Column -> IO MutableColumn
+ DataFrame.Internal.DataFrame: [rowToGroup] :: GroupedDataFrame -> Vector Int
+ DataFrame.Internal.DataFrame: instance Control.DeepSeq.NFData DataFrame.Internal.DataFrame.DataFrame
+ DataFrame.Internal.Expression: MeanAcc :: {-# UNPACK #-} !Double -> {-# UNPACK #-} !Int -> MeanAcc
+ DataFrame.Internal.Expression: [MergeAgg] :: forall acc b a. Columnable acc => Text -> acc -> (acc -> b -> acc) -> (acc -> acc -> acc) -> (acc -> a) -> AggStrategy a b
+ DataFrame.Internal.Expression: data MeanAcc
+ DataFrame.Internal.Expression: instance Control.DeepSeq.NFData DataFrame.Internal.Expression.MeanAcc
+ DataFrame.Internal.Expression: instance GHC.Classes.Eq DataFrame.Internal.Expression.MeanAcc
+ DataFrame.Internal.Expression: instance GHC.Classes.Ord DataFrame.Internal.Expression.MeanAcc
+ DataFrame.Internal.Expression: instance GHC.Read.Read DataFrame.Internal.Expression.MeanAcc
+ DataFrame.Internal.Expression: instance GHC.Show.Show DataFrame.Internal.Expression.MeanAcc
+ DataFrame.Internal.Interpreter: FlatCtx :: DataFrame -> Ctx
+ DataFrame.Internal.Interpreter: GroupCtx :: GroupedDataFrame -> Ctx
+ DataFrame.Internal.Interpreter: [Flat] :: forall a. Columnable a => Column -> Value a
+ DataFrame.Internal.Interpreter: [Group] :: forall a. Columnable a => Vector Column -> Value a
+ DataFrame.Internal.Interpreter: [Scalar] :: forall a. Columnable a => a -> Value a
+ DataFrame.Internal.Interpreter: data Ctx
+ DataFrame.Internal.Interpreter: data Value a
+ DataFrame.Internal.Interpreter: eval :: Columnable a => Ctx -> Expr a -> Either DataFrameException (Value a)
+ DataFrame.Internal.Interpreter: materialize :: Columnable a => Int -> Value a -> Column
+ DataFrame.Internal.Parsing: countRows :: Char -> FilePath -> IO Int
+ DataFrame.Internal.Parsing: field :: Char -> Parser Text
+ DataFrame.Internal.Parsing: inferValueType :: Text -> Text
+ DataFrame.Internal.Parsing: isNullishBS :: ByteString -> Bool
+ DataFrame.Internal.Parsing: lineEnd :: Parser ()
+ DataFrame.Internal.Parsing: parseRow :: Char -> Parser [Text]
+ DataFrame.Internal.Parsing: parseSep :: Char -> Text -> [Text]
+ DataFrame.Internal.Parsing: quotedField :: Parser Text
+ DataFrame.Internal.Parsing: readByteStringBool :: ByteString -> Maybe Bool
+ DataFrame.Internal.Parsing: readByteStringDate :: String -> ByteString -> Maybe Day
+ DataFrame.Internal.Parsing: readSingleLine :: Char -> Text -> Handle -> IO ([Text], Text)
+ DataFrame.Internal.Parsing: record :: Char -> Parser [Text]
+ DataFrame.Internal.Parsing: unquotedField :: Char -> Parser Text
+ DataFrame.Internal.Parsing: unquotedTerminators :: Char -> Set Char
+ DataFrame.Internal.Row: instance Control.DeepSeq.NFData DataFrame.Internal.Row.Any
+ DataFrame.Lazy.IO.Binary: readSpilled :: FilePath -> IO DataFrame
+ DataFrame.Lazy.IO.Binary: spillToDisk :: FilePath -> DataFrame -> IO ()
+ DataFrame.Lazy.IO.Binary: withSpilled :: DataFrame -> (FilePath -> IO a) -> IO a
+ DataFrame.Lazy.IO.CSV: findUnquotedNewline :: ByteString -> Maybe Int
+ DataFrame.Lazy.IO.CSV: getNthFieldBs :: Word8 -> Int -> ByteString -> ByteString
+ DataFrame.Lazy.IO.CSV: makeCol :: Int -> SchemaType -> IO MutableColumn
+ DataFrame.Lazy.IO.CSV: openCsvStream :: Char -> Schema -> FilePath -> IO (Handle, [(Int, Text, SchemaType)])
+ DataFrame.Lazy.IO.CSV: readBatch :: Char -> [(Int, Text, SchemaType)] -> Int -> ByteString -> Handle -> IO (Maybe (DataFrame, ByteString))
+ DataFrame.Lazy.IO.CSV: sliceCol :: Int -> MutableColumn -> MutableColumn
+ DataFrame.Lazy.IO.CSV: writeColumnBs :: Int -> ByteString -> MutableColumn -> IO (Either Text Bool)
+ DataFrame.Lazy.Internal.DataFrame: [plan] :: LazyDataFrame -> LogicalPlan
+ DataFrame.Lazy.Internal.DataFrame: fromDataFrame :: DataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: groupBy :: [Text] -> [(Text, UExpr)] -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: join :: JoinType -> Text -> Text -> LazyDataFrame -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: limit :: Int -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: scanParquet :: Schema -> Text -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: scanSeparated :: Char -> Schema -> Text -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: sortBy :: [(Text, SortOrder)] -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.Executor: ExecutorConfig :: !Int -> FilePath -> !Int -> ExecutorConfig
+ DataFrame.Lazy.Internal.Executor: [defaultBatchSize] :: ExecutorConfig -> !Int
+ DataFrame.Lazy.Internal.Executor: [memoryBudgetBytes] :: ExecutorConfig -> !Int
+ DataFrame.Lazy.Internal.Executor: [spillDirectory] :: ExecutorConfig -> FilePath
+ DataFrame.Lazy.Internal.Executor: data ExecutorConfig
+ DataFrame.Lazy.Internal.Executor: defaultExecutorConfig :: ExecutorConfig
+ DataFrame.Lazy.Internal.Executor: execute :: PhysicalPlan -> ExecutorConfig -> IO DataFrame
+ DataFrame.Lazy.Internal.Executor: foldBatches :: (b -> DataFrame -> IO b) -> b -> PhysicalPlan -> ExecutorConfig -> IO b
+ DataFrame.Lazy.Internal.LogicalPlan: Aggregate :: [Text] -> [(Text, UExpr)] -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Ascending :: SortOrder
+ DataFrame.Lazy.Internal.LogicalPlan: CsvSource :: FilePath -> Char -> DataSource
+ DataFrame.Lazy.Internal.LogicalPlan: Derive :: Text -> UExpr -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Descending :: SortOrder
+ DataFrame.Lazy.Internal.LogicalPlan: Filter :: Expr Bool -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Join :: JoinType -> Text -> Text -> LogicalPlan -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Limit :: Int -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: ParquetSource :: FilePath -> DataSource
+ DataFrame.Lazy.Internal.LogicalPlan: Project :: [Text] -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Scan :: DataSource -> Schema -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: Sort :: [(Text, SortOrder)] -> LogicalPlan -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: SourceDF :: DataFrame -> LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: data DataSource
+ DataFrame.Lazy.Internal.LogicalPlan: data LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: data SortOrder
+ DataFrame.Lazy.Internal.LogicalPlan: instance GHC.Classes.Eq DataFrame.Lazy.Internal.LogicalPlan.SortOrder
+ DataFrame.Lazy.Internal.LogicalPlan: instance GHC.Classes.Ord DataFrame.Lazy.Internal.LogicalPlan.SortOrder
+ DataFrame.Lazy.Internal.LogicalPlan: instance GHC.Show.Show DataFrame.Lazy.Internal.LogicalPlan.DataSource
+ DataFrame.Lazy.Internal.LogicalPlan: instance GHC.Show.Show DataFrame.Lazy.Internal.LogicalPlan.LogicalPlan
+ DataFrame.Lazy.Internal.LogicalPlan: instance GHC.Show.Show DataFrame.Lazy.Internal.LogicalPlan.SortOrder
+ DataFrame.Lazy.Internal.Optimizer: optimize :: Int -> LogicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalDerive :: Text -> UExpr -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalFilter :: Expr Bool -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalHashAggregate :: [Text] -> [(Text, UExpr)] -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalHashJoin :: JoinType -> Text -> Text -> PhysicalPlan -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalLimit :: Int -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalProject :: [Text] -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalScan :: DataSource -> ScanConfig -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalSort :: [(Text, SortOrder)] -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalSortMergeJoin :: JoinType -> Text -> Text -> PhysicalPlan -> PhysicalPlan -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalSourceDF :: DataFrame -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: PhysicalSpill :: PhysicalPlan -> FilePath -> PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: ScanConfig :: !Int -> !Char -> !Schema -> !Maybe (Expr Bool) -> ScanConfig
+ DataFrame.Lazy.Internal.PhysicalPlan: [scanBatchSize] :: ScanConfig -> !Int
+ DataFrame.Lazy.Internal.PhysicalPlan: [scanPushdownPredicate] :: ScanConfig -> !Maybe (Expr Bool)
+ DataFrame.Lazy.Internal.PhysicalPlan: [scanSchema] :: ScanConfig -> !Schema
+ DataFrame.Lazy.Internal.PhysicalPlan: [scanSeparator] :: ScanConfig -> !Char
+ DataFrame.Lazy.Internal.PhysicalPlan: data PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: data ScanConfig
+ DataFrame.Lazy.Internal.PhysicalPlan: instance GHC.Show.Show DataFrame.Lazy.Internal.PhysicalPlan.PhysicalPlan
+ DataFrame.Lazy.Internal.PhysicalPlan: instance GHC.Show.Show DataFrame.Lazy.Internal.PhysicalPlan.ScanConfig
+ DataFrame.Operations.Aggregation: buildRowToGroup :: Int -> Vector Int -> Vector Int -> Vector Int
+ DataFrame.Operations.Join: buildHashColumn :: [Text] -> DataFrame -> Vector Int
+ DataFrame.Operations.Join: hashProbeKernel :: CompactIndex -> Vector Int -> (Vector Int, Vector Int)
+ DataFrame.Operations.Join: instance GHC.Show.Show DataFrame.Operations.Join.JoinType
+ DataFrame.Operations.Join: maxJoinOutputRows :: Int
- DataFrame: SpecifyTypes :: [SchemaType] -> TypeSpec
+ DataFrame: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec
- DataFrame.Functions: mean :: (Columnable a, Real a, Unbox a) => Expr a -> Expr Double
+ DataFrame.Functions: mean :: (Columnable a, Real a) => Expr a -> Expr Double
- DataFrame.IO.CSV: SpecifyTypes :: [SchemaType] -> TypeSpec
+ DataFrame.IO.CSV: SpecifyTypes :: [(Text, SchemaType)] -> TypeSpec
- DataFrame.IO.CSV: initializeColumns :: [ByteString] -> ReadOptions -> IO [BuilderColumn]
+ DataFrame.IO.CSV: initializeColumns :: [Text] -> [ByteString] -> ReadOptions -> IO [BuilderColumn]
- DataFrame.IO.CSV: processRow :: [Text] -> Vector ByteString -> [BuilderColumn] -> IO ()
+ DataFrame.IO.CSV: processRow :: [Text] -> Vector ByteString -> Vector BuilderColumn -> IO ()
- DataFrame.IO.CSV: processStream :: [Text] -> Records (Vector ByteString) -> [BuilderColumn] -> Maybe Int -> IO ()
+ DataFrame.IO.CSV: processStream :: [Text] -> Records (Vector ByteString) -> Vector BuilderColumn -> Maybe Int -> IO ()
- DataFrame.IO.Parquet: readMetadataFromPath :: FilePath -> IO FileMetadata
+ DataFrame.IO.Parquet: readMetadataFromPath :: FilePath -> IO (FileMetadata, ByteString)
- DataFrame.IO.Parquet.Dictionary: decodeDictV1 :: Maybe DictVals -> Int -> [Int] -> Int -> ByteString -> IO Column
+ DataFrame.IO.Parquet.Dictionary: decodeDictV1 :: Maybe DictVals -> Int -> Int -> [Int] -> [Int] -> Int -> ByteString -> IO Column
- DataFrame.IO.Parquet.Encoding: decodeDictIndicesV1 :: Int -> Int -> ByteString -> ([Int], ByteString)
+ DataFrame.IO.Parquet.Encoding: decodeDictIndicesV1 :: Int -> Int -> ByteString -> (Vector Int, ByteString)
- DataFrame.IO.Parquet.Types: DBool :: [Bool] -> DictVals
+ DataFrame.IO.Parquet.Types: DBool :: Vector Bool -> DictVals
- DataFrame.IO.Parquet.Types: DDouble :: [Double] -> DictVals
+ DataFrame.IO.Parquet.Types: DDouble :: Vector Double -> DictVals
- DataFrame.IO.Parquet.Types: DFloat :: [Float] -> DictVals
+ DataFrame.IO.Parquet.Types: DFloat :: Vector Float -> DictVals
- DataFrame.IO.Parquet.Types: DInt32 :: [Int32] -> DictVals
+ DataFrame.IO.Parquet.Types: DInt32 :: Vector Int32 -> DictVals
- DataFrame.IO.Parquet.Types: DInt64 :: [Int64] -> DictVals
+ DataFrame.IO.Parquet.Types: DInt64 :: Vector Int64 -> DictVals
- DataFrame.IO.Parquet.Types: DInt96 :: [UTCTime] -> DictVals
+ DataFrame.IO.Parquet.Types: DInt96 :: Vector UTCTime -> DictVals
- DataFrame.IO.Parquet.Types: DText :: [Text] -> DictVals
+ DataFrame.IO.Parquet.Types: DText :: Vector Text -> DictVals
- DataFrame.Internal.DataFrame: Grouped :: DataFrame -> [Text] -> Vector Int -> Vector Int -> GroupedDataFrame
+ DataFrame.Internal.DataFrame: Grouped :: DataFrame -> [Text] -> Vector Int -> Vector Int -> Vector Int -> GroupedDataFrame
- DataFrame.Internal.Types: type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a)
+ DataFrame.Internal.Types: type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a, NFData a)
- DataFrame.Lazy.Internal.DataFrame: LazyDataFrame :: FilePath -> InputType -> [LazyOperation] -> Int -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: LazyDataFrame :: LogicalPlan -> Int -> LazyDataFrame
- DataFrame.Lazy.Internal.DataFrame: filter :: Columnable a => Expr Bool -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: filter :: Expr Bool -> LazyDataFrame -> LazyDataFrame
- DataFrame.Lazy.Internal.DataFrame: runDataFrame :: Columnable a => LazyDataFrame -> IO DataFrame
+ DataFrame.Lazy.Internal.DataFrame: runDataFrame :: LazyDataFrame -> IO DataFrame
- DataFrame.Lazy.Internal.DataFrame: scanCsv :: Text -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: scanCsv :: Schema -> Text -> LazyDataFrame
- DataFrame.Lazy.Internal.DataFrame: select :: Columnable a => [Text] -> LazyDataFrame -> LazyDataFrame
+ DataFrame.Lazy.Internal.DataFrame: select :: [Text] -> LazyDataFrame -> LazyDataFrame
- DataFrame.Typed: impute :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a) => a -> TypedDataFrame cols -> TypedDataFrame (Impute name cols)
+ DataFrame.Typed: impute :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, Maybe a ~ Lookup name cols) => a -> TypedDataFrame cols -> TypedDataFrame (Impute name cols)
- DataFrame.Typed: mean :: forall a (cols :: [Type]). (Columnable a, Real a, Unbox a) => TExpr cols a -> TExpr cols Double
+ DataFrame.Typed: mean :: forall a (cols :: [Type]). (Columnable a, Real a) => TExpr cols a -> TExpr cols Double
- DataFrame.Typed.Expr: mean :: forall a (cols :: [Type]). (Columnable a, Real a, Unbox a) => TExpr cols a -> TExpr cols Double
+ DataFrame.Typed.Expr: mean :: forall a (cols :: [Type]). (Columnable a, Real a) => TExpr cols a -> TExpr cols Double
- DataFrame.Typed.Operations: impute :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a) => a -> TypedDataFrame cols -> TypedDataFrame (Impute name cols)
+ DataFrame.Typed.Operations: impute :: forall (name :: Symbol) a (cols :: [Type]). (KnownSymbol name, Columnable a, Maybe a ~ Lookup name cols) => a -> TypedDataFrame cols -> TypedDataFrame (Impute name cols)
Files
- CHANGELOG.md +10/−0
- app/LazyBenchmark.hs +311/−0
- benchmark/Main.hs +164/−9
- dataframe.cabal +32/−2
- src/DataFrame/Functions.hs +32/−8
- src/DataFrame/IO/CSV.hs +191/−44
- src/DataFrame/IO/Parquet.hs +226/−116
- src/DataFrame/IO/Parquet/Binary.hs +30/−3
- src/DataFrame/IO/Parquet/Dictionary.hs +160/−44
- src/DataFrame/IO/Parquet/Encoding.hs +24/−25
- src/DataFrame/IO/Parquet/Levels.hs +79/−10
- src/DataFrame/IO/Parquet/Page.hs +25/−2
- src/DataFrame/IO/Parquet/Types.hs +8/−7
- src/DataFrame/IO/Unstable/CSV.hs +36/−19
- src/DataFrame/Internal/Column.hs +418/−5
- src/DataFrame/Internal/DataFrame.hs +11/−2
- src/DataFrame/Internal/Expression.hs +22/−1
- src/DataFrame/Internal/Interpreter.hs +478/−838
- src/DataFrame/Internal/Parsing.hs +130/−2
- src/DataFrame/Internal/Row.hs +4/−0
- src/DataFrame/Internal/Types.hs +2/−1
- src/DataFrame/Lazy/IO/Binary.hs +444/−0
- src/DataFrame/Lazy/IO/CSV.hs +231/−174
- src/DataFrame/Lazy/Internal/DataFrame.hs +110/−79
- src/DataFrame/Lazy/Internal/Executor.hs +543/−0
- src/DataFrame/Lazy/Internal/LogicalPlan.hs +44/−0
- src/DataFrame/Lazy/Internal/Optimizer.hs +209/−0
- src/DataFrame/Lazy/Internal/PhysicalPlan.hs +36/−0
- src/DataFrame/Operations/Aggregation.hs +27/−7
- src/DataFrame/Operations/Join.hs +425/−192
- src/DataFrame/Typed.hs +0/−3
- src/DataFrame/Typed/Expr.hs +16/−5
- src/DataFrame/Typed/Join.hs +2/−2
- src/DataFrame/Typed/Operations.hs +1/−12
- tests/IO/JSON.hs +112/−0
- tests/Internal/Parsing.hs +237/−0
- tests/LazyParquet.hs +131/−0
- tests/Main.hs +79/−5149
- tests/Operations/Aggregations.hs +77/−0
- tests/Operations/Apply.hs +39/−0
- tests/Operations/GroupBy.hs +2/−2
- tests/Operations/Join.hs +99/−6
- tests/Operations/Statistics.hs +61/−0
- tests/Operations/Typing.hs +5087/−0
- tests/Parquet.hs +757/−0
- tests/Properties.hs +37/−0
CHANGELOG.md view
@@ -1,5 +1,15 @@ # Revision history for dataframe +## 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.+* You can now explicitly set the schema of a subset of columns (named) as opposed to setting a list.+* Join order is changed from pipelining style to function call style.+* Bug in division being marked as commutative.+* Columnable now requires nfdata.+* Parquet reads are now faster across the board.+* Better test coverage.+ ## 0.6.0.0 * New typed API see https://dataframe.readthedocs.io/en/latest/using_dataframe_in_a_standalone_script.html * Faster joins
+ app/LazyBenchmark.hs view
@@ -0,0 +1,311 @@+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++{- | End-to-end smoke test and benchmark for the Lazy streaming API.++ Usage:+ cabal run lazy-bench [-- [OPTIONS]]++ Options:+ --rows N Number of rows to generate (default: 1_000_000_000)+ --file PATH Output CSV path (default: /tmp/lazy_1b.csv)+ --skip-gen Skip generation if the file already exists++ The executable generates a CSV file (streaming, constant memory) then+ runs five Lazy queries over it, printing timing and result summaries.++ For heap/GC stats run with:+ cabal run lazy-bench -- +RTS -s -RTS+-}+module Main where++import Control.Monad (foldM, forM_, when)+import qualified Data.ByteString.Builder as Builder+import qualified Data.Map as M+import qualified Data.Text as T+import Data.Time (UTCTime, diffUTCTime, getCurrentTime)+import qualified DataFrame as D+import DataFrame.Internal.Schema (Schema (..), schemaType)+import qualified DataFrame.Lazy as L+import DataFrame.Operators+import System.Directory (doesFileExist, getFileSize)+import System.Environment (getArgs)+import System.Exit (exitFailure)+import System.IO (+ BufferMode (..),+ IOMode (..),+ hFlush,+ hSetBuffering,+ stdout,+ withFile,+ )+import System.Random.Stateful++-- ---------------------------------------------------------------------------+-- Defaults+-- ---------------------------------------------------------------------------++defaultRows :: Int+defaultRows = 1_000_000_000++defaultFile :: FilePath+defaultFile = "/tmp/lazy_1b.csv"++-- Rows written per Builder flush to disk.+chunkSize :: Int+chunkSize = 500_000++-- ---------------------------------------------------------------------------+-- Argument parsing+-- ---------------------------------------------------------------------------++data Opts = Opts+ { optRows :: Int+ , optFile :: FilePath+ , optSkipGen :: Bool+ }++parseArgs :: [String] -> Either String Opts+parseArgs = go (Opts defaultRows defaultFile False)+ where+ go opts [] = Right opts+ go opts ("--rows" : n : rest) = case reads n of+ [(v, "")] -> go opts{optRows = v} rest+ _ -> Left $ "bad --rows value: " ++ n+ go opts ("--file" : p : rest) = go opts{optFile = p} rest+ go opts ("--skip-gen" : rest) = go opts{optSkipGen = True} rest+ go _ (flag : _) = Left $ "unknown flag: " ++ flag++-- ---------------------------------------------------------------------------+-- CSV generation+-- ---------------------------------------------------------------------------++{- | Write @n@ rows of schema++ id (Int), x (Double), y (Double), category (Text: A/B/C/D)++ to @path@ using a streaming Builder, keeping heap usage constant.+-}+generateCsv :: FilePath -> Int -> IO ()+generateCsv path n = do+ g <- newIOGenM =<< newStdGen+ t0 <- getCurrentTime+ withFile path WriteMode $ \h -> do+ hSetBuffering h (BlockBuffering (Just (4 * 1024 * 1024)))+ Builder.hPutBuilder h (Builder.byteString "id,x,y,category\n")+ let numChunks = n `div` chunkSize+ remainder = n `mod` chunkSize+ forM_ [0 .. numChunks - 1] $ \c -> do+ bldr <- buildChunk g (c * chunkSize) chunkSize+ Builder.hPutBuilder h bldr+ when (c `mod` 200 == 0) $ do+ let done = c * chunkSize+ pct = (done * 100) `div` n+ putStr $ "\r " ++ show pct ++ "% — " ++ commas done ++ " rows"+ hFlush stdout+ when (remainder > 0) $ do+ bldr <- buildChunk g (numChunks * chunkSize) remainder+ Builder.hPutBuilder h bldr+ t1 <- getCurrentTime+ putStrLn $ "\r 100% — " ++ commas n ++ " rows written in " ++ showDiff t0 t1++buildChunk :: IOGenM StdGen -> Int -> Int -> IO Builder.Builder+buildChunk g baseId count =+ foldM (\acc i -> (acc <>) <$> buildRow g baseId i) mempty [0 .. count - 1]++buildRow :: IOGenM StdGen -> Int -> Int -> IO Builder.Builder+buildRow g baseId i = do+ x <- uniformRM (0.0 :: Double, 1.0) g+ y <- uniformRM (0.0 :: Double, 1.0) g+ c <- uniformRM (0 :: Int, 3) g+ return $+ Builder.intDec (baseId + i)+ <> Builder.char7 ','+ <> buildDouble x+ <> Builder.char7 ','+ <> buildDouble y+ <> Builder.char7 ','+ <> catChar c+ <> Builder.char7 '\n'++buildDouble :: Double -> Builder.Builder+buildDouble x =+ let scaled = round (x * 1_000_000) :: Int+ whole = scaled `div` 1_000_000+ frac = scaled `mod` 1_000_000+ in Builder.intDec whole+ <> Builder.char7 '.'+ <> pad6 frac++pad6 :: Int -> Builder.Builder+pad6 n+ | n < 10 = Builder.byteString "00000" <> Builder.intDec n+ | n < 100 = Builder.byteString "0000" <> Builder.intDec n+ | n < 1000 = Builder.byteString "000" <> Builder.intDec n+ | n < 10_000 = Builder.byteString "00" <> Builder.intDec n+ | n < 100_000 = Builder.byteString "0" <> Builder.intDec n+ | otherwise = Builder.intDec n++catChar :: Int -> Builder.Builder+catChar 0 = Builder.char7 'A'+catChar 1 = Builder.char7 'B'+catChar 2 = Builder.char7 'C'+catChar _ = Builder.char7 'D'++-- ---------------------------------------------------------------------------+-- Lazy queries+-- ---------------------------------------------------------------------------++runQuery :: String -> IO D.DataFrame -> IO ()+runQuery label action = do+ putStrLn $ "\n── " ++ label+ t0 <- getCurrentTime+ df <- action+ t1 <- getCurrentTime+ let (rows, cols) = D.dimensions df+ putStrLn $ " rows returned : " ++ commas rows+ putStrLn $ " columns : " ++ show cols+ putStrLn $ " time : " ++ showDiff t0 t1+ when (rows > 0 && rows <= 30) $ print df++-- ---------------------------------------------------------------------------+-- Main+-- ---------------------------------------------------------------------------++main :: IO ()+main = do+ hSetBuffering stdout LineBuffering+ args <- getArgs+ opts <- case parseArgs args of+ Left err -> putStrLn ("Error: " ++ err) >> exitFailure+ Right o -> return o++ let path = optFile opts+ n = optRows opts+ pathT = T.pack path++ -- -----------------------------------------------------------------------+ -- Phase 1: Generate+ -- -----------------------------------------------------------------------+ putStrLn "=== Lazy API 1B-row benchmark ==="+ putStrLn $ " rows : " ++ commas n+ putStrLn $ " file : " ++ path+ putStrLn " tip : run with '+RTS -s -RTS' for heap stats"+ putStrLn ""++ exists <- doesFileExist path+ if optSkipGen opts && exists+ then do+ sz <- getFileSize path+ putStrLn $ "Skipping generation — file exists (" ++ showBytes sz ++ ")"+ else do+ putStrLn "Phase 1: Generating CSV …"+ generateCsv path n+ sz <- getFileSize path+ putStrLn $ " file size: " ++ showBytes sz++ -- -----------------------------------------------------------------------+ -- Phase 2: Lazy queries+ -- -----------------------------------------------------------------------+ putStrLn "\nPhase 2: Lazy queries"++ -- Schema for the generated CSV: id (Int), x (Double), y (Double), category (Text)+ let schema =+ Schema $+ M.fromList+ [ ("id", schemaType @Int)+ , ("x", schemaType @Double)+ , ("y", schemaType @Double)+ , ("category", schemaType @T.Text)+ ]++ -- Q1: Preview — limit 20, no filter.+ -- Demonstrates that the executor reads only the first batch.+ runQuery "Q1 — preview first 20 rows (no filter)" $+ L.runDataFrame $+ L.limit 20 $+ L.scanCsv schema pathT++ -- Q2: Filter + limit.+ -- x > 0.999 ≈ 0.1% of rows. With a 512K-row batch the executor finds+ -- ~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.scanCsv schema pathT++ -- Q3: Filter + derive + select + limit.+ -- Shows projection pushdown: only id/x/y/category are read, z is derived.+ -- 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.select ["id", "z"] $+ L.derive "z" (col @Double "x" * col @Double "y") $+ L.filter (col @Double "x" .> lit 0.999) $+ L.scanCsv schema pathT++ -- Q4: Filter fusion demo.+ -- Two consecutive filters are fused into one AND predicate by the optimizer.+ -- Result: rows where x > 0.5 AND y > 0.5 (≈ 25% of total).+ -- 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.scanCsv schema pathT++ -- Q5: Full scan, heavy filter, count results.+ -- x > 0.999 across the whole file ≈ 0.1% × N rows.+ -- For 1B rows that is ~1M results — materialised into one DataFrame.+ -- This query exercises streaming across all batches.+ runQuery+ ( "Q5 — full scan, filter (x > 0.999), count (~"+ ++ approx (n `div` 1000)+ ++ " rows expected)"+ )+ $ L.runDataFrame+ $ L.select ["id", "x"]+ $ L.filter (col @Double "x" .> lit 0.999)+ $ L.scanCsv schema pathT++ putStrLn "\nDone."++-- ---------------------------------------------------------------------------+-- Formatting helpers+-- ---------------------------------------------------------------------------++showDiff :: UTCTime -> UTCTime -> String+showDiff t0 t1 = show (diffUTCTime t1 t0)++commas :: Int -> String+commas n+ | n < 1000 = show n+ | otherwise = commas (n `div` 1000) ++ "," ++ pad3 (n `mod` 1000)+ where+ pad3 x+ | x < 10 = "00" ++ show x+ | x < 100 = "0" ++ show x+ | otherwise = show x++approx :: Int -> String+approx n+ | n >= 1_000_000 = show (n `div` 1_000_000) ++ "M"+ | n >= 1_000 = show (n `div` 1_000) ++ "K"+ | otherwise = show n++showBytes :: Integer -> String+showBytes b+ | b >= 1_073_741_824 = fmt (fromIntegral b / 1_073_741_824) ++ " GiB"+ | b >= 1_048_576 = fmt (fromIntegral b / 1_048_576) ++ " MiB"+ | b >= 1_024 = fmt (fromIntegral b / 1_024) ++ " KiB"+ | otherwise = show b ++ " B"+ where+ fmt :: Double -> String+ fmt x =+ show (fromIntegral (round (x * 10) :: Int) `div` 10 :: Int)+ ++ "."+ ++ show (fromIntegral (round (x * 10) :: Int) `mod` 10 :: Int)
benchmark/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} @@ -6,8 +7,10 @@ import Control.Monad (void) import Criterion.Main+import DataFrame.Operations.Join import DataFrame.Operators-import System.Process+import System.Process hiding (env)+import System.Random.Stateful haskell :: IO () haskell = do@@ -99,6 +102,101 @@ parseMeasurementsTXT :: IO () parseMeasurementsTXT = parseFile "./data/measurements.txt" +{- | Generate a pair of dataframes for a 1:1 join scenario.+ Left has keys [0..n-1], right has keys [0..n-1] shuffled.+ overlap controls what fraction of keys appear in both sides.+-}+mkOneToOne :: Int -> Double -> IO (D.DataFrame, D.DataFrame)+mkOneToOne n overlap = do+ g <- newIOGenM =<< newStdGen+ let rightSize = max 1 (round (fromIntegral n * overlap))+ -- Left: keys 0..n-1 with a payload column+ let leftKeys = [0 :: Int .. n - 1]+ leftVals = [0 :: Int .. n - 1]+ leftDf =+ D.fromNamedColumns+ [ ("key", D.fromList leftKeys)+ , ("A", D.fromList leftVals)+ ]+ -- Right: take first `rightSize` keys, add non-overlapping keys for the rest+ rightPayload <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightSize]+ let rightKeys = [0 :: Int .. rightSize - 1]+ rightDf =+ D.fromNamedColumns+ [ ("key", D.fromList rightKeys)+ , ("B", D.fromList rightPayload)+ ]+ return (leftDf, rightDf)++{- | Generate a pair of dataframes for a many-to-many join scenario.+ Keys are drawn from [0..cardinality-1], so rows share keys.+-}+mkManyToMany :: Int -> Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkManyToMany leftRows rightRows cardinality = do+ g <- newIOGenM =<< newStdGen+ leftKeys <- mapM (\_ -> uniformRM (0 :: Int, cardinality - 1) g) [1 .. leftRows]+ rightKeys <-+ mapM (\_ -> uniformRM (0 :: Int, cardinality - 1) g) [1 .. rightRows]+ leftVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. leftRows]+ rightVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightRows]+ let leftDf =+ D.fromNamedColumns+ [ ("key", D.fromList leftKeys)+ , ("A", D.fromList leftVals)+ ]+ rightDf =+ D.fromNamedColumns+ [ ("key", D.fromList rightKeys)+ , ("B", D.fromList rightVals)+ ]+ return (leftDf, rightDf)++{- | Generate a pair of dataframes for a many-to-one join+ (fact table joining a dimension table).+ Left has n rows with keys drawn from [0..dimSize-1].+ Right has exactly dimSize rows with unique keys.+-}+mkManyToOne :: Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkManyToOne factRows dimSize = do+ g <- newIOGenM =<< newStdGen+ factKeys <- mapM (\_ -> uniformRM (0 :: Int, dimSize - 1) g) [1 .. factRows]+ factVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. factRows]+ dimVals <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. dimSize]+ let factDf =+ D.fromNamedColumns+ [ ("key", D.fromList factKeys)+ , ("A", D.fromList factVals)+ ]+ dimDf =+ D.fromNamedColumns+ [ ("key", D.fromList [0 :: Int .. dimSize - 1])+ , ("B", D.fromList dimVals)+ ]+ return (factDf, dimDf)++mkMultiKey :: Int -> Int -> IO (D.DataFrame, D.DataFrame)+mkMultiKey leftRows rightRows = do+ g <- newIOGenM =<< newStdGen+ lk1 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. leftRows]+ lk2 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. leftRows]+ rk1 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. rightRows]+ rk2 <- mapM (\_ -> uniformRM (0 :: Int, 99) g) [1 .. rightRows]+ lv <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. leftRows]+ rv <- mapM (\_ -> uniformRM (0 :: Int, 1_000_000) g) [1 .. rightRows]+ let leftDf =+ D.fromNamedColumns+ [ ("key1", D.fromList lk1)+ , ("key2", D.fromList lk2)+ , ("A", D.fromList lv)+ ]+ rightDf =+ D.fromNamedColumns+ [ ("key1", D.fromList rk1)+ , ("key2", D.fromList rk2)+ , ("B", D.fromList rv)+ ]+ return (leftDf, rightDf)+ main :: IO () main = do output <- readProcess "cabal" ["build", "-O2"] ""@@ -135,12 +233,69 @@ parseFileUnstableSIMD "./data/effects-of-covid-19-on-trade-at-15-december-2021-provisional.csv" ]- -- this file was removed as it's too large you may substitute- -- with your own if you wish to run it locally on a large file- -- , bgroup- -- "customers.csv (334 MB)"- -- [ bench "Attoparsec" $ nfIO $ parseFile "./data/customers.csv"- -- , bench "Native Haskell" $ nfIO $ parseFileUnstable "./data/customers.csv"- -- , bench "SIMD" $ nfIO $ parseFileUnstableSIMD "./data/customers.csv"- -- ]+ , bgroup+ "join/inner/1:1"+ [ env (mkOneToOne 1_000 1.0) $ \ ~(l, r) ->+ bench "1K rows" $ nf (innerJoin ["key"] r) l+ , env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+ bench "10K rows" $ nf (innerJoin ["key"] r) l+ , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+ bench "100K rows" $ nf (innerJoin ["key"] r) l+ ]+ , bgroup+ "join/inner/1:1-partial-overlap"+ [ env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+ bench "100K rows, 50% overlap" $ nf (innerJoin ["key"] r) l+ , env (mkOneToOne 100_000 0.1) $ \ ~(l, r) ->+ bench "100K rows, 10% overlap" $ nf (innerJoin ["key"] r) l+ ]+ , bgroup+ "join/inner/many:1"+ [ env (mkManyToOne 10_000 100) $ \ ~(fact, dim) ->+ bench "10K fact x 100 dim" $ nf (innerJoin ["key"] dim) fact+ , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+ bench "100K fact x 1K dim" $ nf (innerJoin ["key"] dim) fact+ , env (mkManyToOne 100_000 100) $ \ ~(fact, dim) ->+ bench "100K fact x 100 dim" $ nf (innerJoin ["key"] dim) fact+ ]+ , bgroup+ "join/inner/many:many"+ [ env (mkManyToMany 1_000 1_000 100) $ \ ~(l, r) ->+ bench "1Kx1K, 100 keys" $ nf (innerJoin ["key"] r) l+ , env (mkManyToMany 10_000 10_000 1_000) $ \ ~(l, r) ->+ bench "10Kx10K, 1K keys" $ nf (innerJoin ["key"] r) l+ , env (mkManyToMany 10_000 10_000 100) $ \ ~(l, r) ->+ bench "10Kx10K, 100 keys" $ nf (innerJoin ["key"] r) l+ ]+ , bgroup+ "join/left"+ [ env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+ bench "1:1, 10K rows" $ nf (leftJoin ["key"] r) l+ , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+ bench "1:1, 100K rows" $ nf (leftJoin ["key"] r) l+ , env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+ bench "1:1, 100K rows, 50%" $ nf (leftJoin ["key"] r) l+ , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+ bench "many:1, 100K x 1K" $ nf (leftJoin ["key"] dim) fact+ , env (mkManyToMany 10_000 10_000 1_000) $ \ ~(l, r) ->+ bench "many:many, 10Kx10K" $ nf (leftJoin ["key"] r) l+ ]+ , bgroup+ "join/fullOuter"+ [ env (mkOneToOne 10_000 1.0) $ \ ~(l, r) ->+ bench "1:1, 10K rows" $ nf (fullOuterJoin ["key"] r) l+ , env (mkOneToOne 100_000 1.0) $ \ ~(l, r) ->+ bench "1:1, 100K rows" $ nf (fullOuterJoin ["key"] r) l+ , env (mkOneToOne 100_000 0.5) $ \ ~(l, r) ->+ bench "1:1, 100K rows, 50%" $ nf (fullOuterJoin ["key"] r) l+ , env (mkManyToOne 100_000 1_000) $ \ ~(fact, dim) ->+ bench "many:1, 100K x 1K" $ nf (fullOuterJoin ["key"] dim) fact+ ]+ , bgroup+ "join/multiKey"+ [ env (mkMultiKey 10_000 10_000) $ \ ~(l, r) ->+ bench "inner 10Kx10K, 2 keys" $ nf (innerJoin ["key1", "key2"] r) l+ , env (mkMultiKey 10_000 10_000) $ \ ~(l, r) ->+ bench "left 10Kx10K, 2 keys" $ nf (leftJoin ["key1", "key2"] r) l+ ] ]
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: dataframe-version: 0.6.0.0+version: 0.7.0.0 synopsis: A fast, safe, and intuitive DataFrame library. @@ -89,7 +89,12 @@ DataFrame.IO.Parquet.Time, DataFrame.IO.Parquet.Types, DataFrame.Lazy.IO.CSV,+ DataFrame.Lazy.IO.Binary, DataFrame.Lazy.Internal.DataFrame,+ DataFrame.Lazy.Internal.LogicalPlan,+ DataFrame.Lazy.Internal.PhysicalPlan,+ DataFrame.Lazy.Internal.Optimizer,+ DataFrame.Lazy.Internal.Executor, DataFrame.Monad, DataFrame.DecisionTree, DataFrame.Typed.Types,@@ -103,6 +108,7 @@ DataFrame.Typed.Expr, DataFrame.Typed build-depends: base >= 4 && <5,+ deepseq >= 1 && < 2, aeson >= 0.11.0.0 && < 3, array >= 0.5.4.0 && < 0.6, attoparsec >= 0.12 && < 0.15,@@ -129,6 +135,7 @@ zstd >= 0.1.2.0 && < 0.2, mmap >= 0.5.8 && < 0.6, parallel >= 3.2.2.0 && < 5,+ stm >= 2.5 && < 3, filepath >= 1.4 && < 2, Glob >= 0.10 && < 1, @@ -176,6 +183,21 @@ default-language: Haskell2010 ghc-options: -rtsopts -threaded -with-rtsopts=-N +executable lazy-bench+ import: warnings+ main-is: LazyBenchmark.hs+ build-depends: base >= 4 && < 5,+ bytestring >= 0.11 && < 0.13,+ containers >= 0.6.7 && < 0.9,+ dataframe >= 0.5 && < 1,+ directory >= 1.3.0.0 && < 2,+ random >= 1 && < 2,+ text >= 2.0 && < 3,+ time >= 1.12 && < 2,+ hs-source-dirs: app+ default-language: Haskell2010+ ghc-options: -rtsopts -threaded -with-rtsopts=-N+ benchmark dataframe-benchmark import: warnings type: exitcode-stdio-1.0@@ -184,7 +206,8 @@ build-depends: base >= 4 && < 5, criterion >= 1 && < 2, process >= 1.6 && < 2,- dataframe >= 0.5 && < 1+ dataframe >= 0.5 && < 1,+ random >= 1 && < 2, default-language: Haskell2010 ghc-options: -threaded@@ -198,6 +221,8 @@ other-modules: Assertions, Functions, GenDataFrame,+ Internal.Parsing,+ IO.JSON, Operations.Aggregations, Operations.Apply, Operations.Core,@@ -213,9 +238,13 @@ Operations.Subset, Operations.Statistics, Operations.Take,+ Operations.Typing,+ LazyParquet, Parquet,+ Properties, Monad build-depends: base >= 4 && < 5,+ bytestring >= 0.11 && < 0.13, dataframe >= 0.5 && < 1, directory >= 1.3.0.0 && < 2, HUnit ^>= 1.6,@@ -229,3 +258,4 @@ containers >= 0.6.7 && < 0.9 hs-source-dirs: tests default-language: Haskell2010+
src/DataFrame/Functions.hs view
@@ -18,6 +18,7 @@ import DataFrame.Internal.Column import DataFrame.Internal.DataFrame ( DataFrame (..),+ empty, unsafeGetColumn, ) import DataFrame.Internal.Expression hiding (normalize)@@ -46,6 +47,9 @@ import Debug.Trace (trace) import Language.Haskell.TH import qualified Language.Haskell.TH.Syntax as TH+import System.Directory (doesDirectoryExist)+import System.FilePath ((</>))+import System.FilePath.Glob (glob) import Text.Regex.TDFA import Prelude hiding (maximum, minimum) import Prelude as P@@ -138,7 +142,7 @@ (MkUnaryOp{unaryFn = Prelude.not, unaryName = "not", unarySymbol = Just "~"}) count :: (Columnable a) => Expr a -> Expr Int-count = Agg (FoldAgg "count" (Just 0) (\acc _ -> acc + 1))+count = Agg (MergeAgg "count" (0 :: Int) (\c _ -> c + 1) (+) id) collect :: (Columnable a) => Expr a -> Expr [a] collect = Agg (FoldAgg "collect" (Just []) (flip (:)))@@ -170,11 +174,16 @@ sumMaybe :: forall a. (Columnable a, Num a) => Expr (Maybe a) -> Expr a sumMaybe = Agg (CollectAgg "sumMaybe" (P.sum . Maybe.catMaybes . V.toList)) -mean :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double-mean = Agg (CollectAgg "mean" mean')-{-# SPECIALIZE DataFrame.Functions.mean :: Expr Double -> Expr Double #-}-{-# SPECIALIZE DataFrame.Functions.mean :: Expr Int -> Expr Double #-}-{-# INLINEABLE DataFrame.Functions.mean #-}+mean :: (Columnable a, Real a) => Expr a -> Expr Double+mean =+ Agg+ ( MergeAgg+ "mean"+ (MeanAcc 0.0 0)+ (\(MeanAcc s c) x -> MeanAcc (s + realToFrac x) (c + 1))+ (\(MeanAcc s1 c1) (MeanAcc s2 c2) -> MeanAcc (s1 + s2) (c1 + c2))+ (\(MeanAcc s c) -> if c == 0 then 0 / 0 else s / fromIntegral c)+ ) meanMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double meanMaybe = Agg (CollectAgg "meanMaybe" (mean' . optionalToDoubleVector))@@ -442,8 +451,23 @@ declareColumnsFromParquetFile :: String -> DecsQ declareColumnsFromParquetFile path = do- metadata <- liftIO (Parquet.readMetadataFromPath path)- let df = schemaToEmptyDataFrame (schema metadata)+ 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+ )+ DataFrame.Internal.DataFrame.empty+ files declareColumns df schemaToEmptyDataFrame :: [SchemaElement] -> DataFrame
src/DataFrame/IO/CSV.hs view
@@ -10,6 +10,8 @@ module DataFrame.IO.CSV where +import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Lazy as BL import qualified Data.List as L import qualified Data.Map.Strict as M@@ -66,6 +68,7 @@ = BuilderInt !(PagedUnboxedVector Int) !(PagedUnboxedVector Word8) | BuilderDouble !(PagedUnboxedVector Double) !(PagedUnboxedVector Word8) | BuilderText !(PagedVector T.Text) !(PagedUnboxedVector Word8)+ | BuilderBS !(PagedVector BS.ByteString) !(PagedUnboxedVector Word8) newPagedVector :: IO (PagedVector a) newPagedVector = do@@ -123,10 +126,21 @@ active <- readIORef activeRef chunks <- readIORef chunksRef - lastChunk <- V.unsafeFreeze (VM.slice 0 count active)+ writeIORef chunksRef [] -- release chunk references+ let frozenChunks = reverse chunks+ totalLen = count + sum (map V.length frozenChunks) - return $! V.concat (reverse (lastChunk : chunks))+ mv <- VM.unsafeNew totalLen + let copyChunk !offset chunk = do+ V.copy (VM.slice offset (V.length chunk) mv) chunk+ pure (offset + V.length chunk)++ offset <- foldM copyChunk 0 frozenChunks+ VM.copy (VM.slice offset count mv) (VM.slice 0 count active)++ V.unsafeFreeze mv+ freezePagedUnboxedVector :: (VUM.Unbox a) => PagedUnboxedVector a -> IO (VU.Vector a) freezePagedUnboxedVector (PagedUnboxedVector chunksRef activeRef countRef) = do@@ -134,14 +148,29 @@ active <- readIORef activeRef chunks <- readIORef chunksRef - lastChunk <- VU.unsafeFreeze (VUM.slice 0 count active)- return $! VU.concat (reverse (lastChunk : chunks))+ writeIORef chunksRef [] -- release chunk references+ let frozenChunks = reverse chunks+ totalLen = count + sum (map VU.length frozenChunks) + mv <- VUM.unsafeNew totalLen++ let copyChunk !offset chunk = do+ VU.copy (VUM.slice offset (VU.length chunk) mv) chunk+ pure (offset + VU.length chunk)++ offset <- foldM copyChunk 0 frozenChunks+ VUM.copy (VUM.slice offset count mv) (VUM.slice 0 count active)++ VU.unsafeFreeze mv+ -- | STANDARD CONFIG TYPES data HeaderSpec = NoHeader | UseFirstRow | ProvideNames [T.Text] deriving (Eq, Show) -data TypeSpec = InferFromSample Int | SpecifyTypes [SchemaType] | NoInference+data TypeSpec+ = InferFromSample Int+ | SpecifyTypes [(T.Text, SchemaType)]+ | NoInference -- | CSV read parameters. data ReadOptions = ReadOptions@@ -175,9 +204,9 @@ shouldInferFromSample (InferFromSample _) = True shouldInferFromSample _ = False -schemaTypes :: TypeSpec -> [SchemaType]-schemaTypes (SpecifyTypes xs) = xs-schemaTypes _ = []+schemaTypeMap :: TypeSpec -> M.Map T.Text SchemaType+schemaTypeMap (SpecifyTypes xs) = M.fromList xs+schemaTypeMap _ = M.empty typeInferenceSampleSize :: TypeSpec -> Int typeInferenceSampleSize (InferFromSample n) = n@@ -270,46 +299,42 @@ ) (sampleRow, _) <- peekStream rowsToProcess- builderCols <- initializeColumns (V.toList sampleRow) opts+ builderCols <- initializeColumns columnNames (V.toList sampleRow) opts+ let !builderColsV = V.fromList builderCols processStream (missingIndicators opts) rowsToProcess- builderCols+ builderColsV (numColumns opts) - frozenCols <- V.fromList <$> mapM freezeBuilderColumn builderCols+ frozenCols <- V.mapM (finalizeBuilderColumn opts) builderColsV let numRows = maybe 0 columnLength (frozenCols V.!? 0) - let df =- DataFrame- frozenCols- (M.fromList (zip columnNames [0 ..]))- (numRows, V.length frozenCols)- M.empty -- TODO give typed column references return $- if shouldInferFromSample (typeSpec opts)- then- parseDefaults- (missingIndicators opts)- (typeInferenceSampleSize (typeSpec opts))- (safeRead opts)- (dateFormat opts)- df- else- if not (null (schemaTypes (typeSpec opts)))- then parseWithTypes (schemaTypes (typeSpec opts)) df- else df+ DataFrame+ frozenCols+ (M.fromList (zip columnNames [0 ..]))+ (numRows, V.length frozenCols)+ M.empty -- TODO give typed column references -initializeColumns :: [BL.ByteString] -> ReadOptions -> IO [BuilderColumn]-initializeColumns row opts = case typeSpec opts of- NoInference -> zipWithM initColumn row (expandTypes [])- InferFromSample _ -> zipWithM initColumn row (expandTypes [])- SpecifyTypes ts -> zipWithM initColumn row (expandTypes ts)+initializeColumns ::+ [T.Text] -> [BL.ByteString] -> ReadOptions -> IO [BuilderColumn]+initializeColumns names row opts = zipWithM initColumn names (map lookupType names) where- expandTypes xs = xs ++ replicate (length row - length xs) (schemaType @T.Text)- initColumn :: BL.ByteString -> SchemaType -> IO BuilderColumn- initColumn _ t = do+ typeMap = schemaTypeMap (typeSpec opts)+ -- Return Nothing for columns that should be inferred from BS+ shouldInfer = case typeSpec opts of+ InferFromSample _ -> True+ SpecifyTypes _ -> True+ NoInference -> False+ lookupType name = M.lookup name typeMap+ initColumn :: T.Text -> Maybe SchemaType -> IO BuilderColumn+ initColumn _ Nothing | shouldInfer = do validityRef <- newPagedUnboxedVector+ BuilderBS <$> newPagedVector <*> pure validityRef+ initColumn _ mtype = do+ validityRef <- newPagedUnboxedVector+ let t = fromMaybe (schemaType @T.Text) mtype case t of SType (_ :: P.Proxy a) -> case testEquality (typeRep @a) (typeRep @Int) of Just Refl -> BuilderInt <$> newPagedUnboxedVector <*> pure validityRef@@ -320,7 +345,7 @@ processStream :: [T.Text] -> CsvStream.Records (V.Vector BL.ByteString) ->- [BuilderColumn] ->+ V.Vector BuilderColumn -> Maybe Int -> IO () processStream _ _ _ (Just 0) = return ()@@ -330,11 +355,12 @@ processStream missing (Cons (Left err) _) _ _ = error ("CSV Parse Error: " ++ err) processStream missing (Nil _ _) _ _ = return () -processRow :: [T.Text] -> V.Vector BL.ByteString -> [BuilderColumn] -> IO ()-processRow missing !vals !cols = V.zipWithM_ processValue vals (V.fromList cols)+processRow ::+ [T.Text] -> V.Vector BL.ByteString -> V.Vector BuilderColumn -> IO ()+processRow missing !vals !cols = V.zipWithM_ processValue vals cols where processValue !bs !col = do- let bs' = BL.toStrict bs+ let !bs' = BL.toStrict bs case col of BuilderInt gv valid -> case readByteStringInt bs' of Just !i -> appendPagedUnboxedVector gv i >> appendPagedUnboxedVector valid 1@@ -347,26 +373,147 @@ if isNullish val || 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+ then appendPagedVector gv BS.empty >> appendPagedUnboxedVector valid 0+ else appendPagedVector gv bs'' >> appendPagedUnboxedVector valid 1 freezeBuilderColumn :: BuilderColumn -> IO Column freezeBuilderColumn (BuilderInt gv validRef) = do vec <- freezePagedUnboxedVector gv valid <- freezePagedUnboxedVector validRef if VU.all (== 1) valid- then return $ UnboxedColumn vec+ then return $! UnboxedColumn vec else constructOptional vec valid freezeBuilderColumn (BuilderDouble gv validRef) = do vec <- freezePagedUnboxedVector gv valid <- freezePagedUnboxedVector validRef if VU.all (== 1) valid- then return $ UnboxedColumn vec+ then return $! UnboxedColumn vec else constructOptional vec valid freezeBuilderColumn (BuilderText gv validRef) = do vec <- freezePagedVector gv valid <- freezePagedUnboxedVector validRef if VU.all (== 1) valid- then return $ BoxedColumn vec+ then return $! BoxedColumn vec else constructOptionalBoxed vec valid+freezeBuilderColumn (BuilderBS _ _) =+ error+ "freezeBuilderColumn: BuilderBS must be finalized via finalizeBuilderColumn"++finalizeBuilderColumn :: ReadOptions -> BuilderColumn -> IO Column+finalizeBuilderColumn opts (BuilderBS gv validRef) = do+ vec <- freezePagedVector gv+ valid <- freezePagedUnboxedVector validRef+ return $! inferColumnFromBS opts vec valid+finalizeBuilderColumn _ bc = freezeBuilderColumn bc++inferColumnFromBS ::+ ReadOptions -> V.Vector BS.ByteString -> VU.Vector Word8 -> Column+inferColumnFromBS opts vec valid =+ let sampleN = let n = typeInferenceSampleSize (typeSpec opts) in if n == 0 then 100 else n+ dfmt = dateFormat opts+ asMaybeFull = V.generate (V.length vec) $ \i ->+ if valid VU.! i == 1 then Just (vec V.! i) else Nothing+ samples = V.take sampleN asMaybeFull+ assumption = makeParsingAssumptionBS dfmt samples+ in case assumption of+ IntAssumption -> handleBSInt dfmt asMaybeFull+ DoubleAssumption -> handleBSDouble asMaybeFull+ BoolAssumption -> handleBSBool asMaybeFull+ DateAssumption -> handleBSDate dfmt asMaybeFull+ TextAssumption -> handleBSText asMaybeFull+ NoAssumption -> handleBSNo dfmt asMaybeFull++makeParsingAssumptionBS ::+ String -> V.Vector (Maybe BS.ByteString) -> ParsingAssumption+makeParsingAssumptionBS dfmt asMaybe+ | V.all (== Nothing) asMaybe = NoAssumption+ | vecSameConstructor asMaybe asMaybeBool = BoolAssumption+ | vecSameConstructor asMaybe asMaybeInt+ && vecSameConstructor asMaybe asMaybeDouble =+ IntAssumption+ | vecSameConstructor asMaybe asMaybeDouble = DoubleAssumption+ | vecSameConstructor asMaybe asMaybeDate = DateAssumption+ | otherwise = TextAssumption+ where+ asMaybeBool = V.map (>>= readByteStringBool) asMaybe+ asMaybeInt = V.map (>>= readByteStringInt) asMaybe+ asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe+ asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe++handleBSBool :: V.Vector (Maybe BS.ByteString) -> Column+handleBSBool asMaybe+ | parsableAsBool =+ maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)+ | otherwise = handleBSText asMaybe+ where+ asMaybeBool = V.map (>>= readByteStringBool) asMaybe+ parsableAsBool = vecSameConstructor asMaybe asMaybeBool++handleBSInt :: String -> V.Vector (Maybe BS.ByteString) -> Column+handleBSInt dfmt asMaybe+ | parsableAsInt =+ maybe (fromVector asMaybeInt) fromVector (sequenceA asMaybeInt)+ | parsableAsDouble =+ maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)+ | otherwise = handleBSText asMaybe+ where+ asMaybeInt = V.map (>>= readByteStringInt) asMaybe+ asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe+ parsableAsInt =+ vecSameConstructor asMaybe asMaybeInt+ && vecSameConstructor asMaybe asMaybeDouble+ parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble++handleBSDouble :: V.Vector (Maybe BS.ByteString) -> Column+handleBSDouble asMaybe+ | parsableAsDouble =+ maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)+ | otherwise = handleBSText asMaybe+ where+ asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe+ parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble++handleBSDate :: String -> V.Vector (Maybe BS.ByteString) -> Column+handleBSDate dfmt asMaybe+ | parsableAsDate =+ maybe (fromVector asMaybeDate) fromVector (sequenceA asMaybeDate)+ | otherwise = handleBSText asMaybe+ where+ asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe+ parsableAsDate = vecSameConstructor asMaybe asMaybeDate++handleBSText :: V.Vector (Maybe BS.ByteString) -> Column+handleBSText asMaybe =+ let asMaybeText = V.map (fmap TE.decodeUtf8Lenient) asMaybe+ in maybe (fromVector asMaybeText) fromVector (sequenceA asMaybeText)++handleBSNo :: String -> V.Vector (Maybe BS.ByteString) -> Column+handleBSNo dfmt asMaybe+ | V.all (== Nothing) asMaybe =+ fromVector (V.map (const (Nothing :: Maybe T.Text)) asMaybe)+ | parsableAsBool =+ maybe (fromVector asMaybeBool) fromVector (sequenceA asMaybeBool)+ | parsableAsInt =+ maybe (fromVector asMaybeInt) fromVector (sequenceA asMaybeInt)+ | parsableAsDouble =+ maybe (fromVector asMaybeDouble) fromVector (sequenceA asMaybeDouble)+ | parsableAsDate =+ maybe (fromVector asMaybeDate) fromVector (sequenceA asMaybeDate)+ | otherwise = handleBSText asMaybe+ where+ asMaybeBool = V.map (>>= readByteStringBool) asMaybe+ asMaybeInt = V.map (>>= readByteStringInt) asMaybe+ asMaybeDouble = V.map (>>= readByteStringDouble) asMaybe+ asMaybeDate = V.map (>>= readByteStringDate dfmt) asMaybe+ parsableAsBool = vecSameConstructor asMaybe asMaybeBool+ parsableAsInt =+ vecSameConstructor asMaybe asMaybeInt+ && vecSameConstructor asMaybe asMaybeDouble+ parsableAsDouble = vecSameConstructor asMaybe asMaybeDouble+ parsableAsDate = vecSameConstructor asMaybe asMaybeDate constructOptional :: (VU.Unbox a, Columnable a) => VU.Vector a -> VU.Vector Word8 -> IO Column
src/DataFrame/IO/Parquet.hs view
@@ -40,6 +40,8 @@ import qualified Data.Vector.Unboxed as VU import System.FilePath ((</>)) +-- Options -----------------------------------------------------------------+ {- | Options for reading Parquet data. These options are applied in this order:@@ -82,6 +84,8 @@ , rowRange = Nothing } +-- Public API --------------------------------------------------------------+ {- | Read a parquet file from path and load it into a dataframe. ==== __Example__@@ -104,9 +108,31 @@ When @selectedColumns@ is set and @predicate@ references other columns, those predicate columns are auto-included for decoding, then projected back to the requested output columns. -}++{- | Strip Parquet encoding artifact names (REPEATED wrappers and their single+ list-element children) from a raw column path, leaving user-visible names.+-}+cleanColPath :: [SNode] -> [String] -> [String]+cleanColPath nodes path = go nodes path False+ where+ go _ [] _ = []+ go ns (p : ps) skipThis =+ case L.find (\n -> sName n == p) ns of+ Nothing -> []+ Just n+ | sRep n == REPEATED && not (null (sChildren n)) ->+ let skipChildren = length (sChildren n) == 1+ in go (sChildren n) ps skipChildren+ | skipThis ->+ go (sChildren n) ps False+ | null (sChildren n) ->+ [p]+ | otherwise ->+ p : go (sChildren n) ps False+ readParquetWithOpts :: ParquetReadOptions -> FilePath -> IO DataFrame readParquetWithOpts opts path = do- fileMetadata <- readMetadataFromPath path+ (fileMetadata, contents) <- readMetadataFromPath path let columnPaths = getColumnPaths (drop 1 $ schema fileMetadata) let columnNames = map fst columnPaths let leafNames = map (last . T.splitOn ".") columnNames@@ -135,12 +161,13 @@ ) ) - colMap <- newIORef (M.empty :: M.Map T.Text DI.Column)+ let totalRows = sum (map (fromIntegral . rowGroupNumRows) (rowGroups fileMetadata)) :: Int+ colMutMap <- newIORef (M.empty :: M.Map T.Text DI.MutableColumn)+ colOffMap <- newIORef (M.empty :: M.Map T.Text Int) lTypeMap <- newIORef (M.empty :: M.Map T.Text LogicalType) - contents <- BSO.readFile path- let schemaElements = schema fileMetadata+ let sNodes = parseAll (drop 1 schemaElements) let getTypeLength :: [String] -> Maybe Int32 getTypeLength path = findTypeLength schemaElements path 0 where@@ -159,12 +186,17 @@ forM_ (zip (rowGroupColumns rowGroup) [0 ..]) $ \(colChunk, colIdx) -> do let metadata = columnMetaData colChunk let colPath = columnPathInSchema metadata- let colName =- if null colPath+ let cleanPath = cleanColPath sNodes colPath+ let colLeafName =+ if null cleanPath then T.pack $ "col_" ++ show colIdx- else T.pack $ last colPath+ else T.pack $ last cleanPath+ let colFullName =+ if null cleanPath+ then colLeafName+ else T.intercalate "." $ map T.pack cleanPath - when (shouldReadColumn colName colPath) $ do+ when (shouldReadColumn colLeafName colPath) $ do let colDataPageOffset = columnDataPageOffset metadata let colDictionaryPageOffset = columnDictionaryPageOffset metadata let colStart =@@ -186,7 +218,11 @@ let schemaTail = drop 1 (schema fileMetadata) let (maxDef, maxRep) = levelsForPath schemaTail colPath- let lType = logicalType (schemaTail !! colIdx)+ let lType =+ maybe+ LOGICAL_TYPE_UNKNOWN+ logicalType+ (findLeafSchema schemaTail colPath) column <- processColumnPages (maxDef, maxRep)@@ -196,10 +232,22 @@ maybeTypeLength lType - modifyIORef colMap (M.insertWith DI.concatColumnsEither colName column)- modifyIORef lTypeMap (M.insert colName lType)+ mutMapSnap <- readIORef colMutMap+ case M.lookup colFullName mutMapSnap of+ 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))+ 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) - finalColMap <- readIORef colMap+ finalMutMap <- readIORef colMutMap+ finalColMap <-+ M.traverseWithKey (\_ mc -> DI.freezeMutableColumn mc) finalMutMap finalLTypeMap <- readIORef lTypeMap let orderedColumns = map@@ -237,7 +285,7 @@ readParquetFilesWithOpts opts path = do isDir <- doesDirectoryExist path - let pat = if isDir then path </> "*" else path+ let pat = if isDir then path </> "*.parquet" else path matches <- glob pat @@ -252,6 +300,8 @@ dfs <- mapM (readParquetWithOpts optsWithoutRowRange) files pure (applyRowRange opts (mconcat dfs)) +-- Options application -----------------------------------------------------+ applyRowRange :: ParquetReadOptions -> DataFrame -> DataFrame applyRowRange opts df = maybe df (`DS.range` df) (rowRange opts)@@ -270,12 +320,15 @@ . applySelectedColumns opts . applyPredicate opts -readMetadataFromPath :: FilePath -> IO FileMetadata+-- File and metadata parsing -----------------------------------------------++readMetadataFromPath :: FilePath -> IO (FileMetadata, BSO.ByteString) readMetadataFromPath path = do contents <- BSO.readFile path let (size, magicString) = contents `seq` readMetadataSizeFromFooter contents when (magicString /= "PAR1") $ error "Invalid Parquet file"- readMetadata contents size+ meta <- readMetadata contents size+ pure (meta, contents) readMetadataSizeFromFooter :: BSO.ByteString -> (Int, BSO.ByteString) readMetadataSizeFromFooter contents =@@ -291,22 +344,44 @@ in (size, magicString) +-- Schema navigation -------------------------------------------------------+ getColumnPaths :: [SchemaElement] -> [(T.Text, Int)]-getColumnPaths schema = extractLeafPaths schema 0 []+getColumnPaths schemaElements =+ let nodes = parseAll schemaElements+ in go nodes 0 [] False where- extractLeafPaths :: [SchemaElement] -> Int -> [T.Text] -> [(T.Text, Int)]- extractLeafPaths [] _ _ = []- extractLeafPaths (s : ss) idx path- | numChildren s == 0 =- let fullPath = T.intercalate "." (path ++ [elementName s])- in (fullPath, idx) : extractLeafPaths ss (idx + 1) path+ go [] _ _ _ = []+ go (n : ns) idx path skipThis+ | null (sChildren n) =+ let newPath = if skipThis then path else path ++ [T.pack (sName n)]+ fullPath = T.intercalate "." newPath+ in (fullPath, idx) : go ns (idx + 1) path skipThis+ | sRep n == REPEATED =+ let skipChildren = length (sChildren n) == 1+ childLeaves = go (sChildren n) idx path skipChildren+ in childLeaves ++ go ns (idx + length childLeaves) path skipThis+ | skipThis =+ let childLeaves = go (sChildren n) idx path False+ in childLeaves ++ go ns (idx + length childLeaves) path skipThis | otherwise =- let newPath = if T.null (elementName s) then path else path ++ [elementName s]- childrenCount = fromIntegral (numChildren s)- (children, remaining) = splitAt childrenCount ss- childResults = extractLeafPaths children idx newPath- in childResults ++ extractLeafPaths remaining (idx + length childResults) path+ let subPath = path ++ [T.pack (sName n)]+ childLeaves = go (sChildren n) idx subPath False+ in childLeaves ++ go ns (idx + length childLeaves) path skipThis +findLeafSchema :: [SchemaElement] -> [String] -> Maybe SchemaElement+findLeafSchema elems path =+ case go (parseAll elems) path of+ Just node -> L.find (\e -> T.unpack (elementName e) == sName node) elems+ Nothing -> Nothing+ where+ go [] _ = Nothing+ go _ [] = Nothing+ go nodes [p] = L.find (\n -> sName n == p) nodes+ go nodes (p : ps) = L.find (\n -> sName n == p) nodes >>= \n -> go (sChildren n) ps++-- Page decoding -----------------------------------------------------------+ processColumnPages :: (Int, Int) -> [Page] ->@@ -327,59 +402,32 @@ DictionaryPageHeader{..} -> let countForBools = if pType == PBOOLEAN- then error "is bool" Just dictionaryPageHeaderNumValues+ then Just dictionaryPageHeaderNumValues else maybeTypeLength in Just (readDictVals pType (pageBytes dictPage) countForBools) _ -> Nothing cols <- forM dataPages $ \page -> do+ let bs0 = pageBytes page case pageTypeHeader (pageHeader page) of DataPageHeader{..} -> do let n = fromIntegral dataPageHeaderNumValues- let bs0 = pageBytes page- let (defLvls, _repLvls, afterLvls) = readLevelsV1 n maxDef maxRep bs0- let nPresent = length (filter (== maxDef) defLvls)-- case dataPageHeaderEncoding of- EPLAIN ->- case pType of- PBOOLEAN ->- let (vals, _) = readNBool nPresent afterLvls- in pure (toMaybeBool maxDef defLvls vals)- PINT32 ->- let (vals, _) = readNInt32 nPresent afterLvls- in pure (toMaybeInt32 maxDef defLvls vals)- PINT64 ->- let (vals, _) = readNInt64 nPresent afterLvls- in pure (toMaybeInt64 maxDef defLvls vals)- PINT96 ->- let (vals, _) = readNInt96Times nPresent afterLvls- in pure (toMaybeUTCTime maxDef defLvls vals)- PFLOAT ->- let (vals, _) = readNFloat nPresent afterLvls- in pure (toMaybeFloat maxDef defLvls vals)- PDOUBLE ->- let (vals, _) = readNDouble nPresent afterLvls- in pure (toMaybeDouble maxDef defLvls vals)- PBYTE_ARRAY ->- let (raws, _) = readNByteArrays nPresent afterLvls- texts = map decodeUtf8 raws- in pure (toMaybeText maxDef defLvls texts)- PFIXED_LEN_BYTE_ARRAY ->- case maybeTypeLength of- Just len ->- let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls- texts = map decodeUtf8 raws- in pure (toMaybeText maxDef defLvls texts)- Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"- PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"- ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls- EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls- other -> error ("Unsupported v1 encoding: " ++ show other)+ (defLvls, repLvls, afterLvls) = readLevelsV1 n maxDef maxRep bs0+ nPresent = length (filter (== maxDef) defLvls)+ decodePageData+ dictValsM+ (maxDef, maxRep)+ pType+ maybeTypeLength+ dataPageHeaderEncoding+ defLvls+ repLvls+ nPresent+ afterLvls+ "v1" DataPageHeaderV2{..} -> do let n = fromIntegral dataPageHeaderV2NumValues- let bs0 = pageBytes page- let (defLvls, _repLvls, afterLvls) =+ (defLvls, repLvls, afterLvls) = readLevelsV2 n maxDef@@ -387,57 +435,119 @@ definitionLevelByteLength repetitionLevelByteLength bs0- let nPresent =- if dataPageHeaderV2NumNulls > 0- then fromIntegral (dataPageHeaderV2NumValues - dataPageHeaderV2NumNulls)- else length (filter (== maxDef) defLvls)+ nPresent+ | dataPageHeaderV2NumNulls > 0 =+ fromIntegral (dataPageHeaderV2NumValues - dataPageHeaderV2NumNulls)+ | otherwise = length (filter (== maxDef) defLvls)+ decodePageData+ dictValsM+ (maxDef, maxRep)+ pType+ maybeTypeLength+ dataPageHeaderV2Encoding+ defLvls+ repLvls+ nPresent+ afterLvls+ "v2" - case dataPageHeaderV2Encoding of- EPLAIN ->- case pType of- PBOOLEAN ->- let (vals, _) = readNBool nPresent afterLvls- in pure (toMaybeBool maxDef defLvls vals)- PINT32 ->- let (vals, _) = readNInt32 nPresent afterLvls- in pure (toMaybeInt32 maxDef defLvls vals)- PINT64 ->- let (vals, _) = readNInt64 nPresent afterLvls- in pure (toMaybeInt64 maxDef defLvls vals)- PINT96 ->- let (vals, _) = readNInt96Times nPresent afterLvls- in pure (toMaybeUTCTime maxDef defLvls vals)- PFLOAT ->- let (vals, _) = readNFloat nPresent afterLvls- in pure (toMaybeFloat maxDef defLvls vals)- PDOUBLE ->- let (vals, _) = readNDouble nPresent afterLvls- in pure (toMaybeDouble maxDef defLvls vals)- PBYTE_ARRAY ->- let (raws, _) = readNByteArrays nPresent afterLvls- texts = map decodeUtf8 raws- in pure (toMaybeText maxDef defLvls texts)- PFIXED_LEN_BYTE_ARRAY ->- case maybeTypeLength of- Just len ->- let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls- texts = map decodeUtf8 raws- in pure (toMaybeText maxDef defLvls texts)- Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"- PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"- ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls- EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef defLvls nPresent afterLvls- other -> error ("Unsupported v2 encoding: " ++ show other) -- Cannot happen as these are filtered out by isDataPage above DictionaryPageHeader{} -> error "processColumnPages: impossible DictionaryPageHeader" INDEX_PAGE_HEADER -> error "processColumnPages: impossible INDEX_PAGE_HEADER" PAGE_TYPE_HEADER_UNKNOWN -> error "processColumnPages: impossible PAGE_TYPE_HEADER_UNKNOWN"- -- This is N^2. We should probably use mutable columns here.- case cols of- [] -> pure $ DI.fromList ([] :: [Maybe Int])- (c : cs) ->- pure $- L.foldl' (\l r -> fromRight (error "concat failed") (DI.concatColumns l r)) c cs+ pure $ DI.concatManyColumns cols++decodePageData ::+ Maybe DictVals ->+ (Int, Int) ->+ ParquetType ->+ Maybe Int32 ->+ ParquetEncoding ->+ [Int] ->+ [Int] ->+ Int ->+ BSO.ByteString ->+ String ->+ IO DI.Column+decodePageData dictValsM (maxDef, maxRep) pType maybeTypeLength encoding defLvls repLvls nPresent afterLvls versionLabel =+ case encoding of+ EPLAIN ->+ case pType of+ PBOOLEAN ->+ let (vals, _) = readNBool nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepBool maxRep maxDef repLvls defLvls vals+ else toMaybeBool maxDef defLvls vals+ PINT32+ | maxDef == 0+ , maxRep == 0 ->+ pure $ DI.fromUnboxedVector (readNInt32Vec nPresent afterLvls)+ PINT32 ->+ let (vals, _) = readNInt32 nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepInt32 maxRep maxDef repLvls defLvls vals+ else toMaybeInt32 maxDef defLvls vals+ PINT64+ | maxDef == 0+ , maxRep == 0 ->+ pure $ DI.fromUnboxedVector (readNInt64Vec nPresent afterLvls)+ PINT64 ->+ let (vals, _) = readNInt64 nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepInt64 maxRep maxDef repLvls defLvls vals+ else toMaybeInt64 maxDef defLvls vals+ PINT96 ->+ let (vals, _) = readNInt96Times nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepUTCTime maxRep maxDef repLvls defLvls vals+ else toMaybeUTCTime maxDef defLvls vals+ PFLOAT+ | maxDef == 0+ , maxRep == 0 ->+ pure $ DI.fromUnboxedVector (readNFloatVec nPresent afterLvls)+ PFLOAT ->+ let (vals, _) = readNFloat nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepFloat maxRep maxDef repLvls defLvls vals+ else toMaybeFloat maxDef defLvls vals+ PDOUBLE+ | maxDef == 0+ , maxRep == 0 ->+ pure $ DI.fromUnboxedVector (readNDoubleVec nPresent afterLvls)+ PDOUBLE ->+ let (vals, _) = readNDouble nPresent afterLvls+ in pure $+ if maxRep > 0+ then stitchForRepDouble maxRep maxDef repLvls defLvls vals+ else toMaybeDouble maxDef defLvls vals+ PBYTE_ARRAY ->+ let (raws, _) = readNByteArrays nPresent afterLvls+ texts = map decodeUtf8Lenient raws+ in pure $+ if maxRep > 0+ then stitchForRepText maxRep maxDef repLvls defLvls texts+ else toMaybeText maxDef defLvls texts+ PFIXED_LEN_BYTE_ARRAY ->+ case maybeTypeLength of+ Just len ->+ let (raws, _) = splitFixed nPresent (fromIntegral len) afterLvls+ texts = map decodeUtf8Lenient raws+ in pure $+ if maxRep > 0+ then stitchForRepText maxRep maxDef repLvls defLvls texts+ else toMaybeText maxDef defLvls texts+ Nothing -> error "FIXED_LEN_BYTE_ARRAY requires type length"+ PARQUET_TYPE_UNKNOWN -> error "Cannot read unknown Parquet type"+ ERLE_DICTIONARY -> decodeDictV1 dictValsM maxDef maxRep repLvls defLvls nPresent afterLvls+ EPLAIN_DICTIONARY -> decodeDictV1 dictValsM maxDef maxRep repLvls defLvls nPresent afterLvls+ other -> error ("Unsupported " ++ versionLabel ++ " encoding: " ++ show other)++-- Logical type conversion ------------------------------------------------- applyLogicalType :: LogicalType -> DI.Column -> DI.Column applyLogicalType (TimestampType _ unit) col =
src/DataFrame/IO/Parquet/Binary.hs view
@@ -2,13 +2,18 @@ module DataFrame.IO.Parquet.Binary where +import Control.Exception (bracketOnError) import Control.Monad import Data.Bits import qualified Data.ByteString as BS+import qualified Data.ByteString.Unsafe as BSU import Data.Char import Data.IORef import Data.Int import Data.Word+import qualified Foreign.Marshal.Alloc as Foreign+import qualified Foreign.Ptr as Foreign+import qualified Foreign.Storable as Foreign littleEndianWord32 :: BS.ByteString -> Word32 littleEndianWord32 bytes@@ -124,7 +129,7 @@ readString :: BS.ByteString -> IORef Int -> IO String readString buf pos = do nameSize <- readVarIntFromBuffer @Int buf pos- map (chr . fromIntegral) <$> replicateM nameSize (readAndAdvance pos buf)+ replicateM nameSize (chr . fromIntegral <$> readAndAdvance pos buf) readByteStringFromBytes :: BS.ByteString -> (BS.ByteString, BS.ByteString) readByteStringFromBytes xs =@@ -136,10 +141,32 @@ readByteString :: BS.ByteString -> IORef Int -> IO BS.ByteString readByteString buf pos = do size <- readVarIntFromBuffer @Int buf pos- BS.pack <$> replicateM size (readAndAdvance pos buf)+ fillByteStringByWord8 size (\_ -> readAndAdvance pos buf) readByteString' :: BS.ByteString -> Int64 -> IO BS.ByteString-readByteString' buf size = BS.pack <$> mapM (`readSingleByte` buf) [0 .. (size - 1)]+readByteString' buf size =+ fillByteStringByWord8+ (fromIntegral size)+ ((`readSingleByte` buf) . fromIntegral)++{- | Allocate a fixed-size buffer, repeat the action on each index.+Fill it into the buffer to get a ByteString.+-}+fillByteStringByWord8 :: Int -> (Int -> IO Word8) -> IO BS.ByteString+fillByteStringByWord8 size getByte = do+ bracketOnError+ (Foreign.mallocBytes size :: IO (Foreign.Ptr Word8))+ Foreign.free+ -- \^ ensures p is freed if (IO Word8) throws.+ ( \p -> do+ fill 0 p+ BSU.unsafePackCStringFinalizer p size (Foreign.free p)+ )+ where+ fill i p+ | i >= size = pure ()+ | otherwise = getByte i >>= Foreign.pokeByteOff p i >> fill (i + 1) p+{-# INLINE fillByteStringByWord8 #-} readSingleByte :: Int64 -> BS.ByteString -> IO Word8 readSingleByte pos buffer = return $ BS.index buffer (fromIntegral pos)
src/DataFrame/IO/Parquet/Dictionary.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE OverloadedStrings #-} module DataFrame.IO.Parquet.Dictionary where@@ -5,12 +8,15 @@ import Control.Monad import Data.Bits import qualified Data.ByteString as BS-import Data.Char+import Data.IORef import Data.Int import Data.Maybe import qualified Data.Text as T import Data.Text.Encoding import Data.Time+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@@ -20,23 +26,23 @@ import GHC.Float dictCardinality :: DictVals -> Int-dictCardinality (DBool ds) = length ds-dictCardinality (DInt32 ds) = length ds-dictCardinality (DInt64 ds) = length ds-dictCardinality (DInt96 ds) = length ds-dictCardinality (DFloat ds) = length ds-dictCardinality (DDouble ds) = length ds-dictCardinality (DText ds) = length ds+dictCardinality (DBool ds) = V.length ds+dictCardinality (DInt32 ds) = V.length ds+dictCardinality (DInt64 ds) = V.length ds+dictCardinality (DInt96 ds) = V.length ds+dictCardinality (DFloat ds) = V.length ds+dictCardinality (DDouble ds) = V.length ds+dictCardinality (DText ds) = V.length ds readDictVals :: ParquetType -> BS.ByteString -> Maybe Int32 -> DictVals-readDictVals PBOOLEAN bs (Just count) = DBool (take (fromIntegral count) $ readPageBool bs)-readDictVals PINT32 bs _ = DInt32 (readPageInt32 bs)-readDictVals PINT64 bs _ = DInt64 (readPageInt64 bs)-readDictVals PINT96 bs _ = DInt96 (readPageInt96Times bs)-readDictVals PFLOAT bs _ = DFloat (readPageFloat bs)-readDictVals PDOUBLE bs _ = DDouble (readPageWord64 bs)-readDictVals PBYTE_ARRAY bs _ = DText (readPageBytes bs)-readDictVals PFIXED_LEN_BYTE_ARRAY bs (Just len) = DText (readPageFixedBytes bs (fromIntegral len))+readDictVals PBOOLEAN bs (Just count) = DBool (V.fromList (take (fromIntegral count) $ readPageBool bs))+readDictVals PINT32 bs _ = DInt32 (V.fromList (readPageInt32 bs))+readDictVals PINT64 bs _ = DInt64 (V.fromList (readPageInt64 bs))+readDictVals PINT96 bs _ = DInt96 (V.fromList (readPageInt96Times bs))+readDictVals PFLOAT bs _ = DFloat (V.fromList (readPageFloat bs))+readDictVals PDOUBLE bs _ = DDouble (V.fromList (readPageWord64 bs))+readDictVals PBYTE_ARRAY bs _ = DText (V.fromList (readPageBytes bs))+readDictVals PFIXED_LEN_BYTE_ARRAY bs (Just len) = DText (V.fromList (readPageFixedBytes bs (fromIntegral len))) readDictVals t _ _ = error $ "Unsupported dictionary type: " ++ show t readPageInt32 :: BS.ByteString -> [Int32]@@ -57,7 +63,7 @@ | otherwise = let lenBytes = fromIntegral (littleEndianInt32 $ BS.take 4 xs) totalBytesRead = lenBytes + 4- in T.pack (map (chr . fromIntegral) $ take lenBytes (BS.unpack (BS.drop 4 xs)))+ in decodeUtf8Lenient (BS.take lenBytes (BS.drop 4 xs)) : readPageBytes (BS.drop totalBytesRead xs) readPageBool :: BS.ByteString -> [Bool]@@ -97,44 +103,154 @@ readPageFixedBytes xs len | BS.null xs = [] | otherwise =- decodeUtf8 (BS.take len xs) : readPageFixedBytes (BS.drop len xs) len+ decodeUtf8Lenient (BS.take len xs) : readPageFixedBytes (BS.drop len xs) len +{- | Dispatch to the right multi-level list stitching function.+For maxRep=1 uses stitchList; for 2/3 uses stitchList2/3 with computed thresholds.+Threshold formula: defT_r = maxDef - 2*(maxRep - r).+-}+stitchForRepBool :: Int -> Int -> [Int] -> [Int] -> [Bool] -> DI.Column+stitchForRepBool maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepInt32 :: Int -> Int -> [Int] -> [Int] -> [Int32] -> DI.Column+stitchForRepInt32 maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepInt64 :: Int -> Int -> [Int] -> [Int] -> [Int64] -> DI.Column+stitchForRepInt64 maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepUTCTime :: Int -> Int -> [Int] -> [Int] -> [UTCTime] -> DI.Column+stitchForRepUTCTime maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepFloat :: Int -> Int -> [Int] -> [Int] -> [Float] -> DI.Column+stitchForRepFloat maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepDouble :: Int -> Int -> [Int] -> [Int] -> [Double] -> DI.Column+stitchForRepDouble maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++stitchForRepText :: Int -> Int -> [Int] -> [Int] -> [T.Text] -> DI.Column+stitchForRepText maxRep maxDef rep def vals = case maxRep of+ 2 -> DI.fromList (stitchList2 (maxDef - 2) maxDef rep def vals)+ 3 -> DI.fromList (stitchList3 (maxDef - 4) (maxDef - 2) maxDef rep def vals)+ _ -> DI.fromList (stitchList maxDef rep def vals)++{- | Build a Column from a dictionary + index vector + def levels in a single+mutable-vector pass, avoiding the intermediate [a] and [Maybe a] lists.+For maxRep > 0 (list columns) the caller must use the rep-stitching path instead.+-}+applyDictToColumn ::+ (DI.Columnable a, DI.Columnable (Maybe a)) =>+ V.Vector a ->+ VU.Vector Int ->+ Int -> -- maxDef+ [Int] -> -- defLvls+ IO DI.Column+applyDictToColumn dict idxs maxDef defLvls+ | maxDef == 0 = do+ -- All rows are required; no nullability to check.+ let n = VU.length idxs+ pure $ DI.fromVector (V.generate n (\i -> dict V.! (idxs VU.! i)))+ | otherwise = do+ let n = length defLvls+ mv <- VM.new n+ hasNullRef <- newIORef False+ let go _ _ [] = pure ()+ go !i !j (d : ds)+ | d == maxDef = do+ VM.write mv i (Just (dict V.! (idxs VU.! j)))+ go (i + 1) (j + 1) ds+ | otherwise = do+ writeIORef hasNullRef True+ VM.write mv i Nothing+ go (i + 1) j ds+ go 0 0 defLvls+ vec <- V.freeze mv+ hasNull <- readIORef hasNullRef+ pure $+ if hasNull+ then DI.fromVector vec -- VB.Vector (Maybe a) → OptionalColumn+ else DI.fromVector (V.map fromJust vec) -- VB.Vector a → BoxedColumn/UnboxedColumn+ decodeDictV1 ::- Maybe DictVals -> Int -> [Int] -> Int -> BS.ByteString -> IO DI.Column-decodeDictV1 dictValsM maxDef defLvls nPresent bytes =+ Maybe DictVals ->+ Int ->+ Int ->+ [Int] ->+ [Int] ->+ Int ->+ BS.ByteString ->+ IO DI.Column+decodeDictV1 dictValsM maxDef maxRep repLvls defLvls nPresent bytes = case dictValsM of Nothing -> error "Dictionary-encoded page but dictionary is missing" Just dictVals -> let (idxs, _rest) = decodeDictIndicesV1 nPresent (dictCardinality dictVals) bytes in do- when (length idxs /= nPresent) $+ when (VU.length idxs /= nPresent) $ error $ "dict index count mismatch: got "- ++ show (length idxs)+ ++ show (VU.length idxs) ++ ", expected " ++ show nPresent- case dictVals of- DBool ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeBool maxDef defLvls values)- DInt32 ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeInt32 maxDef defLvls values)- DInt64 ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeInt64 maxDef defLvls values)- DInt96 ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeUTCTime maxDef defLvls values)- DFloat ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeFloat maxDef defLvls values)- DDouble ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeDouble maxDef defLvls values)- DText ds -> do- let values = [ds !! i | i <- idxs]- pure (toMaybeText maxDef defLvls values)+ if maxRep > 0+ then do+ case dictVals of+ DBool ds ->+ pure $+ stitchForRepBool maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ DInt32 ds ->+ pure $+ stitchForRepInt32 maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ DInt64 ds ->+ pure $+ stitchForRepInt64 maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ DInt96 ds ->+ pure $+ stitchForRepUTCTime+ maxRep+ maxDef+ repLvls+ defLvls+ (map (ds V.!) (VU.toList idxs))+ DFloat ds ->+ pure $+ stitchForRepFloat maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ DDouble ds ->+ pure $+ stitchForRepDouble maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ DText ds ->+ pure $+ stitchForRepText maxRep maxDef repLvls defLvls (map (ds V.!) (VU.toList idxs))+ else case dictVals of+ -- Fast path: unboxable types, no nulls — one allocation via VU.map+ DInt32 ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)+ DInt64 ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)+ DFloat ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)+ DDouble ds | maxDef == 0 -> pure $ DI.fromUnboxedVector (VU.map (ds V.!) idxs)+ DBool ds -> applyDictToColumn ds idxs maxDef defLvls+ DInt32 ds -> applyDictToColumn ds idxs maxDef defLvls+ DInt64 ds -> applyDictToColumn ds idxs maxDef defLvls+ DInt96 ds -> applyDictToColumn ds idxs maxDef defLvls+ DFloat ds -> applyDictToColumn ds idxs maxDef defLvls+ DDouble ds -> applyDictToColumn ds idxs maxDef defLvls+ DText ds -> applyDictToColumn ds idxs maxDef defLvls toMaybeInt32 :: Int -> [Int] -> [Int32] -> DI.Column toMaybeInt32 maxDef def xs =
src/DataFrame/IO/Parquet/Encoding.hs view
@@ -1,8 +1,12 @@+{-# LANGUAGE BangPatterns #-}+ module DataFrame.IO.Parquet.Encoding where import Data.Bits import qualified Data.ByteString as BS+import qualified Data.ByteString.Unsafe as BSU import Data.List (foldl')+import qualified Data.Vector.Unboxed as VU import Data.Word import DataFrame.IO.Parquet.Binary @@ -22,32 +26,26 @@ | count <= 0 = ([], bs) | BS.null bs = ([], bs) | otherwise =- let totalBits = bw * count- totalBytes = (totalBits + 7) `div` 8+ let totalBytes = (bw * count + 7) `div` 8 chunk = BS.take totalBytes bs rest = BS.drop totalBytes bs- bits =- BS.concatMap- (\b -> BS.map (\i -> (b `shiftR` fromIntegral i) .&. 1) (BS.pack [0 .. 7]))- chunk- toN :: BS.ByteString -> Word32- toN =- fst- . BS.foldl'- (\(a, i) b -> (a .|. (fromIntegral b `shiftL` i), i + 1))- (0 :: Word32, 0 :: Int)-- extractValues :: Int -> BS.ByteString -> [Word32]- extractValues n bitsLeft- | BS.null bitsLeft = []- | n <= 0 = []- | BS.length bitsLeft < bw = []- | otherwise =- let (this, bitsLeft') = BS.splitAt bw bitsLeft- in toN this : extractValues (n - 1) bitsLeft'+ in (extractBits bw count chunk, rest) - vals = extractValues count bits- in (vals, rest)+-- | LSB-first bit accumulator: reads each byte once with no intermediate ByteString allocation.+extractBits :: Int -> Int -> BS.ByteString -> [Word32]+extractBits bw count bs = go 0 (0 :: Word64) 0 count+ where+ !mask = if bw == 32 then maxBound else (1 `shiftL` bw) - 1 :: Word64+ !len = BS.length bs+ go !byteIdx !acc !accBits !remaining+ | remaining <= 0 = []+ | accBits >= bw =+ fromIntegral (acc .&. mask)+ : go byteIdx (acc `shiftR` bw) (accBits - bw) (remaining - 1)+ | byteIdx >= len = []+ | otherwise =+ let b = fromIntegral (BSU.unsafeIndex bs byteIdx) :: Word64+ in go (byteIdx + 1) (acc .|. (b `shiftL` accBits)) (accBits + 8) remaining decodeRLEBitPackedHybrid :: Int -> Int -> BS.ByteString -> ([Word32], BS.ByteString)@@ -81,11 +79,12 @@ takeN = min n runLen in go (n - takeN) afterV (replicate takeN val ++ acc) -decodeDictIndicesV1 :: Int -> Int -> BS.ByteString -> ([Int], BS.ByteString)+decodeDictIndicesV1 ::+ Int -> Int -> BS.ByteString -> (VU.Vector Int, BS.ByteString) decodeDictIndicesV1 need dictCard bs = case BS.uncons bs of Nothing -> error "empty dictionary index stream" Just (w0, rest0) -> let bw = fromIntegral w0 :: Int (u32s, rest1) = decodeRLEBitPackedHybrid bw need rest0- in (map fromIntegral u32s, rest1)+ in (VU.fromList (map fromIntegral u32s), rest1)
src/DataFrame/IO/Parquet/Levels.hs view
@@ -4,7 +4,6 @@ import Data.Int import Data.List import qualified Data.Text as T-import Data.Word import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Encoding@@ -17,7 +16,7 @@ let bwDef = bitWidthForMaxLevel maxDef bwRep = bitWidthForMaxLevel maxRep - (repLvlsU32, afterRep) =+ (repLvls, afterRep) = if bwRep == 0 then (replicate n 0, bs) else@@ -25,9 +24,9 @@ repData = BS.take (fromIntegral repLength) (BS.drop 4 bs) afterRepData = BS.drop (4 + fromIntegral repLength) bs (repVals, _) = decodeRLEBitPackedHybrid bwRep n repData- in (repVals, afterRepData)+ in (map fromIntegral repVals, afterRepData) - (defLvlsU32, afterDef) =+ (defLvls, afterDef) = if bwDef == 0 then (replicate n 0, afterRep) else@@ -35,8 +34,8 @@ defData = BS.take (fromIntegral defLength) (BS.drop 4 afterRep) afterDefData = BS.drop (4 + fromIntegral defLength) afterRep (defVals, _) = decodeRLEBitPackedHybrid bwDef n defData- in (defVals, afterDefData)- in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDef)+ in (map fromIntegral defVals, afterDefData)+ in (defLvls, repLvls, afterDef) readLevelsV2 :: Int ->@@ -51,15 +50,15 @@ (defBytes, afterDefBytes) = BS.splitAt (fromIntegral defLen) afterRepBytes bwDef = bitWidthForMaxLevel maxDef bwRep = bitWidthForMaxLevel maxRep- (repLvlsU32, _) =+ (repLvlsRaw, _) = if bwRep == 0 then (replicate n 0, repBytes) else decodeRLEBitPackedHybrid bwRep n repBytes- (defLvlsU32, _) =+ (defLvlsRaw, _) = if bwDef == 0 then (replicate n 0, defBytes) else decodeRLEBitPackedHybrid bwDef n defBytes- in (map fromIntegral defLvlsU32, map fromIntegral repLvlsU32, afterDefBytes)+ in (map fromIntegral defLvlsRaw, map fromIntegral repLvlsRaw, afterDefBytes) stitchNullable :: Int -> [Int] -> [a] -> [Maybe a] stitchNullable maxDef = go@@ -102,6 +101,76 @@ parseAll [] = [] parseAll xs = let (n, xs') = parseOne xs in n : parseAll xs' +-- | Tag leaf values as Just/Nothing according to maxDef.+pairWithVals :: Int -> [(Int, Int)] -> [a] -> [(Int, Int, Maybe a)]+pairWithVals _ [] _ = []+pairWithVals maxDef ((r, d) : rds) vs+ | d == maxDef = case vs of+ (v : vs') -> (r, d, Just v) : pairWithVals maxDef rds vs'+ [] -> error "pairWithVals: value stream exhausted"+ | otherwise = (r, d, Nothing) : pairWithVals maxDef rds vs++-- | Split triplets into groups; a new group begins whenever rep <= bound.+splitAtRepBound :: Int -> [(Int, Int, Maybe a)] -> [[(Int, Int, Maybe a)]]+splitAtRepBound _ [] = []+splitAtRepBound bound (t : ts) =+ let (rest, remaining) = span (\(r, _, _) -> r > bound) ts+ in (t : rest) : splitAtRepBound bound remaining++{- | Reconstruct a list column from Dremel encoding levels.+rep=0 starts a new top-level row; def=0 means the entire list slot is null.+Returns one Maybe [Maybe a] per row.+-}+stitchList :: Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe a]]+stitchList maxDef repLvls defLvls vals =+ let triplets = pairWithVals maxDef (zip repLvls defLvls) vals+ rows = splitAtRepBound 0 triplets+ in map toRow rows+ where+ toRow [] = Nothing+ toRow ((_, d, _) : _) | d == 0 = Nothing+ toRow grp = Just [v | (_, _, v) <- grp]++{- | Reconstruct a 2-level nested list (maxRep=2) from Dremel triplets.+defT1: def threshold at which the depth-1 element is present (not null).+maxDef: def threshold at which the leaf is present.+-}+stitchList2 :: Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe a]]]+stitchList2 defT1 maxDef repLvls defLvls vals =+ let triplets = pairWithVals maxDef (zip repLvls defLvls) vals+ in map toRow (splitAtRepBound 0 triplets)+ where+ toRow [] = Nothing+ toRow ((_, d, _) : _) | d == 0 = Nothing+ toRow row = Just (map toOuter (splitAtRepBound 1 row))+ toOuter [] = Nothing+ toOuter ((_, d, _) : _) | d < defT1 = Nothing+ toOuter outer = Just (map toLeaf (splitAtRepBound 2 outer))+ toLeaf [] = Nothing+ toLeaf ((_, _, v) : _) = v++{- | Reconstruct a 3-level nested list (maxRep=3) from Dremel triplets.+defT1, defT2: def thresholds at which depth-1 and depth-2 elements are present.+maxDef: def threshold at which the leaf is present.+-}+stitchList3 ::+ Int -> Int -> Int -> [Int] -> [Int] -> [a] -> [Maybe [Maybe [Maybe [Maybe a]]]]+stitchList3 defT1 defT2 maxDef repLvls defLvls vals =+ let triplets = pairWithVals maxDef (zip repLvls defLvls) vals+ in map toRow (splitAtRepBound 0 triplets)+ where+ toRow [] = Nothing+ toRow ((_, d, _) : _) | d == 0 = Nothing+ toRow row = Just (map toOuter (splitAtRepBound 1 row))+ toOuter [] = Nothing+ toOuter ((_, d, _) : _) | d < defT1 = Nothing+ toOuter outer = Just (map toMiddle (splitAtRepBound 2 outer))+ toMiddle [] = Nothing+ toMiddle ((_, d, _) : _) | d < defT2 = Nothing+ toMiddle middle = Just (map toLeaf (splitAtRepBound 3 middle))+ toLeaf [] = Nothing+ toLeaf ((_, _, v) : _) = v+ levelsForPath :: [SchemaElement] -> [String] -> (Int, Int) levelsForPath schemaTail = go 0 0 (parseAll schemaTail) where@@ -110,6 +179,6 @@ case find (\n -> sName n == p) nodes of Nothing -> (defC, repC) Just n ->- let defC' = defC + (if sRep n == OPTIONAL then 1 else 0)+ let defC' = defC + (if sRep n == OPTIONAL || sRep n == REPEATED then 1 else 0) repC' = repC + (if sRep n == REPEATED then 1 else 0) in go defC' repC' (sChildren n) ps
src/DataFrame/IO/Parquet/Page.hs view
@@ -10,6 +10,7 @@ import qualified Data.ByteString.Lazy as LB import Data.Int import Data.Maybe (fromMaybe)+import qualified Data.Vector.Unboxed as VU import DataFrame.IO.Parquet.Binary import DataFrame.IO.Parquet.Thrift import DataFrame.IO.Parquet.Types@@ -46,9 +47,9 @@ drainZstd result BS.empty acc drainZstd (Zstd.Produce chunk next) _ acc = do result <- next- drainZstd result BS.empty (acc <> [chunk])+ drainZstd result BS.empty (chunk : acc) drainZstd (Zstd.Done final) _ acc =- pure $ BS.concat (acc <> [final])+ pure $ BS.concat (reverse (final : acc)) drainZstd (Zstd.Error msg msg2) _ _ = error ("ZSTD error: " ++ msg ++ " " ++ msg2) SNAPPY -> case Snappy.decompress compressed of@@ -295,6 +296,28 @@ case maybePage of Nothing -> return (reverse acc) Just page -> go remaining (page : acc)++-- | Read n Int32 values directly into an unboxed vector (no intermediate list).+readNInt32Vec :: Int -> BS.ByteString -> VU.Vector Int32+readNInt32Vec n bs = VU.generate n (\i -> littleEndianInt32 (BS.drop (4 * i) bs))++-- | Read n Int64 values directly into an unboxed vector.+readNInt64Vec :: Int -> BS.ByteString -> VU.Vector Int64+readNInt64Vec n bs = VU.generate n (\i -> fromIntegral (littleEndianWord64 (BS.drop (8 * i) bs)))++-- | Read n Float values directly into an unboxed vector.+readNFloatVec :: Int -> BS.ByteString -> VU.Vector Float+readNFloatVec n bs =+ VU.generate+ n+ (\i -> castWord32ToFloat (littleEndianWord32 (BS.drop (4 * i) bs)))++-- | Read n Double values directly into an unboxed vector.+readNDoubleVec :: Int -> BS.ByteString -> VU.Vector Double+readNDoubleVec n bs =+ VU.generate+ n+ (\i -> castWord64ToDouble (littleEndianWord64 (BS.drop (8 * i) bs))) readNInt32 :: Int -> BS.ByteString -> ([Int32], BS.ByteString) readNInt32 0 bs = ([], bs)
src/DataFrame/IO/Parquet/Types.hs view
@@ -4,6 +4,7 @@ import Data.Int import qualified Data.Text as T import Data.Time+import qualified Data.Vector as V data ParquetType = PBOOLEAN@@ -178,13 +179,13 @@ deriving (Show, Eq) data DictVals- = DBool [Bool]- | DInt32 [Int32]- | DInt64 [Int64]- | DInt96 [UTCTime]- | DFloat [Float]- | DDouble [Double]- | DText [T.Text]+ = DBool (V.Vector Bool)+ | DInt32 (V.Vector Int32)+ | DInt64 (V.Vector Int64)+ | DInt96 (V.Vector UTCTime)+ | DFloat (V.Vector Float)+ | DDouble (V.Vector Double)+ | DText (V.Vector T.Text) deriving (Show, Eq) data Page = Page
src/DataFrame/IO/Unstable/CSV.hs view
@@ -7,6 +7,7 @@ readCsvUnstable, fastReadTsvUnstable, readTsvUnstable,+ getDelimiterIndices, ) where import qualified Data.Vector as Vector@@ -195,30 +196,46 @@ getDelimiterIndices separator originalLen csvFile = VS.unsafeWith csvFile $ \buffer -> do let paddedLen = VS.length csvFile- -- then number of delimiters cannot exceed the size- -- of the input array (which would be a series of- -- empty fields)- indices <- mallocArray paddedLen+ -- GC-managed pinned memory: freed automatically, no leak in streaming use.+ resultMV <- VSM.unsafeNew paddedLen num_fields <-- get_delimiter_indices- (castPtr buffer)- (fromIntegral paddedLen)- (fromIntegral separator)- (castPtr indices)+ VSM.unsafeWith resultMV $ \indicesPtr ->+ get_delimiter_indices+ (castPtr buffer)+ (fromIntegral paddedLen)+ (fromIntegral separator)+ (castPtr indicesPtr) if num_fields == -1- then getDelimiterIndices_ separator originalLen csvFile indices+ then do+ -- Haskell state-machine fallback, writing directly into resultMV.+ let trans = stateTransitionTable separator+ processChar (!state, !idx) i byte =+ case state of+ UnEscaped ->+ if byte == lf || byte == separator+ then do+ VSM.unsafeWrite resultMV idx (fromIntegral i)+ return (toEnum (trans ! (fromEnum state, byte)), idx + 1)+ else return (toEnum (trans ! (fromEnum state, byte)), idx)+ Escaped ->+ return (toEnum (trans ! (fromEnum state, byte)), idx)+ (_, finalIdx) <- VS.ifoldM' processChar (UnEscaped, 0 :: Int) csvFile+ finalLen <-+ if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf+ then do+ VSM.unsafeWrite resultMV finalIdx (fromIntegral originalLen)+ return (finalIdx + 1)+ else return finalIdx+ VS.unsafeFreeze (VSM.slice 0 finalLen resultMV) else do- indices' <- newForeignPtr_ indices- let resultVector = VSM.unsafeFromForeignPtr0 indices' paddedLen- -- Handle the case where the file doesn't end with a newline- -- We need to add a final delimiter for the last field- finalResultLen <-+ let n = fromIntegral num_fields+ finalLen <- if originalLen > 0 && csvFile VS.! (originalLen - 1) /= lf then do- VSM.write resultVector (fromIntegral num_fields) (fromIntegral originalLen)- return (fromIntegral num_fields + 1)- else return (fromIntegral num_fields)- VS.unsafeFreeze $ VSM.slice 0 finalResultLen resultVector+ VSM.write resultMV n (fromIntegral originalLen)+ return (n + 1)+ else return n+ VS.unsafeFreeze (VSM.slice 0 finalLen resultMV) -- We have a Native version in case the C version -- cannot be used. For example if neither ARM_NEON
src/DataFrame/Internal/Column.hs view
@@ -27,6 +27,7 @@ import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM +import Control.DeepSeq (NFData (..), rnf) import Control.Exception (throw) import Control.Monad.ST (runST) import Data.Kind (Type)@@ -36,6 +37,7 @@ import DataFrame.Errors import DataFrame.Internal.Parsing import DataFrame.Internal.Types+import System.IO.Unsafe (unsafePerformIO) import Type.Reflection {- | Our representation of a column is a GADT that can store data based on the underlying data.@@ -51,6 +53,7 @@ data MutableColumn where MBoxedColumn :: (Columnable a) => VBM.IOVector a -> MutableColumn MUnboxedColumn :: (Columnable a, VU.Unbox a) => VUM.IOVector a -> MutableColumn+ MOptionalColumn :: (Columnable a) => VBM.IOVector (Maybe a) -> MutableColumn {- | A TypedColumn is a wrapper around our type-erased column. It is used to type check expressions on columns.@@ -126,6 +129,11 @@ show :: (Show a) => TypedColumn a -> String show (TColumn col) = show col +instance NFData Column where+ rnf (BoxedColumn (v :: VB.Vector a)) = rnf v+ rnf (UnboxedColumn v) = v `seq` ()+ rnf (OptionalColumn (v :: VB.Vector (Maybe a))) = rnf v+ instance Show Column where show :: Column -> String show (BoxedColumn column) = show column@@ -282,9 +290,6 @@ STrue -> UnboxedColumn (VU.map f col) SFalse -> fromVector @c (VB.generate (VU.length col) (f . VU.unsafeIndex col)) Nothing -> throwTypeMismatch @a @b-{-# SPECIALIZE mapColumn ::- (Double -> Double) -> Column -> Either DataFrameException Column- #-} {-# INLINEABLE mapColumn #-} -- | Applies a function that returns an unboxed result to an unboxed vector, storing the result in a column.@@ -353,9 +358,17 @@ -- | 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) = BoxedColumn $ VG.unsafeBackpermute column (VG.convert indexes)+atIndicesStable indexes (BoxedColumn column) =+ BoxedColumn $+ VB.generate+ (VU.length indexes)+ (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i)) atIndicesStable indexes (UnboxedColumn column) = UnboxedColumn $ VU.unsafeBackpermute column indexes-atIndicesStable indexes (OptionalColumn column) = OptionalColumn $ VG.unsafeBackpermute column (VG.convert indexes)+atIndicesStable indexes (OptionalColumn column) =+ OptionalColumn $+ VB.generate+ (VU.length indexes)+ (\i -> column `VB.unsafeIndex` (indexes `VU.unsafeIndex` i)) {-# INLINE atIndicesStable #-} {- | Like 'atIndicesStable' but treats negative indices as null,@@ -533,6 +546,50 @@ } ) +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+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @d)+ , callingFunctionName = Just "foldlColumnWith"+ , 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+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @d)+ , callingFunctionName = Just "foldlColumnWith"+ , 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+ Nothing ->+ Left $+ TypeMismatchException+ ( MkTypeErrorContext+ { userType = Right (typeRep @a)+ , expectedType = Right (typeRep @d)+ , callingFunctionName = Just "foldlColumnWith"+ , errorColumnName = Nothing+ }+ )+ foldl1Column :: forall a. (Columnable a) =>@@ -574,6 +631,292 @@ } ) +{- | 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.+-}+foldl1DirectGroups ::+ forall a.+ (Columnable a) =>+ (a -> a -> a) ->+ Column ->+ VU.Vector Int ->+ VU.Vector Int ->+ Either DataFrameException (VB.Vector a)+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+ }+{-# INLINEABLE foldl1DirectGroups #-}++{- | O(n) fold over groups by scanning the column LINEARLY.+rowToGroup[i] = group index for row i.+Avoids random column reads; random writes go to the accumulator array which is+small (nGroups entries) and typically cache-resident.+When @acc@ is unboxable, uses an unboxed mutable vector for the accumulator+array, eliminating pointer indirection on every read/write.+-}+foldLinearGroups ::+ forall b acc.+ (Columnable b, Columnable acc) =>+ (acc -> b -> acc) ->+ acc ->+ Column ->+ VU.Vector Int -> -- rowToGroup (length n)+ Int -> -- nGroups+ Either DataFrameException Column+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+ }+ where+ -- \| 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 —+ -- avoiding a round-trip through VB.Vector.+ runWith :: ((Int -> IO acc) -> (Int -> acc -> IO ()) -> IO ()) -> IO Column+ runWith body = case sUnbox @acc of+ STrue -> do+ accs <- VUM.replicate nGroups seed+ body (VUM.unsafeRead accs) (VUM.unsafeWrite accs)+ UnboxedColumn <$> VU.unsafeFreeze accs+ SFalse -> do+ accs <- VBM.replicate nGroups seed+ body (VBM.unsafeRead accs) (VBM.unsafeWrite accs)+ fromVector @acc <$> VB.unsafeFreeze accs+ {-# INLINE runWith #-}+{-# 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 ->@@ -736,6 +1079,15 @@ else VBM.unsafeWrite col i value >> return (Right True) ) Nothing -> return (Left value)+writeColumn i value (MOptionalColumn (col :: VBM.IOVector (Maybe a))) =+ let+ in case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl ->+ ( if isNullish value+ then VBM.unsafeWrite col i Nothing >> return (Left $! value)+ else VBM.unsafeWrite col i (Just value) >> return (Right True)+ )+ Nothing -> return (Left value) writeColumn i value (MUnboxedColumn (col :: VUM.IOVector a)) = case testEquality (typeRep @a) (typeRep @Int) of Just Refl -> case readInt value of@@ -749,6 +1101,7 @@ {-# INLINE writeColumn #-} freezeColumn' :: [(Int, T.Text)] -> MutableColumn -> IO Column+freezeColumn' nulls (MOptionalColumn col) = OptionalColumn <$> VB.unsafeFreeze col freezeColumn' nulls (MBoxedColumn col) | null nulls = BoxedColumn <$> VB.unsafeFreeze col | all (isNullish . snd) nulls =@@ -874,6 +1227,34 @@ E.g. combining Column containing [1,2] with Column containing ["a","b"] will result in a Column containing [Left 1, Left 2, Right "a", Right "b"]. -}++{- | O(n) Concatenate a list of same-type columns in a single allocation.+All columns must have the same constructor and element type (as they will+within a single Parquet column). Calls 'error' on mismatch.+-}+concatManyColumns :: [Column] -> Column+concatManyColumns [] = fromList ([] :: [Maybe Int])+concatManyColumns [c] = c+concatManyColumns (c0 : cs) = case c0 of+ OptionalColumn v0 ->+ let getVec (OptionalColumn v) = case testEquality (typeOf v0) (typeOf v) of+ Just Refl -> v+ Nothing -> error "concatManyColumns: OptionalColumn type mismatch"+ getVec _ = error "concatManyColumns: column constructor mismatch"+ in OptionalColumn (VB.concat (v0 : map getVec cs))+ BoxedColumn v0 ->+ let getVec (BoxedColumn v) = case testEquality (typeOf v0) (typeOf v) of+ Just Refl -> v+ Nothing -> error "concatManyColumns: BoxedColumn type mismatch"+ getVec _ = error "concatManyColumns: column constructor mismatch"+ in BoxedColumn (VB.concat (v0 : map getVec cs))+ UnboxedColumn v0 ->+ let getVec (UnboxedColumn v) = case testEquality (typeOf v0) (typeOf v) of+ Just Refl -> v+ Nothing -> error "concatManyColumns: UnboxedColumn type mismatch"+ getVec _ = error "concatManyColumns: column constructor mismatch"+ in UnboxedColumn (VU.concat (v0 : map getVec cs))+ concatColumnsEither :: Column -> Column -> Column concatColumnsEither (OptionalColumn left) (OptionalColumn right) = case testEquality (typeOf left) (typeOf right) of Nothing ->@@ -912,6 +1293,38 @@ Just Refl -> OptionalColumn $ fmap Just (VG.convert left) <> right Nothing -> OptionalColumn $ fmap (Just . Left) (VG.convert left) <> fmap (fmap Right) right++-- | Allocate a mutable column of size @n@ matching the constructor/type of the given column.+newMutableColumn :: Int -> Column -> IO MutableColumn+newMutableColumn n (OptionalColumn (_ :: VB.Vector (Maybe a))) =+ MOptionalColumn <$> (VBM.new n :: IO (VBM.IOVector (Maybe a)))+newMutableColumn n (BoxedColumn (_ :: VB.Vector a)) =+ MBoxedColumn <$> (VBM.new n :: IO (VBM.IOVector a))+newMutableColumn n (UnboxedColumn (_ :: VU.Vector a)) =+ MUnboxedColumn <$> (VUM.new n :: IO (VUM.IOVector a))++-- | Copy a column chunk into a mutable column starting at offset @off@.+copyIntoMutableColumn :: MutableColumn -> Int -> Column -> IO ()+copyIntoMutableColumn (MOptionalColumn (mv :: VBM.IOVector (Maybe b))) off (OptionalColumn (v :: VB.Vector (Maybe a))) =+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v+ Nothing -> error "copyIntoMutableColumn: Optional type mismatch"+copyIntoMutableColumn (MBoxedColumn (mv :: VBM.IOVector b)) off (BoxedColumn (v :: VB.Vector a)) =+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> VG.imapM_ (\i x -> VBM.unsafeWrite mv (off + i) x) v+ Nothing -> error "copyIntoMutableColumn: Boxed type mismatch"+copyIntoMutableColumn (MUnboxedColumn (mv :: VUM.IOVector b)) off (UnboxedColumn (v :: VU.Vector a)) =+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> VG.imapM_ (\i x -> VUM.unsafeWrite mv (off + i) x) v+ Nothing -> error "copyIntoMutableColumn: Unboxed type mismatch"+copyIntoMutableColumn _ _ _ =+ error "copyIntoMutableColumn: constructor mismatch"++-- | Freeze a mutable column into an immutable column.+freezeMutableColumn :: MutableColumn -> IO Column+freezeMutableColumn (MOptionalColumn mv) = OptionalColumn <$> VB.unsafeFreeze mv+freezeMutableColumn (MBoxedColumn mv) = BoxedColumn <$> VB.unsafeFreeze mv+freezeMutableColumn (MUnboxedColumn mv) = UnboxedColumn <$> VU.unsafeFreeze mv {- | O(n) Converts a column to a list. Throws an exception if the wrong type is specified.
src/DataFrame/Internal/DataFrame.hs view
@@ -13,6 +13,7 @@ import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU +import Control.DeepSeq (NFData (..), rnf) import Control.Exception (throw) import Data.Function (on) import Data.List (sortBy, transpose, (\\))@@ -36,6 +37,10 @@ , derivingExpressions :: M.Map T.Text UExpr } +instance NFData DataFrame where+ rnf (DataFrame cols idx dims _exprs) =+ rnf cols `seq` rnf idx `seq` rnf dims+ {- | A record that contains information about how and what rows are grouped in the dataframe. This can only be used with `aggregate`.@@ -45,17 +50,21 @@ , groupedColumns :: [T.Text] , valueIndices :: VU.Vector Int , offsets :: VU.Vector Int+ , rowToGroup :: VU.Vector Int+ {- ^ rowToGroup[i] = group index for row i. Length n (one per row).+ Built once in 'groupBy'; reused by every aggregation.+ -} } instance Show GroupedDataFrame where- show (Grouped df cols indices os) =+ show (Grouped df cols _indices _os _rtg) = printf "{ keyColumns: %s groupedColumns: %s }" (show cols) (show (M.keys (columnIndices df) \\ cols)) instance Eq GroupedDataFrame where- (==) (Grouped df cols indices os) (Grouped df' cols' indices' os') = (df == df') && (cols == cols')+ (==) (Grouped df cols _indices _os _rtg) (Grouped df' cols' _indices' _os' _rtg') = (df == df') && (cols == cols') instance Eq DataFrame where (==) :: DataFrame -> DataFrame -> Bool
src/DataFrame/Internal/Expression.hs view
@@ -13,6 +13,7 @@ module DataFrame.Internal.Expression where +import Control.DeepSeq (NFData (..)) import Data.String import qualified Data.Text as T import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))@@ -34,10 +35,24 @@ , binaryPrecedence :: Int } +data MeanAcc = MeanAcc {-# UNPACK #-} !Double {-# UNPACK #-} !Int+ deriving (Show, Eq, Ord, Read)++instance NFData MeanAcc where+ rnf (MeanAcc _ _) = ()+ data AggStrategy a b where CollectAgg :: (VG.Vector v b, Typeable v) => T.Text -> (v b -> a) -> AggStrategy a b FoldAgg :: T.Text -> Maybe a -> (a -> b -> a) -> AggStrategy a b+ MergeAgg ::+ (Columnable acc) =>+ T.Text ->+ acc ->+ (acc -> b -> acc) ->+ (acc -> acc -> acc) ->+ (acc -> a) ->+ AggStrategy a b data Expr a where Col :: (Columnable a) => T.Text -> Expr a@@ -132,7 +147,7 @@ { binaryFn = (/) , binaryName = "divide" , binarySymbol = Just "/"- , binaryCommutative = True+ , binaryCommutative = False , binaryPrecedence = 7 } )@@ -219,6 +234,7 @@ show (Binary op a b) = "(" ++ T.unpack (binaryName op) ++ " " ++ show a ++ " " ++ show b ++ ")" show (Agg (CollectAgg op _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")" show (Agg (FoldAgg op _ _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")"+ show (Agg (MergeAgg op _ _ _ _) expr) = "(" ++ T.unpack op ++ " " ++ show expr ++ ")" normalize :: (Eq a, Ord a, Show a, Typeable a) => Expr a -> Expr a normalize expr = case expr of@@ -251,6 +267,7 @@ exprKey (Binary op e1 e2) = "4:" ++ T.unpack (binaryName op) ++ exprKey e1 ++ exprKey e2 exprKey (Agg (CollectAgg name _) e) = "5:" ++ T.unpack name ++ exprKey e exprKey (Agg (FoldAgg name _ _) e) = "5:" ++ T.unpack name ++ exprKey e+ exprKey (Agg (MergeAgg name _ _ _ _) e) = "5:" ++ T.unpack name ++ exprKey e instance (Eq a, Columnable a) => Eq (Expr a) where (==) l r = eqNormalized (normalize l) (normalize r)@@ -270,6 +287,8 @@ n1 == n2 && e1 `exprEq` e2 eqNormalized (Agg (FoldAgg n1 _ _) e1) (Agg (FoldAgg n2 _ _) e2) = n1 == n2 && e1 `exprEq` e2+ eqNormalized (Agg (MergeAgg n1 _ _ _ _) e1) (Agg (MergeAgg n2 _ _ _ _) e2) =+ n1 == n2 && e1 `exprEq` e2 eqNormalized _ _ = False instance (Ord a, Columnable a) => Ord (Expr a) where@@ -284,6 +303,7 @@ compare (binaryName op1) (binaryName op2) <> exprComp a1 a2 <> exprComp b1 b2 (Agg (CollectAgg n1 _) e1', Agg (CollectAgg n2 _) e2') -> compare n1 n2 <> exprComp e1' e2' (Agg (FoldAgg n1 _ _) e1', Agg (FoldAgg n2 _ _) e2') -> compare n1 n2 <> exprComp e1' e2'+ (Agg (MergeAgg n1 _ _ _ _) e1', Agg (MergeAgg n2 _ _ _ _) e2') -> compare n1 n2 <> exprComp e1' e2' -- Different constructors - compare by priority (Col _, _) -> LT (_, Col _) -> GT@@ -372,3 +392,4 @@ in if prec > p then "(" ++ inner ++ ")" else inner Agg (CollectAgg op _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")" Agg (FoldAgg op _ _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")"+ Agg (MergeAgg op _ _ _ _) arg -> T.unpack op ++ "(" ++ go depth 0 arg ++ ")"
src/DataFrame/Internal/Interpreter.hs view
@@ -6,841 +6,481 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-}--module DataFrame.Internal.Interpreter where--import Control.Monad.ST (runST)-import Data.Bifunctor-import qualified Data.Map as M-import qualified Data.Text as T-import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))-import qualified Data.Vector as V-import qualified Data.Vector.Mutable as VM-import qualified Data.Vector.Unboxed as VU-import qualified Data.Vector.Unboxed.Mutable as VUM-import DataFrame.Errors-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame-import DataFrame.Internal.Expression-import DataFrame.Internal.Types-import Type.Reflection (TypeRep, Typeable, typeOf, typeRep, pattern App)--interpret ::- forall a.- (Columnable a) =>- DataFrame -> Expr a -> Either DataFrameException (TypedColumn a)-interpret df (Lit value) = case sUnbox @a of- -- Specialize the creation of unboxed columns to avoid an extra allocation.- STrue -> pure $ TColumn $ fromUnboxedVector $ VU.replicate (numRows df) value- SFalse -> pure $ TColumn $ fromVector $ V.replicate (numRows df) value-interpret df (Col name) = maybe columnNotFound (pure . TColumn) (getColumn name df)- where- columnNotFound = Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)--- Unary operations.-interpret df expr@(Unary (op :: UnaryOp b c) value) = first (handleInterpretException (show expr)) $ do- (TColumn value') <- interpret df value- fmap TColumn (mapColumn (unaryFn op) value')--- Variations of binary operations.-interpret df expr@(Binary (op :: BinaryOp c b a) left right) = first (handleInterpretException (show expr)) $ case (left, right) of- (Lit left, Lit right) -> interpret df (Lit (binaryFn op left right))- (Lit left, right) -> do- -- If we have a literal then we don't have to materialise- -- the column.- (TColumn value') <- interpret df right- fmap TColumn (mapColumn (binaryFn op left) value')- (left, Lit right) -> do- -- Same as the above except the right side is the- -- literl.- (TColumn value') <- interpret df left- fmap TColumn (mapColumn (flip (binaryFn op) right) value')- (_, _) -> do- -- In the general case we interpret and zip.- (TColumn left') <- interpret df left- (TColumn right') <- interpret df right- fmap TColumn (zipWithColumns (binaryFn op) left' right')--- Conditionals-interpret df expr@(If cond l r) = first (handleInterpretException (show expr)) $ do- (TColumn conditions) <- interpret df cond- (TColumn left) <- interpret df l- (TColumn right) <- interpret df r- let branch (c :: Bool) (l' :: a, r' :: a) = if c then l' else r'- fmap TColumn (zipWithColumns branch conditions (zipColumns left right))-interpret df expression@(Agg (CollectAgg op (f :: v b -> c)) expr) = do- (TColumn column) <- interpret df expr- -- Helper for errors. Should probably find a way of throwing this- -- without leaking the fact that we use `Vector` to users.- let aggTypeError expected =- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @(v b))- , expectedType = Left expected :: Either String (TypeRep ())- , callingFunctionName = Just "interpret"- , errorColumnName = Nothing- }- )- let processColumn ::- (Columnable d) => d -> Either DataFrameException (TypedColumn a)- processColumn col = case testEquality (typeRep @(v b)) (typeOf col) of- Just Refl -> interpret @c df (Lit (f col))- Nothing -> Left $ aggTypeError (show (typeOf col))- case column of- (BoxedColumn col) -> processColumn col- (OptionalColumn col) -> processColumn col- (UnboxedColumn col) -> processColumn col-interpret df expression@(Agg (FoldAgg op (Just v) f) expr) = first (handleInterpretException (show expr)) $ do- (TColumn column) <- interpret df expr- value <- foldlColumn f v column- pure $ TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value-interpret df expression@(Agg (FoldAgg op Nothing (f :: a -> b -> a)) expr) = first (handleInterpretException (show expr)) $ do- (TColumn column) <- interpret df expr- case testEquality (typeRep @a) (typeRep @b) of- Just Refl -> do- value <- foldl1Column f column- pure $ TColumn $ fromVector $ V.replicate (fst $ dataframeDimensions df) value- Nothing -> error "Type error"--data AggregationResult a- = UnAggregated Column- | Aggregated (TypedColumn a)--interpretAggregation ::- forall a.- (Columnable a) =>- GroupedDataFrame -> Expr a -> Either DataFrameException (AggregationResult a)-interpretAggregation gdf (Lit value) =- Right $- Aggregated $- TColumn $- fromVector $- V.replicate (VU.length (offsets gdf) - 1) value-interpretAggregation gdf@(Grouped df names indices os) (Col name) = case getColumn name df of- Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)- Just (BoxedColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices- Just (OptionalColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices- Just (UnboxedColumn col) ->- Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnUnboxed col os indices-interpretAggregation gdf expression@(Unary (op :: UnaryOp b a) expr) =- case interpretAggregation @b gdf expr of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expr)- }- )- Left e -> Left e- Right (UnAggregated unaggregated) -> case unaggregated of- BoxedColumn (col :: V.Vector c) -> case testEquality (typeRep @c) (typeRep @(V.Vector b)) of- Just Refl -> case sUnbox @a of- SFalse -> Right $ UnAggregated $ fromVector $ V.map (V.map (unaryFn op)) col- STrue ->- Right $- UnAggregated $- fromVector $- V.map (V.convert @V.Vector @a @VU.Vector . V.map (unaryFn op)) col- Nothing -> case testEquality (typeRep @c) (typeRep @(VU.Vector b)) of- Nothing -> Left $ nestedTypeException @c @a (show expression)- Just Refl -> case (sUnbox @b, sUnbox @a) of- (SFalse, _) -> Left $ InternalException "Boxed type inside an unboxed column"- (STrue, STrue) -> Right $ UnAggregated $ fromVector $ V.map (VU.map (unaryFn op)) col- (STrue, _) ->- Right $ UnAggregated $ fromVector $ V.map (V.map (unaryFn op) . VU.convert) col- _ -> Left $ InternalException "Aggregated into a non-boxed column"- Right (Aggregated (TColumn aggregated)) -> case mapColumn (unaryFn op) aggregated of- Left e -> Left e- Right col -> Right $ Aggregated $ TColumn col-interpretAggregation gdf expression@(Binary (op :: t c b a) left (Lit (right :: b))) =- interpretAggregation- gdf- (Unary (MkUnaryOp (flip (binaryFn op) right) "udf" Nothing) left)-interpretAggregation gdf expression@(Binary (op :: BinaryOp c b a) (Lit (left :: c)) right) =- interpretAggregation- gdf- (Unary (MkUnaryOp (binaryFn op left) "udf" Nothing) right)-interpretAggregation gdf expression@(Binary (op :: BinaryOp c b a) left right) =- case (interpretAggregation gdf left, interpretAggregation gdf right) of- (Right (Aggregated (TColumn left')), Right (Aggregated (TColumn right'))) -> case zipWithColumns (binaryFn op) left' right' of- Left e -> Left e- Right col -> Right $ Aggregated $ TColumn col- (Right (UnAggregated left'), Right (UnAggregated right')) -> case (left', right') of- (BoxedColumn (l :: V.Vector m), BoxedColumn (r :: V.Vector n)) -> case testEquality (typeRep @m) (typeRep @(VU.Vector c)) of- Just Refl -> case testEquality (typeRep @n) (typeRep @(VU.Vector b)) of- Just Refl -> case (sUnbox @c, sUnbox @b, sUnbox @a) of- (STrue, STrue, STrue) ->- Right $ UnAggregated $ fromVector $ V.zipWith (VU.zipWith (binaryFn op)) l r- (STrue, STrue, SFalse) ->- Right $- UnAggregated $- fromVector $- V.zipWith (\l' r' -> V.zipWith (binaryFn op) (V.convert l') (V.convert r')) l r- (_, _, _) -> Left $ InternalException "Boxed vectors contain unboxed types"- Nothing -> case testEquality (typeRep @n) (typeRep @(V.Vector b)) of- Just Refl -> case sUnbox @c of- STrue ->- Right $- UnAggregated $- fromVector $- V.zipWith (V.zipWith (binaryFn op) . V.convert) l r- SFalse -> Left $ InternalException "Unboxed vectors contain boxed types"- Nothing -> Left $ nestedTypeException @n @b (show right)- Nothing -> case testEquality (typeRep @m) (typeRep @(V.Vector c)) of- Nothing -> Left $ nestedTypeException @m @c (show left)- Just Refl -> case testEquality (typeRep @n) (typeRep @(VU.Vector b)) of- Just Refl -> case (sUnbox @b, sUnbox @a) of- (STrue, STrue) ->- Right $- UnAggregated $- fromVector $- V.zipWith- ( \l' r' ->- V.convert @V.Vector @a @VU.Vector $ V.zipWith (binaryFn op) l' (V.convert r')- )- l- r- (STrue, SFalse) ->- Right $- UnAggregated $- fromVector $- V.zipWith (\l' r' -> V.zipWith (binaryFn op) l' (V.convert r')) l r- (_, _) -> Left $ InternalException "Unboxed vectors contain boxed types"- Nothing -> case testEquality (typeRep @n) (typeRep @(V.Vector b)) of- Just Refl -> case sUnbox @a of- SFalse ->- Right $- UnAggregated $- fromVector $- V.zipWith (V.zipWith (binaryFn op) . V.convert) l r- STrue ->- Right $- UnAggregated $- fromVector $- V.zipWith- (\l' r' -> V.convert @V.Vector @a @VU.Vector $ V.zipWith (binaryFn op) l' r')- l- r- Nothing -> Left $ nestedTypeException @n @b (show right)- _ -> Left $ InternalException "Aggregated into a non-boxed column"- (Right _, Right _) ->- Left $- AggregatedAndNonAggregatedException (T.pack $ show left) (T.pack $ show right)- (Left (TypeMismatchException context), _) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show left)- }- )- (Left e, _) -> Left e- (_, Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show right)- }- )- (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond (Lit l) (Lit r)) =- case interpretAggregation @Bool gdf cond of- Right (Aggregated (TColumn conditions)) -> case mapColumn- (\(c :: Bool) -> if c then l else r)- conditions of- Left e -> Left e- Right v -> Right $ Aggregated (TColumn v)- Right (UnAggregated conditions) -> case sUnbox @a of- STrue -> case mapColumn- (\(c :: VU.Vector Bool) -> VU.map (\c' -> if c' then l else r) c)- conditions of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- SFalse -> case mapColumn- (\(c :: VU.Vector Bool) -> V.map (\c' -> if c' then l else r) (VU.convert c))- conditions of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show cond)- }- )- Left e -> Left e-interpretAggregation gdf expression@(If cond (Lit l) r) =- case ( interpretAggregation @Bool gdf cond- , interpretAggregation @a gdf r- ) of- ( Right (Aggregated (TColumn conditions))- , Right (Aggregated (TColumn right))- ) -> case zipWithColumns- (\(c :: Bool) (r' :: a) -> if c then l else r')- conditions- right of- Left e -> Left e- Right v -> Right $ Aggregated (TColumn v)- ( Right (UnAggregated conditions)- , Right (UnAggregated right@(BoxedColumn (right' :: V.Vector c)))- ) -> case testEquality (typeRep @(V.Vector a)) (typeRep @c) of- Just Refl -> case zipWithColumns- ( \(c :: VU.Vector Bool) (r' :: V.Vector a) ->- V.zipWith- (\c' r'' -> if c' then l else r'')- (V.convert c)- r'- )- conditions- right of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @c) of- Nothing -> Left $ nestedTypeException @c @a (show expression)- Just Refl -> case sUnbox @a of- SFalse -> Left $ InternalException "Boxed type in unboxed column"- STrue -> case zipWithColumns- ( \(c :: VU.Vector Bool) (r' :: VU.Vector a) ->- VU.zipWith- (\c' r'' -> if c' then l else r'')- c- r'- )- conditions- right of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- (Right _, Right _) ->- Left $- AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)- (Left (TypeMismatchException context), _) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show cond)- }- )- (Left e, _) -> Left e- (_, Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show r)- }- )- (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond l (Lit r)) =- case ( interpretAggregation @Bool gdf cond- , interpretAggregation @a gdf l- ) of- ( Right (Aggregated (TColumn conditions))- , Right (Aggregated (TColumn left))- ) -> case zipWithColumns- (\(c :: Bool) (l' :: a) -> if c then l' else r)- conditions- left of- Left e -> Left e- Right v -> Right $ Aggregated (TColumn v)- ( Right (UnAggregated conditions)- , Right (UnAggregated left@(BoxedColumn (left' :: V.Vector c)))- ) -> case testEquality (typeRep @(V.Vector a)) (typeRep @c) of- Just Refl -> case zipWithColumns- ( \(c :: VU.Vector Bool) (l' :: V.Vector a) ->- V.zipWith- (\c' l'' -> if c' then l'' else r)- (V.convert c)- l'- )- conditions- left of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @c) of- Nothing -> Left $ nestedTypeException @c @a (show expression)- Just Refl -> case sUnbox @a of- SFalse -> Left $ InternalException "Boxed type in unboxed column"- STrue -> case zipWithColumns- ( \(c :: VU.Vector Bool) (l' :: VU.Vector a) ->- VU.zipWith- (\c' l'' -> if c' then l'' else r)- c- l'- )- conditions- left of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- (Right _, Right _) ->- Left $- AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)- (Left (TypeMismatchException context), _) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show cond)- }- )- (Left e, _) -> Left e- (_, Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show r)- }- )- (_, Left e) -> Left e-interpretAggregation gdf expression@(If cond l r) =- case ( interpretAggregation @Bool gdf cond- , interpretAggregation @a gdf l- , interpretAggregation @a gdf r- ) of- ( Right (Aggregated (TColumn conditions))- , Right (Aggregated (TColumn left))- , Right (Aggregated (TColumn right))- ) -> case zipWithColumns- (\(c :: Bool) (l' :: a, r' :: a) -> if c then l' else r')- conditions- (zipColumns left right) of- Left e -> Left e- Right v -> Right $ Aggregated (TColumn v)- ( Right (UnAggregated conditions)- , Right (UnAggregated left@(BoxedColumn (left' :: V.Vector b)))- , Right (UnAggregated right@(BoxedColumn (right' :: V.Vector c)))- ) -> case testEquality (typeRep @b) (typeRep @c) of- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @c)- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Just Refl -> case testEquality (typeRep @(V.Vector a)) (typeRep @b) of- Just Refl -> case zipWithColumns- ( \(c :: VU.Vector Bool) (l' :: V.Vector a, r' :: V.Vector a) ->- V.zipWith- (\c' (l'', r'') -> if c' then l'' else r'')- (V.convert c)- (V.zip l' r')- )- conditions- (zipColumns left right) of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @b) of- Nothing -> Left $ nestedTypeException @b @a (show expression)- Just Refl -> case sUnbox @a of- SFalse -> Left $ InternalException "Boxed type in unboxed column"- STrue -> case zipWithColumns- ( \(c :: VU.Vector Bool) (l' :: VU.Vector a, r' :: VU.Vector a) ->- VU.zipWith- (\c' (l'', r'') -> if c' then l'' else r'')- c- (VU.zip l' r')- )- conditions- (zipColumns left right) of- Left (TypeMismatchException context) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- Left e -> Left e- Right v -> Right $ UnAggregated v- (Right _, Right _, Right _) ->- Left $- AggregatedAndNonAggregatedException (T.pack $ show l) (T.pack $ show r)- (Left (TypeMismatchException context), _, _) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show cond)- }- )- (Left e, _, _) -> Left e- (_, Left (TypeMismatchException context), _) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show l)- }- )- (_, Left e, _) -> Left e- (_, _, Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show r)- }- )- (_, _, Left e) -> Left e-interpretAggregation gdf@(Grouped df names indices os) expression@(Agg (CollectAgg op (f :: v b -> c)) expr) =- case interpretAggregation @b gdf expr of- Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(v b)) (typeRep @d) of- Nothing -> Left $ nestedTypeException @d @b (show expr)- Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of- Nothing -> Right $ Aggregated $ TColumn $ fromVector $ V.map (f . V.convert) col- Just Refl -> Right $ Aggregated $ TColumn $ fromVector $ V.map f col- Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"- Right (Aggregated (TColumn (BoxedColumn (col :: V.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of- Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of- Just Refl -> interpretAggregation @c gdf (Lit (f col))- Nothing -> interpretAggregation @c gdf (Lit ((f . V.convert) col))- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expr)- }- )- Right (Aggregated (TColumn (UnboxedColumn (col :: VU.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of- Just Refl -> case testEquality (typeRep @v) (typeRep @VU.Vector) of- Just Refl -> interpretAggregation @c gdf (Lit (f col))- Nothing -> interpretAggregation @c gdf (Lit ((f . VU.convert) col))- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expr)- }- )- Right (Aggregated (TColumn (OptionalColumn (col :: V.Vector d)))) -> case testEquality (typeRep @b) (typeRep @d) of- Just Refl -> case testEquality (typeRep @v) (typeRep @V.Vector) of- Just Refl -> interpretAggregation @c gdf (Lit (f col))- Nothing -> interpretAggregation @c gdf (Lit ((f . V.convert) col))- Nothing ->- Left $- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @b)- , expectedType = Right (typeRep @d)- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expr)- }- )- (Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- (Left e) -> Left e-interpretAggregation gdf@(Grouped df names indices os) expression@(Agg (FoldAgg op Nothing (f :: a -> b -> a)) (Col name)) = case testEquality (typeRep @a) (typeRep @b) of- Nothing -> error "Type mismatch"- Just Refl -> case getColumn name df of- Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)- Just (BoxedColumn (col :: V.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of- Nothing -> error "Type mismatch"- Just Refl ->- Right $- Aggregated $- TColumn $- fromVector $- mkReducedColumnBoxed col os indices f- Just (OptionalColumn (col :: V.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of- Nothing -> error "Type mismatch"- Just Refl ->- Right $- Aggregated $- TColumn $- fromVector $- mkReducedColumnBoxed col os indices f- Just (UnboxedColumn (col :: VU.Vector d)) -> case testEquality (typeRep @a) (typeRep @d) of- Just Refl ->- Right $- Aggregated $- TColumn $- fromUnboxedVector $- mkReducedColumnUnboxed col os indices f- Nothing -> error "Type mismatch"-interpretAggregation gdf@(Grouped df names indices os) expression@(Agg (FoldAgg op Nothing (f :: a -> b -> a)) expr) = case testEquality (typeRep @a) (typeRep @b) of- Nothing -> error "Type mismatch"- Just Refl -> case interpretAggregation @a gdf expr of- (Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- (Left e) -> Left e- Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(V.Vector a)) (typeRep @d) of- Nothing -> case testEquality (typeRep @(VU.Vector a)) (typeRep @d) of- Nothing -> Left $ nestedTypeException @d @a (show expr)- Just Refl -> case sUnbox @a of- STrue ->- Right $- Aggregated $- TColumn $- fromVector $- V.map (VU.foldl1' f) col- SFalse -> Left $ InternalException "Boxed type inside an unboxed column"- Just Refl ->- Right $- Aggregated $- TColumn $- fromVector $- V.map (V.foldl1' f) col- Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"- Right (Aggregated (TColumn column)) -> case foldl1Column f column of- Left e -> Left e- Right value -> interpretAggregation @a gdf (Lit value)-interpretAggregation gdf@(Grouped df names indices os) expression@(Agg (FoldAgg op (Just s) (f :: a -> b -> a)) expr) =- case interpretAggregation gdf expr of- (Left (TypeMismatchException context)) ->- Left $- TypeMismatchException- ( context- { callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just (show expression)- }- )- (Left e) -> Left e- Right (UnAggregated (BoxedColumn (col :: V.Vector d))) -> case testEquality (typeRep @(V.Vector b)) (typeRep @d) of- Just Refl -> Right $ Aggregated $ TColumn $ fromVector $ V.map (V.foldl' f s) col- Nothing -> case testEquality (typeRep @(VU.Vector b)) (typeRep @d) of- Just Refl -> case sUnbox @b of- STrue ->- Right $ Aggregated $ TColumn $ fromVector $ V.map (VU.foldl' f s) col- SFalse -> Left $ InternalException "Boxed type inside an unboxed column"- Nothing -> Left $ nestedTypeException @d @b (show expr)- Right (UnAggregated _) -> Left $ InternalException "Aggregated into non-boxed column"- Right (Aggregated (TColumn column)) -> case foldlColumn f s column of- Left e -> Left e- Right value -> interpretAggregation @a gdf (Lit value)--handleInterpretException :: String -> DataFrameException -> DataFrameException-handleInterpretException errorLocation (TypeMismatchException context) = mkTypeMismatchException (Just "interpret") (Just errorLocation) context-handleInterpretException _ e = e--numRows :: DataFrame -> Int-numRows df = fst (dataframeDimensions df)--mkUnaggregatedColumnBoxed ::- forall a.- (Columnable a) =>- V.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (V.Vector a)-mkUnaggregatedColumnBoxed col os indices =- let- sorted = V.unsafeBackpermute col (V.convert indices)- n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)- start i = os `VU.unsafeIndex` i- in- V.generate- (VU.length os - 1)- ( \i ->- V.unsafeSlice (start i) (n i) sorted- )--mkUnaggregatedColumnUnboxed ::- forall a.- (Columnable a, VU.Unbox a) =>- VU.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (VU.Vector a)-mkUnaggregatedColumnUnboxed col os indices =- let- sorted = VU.unsafeBackpermute col indices- n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)- start i = os `VU.unsafeIndex` i- in- V.generate- (VU.length os - 1)- ( \i ->- VU.unsafeSlice (start i) (n i) sorted- )--mkAggregatedColumnUnboxed ::- forall a b.- (Columnable a, VU.Unbox a, Columnable b, VU.Unbox b) =>- VU.Vector a ->- VU.Vector Int ->- VU.Vector Int ->- (VU.Vector a -> b) ->- VU.Vector b-mkAggregatedColumnUnboxed col os indices f =- let- sorted = VU.unsafeBackpermute col indices- n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)- start i = os `VU.unsafeIndex` i- in- VU.generate- (VU.length os - 1)- ( \i ->- f (VU.unsafeSlice (start i) (n i) sorted)- )--mkReducedColumnUnboxed ::- forall a.- (VU.Unbox a) =>- VU.Vector a ->- VU.Vector Int ->- VU.Vector Int ->- (a -> a -> a) ->- VU.Vector a-mkReducedColumnUnboxed col os indices f = runST $ do- let len = VU.length os - 1- mvec <- VUM.unsafeNew len-- let loopOut i- | i == len = return ()- | otherwise = do- let !start = os `VU.unsafeIndex` i- let !end = os `VU.unsafeIndex` (i + 1)- let !initVal = col `VU.unsafeIndex` (indices `VU.unsafeIndex` start)-- let loopIn !acc !idx- | idx == end = acc- | otherwise =- let val = col `VU.unsafeIndex` (indices `VU.unsafeIndex` idx)- in loopIn (f acc val) (idx + 1)- let !finalVal = loopIn initVal (start + 1)- VUM.unsafeWrite mvec i finalVal- loopOut (i + 1)-- loopOut 0- VU.unsafeFreeze mvec-{-# INLINE mkReducedColumnUnboxed #-}--mkReducedColumnBoxed ::- V.Vector a ->- VU.Vector Int ->- VU.Vector Int ->- (a -> a -> a) ->- V.Vector a-mkReducedColumnBoxed col os indices f = runST $ do- let len = VU.length os - 1- mvec <- VM.unsafeNew len-- let loopOut i- | i == len = return ()- | otherwise = do- let start = os `VU.unsafeIndex` i- let end = os `VU.unsafeIndex` (i + 1)- let initVal = col `V.unsafeIndex` (indices `VU.unsafeIndex` start)-- let loopIn !acc idx- | idx == end = acc- | otherwise =- let val = col `V.unsafeIndex` (indices `VU.unsafeIndex` idx)- in loopIn (f acc val) (idx + 1)- let !finalVal = loopIn initVal (start + 1)- VM.unsafeWrite mvec i finalVal- loopOut (i + 1)-- loopOut 0- V.unsafeFreeze mvec-{-# INLINE mkReducedColumnBoxed #-}--nestedTypeException ::- forall a b. (Typeable a, Typeable b) => String -> DataFrameException-nestedTypeException expression = case typeRep @a of- App t1 t2 ->- TypeMismatchException- ( MkTypeErrorContext- { userType = Left (show (typeRep @b)) :: Either String (TypeRep ())- , expectedType = Left (show (typeRep @a)) :: Either String (TypeRep ())- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just expression- }- )- t ->- TypeMismatchException- ( MkTypeErrorContext- { userType = Right (typeRep @(VU.Vector b))- , expectedType = Right (typeRep @b)- , callingFunctionName = Just "interpretAggregation"- , errorColumnName = Just expression- }- )--mkTypeMismatchException ::- (Typeable a, Typeable b) =>- Maybe String -> Maybe String -> TypeErrorContext a b -> DataFrameException-mkTypeMismatchException callPoint errorLocation context =- TypeMismatchException- ( context- { callingFunctionName = callPoint- , errorColumnName = errorLocation- }- )+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++module DataFrame.Internal.Interpreter (+ -- * New core API+ Value (..),+ Ctx (..),+ eval,+ materialize,++ -- * Backward-compatible API+ interpret,+ interpretAggregation,+ AggregationResult (..),+) where++import Data.Bifunctor (first)+import qualified Data.Map as M+import qualified Data.Text as T+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import qualified Data.Vector as V+import qualified Data.Vector.Generic as VG+import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors+import DataFrame.Internal.Column+import DataFrame.Internal.DataFrame+import DataFrame.Internal.Expression+import DataFrame.Internal.Types+import Type.Reflection (Typeable, typeRep)++-------------------------------------------------------------------------------+-- Value: the unified result type+-------------------------------------------------------------------------------++{- | The result of interpreting an expression. Keeps literals as scalars+until the point where a concrete column is needed, avoiding premature+broadcast allocations.+-}+data Value a where+ -- | A single value, not yet broadcast to any length.+ Scalar :: (Columnable a) => a -> Value a+ {- | A flat column (one element per row in the flat case, or one+ element per group after aggregation).+ -}+ Flat :: (Columnable a) => Column -> Value a+ {- | A grouped column: one 'Column' slice per group. Only produced+ when interpreting inside a 'GroupCtx'.+ -}+ Group :: (Columnable a) => V.Vector Column -> Value a++-- | The interpretation context.+data Ctx+ = FlatCtx DataFrame+ | GroupCtx GroupedDataFrame++-------------------------------------------------------------------------------+-- Materialisation+-------------------------------------------------------------------------------++{- | Force a 'Value' into a flat 'Column' of the given length. Scalars+are broadcast; flat columns are returned as-is.+-}+materialize :: forall a. (Columnable a) => Int -> Value a -> Column+materialize n (Scalar v) = broadcastScalar @a n v+materialize _ (Flat c) = c+materialize _ (Group _) =+ error "materialize: cannot flatten a grouped value to a single column"++{- | Replicate a scalar to a column of length @n@, choosing the most+efficient representation.+-}+broadcastScalar :: forall a. (Columnable a) => Int -> a -> Column+broadcastScalar n v = case sUnbox @a of+ STrue -> fromUnboxedVector (VU.replicate n v)+ SFalse -> fromVector (V.replicate n v)++-------------------------------------------------------------------------------+-- Lifting: the core combinators+-------------------------------------------------------------------------------++-- | Apply a pure function to a 'Value'.+liftValue ::+ (Columnable b, Columnable a) =>+ (b -> a) -> Value b -> Either DataFrameException (Value a)+liftValue f (Scalar v) = Right (Scalar (f v))+liftValue f (Flat col) = Flat <$> mapColumn f col+liftValue f (Group gs) = Group <$> V.mapM (mapColumn f) gs++{- | Apply a binary function to two 'Value's. When one side is a+'Scalar' the operation degenerates to a 'liftValue' — this is how the+old @Binary op (Lit l) right@ special cases are recovered without+explicit pattern matches in the evaluator.+-}+liftValue2 ::+ (Columnable c, Columnable b, Columnable a) =>+ (c -> b -> a) ->+ Value c ->+ Value b ->+ Either DataFrameException (Value a)+liftValue2 f (Scalar l) (Scalar r) = Right (Scalar (f l r))+liftValue2 f (Scalar l) v = liftValue (f l) v+liftValue2 f v (Scalar r) = liftValue (`f` r) v+liftValue2 f (Flat l) (Flat r) = Flat <$> zipWithColumns f l r+liftValue2 f (Group ls) (Group rs)+ | V.length ls == V.length rs =+ Group <$> V.zipWithM (zipWithColumns f) ls rs+-- Shape mismatches: aggregated vs. non-aggregated.+liftValue2 _ (Flat _) (Group _) =+ Left $ AggregatedAndNonAggregatedException "aggregated" "non-aggregated"+liftValue2 _ (Group _) (Flat _) =+ Left $ AggregatedAndNonAggregatedException "non-aggregated" "aggregated"+liftValue2 _ (Group _) (Group _) =+ Left $ InternalException "Group count mismatch in binary operation"++-- | Branch on a boolean 'Value', selecting from two same-typed 'Value's.+branchValue ::+ forall a.+ (Columnable a) =>+ Value Bool ->+ Value a ->+ Value a ->+ Either DataFrameException (Value a)+branchValue (Scalar True) l _ = Right l+branchValue (Scalar False) _ r = Right r+branchValue cond (Scalar l) (Scalar r) =+ liftValue (\c -> if c then l else r) cond+branchValue cond (Scalar l) r =+ liftValue2 (\c rv -> if c then l else rv) cond r+branchValue cond l (Scalar r) =+ liftValue2 (\c lv -> if c then lv else r) cond l+branchValue (Flat cc) (Flat lc) (Flat rc) =+ Flat <$> branchColumn @a cc lc rc+branchValue (Group cgs) (Group lgs) (Group rgs)+ | V.length cgs == V.length lgs+ && V.length lgs == V.length rgs =+ Group+ <$> V.generateM+ (V.length cgs)+ ( \i ->+ branchColumn @a (cgs V.! i) (lgs V.! i) (rgs V.! i)+ )+branchValue _ _ _ =+ Left $+ AggregatedAndNonAggregatedException+ "if-then-else branches"+ "mismatched shapes"++{- | Low-level column branch: given a boolean column and two same-typed+columns, produce the element-wise selection.+-}+branchColumn ::+ forall a.+ (Columnable a) =>+ Column ->+ Column ->+ Column ->+ Either DataFrameException Column+branchColumn cc lc rc = do+ cs <- toVector @Bool @V.Vector cc+ ls <- toVector @a @V.Vector lc+ rs <- toVector @a @V.Vector rc+ pure $+ fromVector @a $+ V.zipWith3 (\c l r -> if c then l else r) cs ls rs++-------------------------------------------------------------------------------+-- Error enrichment+-------------------------------------------------------------------------------++{- | Wrap an interpretation step so that any 'TypeMismatchException' gets+annotated with the expression that was being evaluated.+-}+addContext ::+ (Show a) => Expr a -> Either DataFrameException b -> Either DataFrameException b+addContext expr = first (enrichError (show expr))++enrichError :: String -> DataFrameException -> DataFrameException+enrichError loc (TypeMismatchException ctx) =+ TypeMismatchException+ ctx+ { callingFunctionName =+ callingFunctionName ctx <|+> Just "eval"+ , errorColumnName =+ errorColumnName ctx <|+> Just loc+ }+ where+ -- Prefer the existing value; fall back to the new one.+ Nothing <|+> b = b+ a <|+> _ = a+enrichError _ e = e++-------------------------------------------------------------------------------+-- Group slicing+-------------------------------------------------------------------------------++{- | Given a flat column and grouping metadata, produce one 'Column' per+group. Each result column is an O(1) slice into a sorted copy of the+input — the sort happens once, not per-group.+-}+sliceGroups :: Column -> VU.Vector Int -> VU.Vector Int -> V.Vector Column+sliceGroups col os indices = case col of+ BoxedColumn vec ->+ let !sorted = V.unsafeBackpermute vec (V.convert indices)+ in V.generate nGroups $ \i ->+ BoxedColumn (V.unsafeSlice (start i) (len i) sorted)+ UnboxedColumn vec ->+ let !sorted = VU.unsafeBackpermute vec indices+ in V.generate nGroups $ \i ->+ UnboxedColumn (VU.unsafeSlice (start i) (len i) sorted)+ OptionalColumn vec ->+ let !sorted = V.unsafeBackpermute vec (V.convert indices)+ in V.generate nGroups $ \i ->+ OptionalColumn (V.unsafeSlice (start i) (len i) sorted)+ where+ !nGroups = VU.length os - 1+ start i = os `VU.unsafeIndex` i+ len i = os `VU.unsafeIndex` (i + 1) - start i+{-# INLINE sliceGroups #-}++numGroups :: GroupedDataFrame -> Int+numGroups gdf = VU.length (offsets gdf) - 1++-------------------------------------------------------------------------------+-- eval: the unified interpreter+-------------------------------------------------------------------------------++{- | Evaluate an expression in a given context, producing a 'Value'.+This single function replaces both the old @interpret@ (flat) and+@interpretAggregation@ (grouped) code paths.+-}+eval ::+ forall a.+ (Columnable a) =>+ Ctx -> Expr a -> Either DataFrameException (Value a)+-- Leaves -----------------------------------------------------------------++eval _ (Lit v) = Right (Scalar v)+eval (FlatCtx df) (Col name) =+ case getColumn name df of+ Nothing ->+ Left $+ ColumnNotFoundException name "" (M.keys $ columnIndices df)+ Just c -> Right (Flat c)+eval (GroupCtx gdf) (Col name) =+ case getColumn name (fullDataframe gdf) of+ Nothing ->+ Left $+ ColumnNotFoundException+ name+ ""+ (M.keys $ columnIndices $ fullDataframe gdf)+ Just c ->+ Right+ ( Group+ (sliceGroups c (offsets gdf) (valueIndices gdf))+ )+-- Unary ------------------------------------------------------------------++eval ctx expr@(Unary (op :: UnaryOp b a) inner) = addContext expr $ do+ v <- eval @b ctx inner+ liftValue (unaryFn op) v++-- Binary -----------------------------------------------------------------++eval ctx expr@(Binary (op :: BinaryOp c b a) left right) =+ addContext expr $ do+ l <- eval @c ctx left+ r <- eval @b ctx right+ liftValue2 (binaryFn op) l r++-- If ---------------------------------------------------------------------++eval ctx expr@(If cond l r) = addContext expr $ do+ c <- eval @Bool ctx cond+ lv <- eval @a ctx l+ rv <- eval @a ctx r+ branchValue c lv rv++-- Fast path: FoldAgg (seeded) on a bare Col in GroupCtx.+-- Avoids the O(n) backpermute in sliceGroups by folding directly over+-- permuted indices. Only matches when inner is exactly (Col name).++eval (GroupCtx gdf) expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) (Col name :: Expr b)) =+ addContext expr $+ case getColumn name (fullDataframe gdf) of+ Nothing ->+ Left $+ ColumnNotFoundException+ name+ ""+ (M.keys $ columnIndices $ fullDataframe gdf)+ Just col ->+ Flat <$> foldLinearGroups @b @a f seed col (rowToGroup gdf) (numGroups gdf)+-- Fast path: FoldAgg (seedless) on a bare Col in GroupCtx.++eval (GroupCtx gdf) expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) (Col name :: Expr b)) =+ addContext expr $+ case testEquality (typeRep @a) (typeRep @b) of+ Nothing ->+ Left $+ InternalException+ "Type mismatch in seedless fold: \+ \accumulator and element types must match"+ Just Refl ->+ case getColumn name (fullDataframe gdf) of+ Nothing ->+ Left $+ ColumnNotFoundException+ name+ ""+ (M.keys $ columnIndices $ fullDataframe gdf)+ Just col ->+ Flat . fromVector+ <$> foldl1DirectGroups @b f col (valueIndices gdf) (offsets gdf)+-- Fast path: MergeAgg on a bare Col in GroupCtx.++eval+ (GroupCtx gdf)+ expr@( Agg+ (MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))+ (Col name :: Expr b)+ ) =+ addContext expr $+ case getColumn name (fullDataframe gdf) of+ Nothing ->+ Left $+ ColumnNotFoundException+ name+ ""+ (M.keys $ columnIndices $ fullDataframe gdf)+ Just col ->+ Flat+ <$> ( foldLinearGroups @b step seed col (rowToGroup gdf) (numGroups gdf)+ >>= mapColumn finalize+ )+-- Aggregation: CollectAgg ------------------------------------------------++eval ctx expr@(Agg (CollectAgg _ (f :: v b -> a)) inner) =+ addContext expr $ do+ v <- eval @b ctx inner+ case v of+ Scalar _ ->+ Left $+ InternalException+ "Cannot apply a collection aggregation to a scalar"+ Flat col ->+ Scalar <$> applyCollect @v @b @a f col+ Group gs ->+ Flat . fromVector+ <$> V.mapM (applyCollect @v @b @a f) gs++-- Aggregation: FoldAgg with seed -----------------------------------------++eval ctx expr@(Agg (FoldAgg _ (Just seed) (f :: a -> b -> a)) inner) =+ addContext expr $ do+ v <- eval @b ctx inner+ case v of+ Scalar _ ->+ Left $+ InternalException+ "Cannot apply a fold aggregation to a scalar"+ Flat col ->+ Scalar <$> foldlColumn @b @a f seed col+ Group gs ->+ Flat . fromVector+ <$> V.mapM (foldlColumn @b @a f seed) gs++-- Aggregation: MergeAgg --------------------------------------------------++eval+ ctx+ expr@( Agg+ (MergeAgg _ seed (step :: acc -> b -> acc) _ (finalize :: acc -> a))+ (inner :: Expr b)+ ) =+ addContext expr $ do+ v <- eval @b ctx inner+ case v of+ Scalar _ ->+ Left $+ InternalException+ "Cannot apply a merge aggregation to a scalar"+ Flat col ->+ Scalar . finalize <$> foldlColumnWith @b step seed col+ Group gs ->+ Flat . fromVector+ <$> V.mapM (fmap finalize . foldlColumnWith @b step seed) gs++-- Aggregation: FoldAgg without seed (fold1) ------------------------------++eval ctx expr@(Agg (FoldAgg _ Nothing (f :: a -> b -> a)) inner) =+ addContext expr $+ case testEquality (typeRep @a) (typeRep @b) of+ Nothing ->+ Left $+ InternalException+ "Type mismatch in seedless fold: \+ \accumulator and element types must match"+ Just Refl -> do+ v <- eval @b ctx inner+ case v of+ Scalar _ ->+ Left $+ InternalException+ "Cannot apply a fold aggregation to a scalar"+ Flat col ->+ Scalar <$> foldl1Column @a f col+ Group gs ->+ Flat . fromVector+ <$> V.mapM (foldl1Column @a f) gs++-------------------------------------------------------------------------------+-- Aggregation helpers+-------------------------------------------------------------------------------++{- | Apply a 'CollectAgg' function to a single column, extracting the+appropriate vector type and applying the aggregation function.+-}+applyCollect ::+ forall v b a.+ (VG.Vector v b, Typeable v, Columnable b, Columnable a) =>+ (v b -> a) -> Column -> Either DataFrameException a+applyCollect f col = f <$> toVector @b @v col++-------------------------------------------------------------------------------+-- Backward-compatible wrappers+-------------------------------------------------------------------------------++{- | Result of interpreting an expression in a grouped context.+Retained for backward compatibility with 'aggregate' and friends.+-}+data AggregationResult a+ = UnAggregated Column+ | Aggregated (TypedColumn a)++{- | Interpret an expression against a flat 'DataFrame', producing a+typed column. This is the original top-level entry point; internally+it calls 'eval' and materialises the result.++NOTE: unlike the old implementation, 'Lit' values are no longer+eagerly broadcast. The broadcast happens here, at the boundary,+via 'materialize'.+-}+interpret ::+ forall a.+ (Columnable a) =>+ DataFrame -> Expr a -> Either DataFrameException (TypedColumn a)+interpret df expr = do+ v <- eval (FlatCtx df) expr+ pure $ TColumn $ materialize @a (fst (dataframeDimensions df)) v++{- | Interpret an expression against a 'GroupedDataFrame',+distinguishing aggregated results from bare column references.+Internally calls 'eval'.+-}+interpretAggregation ::+ forall a.+ (Columnable a) =>+ GroupedDataFrame ->+ Expr a ->+ Either DataFrameException (AggregationResult a)+interpretAggregation gdf expr = do+ v <- eval (GroupCtx gdf) expr+ case v of+ Scalar a ->+ Right $+ Aggregated $+ TColumn $+ broadcastScalar @a (numGroups gdf) a+ Flat col ->+ Right $ Aggregated $ TColumn col+ Group _ ->+ -- The Column payload is intentionally unused — the only+ -- call-site ('aggregate') immediately throws+ -- 'UnaggregatedException' on this constructor.+ Right $ UnAggregated $ BoxedColumn @T.Text V.empty
src/DataFrame/Internal/Parsing.hs view
@@ -1,16 +1,25 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module DataFrame.Internal.Parsing where import qualified Data.ByteString.Char8 as C import qualified Data.Set as S import qualified Data.Text as T+import qualified Data.Text.IO as TIO +import Control.Applicative (many, (<|>))+import Data.Attoparsec.Text hiding (decimal, double, signed) import Data.ByteString.Lex.Fractional+import Data.Foldable (fold) import Data.Maybe (fromMaybe)-import Data.Text.Read+import Data.Text.Read (decimal, double, signed)+import Data.Time (Day, defaultTimeLocale, parseTimeM) import GHC.Stack (HasCallStack)+import System.IO (Handle, IOMode (..), hIsEOF, hTell, withFile) import Text.Read (readMaybe)+import Prelude hiding (takeWhile) isNullish :: T.Text -> Bool isNullish =@@ -19,6 +28,13 @@ ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"] ) +isNullishBS :: C.ByteString -> Bool+isNullishBS =+ ( `S.member`+ S.fromList+ ["Nothing", "NULL", "", " ", "nan", "null", "N/A", "NaN", "NAN", "NA"]+ )+ isTrueish :: T.Text -> Bool isTrueish t = t `elem` ["True", "true", "TRUE"] @@ -27,7 +43,7 @@ readValue :: (HasCallStack, Read a) => T.Text -> a readValue s = case readMaybe (T.unpack s) of- Nothing -> error $ "Could not read value: " ++ T.unpack s+ Nothing -> error ("Could not read value: " <> T.unpack s) Just value -> value readBool :: (HasCallStack) => T.Text -> Maybe Bool@@ -36,6 +52,15 @@ | isFalseish s = Just False | otherwise = Nothing +readByteStringBool :: C.ByteString -> Maybe Bool+readByteStringBool s+ | s `elem` ["True", "true", "TRUE"] = Just True+ | s `elem` ["False", "false", "FALSE"] = Just False+ | otherwise = Nothing++readByteStringDate :: String -> C.ByteString -> Maybe Day+readByteStringDate fmt = parseTimeM True defaultTimeLocale fmt . C.unpack+ readInteger :: (HasCallStack) => T.Text -> Maybe Integer readInteger s = case signed decimal (T.strip s) of Left _ -> Nothing@@ -102,3 +127,106 @@ readWithDefault :: (HasCallStack, Read a) => a -> T.Text -> a readWithDefault v s = fromMaybe v (readMaybe (T.unpack s))++-- ---------------------------------------------------------------------------+-- Attoparsec CSV parser combinators (shared between Lazy.IO.CSV and others)+-- ---------------------------------------------------------------------------++parseSep :: Char -> T.Text -> [T.Text]+parseSep c s = either error id (parseOnly (record c) s)+{-# INLINE parseSep #-}++record :: Char -> Parser [T.Text]+record c =+ field c `sepBy1` char c+ <?> "record"+{-# INLINE record #-}++parseRow :: Char -> Parser [T.Text]+parseRow c = (record c <* lineEnd) <?> "record-new-line"++field :: Char -> Parser T.Text+field c =+ quotedField <|> unquotedField c+ <?> "field"+{-# INLINE field #-}++unquotedTerminators :: Char -> S.Set Char+unquotedTerminators sep = S.fromList [sep, '\n', '\r', '"']++unquotedField :: Char -> Parser T.Text+unquotedField sep =+ takeWhile (not . (`S.member` terminators)) <?> "unquoted field"+ where+ terminators = unquotedTerminators sep+{-# INLINE unquotedField #-}++quotedField :: Parser T.Text+quotedField = char '"' *> contents <* char '"' <?> "quoted field"+ where+ contents = fold <$> many (unquote <|> unescape)+ where+ unquote = takeWhile1 (notInClass "\"\\")+ unescape =+ char '\\' *> do+ T.singleton <$> do+ char '\\' <|> char '"'+{-# INLINE quotedField #-}++lineEnd :: Parser ()+lineEnd =+ (endOfLine <|> endOfInput)+ <?> "end of line"+{-# INLINE lineEnd #-}++-- | First pass to count rows for exact allocation.+countRows :: Char -> FilePath -> IO Int+countRows c path = withFile path ReadMode $! go 0 ""+ where+ go n input h = do+ isEOF <- hIsEOF h+ if isEOF && input == mempty+ then pure n+ else+ parseWith (TIO.hGetChunk h) (parseRow c) input >>= \case+ Fail unconsumed ctx er -> do+ erpos <- hTell h+ fail $+ "Failed to parse CSV file around "+ <> show erpos+ <> " byte; due: "+ <> show er+ <> "; context: "+ <> show ctx+ <> " "+ <> show unconsumed+ Partial _ -> fail $ "Partial handler is called; n = " <> show n+ Done (unconsumed :: T.Text) _ ->+ go (n + 1) unconsumed h+{-# INLINE countRows #-}++-- | Infer the Haskell type name from a text sample.+inferValueType :: T.Text -> T.Text+inferValueType s = case readInt s of+ Just _ -> "Int"+ Nothing -> case readDouble s of+ Just _ -> "Double"+ Nothing -> "Other"+{-# INLINE inferValueType #-}++-- | Read a single CSV row from a handle using the given separator.+readSingleLine :: Char -> T.Text -> Handle -> IO ([T.Text], T.Text)+readSingleLine c unused handle =+ parseWith (TIO.hGetChunk handle) (parseRow c) unused >>= \case+ Fail unconsumed ctx er -> do+ erpos <- hTell handle+ fail $+ "Failed to parse CSV file around "+ <> show erpos+ <> " byte; due: "+ <> show er+ <> "; context: "+ <> show ctx+ Partial _ -> fail "Partial handler is called"+ Done (unconsumed :: T.Text) (row :: [T.Text]) ->+ return (row, unconsumed)
src/DataFrame/Internal/Row.hs view
@@ -17,6 +17,7 @@ import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU +import Control.DeepSeq (NFData (..)) import Control.Exception (throw) import Control.Monad.ST (runST) import Data.Function (on)@@ -56,6 +57,9 @@ instance Show Any where show :: Any -> String show (Value a) = T.unpack (showValue a)++instance NFData Any where+ rnf (Value a) = rnf a showValue :: forall a. (Columnable a) => a -> T.Text showValue v = case testEquality (typeRep @a) (typeRep @T.Text) of
src/DataFrame/Internal/Types.hs view
@@ -13,13 +13,14 @@ module DataFrame.Internal.Types where +import Control.DeepSeq (NFData) import Data.Int (Int16, Int32, Int64, Int8) import Data.Kind (Constraint, Type) import Data.Typeable (Typeable) import qualified Data.Vector.Unboxed as VU import Data.Word (Word16, Word32, Word64, Word8) -type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a)+type Columnable' a = (Typeable a, Show a, Ord a, Eq a, Read a, NFData a) {- | A type with column representations used to select the "right" representation when specializing the `toColumn` function.
+ src/DataFrame/Lazy/IO/Binary.hs view
@@ -0,0 +1,444 @@+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TypeApplications #-}++{- | Simple column-oriented binary spill format (DFBN).++Layout (all integers little-endian):++@+[magic: 4 bytes] "DFBN"+[num_columns: 4 bytes] Word32+ per column:+ [name_len: 2 bytes] Word16 (byte length of UTF-8 name)+ [name: name_len bytes]+ [type_tag: 1 byte] Word8+[num_rows: 8 bytes] Word64++per column data block (order matches schema):+ type_tag 0 (Int): num_rows × Int64 LE+ type_tag 1 (Double): num_rows × Double LE (IEEE 754)+ type_tag 2 (Text): (num_rows+1) × Word32 offsets ++ payload bytes (UTF-8)+ type_tag 3 (Maybe Int): ceil(num_rows/8)-byte null bitmap ++ num_rows × Int64 LE+ type_tag 4 (Maybe Double): ceil(num_rows/8)-byte null bitmap ++ num_rows × Double LE+ type_tag 5 (Maybe Text): ceil(num_rows/8)-byte null bitmap+ ++ (num_rows+1) × Word32 offsets ++ payload bytes+@++Null bitmap: bit @i@ of byte @i\/8@ is 1 when row @i@ is non-null.+-}+module DataFrame.Lazy.IO.Binary (+ spillToDisk,+ readSpilled,+ withSpilled,+) where++import Control.Exception (SomeException, bracket, try)+import Control.Monad (foldM, void, when)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as BSB+import qualified Data.ByteString.Internal as BSI+import qualified Data.ByteString.Unsafe as BSU+import qualified Data.List as L+import qualified Data.Map.Strict 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.Storable as VS+import qualified Data.Vector.Unboxed as VU++import Data.Bits (setBit, shiftL, testBit, (.|.))+import Data.Maybe (fromMaybe, isJust)+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import Data.Word (Word16, Word32, Word64, Word8)+import DataFrame.Internal.Column (Column (..))+import DataFrame.Internal.DataFrame (DataFrame (..))+import Foreign (ForeignPtr, castForeignPtr, plusForeignPtr, sizeOf)+import System.Directory (getTemporaryDirectory, removeFile)+import System.IO (IOMode (..), hClose, openTempFile, withFile)+import Type.Reflection (typeRep)++-- ---------------------------------------------------------------------------+-- Type tags+-- ---------------------------------------------------------------------------++tagInt, tagDouble, tagText, tagMaybeInt, tagMaybeDouble, tagMaybeText :: Word8+tagInt = 0+tagDouble = 1+tagText = 2+tagMaybeInt = 3+tagMaybeDouble = 4+tagMaybeText = 5++-- ---------------------------------------------------------------------------+-- Write+-- ---------------------------------------------------------------------------++-- | Serialise a 'DataFrame' to a DFBN binary file.+spillToDisk :: FilePath -> DataFrame -> IO ()+spillToDisk path df =+ withFile path WriteMode $ \h -> BSB.hPutBuilder h (buildDataFrame df)++buildDataFrame :: DataFrame -> BSB.Builder+buildDataFrame df =+ BSB.byteString "DFBN"+ <> BSB.word32LE ncols+ <> foldMap (uncurry buildColumnSchema) (zip names cols)+ <> BSB.word64LE nrows+ <> foldMap (buildColumnData nrowsInt) cols+ where+ names =+ fmap+ fst+ (L.sortBy (\a b -> compare (snd a) (snd b)) (M.toList (columnIndices df)))+ ncols = fromIntegral (length names) :: Word32+ cols = V.toList (columns df)+ nrowsInt = fst (dataframeDimensions df)+ nrows = fromIntegral nrowsInt :: Word64++buildColumnSchema :: T.Text -> Column -> BSB.Builder+buildColumnSchema name col =+ BSB.word16LE nameLen+ <> BSB.byteString nameBytes+ <> BSB.word8 (columnTypeTag col)+ where+ nameBytes = TE.encodeUtf8 name+ nameLen = fromIntegral (BS.length nameBytes) :: Word16++columnTypeTag :: Column -> Word8+columnTypeTag (UnboxedColumn (_ :: VU.Vector a)) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> tagInt+ Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> tagDouble+ Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"+columnTypeTag (BoxedColumn _) = tagText+columnTypeTag (OptionalColumn (_ :: V.Vector (Maybe a))) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> tagMaybeInt+ Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> tagMaybeDouble+ Nothing -> tagMaybeText++buildColumnData :: Int -> Column -> BSB.Builder+buildColumnData _ (UnboxedColumn (v :: VU.Vector a)) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> buildIntVector v+ Nothing ->+ case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> buildDoubleVector v+ Nothing -> error "spillToDisk: unsupported UnboxedColumn element type"+buildColumnData _ (BoxedColumn (v :: V.Vector a)) =+ case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> buildTextVector v+ Nothing -> error "spillToDisk: unsupported BoxedColumn element type"+buildColumnData _ (OptionalColumn (v :: V.Vector (Maybe a))) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl ->+ buildNullBitmap (V.map isJust v)+ <> buildIntVector (VU.convert (V.map (fromMaybe 0) v))+ Nothing ->+ case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl ->+ buildNullBitmap (V.map isJust v)+ <> buildDoubleVector (VU.convert (V.map (fromMaybe 0.0) v))+ Nothing ->+ let showText x = case testEquality (typeRep @a) (typeRep @T.Text) of+ Just Refl -> x+ Nothing -> T.pack (show x)+ texts = V.map (maybe T.empty showText) v+ in buildNullBitmap (V.map isJust v) <> buildTextVector texts++{- | Bulk-encode an Int vector as 8-byte LE values (native layout on LE platforms).+hPutBuilder flushes synchronously so the underlying ForeignPtr outlives the Builder.+-}+buildIntVector :: VU.Vector Int -> BSB.Builder+buildIntVector v =+ let sv = VU.convert v :: VS.Vector Int+ (fp, n) = VS.unsafeToForeignPtr0 sv+ bs = BSI.fromForeignPtr (castForeignPtr fp) 0 (n * sizeOf (0 :: Int))+ in BSB.byteString bs++-- | Bulk-encode a Double vector as 8-byte LE IEEE 754 values (native layout on LE platforms).+buildDoubleVector :: VU.Vector Double -> BSB.Builder+buildDoubleVector v =+ let sv = VU.convert v :: VS.Vector Double+ (fp, n) = VS.unsafeToForeignPtr0 sv+ bs = BSI.fromForeignPtr (castForeignPtr fp) 0 (n * sizeOf (0 :: Double))+ in BSB.byteString bs++-- | Write a Text vector: (num_rows+1) Word32 offsets followed by UTF-8 payload.+buildTextVector :: V.Vector T.Text -> BSB.Builder+buildTextVector v =+ foldMap BSB.word32LE offsets <> foldMap BSB.byteString encoded+ where+ encoded = V.toList (V.map TE.encodeUtf8 v)+ offsets = scanl (\acc bs -> acc + fromIntegral (BS.length bs)) (0 :: Word32) encoded++-- | Build a null-validity bitmap: 1 bit per row, packed LSB-first into bytes.+buildNullBitmap :: V.Vector Bool -> BSB.Builder+buildNullBitmap valids = foldMap (BSB.word8 . mkByte) [0 .. numBytes - 1]+ where+ n = V.length valids+ numBytes = (n + 7) `div` 8+ mkByte byteIdx =+ foldr+ ( \bit acc ->+ let row = byteIdx * 8 + bit+ in if row < n && (valids V.! row) then setBit acc bit else acc+ )+ (0 :: Word8)+ [0 .. 7]++-- ---------------------------------------------------------------------------+-- Read+-- ---------------------------------------------------------------------------++-- | @(new_offset, value)@+type ParseResult a = Either String (Int, a)++-- | Deserialise a DFBN binary file into a 'DataFrame'.+readSpilled :: FilePath -> IO DataFrame+readSpilled path = do+ bs <- BS.readFile path+ case parseDataFrame bs 0 of+ Left err -> fail ("readSpilled: " <> err)+ Right (_, df) -> return df++parseDataFrame :: BS.ByteString -> Int -> ParseResult DataFrame+parseDataFrame bs off0 = do+ (off1, magic) <- readBytes bs off0 4+ when (magic /= "DFBN") $ Left "bad magic bytes"+ (off2, ncols) <- readWord32LE bs off1+ let ncolsInt = fromIntegral ncols :: Int+ (off3, schema) <- readN ncolsInt (readColumnSchema bs) off2+ (off4, nrows64) <- readWord64LE bs off3+ let nrows = fromIntegral nrows64 :: Int+ (off5, cols) <-+ foldM+ ( \(o, acc) (_, tag) -> do+ (o', col) <- readColumnData bs o nrows tag+ return (o', acc ++ [col])+ )+ (off4, [])+ schema+ let names = fmap fst schema+ return+ ( off5+ , DataFrame+ { columns = V.fromList cols+ , columnIndices = M.fromList (zip names [0 ..])+ , dataframeDimensions = (nrows, ncolsInt)+ , derivingExpressions = M.empty+ }+ )++readColumnSchema :: BS.ByteString -> Int -> ParseResult (T.Text, Word8)+readColumnSchema bs off = do+ (off1, nameLen) <- readWord16LE bs off+ let nameLenInt = fromIntegral nameLen :: Int+ (off2, nameBytes) <- readBytes bs off1 nameLenInt+ (off3, tag) <- readWord8 bs off2+ return (off3, (TE.decodeUtf8 nameBytes, tag))++readColumnData :: BS.ByteString -> Int -> Int -> Word8 -> ParseResult Column+readColumnData bs off nrows tag+ | tag == tagInt = do+ (off', v) <- readIntColumn bs off nrows+ return (off', UnboxedColumn v)+ | tag == tagDouble = do+ (off', v) <- readDoubleColumn bs off nrows+ return (off', UnboxedColumn v)+ | tag == tagText = do+ (off', v) <- readTextColumn bs off nrows+ return (off', BoxedColumn v)+ | tag == tagMaybeInt = do+ (off1, bitmap) <- readNullBitmap bs off nrows+ (off2, v) <- readIntColumn bs off1 nrows+ let maybes =+ V.fromList+ (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::+ V.Vector (Maybe Int)+ return (off2, OptionalColumn maybes)+ | tag == tagMaybeDouble = do+ (off1, bitmap) <- readNullBitmap bs off nrows+ (off2, v) <- readDoubleColumn bs off1 nrows+ let maybes =+ V.fromList+ (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (VU.toList v)) ::+ V.Vector (Maybe Double)+ return (off2, OptionalColumn maybes)+ | tag == tagMaybeText = do+ (off1, bitmap) <- readNullBitmap bs off nrows+ (off2, v) <- readTextColumn bs off1 nrows+ let maybes =+ V.fromList+ (zipWith (\valid x -> if valid then Just x else Nothing) bitmap (V.toList v)) ::+ V.Vector (Maybe T.Text)+ return (off2, OptionalColumn maybes)+ | otherwise = Left ("unknown type tag " <> show tag)++{- | Zero-copy Int column read: reuses the ByteString buffer's ForeignPtr.+Safe as long as 'bs' stays live during the caller's use of the resulting vector.+Only correct on little-endian platforms (aarch64/x86_64).+-}+readIntColumn :: BS.ByteString -> Int -> Int -> ParseResult (VU.Vector Int)+readIntColumn bs off nrows+ | off + nrows * 8 > BS.length bs = Left "unexpected end of input"+ | otherwise =+ let (fp, bsOff, _) = BSI.toForeignPtr bs+ fp' = castForeignPtr (plusForeignPtr fp (bsOff + off)) :: ForeignPtr Int+ sv = VS.unsafeFromForeignPtr0 fp' nrows :: VS.Vector Int+ in Right (off + nrows * 8, VU.convert sv)++{- | Zero-copy Double column read: reuses the ByteString buffer's ForeignPtr.+Safe as long as 'bs' stays live during the caller's use of the resulting vector.+Only correct on little-endian platforms (aarch64/x86_64).+-}+readDoubleColumn ::+ BS.ByteString -> Int -> Int -> ParseResult (VU.Vector Double)+readDoubleColumn bs off nrows+ | off + nrows * 8 > BS.length bs = Left "unexpected end of input"+ | otherwise =+ let (fp, bsOff, _) = BSI.toForeignPtr bs+ fp' = castForeignPtr (plusForeignPtr fp (bsOff + off)) :: ForeignPtr Double+ sv = VS.unsafeFromForeignPtr0 fp' nrows :: VS.Vector Double+ in Right (off + nrows * 8, VU.convert sv)++readTextColumn :: BS.ByteString -> Int -> Int -> ParseResult (V.Vector T.Text)+readTextColumn bs off nrows = do+ offsets <- readWord32Array bs off (nrows + 1)+ let payloadStart = off + (nrows + 1) * 4+ totalPayload = fromIntegral (last offsets) :: Int+ when (payloadStart + totalPayload > BS.length bs) $+ Left "unexpected end of input"+ let sizes =+ zipWith+ (\a b -> fromIntegral b - fromIntegral a :: Int)+ offsets+ (drop 1 offsets)+ texts =+ zipWith+ ( \o sz ->+ TE.decodeUtf8+ (BS.take sz (BS.drop (payloadStart + fromIntegral o) bs))+ )+ offsets+ sizes+ return (payloadStart + totalPayload, V.fromList texts)++-- | Read @nrows@ null-bitmap bits (ceil(nrows\/8) bytes).+readNullBitmap :: BS.ByteString -> Int -> Int -> ParseResult [Bool]+readNullBitmap bs off nrows+ | off + numBytes > BS.length bs = Left "unexpected end of input"+ | otherwise =+ Right+ ( off + numBytes+ , take+ nrows+ [ testBit (BSU.unsafeIndex bs (off + row `div` 8)) (row `mod` 8)+ | row <- [0 ..]+ ]+ )+ where+ numBytes = (nrows + 7) `div` 8++readWord8 :: BS.ByteString -> Int -> ParseResult Word8+readWord8 bs off+ | off >= BS.length bs = Left "unexpected end of input"+ | otherwise = Right (off + 1, BSU.unsafeIndex bs off)++readWord16LE :: BS.ByteString -> Int -> ParseResult Word16+readWord16LE bs off+ | off + 2 > BS.length bs = Left "unexpected end of input"+ | otherwise =+ let b0 = fromIntegral (BSU.unsafeIndex bs off) :: Word16+ b1 = fromIntegral (BSU.unsafeIndex bs (off + 1)) :: Word16+ in Right (off + 2, b0 .|. (b1 `shiftL` 8))++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))++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)+ )++-- | Read @n@ consecutive Word32LE values starting at offset @off@.+readWord32Array :: BS.ByteString -> Int -> Int -> Either String [Word32]+readWord32Array bs off n+ | off + n * 4 > BS.length bs = Left "unexpected end of input"+ | 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)+ | k <- [0 .. n - 1]+ ]++-- | Read @n@ bytes from @bs@ at @off@.+readBytes :: BS.ByteString -> Int -> Int -> ParseResult BS.ByteString+readBytes bs off n+ | off + n > BS.length bs = Left "unexpected end of input"+ | otherwise = Right (off + n, BS.take n (BS.drop off bs))++-- | Apply @f@ @n@ times sequentially, threading the offset.+readN :: Int -> (Int -> ParseResult a) -> Int -> ParseResult [a]+readN 0 _ off = Right (off, [])+readN n f off = do+ (off', x) <- f off+ (off'', xs) <- readN (n - 1) f off'+ return (off'', x : xs)++-- ---------------------------------------------------------------------------+-- Bracket helper+-- ---------------------------------------------------------------------------++{- | Spill a DataFrame to a temporary file, run an action with the path,+then delete the file even if the action throws.+-}+withSpilled :: DataFrame -> (FilePath -> IO a) -> IO a+withSpilled df action = do+ tmpDir <- getTemporaryDirectory+ bracket+ ( do+ (path, h) <- openTempFile tmpDir "dataframe_spill.dfbn"+ hClose h+ spillToDisk path df+ return path+ )+ (\path -> void (try (removeFile path) :: IO (Either SomeException ())))+ action
src/DataFrame/Lazy/IO/CSV.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}@@ -8,25 +9,23 @@ module DataFrame.Lazy.IO.CSV where -import qualified Data.List as L+import qualified Data.ByteString as BS import qualified Data.Map as M-import qualified Data.Set as S+import qualified Data.Proxy as P import qualified Data.Text as T+import qualified Data.Text.Encoding as TextEncoding import qualified Data.Text.IO as TIO import qualified Data.Vector as V import qualified Data.Vector.Mutable as VM-import qualified Data.Vector.Unboxed as VU import qualified Data.Vector.Unboxed.Mutable as VUM -import Control.Applicative (many, (<|>)) import Control.Monad (forM_, unless, when, zipWithM_)-import Data.Attoparsec.Text-import Data.Char-import Data.Foldable (fold)-import Data.Function (on)+import Data.Attoparsec.Text (IResult (..), parseWith)+import Data.Char (intToDigit) import Data.IORef-import Data.Maybe+import Data.Maybe (fromMaybe, isJust) import Data.Type.Equality (TestEquality (testEquality))+import Data.Word (Word8) import DataFrame.Internal.Column ( Column (..), MutableColumn (..),@@ -36,9 +35,10 @@ ) import DataFrame.Internal.DataFrame (DataFrame (..)) import DataFrame.Internal.Parsing+import DataFrame.Internal.Schema (Schema, SchemaType (..), elements) import System.IO import Type.Reflection-import Prelude hiding (concat, takeWhile)+import Prelude hiding (takeWhile) -- | Record for CSV read options. data ReadOptions = ReadOptions@@ -94,11 +94,11 @@ Nothing -> (0, totalRows) Just (start, len) -> (start, min len (totalRows - rowsRead opts)) withFile path ReadMode $ \handle -> do- firstRow <- map T.strip . parseSep c <$> TIO.hGetLine handle+ firstRow <- fmap T.strip . parseSep c <$> TIO.hGetLine handle let columnNames = if hasHeader opts- then map (T.filter (/= '\"')) firstRow- else map (T.singleton . intToDigit) [0 .. (length firstRow - 1)]+ then fmap (T.filter (/= '\"')) firstRow+ else fmap (T.singleton . intToDigit) [0 .. (length firstRow - 1)] -- If there was no header rewind the file cursor. unless (hasHeader opts) $ hSeek handle AbsoluteSeek 0 @@ -110,13 +110,9 @@ let numColumns = length columnNames let numRows = len -- Use this row to infer the types of the rest of the column.- -- TODO: this isn't robust but in so far as this is a guess anyway- -- it's probably fine. But we should probably sample n rows and pick- -- the most likely type from the sample. (dataRow, remainder) <- readSingleLine c (leftOver opts) handle -- This array will track the indices of all null values for each column.- -- If any exist then the column will be an optional type. nullIndices <- VM.unsafeNew numColumns VM.set nullIndices [] mutableCols <- VM.unsafeNew numColumns@@ -136,7 +132,7 @@ { columns = cols , columnIndices = M.fromList (zip columnNames [0 ..]) , dataframeDimensions = (maybe 0 columnLength (cols V.!? 0), V.length cols)- , derivingExpressions = M.empty -- TODO create base expressions.+ , derivingExpressions = M.empty } , (pos, unconsumed, r + 1) )@@ -161,35 +157,6 @@ VM.unsafeWrite mCol i col {-# INLINE getInitialDataVectors #-} -inferValueType :: T.Text -> T.Text-inferValueType s =- let- example = s- in- case readInt example of- Just _ -> "Int"- Nothing -> case readDouble example of- Just _ -> "Double"- Nothing -> "Other"-{-# INLINE inferValueType #-}--readSingleLine :: Char -> T.Text -> Handle -> IO ([T.Text], T.Text)-readSingleLine c unused handle =- parseWith (TIO.hGetChunk handle) (parseRow c) unused >>= \case- Fail unconsumed ctx er -> do- erpos <- hTell handle- fail $- "Failed to parse CSV file around "- <> show erpos- <> " byte; due: "- <> show er- <> "; context: "- <> show ctx- Partial c -> do- fail "Partial handler is called"- Done (unconsumed :: T.Text) (row :: [T.Text]) -> do- return (row, unconsumed)- -- | Reads rows from the handle and stores values in mutable vectors. fillColumns :: Int ->@@ -216,7 +183,7 @@ <> show er <> "; context: " <> show ctx- Partial c -> do+ Partial _ -> do fail "Partial handler is called" Done (unconsumed :: T.Text) (row :: [T.Text]) -> do writeIORef input unconsumed@@ -254,142 +221,232 @@ freezeColumn' (nulls V.! colIndex) col {-# INLINE freezeColumn #-} -parseSep :: Char -> T.Text -> [T.Text]-parseSep c s = either error id (parseOnly (record c) s)-{-# INLINE parseSep #-}+-- ---------------------------------------------------------------------------+-- Streaming scan API+-- --------------------------------------------------------------------------- -record :: Char -> Parser [T.Text]-record c =- field c `sepBy1` char c- <?> "record"-{-# INLINE record #-}+{- | Open a CSV/separated file for streaming, returning an open handle+(positioned just after the header line) and the column specification+for the schema columns that appear in the file header. -parseRow :: Char -> Parser [T.Text]-parseRow c = (record c <* lineEnd) <?> "record-new-line"+The caller is responsible for closing the handle when done.+-}+openCsvStream ::+ Char ->+ Schema ->+ FilePath ->+ IO (Handle, [(Int, T.Text, SchemaType)])+openCsvStream sep schema path = do+ handle <- openFile path ReadMode+ hSetBuffering handle (BlockBuffering (Just (8 * 1024 * 1024)))+ headerLine <- TIO.hGetLine handle+ let headerCols = fmap (T.filter (/= '"') . T.strip) (parseSep sep headerLine)+ let schemaMap = elements schema+ let colSpec =+ [ (idx, name, stype)+ | (idx, name) <- zip [0 ..] headerCols+ , Just stype <- [M.lookup name schemaMap]+ ]+ when (null colSpec) $+ hClose handle+ >> fail+ ("openCsvStream: none of the schema columns appear in the header of " <> path)+ return (handle, colSpec) -field :: Char -> Parser T.Text-field c =- quotedField <|> unquotedField c- <?> "field"-{-# INLINE field #-}+{- | Read up to @batchSz@ rows from the open handle, returning a batch+'DataFrame' and the unconsumed leftover text. Returns 'Nothing' when+the handle is at EOF and there is no leftover input. -unquotedTerminators :: Char -> S.Set Char-unquotedTerminators sep = S.fromList [sep, '\n', '\r', '"']+The caller must pass the leftover returned by the previous call (use @""@+for the first call).+-}+readBatch ::+ Char ->+ [(Int, T.Text, SchemaType)] ->+ Int ->+ BS.ByteString ->+ Handle ->+ IO (Maybe (DataFrame, BS.ByteString))+readBatch sep colSpec batchSz leftover handle = do+ let sepByte = fromIntegral (fromEnum sep) :: Word8+ numCols = length colSpec+ -- Read in 8 MB chunks; only the partial-line tail is copied on refill.+ chunkSize = 8 * 1024 * 1024+ nullsArr <- VM.unsafeNew numCols+ VM.set nullsArr []+ mCols <- VM.unsafeNew numCols+ forM_ (zip [0 ..] colSpec) $ \(ci, (_, _, st)) ->+ VM.unsafeWrite mCols ci =<< makeCol batchSz st+ -- buf holds unprocessed bytes; refilled on demand when no newline is found.+ bufRef <- newIORef leftover+ -- Row-by-row scan. When the buffer has no unquoted newline, fetch another chunk.+ -- The copy on refill is only the partial-line tail (≤ one row ≈ few hundred bytes).+ let loop !rowIdx = do+ remaining <- readIORef bufRef+ if rowIdx >= batchSz+ then return (rowIdx, remaining)+ else case findUnquotedNewline remaining of+ Nothing -> do+ chunk <- BS.hGet handle chunkSize+ if BS.null chunk+ then return (rowIdx, remaining) -- EOF+ else writeIORef bufRef (remaining <> chunk) >> loop rowIdx+ Just nlIdx -> do+ let line = BS.take nlIdx remaining+ rest' = BS.drop (nlIdx + 1) remaining+ line' =+ if not (BS.null line) && BS.last line == 0x0D+ then BS.init line+ else line+ writeIORef bufRef rest'+ forM_ (zip [0 ..] colSpec) $ \(ci, (fi, _, _)) -> do+ let fieldBs = getNthFieldBs sepByte fi line'+ col <- VM.unsafeRead mCols ci+ res <- writeColumnBs rowIdx fieldBs col+ case res of+ Left nv -> VM.unsafeModify nullsArr ((rowIdx, nv) :) ci+ Right _ -> return ()+ loop (rowIdx + 1)+ (completeRows, newLeftover) <- loop 0+ if completeRows == 0+ then return Nothing+ else do+ forM_ [0 .. numCols - 1] $ \ci -> do+ col <- VM.unsafeRead mCols ci+ VM.unsafeWrite mCols ci (sliceCol completeRows col)+ nullsVec <- V.unsafeFreeze nullsArr+ cols <- V.generateM numCols $ \ci -> do+ col <- VM.unsafeRead mCols ci+ freezeColumn' (nullsVec V.! ci) col+ let colNames = [name | (_, name, _) <- colSpec]+ return $+ Just+ ( DataFrame+ { columns = cols+ , columnIndices = M.fromList (zip colNames [0 ..])+ , dataframeDimensions = (completeRows, numCols)+ , derivingExpressions = M.empty+ }+ , newLeftover+ ) -unquotedField :: Char -> Parser T.Text-unquotedField sep =- takeWhile (not . (`S.member` terminators)) <?> "unquoted field"- where- terminators = unquotedTerminators sep-{-# INLINE unquotedField #-}+{- | Write a 'ByteString' field value directly into a mutable column,+parsing numerics without an intermediate 'T.Text' allocation.+-}+writeColumnBs ::+ Int -> BS.ByteString -> MutableColumn -> IO (Either T.Text Bool)+writeColumnBs i bs (MBoxedColumn (col :: VM.IOVector 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 T.empty >> return (Left val)+ else 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)+ Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))+writeColumnBs i bs (MUnboxedColumn (col :: VUM.IOVector a)) =+ case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> case readByteStringDouble bs of+ Just v -> VUM.unsafeWrite col i v >> return (Right True)+ Nothing -> VUM.unsafeWrite col i 0 >> return (Left (TextEncoding.decodeUtf8Lenient bs))+ Nothing -> case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> case readByteStringInt bs of+ Just v -> VUM.unsafeWrite col i v >> return (Right True)+ Nothing -> VUM.unsafeWrite col i 0 >> return (Left (TextEncoding.decodeUtf8Lenient bs))+ Nothing -> return (Left (TextEncoding.decodeUtf8Lenient bs))+{-# INLINE writeColumnBs #-} -quotedField :: Parser T.Text-quotedField = char '"' *> contents <* char '"' <?> "quoted field"+{- | Extracts the Nth field (0-indexed), respecting double quotes and stripping them.+Fast path: uses memchr-based 'BS.break' when no quotes are present in the line.+Slow path: quote-aware character-by-character scan.+-}+getNthFieldBs :: Word8 -> Int -> BS.ByteString -> BS.ByteString+getNthFieldBs sep targetIdx bs+ | not (BS.any (== 0x22) bs) = skipFast targetIdx bs+ | otherwise = go 0 0 False 0 where- contents = fold <$> many (unquote <|> unescape)- where- unquote = takeWhile1 (notInClass "\"\\")- unescape =- char '\\' *> do- T.singleton <$> do- char '\\' <|> char '"'-{-# INLINE quotedField #-}+ -- Fast path: skip fields using elemIndex (memchr); avoids pair allocation.+ skipFast k s =+ case BS.elemIndex sep s of+ Nothing -> if k == 0 then s else BS.empty+ Just i ->+ if k == 0+ then BS.take i s+ else skipFast (k - 1) (BS.drop (i + 1) s) -lineEnd :: Parser ()-lineEnd =- (endOfLine <|> endOfInput)- <?> "end of line"-{-# INLINE lineEnd #-}+ -- Slow path: quote-aware scan.+ quoteChar = 0x22 :: Word8+ len = BS.length bs+ go !idx !start !inQ !pos+ | pos >= len =+ if idx == targetIdx then extract start pos else BS.empty+ | otherwise =+ let c = BS.index bs pos+ in if c == quoteChar+ then go idx start (not inQ) (pos + 1)+ else+ if c == sep && not inQ+ then+ if idx == targetIdx+ then extract start pos+ else go (idx + 1) (pos + 1) False (pos + 1)+ else go idx start inQ (pos + 1) --- | First pass to count rows for exact allocation-countRows :: Char -> FilePath -> IO Int-countRows c path = withFile path ReadMode $! go 0 ""- where- go n input h = do- isEOF <- hIsEOF h- if isEOF && input == mempty- then pure n- else- parseWith (TIO.hGetChunk h) (parseRow c) input >>= \case- Fail unconsumed ctx er -> do- erpos <- hTell h- fail $- "Failed to parse CSV file around "- <> show erpos- <> " byte; due: "- <> show er- <> "; context: "- <> show ctx- <> " "- <> show unconsumed- Partial c -> do- fail $ "Partial handler is called; n = " <> show n- Done (unconsumed :: T.Text) _ ->- go (n + 1) unconsumed h-{-# INLINE countRows #-}+ extract s e =+ let field = BS.take (e - s) (BS.drop s bs)+ in if BS.length field >= 2+ && BS.head field == quoteChar+ && BS.last field == quoteChar+ then BS.init (BS.tail field)+ else field+{-# INLINE getNthFieldBs #-} -writeCsv :: FilePath -> DataFrame -> IO ()-writeCsv = writeSeparated ','+-- | Allocate a fresh 'MutableColumn' for @n@ slots based on a 'SchemaType'.+makeCol :: Int -> SchemaType -> IO MutableColumn+makeCol n (SType (_ :: P.Proxy a)) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> MUnboxedColumn <$> (VUM.unsafeNew n :: IO (VUM.IOVector Int))+ Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+ Just Refl -> MUnboxedColumn <$> (VUM.unsafeNew n :: IO (VUM.IOVector Double))+ Nothing -> MBoxedColumn <$> (VM.unsafeNew n :: IO (VM.IOVector T.Text)) -writeSeparated ::- -- | Separator- Char ->- -- | Path to write to- FilePath ->- DataFrame ->- IO ()-writeSeparated c filepath df = withFile filepath WriteMode $ \handle -> do- let (rows, _) = dataframeDimensions df- let headers = map fst (L.sortBy (compare `on` snd) (M.toList (columnIndices df)))- TIO.hPutStrLn handle (T.intercalate ", " headers)- forM_ [0 .. (rows - 1)] $ \i -> do- let row = getRowAsText df i- TIO.hPutStrLn handle (T.intercalate ", " row)+-- | Slice a 'MutableColumn' to @n@ elements (no-copy view).+sliceCol :: Int -> MutableColumn -> MutableColumn+sliceCol n (MBoxedColumn col) = MBoxedColumn (VM.take n col)+sliceCol n (MUnboxedColumn col) = MUnboxedColumn (VUM.take n col)+sliceCol n (MOptionalColumn col) = MOptionalColumn (VM.take n col) -getRowAsText :: DataFrame -> Int -> [T.Text]-getRowAsText df i = V.ifoldr go [] (columns df)+{- | Finds the index of the next unquoted newline (0x0A).+Fast path: uses memchr (SIMD) and falls back to a quote-aware linear scan+only if a double-quote appears before the candidate newline.+-}+findUnquotedNewline :: BS.ByteString -> Maybe Int+findUnquotedNewline bs =+ case BS.elemIndex 0x0A bs of+ Nothing -> Nothing+ Just nlPos+ -- No quote before the newline → safe to use this position.+ -- Check with elemIndex to avoid allocating a ByteString slice.+ | maybe True (>= nlPos) (BS.elemIndex 0x22 bs) -> Just nlPos+ -- Quote present → may be a newline inside a quoted field; scan carefully.+ | otherwise -> slowScan 0 False where- indexMap = M.fromList (map (\(a, b) -> (b, a)) $ M.toList (columnIndices df))- go k (BoxedColumn (c :: V.Vector a)) acc = case c V.!? i of- Just e -> textRep : acc- where- textRep = case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> e- Nothing -> case typeRep @a of- App t1 t2 -> case eqTypeRep t1 (typeRep @Maybe) of- Just HRefl -> case testEquality t2 (typeRep @T.Text) of- Just Refl -> fromMaybe "null" e- Nothing -> (fromOptional . (T.pack . show)) e- where- fromOptional s- | T.isPrefixOf "Just " s = T.drop (T.length "Just ") s- | otherwise = "null"- Nothing -> (T.pack . show) e- _ -> (T.pack . show) e- Nothing ->- error $- "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i- go k (UnboxedColumn c) acc = case c VU.!? i of- Just e -> T.pack (show e) : acc- Nothing ->- error $- "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i- go k (OptionalColumn (c :: V.Vector (Maybe a))) acc = case c V.!? i of- Just e -> case testEquality (typeRep @a) (typeRep @T.Text) of- Just Refl -> fromMaybe T.empty e : acc- Nothing -> maybe T.empty (T.pack . show) e : acc- Nothing ->- error $- "Column "- ++ T.unpack (indexMap M.! k)- ++ " has less items than "- ++ "the other columns at index "- ++ show i+ len = BS.length bs+ slowScan !pos !inQ+ | pos >= len = Nothing+ | otherwise =+ let c = BS.index bs pos+ in if c == 0x22+ then slowScan (pos + 1) (not inQ)+ else+ if c == 0x0A && not inQ+ then Just pos+ else slowScan (pos + 1) inQ+{-# INLINE findUnquotedNewline #-}
src/DataFrame/Lazy/Internal/DataFrame.hs view
@@ -1,107 +1,138 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module DataFrame.Lazy.Internal.DataFrame where -import Control.Monad (foldM)-import qualified Data.List as L import qualified Data.Text as T import qualified DataFrame.Internal.Column as C import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.Expression as E-import qualified DataFrame.Lazy.IO.CSV as D-import DataFrame.Operations.Merge ()-import qualified DataFrame.Operations.Subset as D-import qualified DataFrame.Operations.Transformations as D--data LazyOperation where- Derive :: (C.Columnable a) => T.Text -> E.Expr a -> LazyOperation- Select :: [T.Text] -> LazyOperation- Filter :: E.Expr Bool -> LazyOperation--instance Show LazyOperation where- show :: LazyOperation -> String- show (Derive name expr) = T.unpack name ++ " := " ++ show expr- show (Select columns) = "select(" ++ show columns ++ ")"- show (Filter expr) = "filter(" ++ show expr ++ ")"+import DataFrame.Internal.Schema (Schema)+import DataFrame.Lazy.Internal.Executor (+ ExecutorConfig (..),+ defaultExecutorConfig,+ execute,+ )+import DataFrame.Lazy.Internal.LogicalPlan (+ DataSource (..),+ LogicalPlan (..),+ SortOrder (..),+ )+import qualified DataFrame.Lazy.Internal.Optimizer as Opt+import DataFrame.Operations.Join (JoinType) -data InputType = ICSV deriving (Show)+{- | A lazy query that has not been executed yet. +The query is represented as a 'LogicalPlan' tree; execution is deferred+until 'runDataFrame' is called.+-} data LazyDataFrame = LazyDataFrame- { inputPath :: FilePath- , inputType :: InputType- , operations :: [LazyOperation]+ { plan :: LogicalPlan , batchSize :: Int }- deriving (Show) -eval :: LazyOperation -> D.DataFrame -> D.DataFrame-eval (Derive name expr) = D.derive name expr-eval (Select columns) = D.select columns-eval (Filter expr) = D.filterWhere expr+instance Show LazyDataFrame where+ show ldf =+ "LazyDataFrame { batchSize = "+ <> (show (batchSize ldf) <> (", plan = " <> (show (plan ldf) <> " }"))) -runDataFrame :: forall a. (C.Columnable a) => LazyDataFrame -> IO D.DataFrame-runDataFrame df = do- let path = inputPath df- totalRows <- D.countRows ',' path- let batches = batchRanges totalRows (batchSize df)- (df', _) <-- foldM- ( \(accDf, (pos, unused, r)) (start, end) -> do- mapM_- putStr- [ "Scanning: "- , show start- , " to "- , show end- , " rows out of "- , show totalRows- , "\n"- ]+-- ---------------------------------------------------------------------------+-- Entry point+-- --------------------------------------------------------------------------- - (sdf, (pos', unconsumed, rowsRead)) <-- D.readSeparated- ','- ( D.defaultOptions- { D.rowRange = Just (start, batchSize df)- , D.totalRows = Just totalRows- , D.seekPos = pos- , D.rowsRead = r- , D.leftOver = unused- }- )- path- let rdf = L.foldl' (flip eval) sdf (operations df)- return (accDf <> rdf, (Just pos', unconsumed, rowsRead + r))- )- (D.empty, (Nothing, "", 0))- batches- return df'+{- | Execute the lazy query: optimise the logical plan, then stream-execute+the resulting physical plan, returning a fully-materialised 'D.DataFrame'.+-}+runDataFrame :: LazyDataFrame -> IO D.DataFrame+runDataFrame ldf = do+ let physPlan = Opt.optimize (batchSize ldf) (plan ldf)+ execute physPlan defaultExecutorConfig{defaultBatchSize = batchSize ldf} -batchRanges :: Int -> Int -> [(Int, Int)]-batchRanges n inc = go n [0, inc .. n]- where- go _ [] = []- go n [x] = [(x, n)]- go n (f : s : rest) = (f, s) : go n (s : rest)+-- ---------------------------------------------------------------------------+-- Builders that construct the logical plan tree+-- --------------------------------------------------------------------------- -scanCsv :: T.Text -> LazyDataFrame-scanCsv path = LazyDataFrame (T.unpack path) ICSV [] 512_000+-- | Lift an already-loaded eager 'D.DataFrame' into the lazy plan.+fromDataFrame :: D.DataFrame -> LazyDataFrame+fromDataFrame df = LazyDataFrame{plan = SourceDF df, batchSize = 1_000_000} -addOperation :: LazyOperation -> LazyDataFrame -> LazyDataFrame-addOperation op df = df{operations = operations df ++ [op]}+-- | Scan a CSV file with the default comma separator.+scanCsv :: Schema -> T.Text -> LazyDataFrame+scanCsv schema path =+ LazyDataFrame+ { plan = Scan (CsvSource (T.unpack path) ',') schema+ , batchSize = 1_000_000+ } +-- | Scan a character-separated file.+scanSeparated :: Char -> Schema -> T.Text -> LazyDataFrame+scanSeparated sep schema path =+ LazyDataFrame+ { plan = Scan (CsvSource (T.unpack path) sep) schema+ , batchSize = 1_000_000+ }++-- | Scan a Parquet file, directory of files, or glob pattern.+scanParquet :: Schema -> T.Text -> LazyDataFrame+scanParquet schema path =+ LazyDataFrame+ { plan = Scan (ParquetSource (T.unpack path)) schema+ , batchSize = 1_000_000+ }++-- | Add a computed column (or overwrite an existing one). derive :: (C.Columnable a) => T.Text -> E.Expr a -> LazyDataFrame -> LazyDataFrame-derive name expr = addOperation (Derive name expr)+derive name expr ldf =+ ldf{plan = Derive name (E.UExpr expr) (plan ldf)} -select :: (C.Columnable a) => [T.Text] -> LazyDataFrame -> LazyDataFrame-select columns = addOperation (Select columns)+-- | Retain only the listed columns.+select :: [T.Text] -> LazyDataFrame -> LazyDataFrame+select cols ldf = ldf{plan = Project cols (plan ldf)} -filter :: (C.Columnable a) => E.Expr Bool -> LazyDataFrame -> LazyDataFrame-filter cond = addOperation (Filter cond)+-- | Keep rows that satisfy the predicate.+filter :: E.Expr Bool -> LazyDataFrame -> LazyDataFrame+filter cond ldf = ldf{plan = Filter cond (plan ldf)}++-- | Join two lazy queries on the given key columns.+join ::+ JoinType ->+ -- | Left join key column name+ T.Text ->+ -- | Right join key column name+ T.Text ->+ -- | Left sub-query+ LazyDataFrame ->+ -- | Right sub-query+ LazyDataFrame ->+ LazyDataFrame+join jt leftKey rightKey left right =+ LazyDataFrame+ { plan = Join jt leftKey rightKey (plan left) (plan right)+ , batchSize = batchSize left+ }++{- | Group by a set of columns and compute aggregate expressions.++Each aggregate expression should use an 'Agg' node (e.g. @sumOf@, @meanOf@).+-}+groupBy ::+ -- | Group-by key columns+ [T.Text] ->+ -- | @[(outputName, aggregateExpr)]@+ [(T.Text, E.UExpr)] ->+ LazyDataFrame ->+ LazyDataFrame+groupBy keys aggs ldf = ldf{plan = Aggregate keys aggs (plan ldf)}++-- | Sort the result by the given @(column, direction)@ pairs.+sortBy :: [(T.Text, SortOrder)] -> LazyDataFrame -> LazyDataFrame+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)}
+ src/DataFrame/Lazy/Internal/Executor.hs view
@@ -0,0 +1,543 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}++{- | Pull-based (iterator) execution engine.++Each operator returns a 'Stream' — an IO action that produces the next+'DataFrame' batch on each call and returns 'Nothing' when exhausted.+Blocking operators (Sort, HashJoin) materialise their input before producing+output. HashAggregate uses streaming partial aggregation when all aggregate+expressions support it.+-}+module DataFrame.Lazy.Internal.Executor (+ ExecutorConfig (..),+ defaultExecutorConfig,+ execute,+ foldBatches,+) where++import Control.Concurrent (forkIO)+import Control.Concurrent.STM (atomically)+import Control.Concurrent.STM.TBQueue (newTBQueueIO, readTBQueue, writeTBQueue)+import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Control.Monad (filterM, when)+import qualified Data.ByteString as BS+import Data.IORef+import qualified Data.Map as M+import qualified Data.Set as S+import qualified Data.Text as T+import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame.IO.Parquet as Parquet+import qualified DataFrame.Internal.Column as C+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.Expression as E+import DataFrame.Internal.Schema (elements)+import qualified DataFrame.Lazy.IO.Binary as Bin+import qualified DataFrame.Lazy.IO.CSV as LCSV+import DataFrame.Lazy.Internal.LogicalPlan (DataSource (..), SortOrder (..))+import DataFrame.Lazy.Internal.PhysicalPlan+import qualified DataFrame.Operations.Aggregation as Agg+import qualified DataFrame.Operations.Core as Core+import qualified DataFrame.Operations.Join as Join+import DataFrame.Operations.Merge ()+import qualified DataFrame.Operations.Permutation as Perm+import qualified DataFrame.Operations.Subset as Sub+import qualified DataFrame.Operations.Transformations as Trans+import System.Directory (doesDirectoryExist)+import System.FilePath ((</>))+import System.FilePath.Glob (glob)+import System.IO (hClose)+import Type.Reflection (typeRep)++-- ---------------------------------------------------------------------------+-- Configuration+-- ---------------------------------------------------------------------------++data ExecutorConfig = ExecutorConfig+ { memoryBudgetBytes :: !Int+ -- ^ Per-node spill threshold (currently informational; not enforced yet).+ , spillDirectory :: FilePath+ , defaultBatchSize :: !Int+ }++defaultExecutorConfig :: ExecutorConfig+defaultExecutorConfig =+ ExecutorConfig+ { memoryBudgetBytes = 512 * 1_048_576 -- 512 MiB+ , spillDirectory = "/tmp"+ , defaultBatchSize = 1_000_000+ }++-- ---------------------------------------------------------------------------+-- Stream abstraction+-- ---------------------------------------------------------------------------++{- | A pull-based stream: each call to the action yields the next batch or+'Nothing' when the stream is exhausted. State is captured by the closure.+-}+newtype Stream = Stream {pullBatch :: IO (Maybe D.DataFrame)}++-- | Drain all batches from a stream and concatenate them into one DataFrame.+collectStream :: Stream -> IO D.DataFrame+collectStream stream = go D.empty+ where+ go acc = do+ mb <- pullBatch stream+ case mb of+ Nothing -> return acc+ Just df -> go (acc <> df)++-- ---------------------------------------------------------------------------+-- Top-level entry point+-- ---------------------------------------------------------------------------++{- | Execute a physical plan, returning the complete result as a single+'DataFrame'.+-}+execute :: PhysicalPlan -> ExecutorConfig -> IO D.DataFrame+execute plan cfg = buildStream plan cfg >>= collectStream++{- | Fold a function over every batch produced by a physical plan.+The fold is strict in the accumulator; each batch is discarded after folding.+-}+foldBatches ::+ (b -> D.DataFrame -> IO b) -> b -> PhysicalPlan -> ExecutorConfig -> IO b+foldBatches f seed plan cfg = do+ stream <- buildStream plan cfg+ let loop !acc = do+ mb <- pullBatch stream+ case mb of+ Nothing -> return acc+ Just batch -> do+ !acc' <- f acc batch+ loop acc'+ loop seed++-- ---------------------------------------------------------------------------+-- Per-operator stream builders+-- ---------------------------------------------------------------------------++buildStream :: PhysicalPlan -> ExecutorConfig -> IO Stream+-- Scan -----------------------------------------------------------------------+buildStream (PhysicalScan (CsvSource path sep) cfg) _ =+ executeCsvScan path sep cfg+buildStream (PhysicalScan (ParquetSource path) cfg) _ =+ executeParquetScan path cfg+buildStream (PhysicalSpill child path) execCfg = do+ df <- execute child execCfg+ Bin.spillToDisk path df+ df' <- Bin.readSpilled path+ ref <- newIORef (Just df')+ return . Stream $+ ( do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+ )+-- Filter ---------------------------------------------------------------------+buildStream (PhysicalFilter p child) execCfg = do+ childStream <- buildStream child execCfg+ return . Stream $+ ( do+ mb <- pullBatch childStream+ return $ fmap (Sub.filterWhere p) mb+ )+-- Project --------------------------------------------------------------------+buildStream (PhysicalProject cols child) execCfg = do+ childStream <- buildStream child execCfg+ return . Stream $+ ( do+ mb <- pullBatch childStream+ return $ fmap (Sub.select cols) mb+ )+-- Derive ---------------------------------------------------------------------+buildStream (PhysicalDerive name uexpr child) execCfg = do+ childStream <- buildStream child execCfg+ return . Stream $+ ( do+ mb <- pullBatch childStream+ return $ fmap (Trans.deriveMany [(name, uexpr)]) mb+ )+-- Limit ----------------------------------------------------------------------+buildStream (PhysicalLimit n child) execCfg = do+ childStream <- buildStream child execCfg+ countRef <- newIORef (0 :: Int)+ return . Stream $+ ( do+ remaining <- readIORef countRef+ if remaining >= n+ then return Nothing+ else do+ mb <- pullBatch childStream+ case mb of+ Nothing -> return Nothing+ Just df -> do+ let toTake = min (Core.nRows df) (n - remaining)+ modifyIORef' countRef (+ toTake)+ return $ Just (Sub.take toTake df)+ )+-- Sort (blocking) ------------------------------------------------------------+buildStream (PhysicalSort cols child) execCfg = do+ df <- execute child execCfg+ let sortOrds = fmap toPermSortOrder cols+ let sorted = Perm.sortBy sortOrds df+ ref <- newIORef (Just sorted)+ return . Stream $+ ( do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+ )+-- HashAggregate --------------------------------------------------------------+buildStream (PhysicalHashAggregate keys aggs child) execCfg = do+ childStream <- buildStream child execCfg+ if all (isStreamableAgg . snd) aggs+ then do+ -- Streaming partial aggregation: O(|groups|) memory+ let (partialAggs, mergeAggs, finalizer) = buildAggPlan aggs+ accRef <- newIORef (Nothing :: Maybe D.DataFrame)+ let loop = do+ mb <- pullBatch childStream+ case mb of+ Nothing -> return ()+ Just batch -> do+ -- Force to NF so the batch DataFrame can be GC'd immediately.+ -- evaluate . force breaks the thunk chain that would otherwise+ -- keep every batch (~60 MB each) alive until the end = OOM.+ !partial <-+ evaluate . force $ Agg.aggregate partialAggs (Agg.groupBy keys batch)+ mAcc <- readIORef accRef+ !newAcc <- case mAcc of+ Nothing -> return partial+ Just acc ->+ evaluate . force $+ Agg.aggregate mergeAggs $+ Agg.groupBy keys (acc <> partial)+ writeIORef accRef (Just newAcc)+ loop+ loop+ mFinal <- fmap (fmap finalizer) (readIORef accRef)+ ref <- newIORef mFinal+ return . Stream $ do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+ else do+ -- Fallback: materialise entire child (for CollectAgg etc.)+ df <- collectStream childStream+ let result = Agg.aggregate aggs (Agg.groupBy keys df)+ ref <- newIORef (Just result)+ return . Stream $ do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+-- SourceDF (split pre-loaded DataFrame into batches) -------------------------+buildStream (PhysicalSourceDF df) execCfg = do+ let bs = defaultBatchSize execCfg+ total = Core.nRows df+ posRef <- newIORef (0 :: Int)+ return . Stream $ do+ i <- readIORef posRef+ if i >= total+ then return Nothing+ else do+ let n = min bs (total - i)+ batch = Sub.range (i, i + n) df+ writeIORef posRef (i + n)+ return (Just batch)+-- HashJoin — streaming probe (INNER/LEFT) or blocking fallback ----------------+buildStream (PhysicalHashJoin jt leftKey rightKey leftPlan rightPlan) execCfg =+ case jt of+ Join.INNER -> streamingHashJoin assembleInnerBatch+ Join.LEFT -> streamingHashJoin assembleLeftBatch+ _ -> do+ -- Blocking fallback for RIGHT / FULL_OUTER+ leftDf <- execute leftPlan execCfg+ rightDf <- execute rightPlan execCfg+ let result = performJoin jt leftKey rightKey leftDf rightDf+ ref <- newIORef (Just result)+ return . Stream $ do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+ where+ streamingHashJoin assembleFn = do+ -- Materialise build (right) side once and build the compact index.+ rightDf <- execute rightPlan execCfg+ let rightDf' =+ if leftKey == rightKey+ then rightDf+ else Core.rename rightKey leftKey rightDf+ joinKey = leftKey+ csSet = S.fromList [joinKey]+ rightHashes = Join.buildHashColumn [joinKey] rightDf'+ ci = Join.buildCompactIndex rightHashes+ -- Stream probe (left) side batch by batch.+ leftStream <- buildStream leftPlan execCfg+ return . Stream $ do+ mBatch <- pullBatch leftStream+ case mBatch of+ Nothing -> return Nothing+ Just probeBatch -> do+ let probeHashes = Join.buildHashColumn [joinKey] probeBatch+ (probeIxs, buildIxs) = Join.hashProbeKernel ci probeHashes+ return . Just $ assembleFn csSet probeBatch rightDf' probeIxs buildIxs++ assembleLeftBatch csSet probeBatch rightDf' probeIxs buildIxs =+ let batchN = Core.nRows probeBatch+ -- Mark which probe rows were matched (may have duplicates — that's fine).+ matched =+ VU.accumulate+ (\_ b -> b)+ (VU.replicate batchN False)+ (VU.map (,True) probeIxs)+ unmatchedIxs = VU.findIndices not matched+ allProbeIxs = probeIxs VU.++ unmatchedIxs+ allBuildIxs = buildIxs VU.++ VU.replicate (VU.length unmatchedIxs) (-1)+ in Join.assembleLeft csSet probeBatch rightDf' allProbeIxs allBuildIxs++ assembleInnerBatch = Join.assembleInner++-- SortMergeJoin (blocking on both sides) -------------------------------------+buildStream (PhysicalSortMergeJoin jt leftKey rightKey leftPlan rightPlan) execCfg = do+ leftDf <- execute leftPlan execCfg+ rightDf <- execute rightPlan execCfg+ let result = performJoin jt leftKey rightKey leftDf rightDf+ ref <- newIORef (Just result)+ return . Stream $+ ( do+ mb <- readIORef ref+ writeIORef ref Nothing+ return mb+ )++-- ---------------------------------------------------------------------------+-- Streaming aggregation helpers+-- ---------------------------------------------------------------------------++{- | True when an aggregate expression can be computed incrementally+(i.e., partial results can be merged without materialising all rows).+-}+isStreamableAgg :: E.UExpr -> Bool+isStreamableAgg (E.UExpr (E.Agg (E.CollectAgg _ _) _)) = False+isStreamableAgg (E.UExpr (E.Agg (E.FoldAgg _ Nothing (_ :: a -> b -> a)) _)) =+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> True -- self-merging: min, max, sum+ Nothing -> False+isStreamableAgg (E.UExpr (E.Agg (E.FoldAgg _ (Just _) (_ :: a -> b -> a)) _)) =+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl -> True -- seeded Int fold (old-style count): merge by sum+ Nothing ->+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl -> True -- seeded self-merging+ Nothing -> False+isStreamableAgg (E.UExpr (E.Agg (E.MergeAgg{}) _)) = True+isStreamableAgg _ = False++{- | Build the partial, merge, and finalizer plan for a list of streamable+aggregate expressions.++* @partialAggs@ — applied per batch, producing one row per group+* @mergeAggs@ — applied when combining two partial-result DataFrames+* @finalizer@ — post-process after all batches (needed for 'MergeAgg'+ where the accumulator type differs from the output type)+-}+buildAggPlan ::+ [(T.Text, E.UExpr)] ->+ ( [(T.Text, E.UExpr)]+ , [(T.Text, E.UExpr)]+ , D.DataFrame -> D.DataFrame+ )+buildAggPlan aggs = foldl combine ([], [], id) (map processAgg aggs)+ where+ combine (p1, m1, f1) (p2, m2, f2) = (p1 ++ p2, m1 ++ m2, f1 . f2)++ processAgg ::+ (T.Text, E.UExpr) ->+ ([(T.Text, E.UExpr)], [(T.Text, E.UExpr)], D.DataFrame -> D.DataFrame)+ processAgg (name, ue) = case ue of+ -- Seedless FoldAgg: min, max, sum (self-merging when a = b)+ E.UExpr (E.Agg (E.FoldAgg n Nothing (f :: a -> b -> a)) (_ :: E.Expr b)) ->+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl ->+ ( [(name, ue)]+ , [(name, E.UExpr (E.Agg (E.FoldAgg n Nothing f) (E.Col @a name)))]+ , id+ )+ Nothing ->+ -- a /= b but a = Int: merge by sum (backward compat)+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl ->+ ( [(name, ue)]+ ,+ [+ ( name+ , E.UExpr+ (E.Agg (E.FoldAgg "sum" Nothing ((+) :: Int -> Int -> Int)) (E.Col @Int name))+ )+ ]+ , id+ )+ Nothing -> ([(name, ue)], [(name, ue)], id)+ -- Seeded FoldAgg: old-style count (a = Int)+ E.UExpr (E.Agg (E.FoldAgg n (Just _) (f :: a -> b -> a)) (_ :: E.Expr b)) ->+ case testEquality (typeRep @a) (typeRep @Int) of+ Just Refl ->+ ( [(name, ue)]+ ,+ [+ ( name+ , E.UExpr+ (E.Agg (E.FoldAgg "sum" Nothing ((+) :: Int -> Int -> Int)) (E.Col @Int name))+ )+ ]+ , id+ )+ Nothing ->+ case testEquality (typeRep @a) (typeRep @b) of+ Just Refl ->+ ( [(name, ue)]+ , [(name, E.UExpr (E.Agg (E.FoldAgg n Nothing f) (E.Col @a name)))]+ , id+ )+ Nothing -> ([(name, ue)], [(name, ue)], id)+ -- MergeAgg: count, mean, etc.+ -- Partial step: accumulate into acc type (using id as finalizer).+ -- Merge step: apply merge function to two acc-typed partial results.+ -- Finalizer: apply fin to convert acc column to output type.+ E.UExpr+ ( E.Agg+ ( E.MergeAgg+ n+ seed+ (step :: acc -> b -> acc)+ (merge :: acc -> acc -> acc)+ (fin :: acc -> a)+ )+ (inner :: E.Expr b)+ ) ->+ let partialExpr =+ E.UExpr+ ( E.Agg+ (E.MergeAgg n seed step merge (id :: acc -> acc))+ inner+ )+ mergeExpr =+ E.UExpr+ ( E.Agg+ (E.FoldAgg ("merge_" <> n) Nothing merge)+ (E.Col @acc name)+ )+ finalize df =+ let accCol = D.unsafeGetColumn name df+ finalCol =+ either+ (error "buildAggPlan: MergeAgg finalize failed")+ id+ (C.mapColumn @acc @a fin accCol)+ in Core.insertColumn name finalCol df+ in ( [(name, partialExpr)]+ , [(name, mergeExpr)]+ , finalize+ )+ _ -> ([(name, ue)], [(name, ue)], id)++-- ---------------------------------------------------------------------------+-- Parquet scan implementation+-- ---------------------------------------------------------------------------++{- | Scan a Parquet file, directory, or glob. Each file becomes one batch.+Column projection and predicate pushdown are forwarded to 'readParquetWithOpts'+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)+ 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++-- ---------------------------------------------------------------------------+-- CSV scan implementation+-- ---------------------------------------------------------------------------++{- | CSV scan with pipeline parallelism: a dedicated reader thread fills a+bounded queue while the caller's thread applies pushdown predicates and+delivers batches to the rest of the pipeline. The queue depth of 8 keeps+at most eight raw batches in flight, bounding memory while hiding I/O latency.+-}+executeCsvScan :: FilePath -> Char -> ScanConfig -> IO Stream+executeCsvScan path sep cfg = do+ (handle, colSpec) <- LCSV.openCsvStream sep (scanSchema cfg) path+ -- Queue carries raw batches; Nothing is the end-of-stream sentinel.+ -- Depth 2: each batch holds ~60 MB (1M Text + Double columns); 8 would be ~480 MB.+ queue <- newTBQueueIO 2+ _ <- forkIO $ do+ let loop lo = do+ result <- LCSV.readBatch sep colSpec (scanBatchSize cfg) lo handle+ case result of+ Nothing ->+ hClose handle >> atomically (writeTBQueue queue Nothing)+ Just (df, lo') ->+ atomically (writeTBQueue queue (Just df)) >> loop lo'+ loop BS.empty+ return . Stream $+ ( do+ mb <- atomically (readTBQueue queue)+ case mb of+ -- Re-insert the sentinel so repeated pulls after EOF stay Nothing.+ Nothing -> atomically (writeTBQueue queue Nothing) >> return Nothing+ Just df ->+ let df' = case scanPushdownPredicate cfg of+ Nothing -> df+ Just p -> Sub.filterWhere p df+ in return (Just df')+ )++-- ---------------------------------------------------------------------------+-- Join helper+-- ---------------------------------------------------------------------------++{- | Route join to the existing Operations.Join implementation.+When the left and right key names differ, rename the right key before joining.+-}+performJoin ::+ Join.JoinType -> T.Text -> T.Text -> D.DataFrame -> D.DataFrame -> D.DataFrame+performJoin jt leftKey rightKey leftDf rightDf =+ if leftKey == rightKey+ then Join.join jt [leftKey] rightDf leftDf+ else+ let rightRenamed = Core.rename rightKey leftKey rightDf+ in Join.join jt [leftKey] rightRenamed leftDf++-- ---------------------------------------------------------------------------+-- Sort order conversion+-- ---------------------------------------------------------------------------++-- | Convert plan-level sort order to the Permutation module's SortOrder.+toPermSortOrder :: (T.Text, SortOrder) -> Perm.SortOrder+toPermSortOrder (col, Ascending) = Perm.Asc (E.Col @T.Text col)+toPermSortOrder (col, Descending) = Perm.Desc (E.Col @T.Text col)
+ src/DataFrame/Lazy/Internal/LogicalPlan.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE GADTs #-}++module DataFrame.Lazy.Internal.LogicalPlan where++import qualified Data.Text as T+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.Expression as E+import DataFrame.Internal.Schema (Schema)+import DataFrame.Operations.Join (JoinType)++-- | Data source for a scan node.+data DataSource+ = -- | path, separator+ CsvSource FilePath Char+ | ParquetSource FilePath+ deriving (Show)++-- | Sort direction used in Sort nodes and the public API.+data SortOrder = Ascending | Descending+ deriving (Show, Eq, Ord)++{- | Relational-algebra tree that represents what the query computes.+No physical decisions (batch size, join strategy) are made here.+-}+data LogicalPlan+ = -- | Read columns described by the schema from a source.+ Scan DataSource Schema+ | -- | Retain only the listed columns.+ Project [T.Text] LogicalPlan+ | -- | Keep rows matching the predicate.+ Filter (E.Expr Bool) LogicalPlan+ | -- | Add or overwrite a column via an expression.+ Derive T.Text E.UExpr LogicalPlan+ | -- | Join two sub-plans on the given key columns.+ Join JoinType T.Text T.Text LogicalPlan LogicalPlan+ | -- | Group then aggregate.+ Aggregate [T.Text] [(T.Text, E.UExpr)] LogicalPlan+ | -- | Sort by a list of (column, direction) pairs.+ Sort [(T.Text, SortOrder)] LogicalPlan+ | -- | Retain at most N rows.+ Limit Int LogicalPlan+ | -- | Lift an already-loaded DataFrame into the lazy plan.+ SourceDF D.DataFrame+ deriving (Show)
+ src/DataFrame/Lazy/Internal/Optimizer.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE OverloadedStrings #-}++module DataFrame.Lazy.Internal.Optimizer (optimize) where++import qualified Data.Map as M+import qualified Data.Set as S+import qualified Data.Text as T+import qualified DataFrame.Internal.Expression as E+import DataFrame.Internal.Schema (Schema (..), elements)+import DataFrame.Lazy.Internal.LogicalPlan+import DataFrame.Lazy.Internal.PhysicalPlan++{- | Optimise a logical plan and lower it to a physical plan.++Rules applied bottom-up (in order):+ 1. Filter fusion — merge consecutive Filter nodes into a conjunction+ 2. Predicate pushdown — move Filter past Derive/Project toward Scan+ 3. Dead column elim — drop Derive nodes whose output is never referenced++After rule application @toPhysical@ selects concrete operators.+-}+optimize :: Int -> LogicalPlan -> PhysicalPlan+optimize batchSz =+ toPhysical batchSz+ . eliminateDeadColumns+ . pushPredicates+ . fuseFilters++-- ---------------------------------------------------------------------------+-- Rule 1: Filter fusion+-- ---------------------------------------------------------------------------++-- | Merge @Filter p1 (Filter p2 child)@ into @Filter (p1 && p2) child@.+fuseFilters :: LogicalPlan -> LogicalPlan+fuseFilters (Filter p1 (Filter p2 child)) =+ fuseFilters (Filter (andExpr p1 p2) (fuseFilters child))+fuseFilters (Filter p child) = Filter p (fuseFilters child)+fuseFilters (Project cols child) = Project cols (fuseFilters child)+fuseFilters (Derive name expr child) = Derive name expr (fuseFilters child)+fuseFilters (Join jt l r left right) =+ Join jt l r (fuseFilters left) (fuseFilters right)+fuseFilters (Aggregate keys aggs child) =+ Aggregate keys aggs (fuseFilters child)+fuseFilters (Sort cols child) = Sort cols (fuseFilters child)+fuseFilters (Limit n child) = Limit n (fuseFilters child)+fuseFilters leaf = leaf++-- | Logical AND of two @Bool@ expressions.+andExpr :: E.Expr Bool -> E.Expr Bool -> E.Expr Bool+andExpr =+ E.Binary+ ( E.MkBinaryOp+ { E.binaryFn = (&&)+ , E.binaryName = "and"+ , E.binarySymbol = Just "&&"+ , E.binaryCommutative = True+ , E.binaryPrecedence = 3+ }+ )++-- ---------------------------------------------------------------------------+-- Rule 2: Predicate pushdown+-- ---------------------------------------------------------------------------++{- | Push Filter nodes as close to the Scan as possible.++* Past a @Derive@ when the predicate doesn't reference the derived column.+* Past a @Project@ when all predicate columns are in the projected set.+* Into @ScanConfig.scanPushdownPredicate@ when the child is a @Scan@.+-}+pushPredicates :: LogicalPlan -> LogicalPlan+pushPredicates (Filter p (Derive name expr child))+ | name `notElem` E.getColumns p =+ Derive name expr (pushPredicates (Filter p child))+ | otherwise =+ Filter p (Derive name expr (pushPredicates child))+pushPredicates (Filter p (Project cols child))+ | all (`elem` cols) (E.getColumns p) =+ Project cols (pushPredicates (Filter p child))+ | otherwise =+ Filter p (Project cols (pushPredicates child))+pushPredicates (Filter p child) = Filter p (pushPredicates child)+pushPredicates (Project cols child) = Project cols (pushPredicates child)+pushPredicates (Derive name expr child) = Derive name expr (pushPredicates child)+pushPredicates (Join jt l r left right) =+ Join jt l r (pushPredicates left) (pushPredicates right)+pushPredicates (Aggregate keys aggs child) =+ Aggregate keys aggs (pushPredicates child)+pushPredicates (Sort cols child) = Sort cols (pushPredicates child)+pushPredicates (Limit n child) = Limit n (pushPredicates child)+pushPredicates leaf = leaf++-- ---------------------------------------------------------------------------+-- Rule 3: Dead column elimination+-- ---------------------------------------------------------------------------++{- | Collect every column name that is explicitly referenced somewhere in the+plan (in filter predicates, sort keys, aggregate keys, projection lists,+join keys, and derived expressions). Returns Nothing when "all columns+are needed" (i.e. no Project restricts the output).+-}+referencedCols :: LogicalPlan -> Maybe (S.Set T.Text)+referencedCols (Scan _ schema) = Just (S.fromList (M.keys (elements schema)))+referencedCols (Project cols _) = Just (S.fromList cols)+referencedCols (Filter p child) =+ fmap (S.union (S.fromList (E.getColumns p))) (referencedCols child)+referencedCols (Derive _ expr child) =+ fmap (S.union (S.fromList (uExprCols expr))) (referencedCols child)+referencedCols (Join _ l r left right) =+ let keySet = S.fromList [l, r]+ lRef = fmap (S.union keySet) (referencedCols left)+ rRef = fmap (S.union keySet) (referencedCols right)+ in liftMaybe2 S.union lRef rRef+referencedCols (Aggregate keys aggs child) =+ let aggCols = S.fromList (keys <> concatMap (uExprCols . snd) aggs)+ in fmap (S.union aggCols) (referencedCols child)+referencedCols (Sort cols child) =+ fmap (S.union (S.fromList (fmap fst cols))) (referencedCols child)+referencedCols (Limit _ child) = referencedCols child+referencedCols (SourceDF _) = Nothing++liftMaybe2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c+liftMaybe2 f (Just a) (Just b) = Just (f a b)+liftMaybe2 _ _ _ = Nothing++uExprCols :: E.UExpr -> [T.Text]+uExprCols (E.UExpr expr) = E.getColumns expr++-- | Drop @Derive@ nodes whose output column is never consumed downstream.+eliminateDeadColumns :: LogicalPlan -> LogicalPlan+eliminateDeadColumns plan = go (referencedCols plan) plan+ where+ go needed (Derive name expr child) =+ case needed of+ Nothing -> Derive name expr (go needed child)+ Just cols+ | name `S.notMember` cols -> go needed child+ | otherwise ->+ Derive+ name+ expr+ (go (Just (S.union cols (S.fromList (uExprCols expr)))) child)+ go needed (Filter p child) =+ Filter p (go (fmap (S.union (S.fromList (E.getColumns p))) needed) child)+ go needed (Project cols child) =+ Project cols (go (Just (S.fromList cols)) child)+ go needed (Join jt l r left right) =+ let keySet = fmap (S.union (S.fromList [l, r])) needed+ in Join jt l r (go keySet left) (go keySet right)+ go needed (Aggregate keys aggs child) =+ let aggCols = fmap (S.union (S.fromList (keys <> concatMap (uExprCols . snd) aggs))) needed+ in Aggregate keys aggs (go aggCols child)+ go needed (Sort cols child) =+ Sort cols (go (fmap (S.union (S.fromList (fmap fst cols))) needed) child)+ go needed (Limit n child) = Limit n (go needed child)+ go needed (Scan ds schema) =+ case needed of+ Nothing -> Scan ds schema+ Just cols ->+ Scan ds (Schema (M.filterWithKey (\k _ -> k `S.member` cols) (elements schema)))+ go _ (SourceDF df) = SourceDF df++-- ---------------------------------------------------------------------------+-- Logical → Physical lowering+-- ---------------------------------------------------------------------------++{- | Lower the (already-optimised) logical plan to a physical plan.++Join strategy: always HashJoin (the executor can fall back to SortMerge+at runtime once statistics are available).+-}+toPhysical :: Int -> LogicalPlan -> PhysicalPlan+-- Special case: Filter directly on a Scan → push into ScanConfig.+toPhysical batchSz (Filter p (Scan (CsvSource path sep) schema)) =+ PhysicalScan+ (CsvSource path sep)+ (ScanConfig batchSz sep schema (Just p))+toPhysical batchSz (Scan (CsvSource path sep) schema) =+ PhysicalScan+ (CsvSource path sep)+ (ScanConfig batchSz sep schema Nothing)+toPhysical batchSz (Filter p (Scan (ParquetSource path) schema)) =+ PhysicalScan+ (ParquetSource path)+ (ScanConfig batchSz ',' schema (Just p))+toPhysical batchSz (Scan (ParquetSource path) schema) =+ PhysicalScan+ (ParquetSource path)+ (ScanConfig batchSz ',' schema Nothing)+toPhysical batchSz (Project cols child) =+ PhysicalProject cols (toPhysical batchSz child)+toPhysical batchSz (Filter p child) =+ PhysicalFilter p (toPhysical batchSz child)+toPhysical batchSz (Derive name expr child) =+ PhysicalDerive name expr (toPhysical batchSz child)+toPhysical batchSz (Join jt l r left right) =+ PhysicalHashJoin+ jt+ l+ r+ (toPhysical batchSz left)+ (toPhysical batchSz right)+toPhysical batchSz (Aggregate keys aggs child) =+ PhysicalHashAggregate keys aggs (toPhysical batchSz child)+toPhysical batchSz (Sort cols child) =+ PhysicalSort cols (toPhysical batchSz child)+toPhysical batchSz (Limit n child) =+ PhysicalLimit n (toPhysical batchSz child)+toPhysical _ (SourceDF df) = PhysicalSourceDF df
+ src/DataFrame/Lazy/Internal/PhysicalPlan.hs view
@@ -0,0 +1,36 @@+module DataFrame.Lazy.Internal.PhysicalPlan where++import qualified Data.Text as T+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Internal.Expression as E+import DataFrame.Internal.Schema (Schema)+import DataFrame.Lazy.Internal.LogicalPlan (DataSource, SortOrder)+import DataFrame.Operations.Join (JoinType)++-- | Scan-level configuration: batch size, separator, optional pushdowns.+data ScanConfig = ScanConfig+ { scanBatchSize :: !Int+ , scanSeparator :: !Char+ , scanSchema :: !Schema+ , scanPushdownPredicate :: !(Maybe (E.Expr Bool))+ }+ deriving (Show)++{- | Physical plan: every node carries enough information for the executor+to allocate resources and choose algorithms without further analysis.+-}+data PhysicalPlan+ = PhysicalScan DataSource ScanConfig+ | PhysicalProject [T.Text] PhysicalPlan+ | PhysicalFilter (E.Expr Bool) PhysicalPlan+ | PhysicalDerive T.Text E.UExpr PhysicalPlan+ | PhysicalHashJoin JoinType T.Text T.Text PhysicalPlan PhysicalPlan+ | PhysicalSortMergeJoin JoinType T.Text T.Text PhysicalPlan PhysicalPlan+ | PhysicalHashAggregate [T.Text] [(T.Text, E.UExpr)] PhysicalPlan+ | PhysicalSort [(T.Text, SortOrder)] PhysicalPlan+ | PhysicalLimit Int PhysicalPlan+ | -- | Materialize child to a binary file on disk (used for build sides).+ PhysicalSpill PhysicalPlan FilePath+ | -- | Emit an already-loaded DataFrame as a stream of batches.+ PhysicalSourceDF D.DataFrame+ deriving (Show)
src/DataFrame/Operations/Aggregation.hs view
@@ -59,12 +59,17 @@ names VU.empty (VU.fromList [0])+ VU.empty | otherwise =- Grouped- df- names- (VU.map fst valueIndices)- (changingPoints valueIndices)+ let !vis = VU.map fst valueIndices+ !os = changingPoints valueIndices+ !n = fst (dimensions df)+ in Grouped+ df+ names+ vis+ os+ (buildRowToGroup n vis os) where indicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` names) (columnIndices df) doubleToInt :: Double -> Int@@ -159,6 +164,21 @@ VA.sortBy numPasses bucketSize radixFunc mv VU.unsafeFreeze mv +{- | Build the rowToGroup lookup vector from valueIndices and offsets.+rowToGroup[i] = k means row i belongs to group k.+-}+buildRowToGroup :: Int -> VU.Vector Int -> VU.Vector Int -> VU.Vector Int+buildRowToGroup n vis os = runST $ do+ rtg <- VUM.new n+ let nGroups = VU.length os - 1+ forM_ [0 .. nGroups - 1] $ \k ->+ let s = VU.unsafeIndex os k+ e = VU.unsafeIndex os (k + 1)+ in forM_ [s .. e - 1] $ \i ->+ VUM.unsafeWrite rtg (VU.unsafeIndex vis i) k+ VU.unsafeFreeze rtg+{-# NOINLINE buildRowToGroup #-}+ changingPoints :: VU.Vector (Int, Int) -> VU.Vector Int changingPoints vs = VU.reverse@@ -260,7 +280,7 @@ All ungrouped columns will be dropped. -} aggregate :: [NamedExpr] -> GroupedDataFrame -> DataFrame-aggregate aggs gdf@(Grouped df groupingColumns valueIndices offsets) =+aggregate aggs gdf@(Grouped df groupingColumns valueIndices offsets _rowToGroup) = let df' = selectIndices@@ -289,4 +309,4 @@ distinct :: DataFrame -> DataFrame distinct df = selectIndices (VU.map (indices VU.!) (VU.init os)) df where- (Grouped _ _ indices os) = groupBy (columnNames df) df+ (Grouped _ _ indices os _rtg) = groupBy (columnNames df) df
src/DataFrame/Operations/Join.hs view
@@ -10,7 +10,7 @@ module DataFrame.Operations.Join where import Control.Applicative ((<|>))-import Control.Monad (forM_)+import Control.Monad (forM_, when) import Control.Monad.ST (ST, runST) import qualified Data.HashMap.Strict as HM import Data.List (foldl')@@ -36,6 +36,7 @@ | LEFT | RIGHT | FULL_OUTER+ deriving (Show) -- | Join two dataframes using SQL join semantics. join ::@@ -72,12 +73,7 @@ buildCompactIndex :: VU.Vector Int -> CompactIndex buildCompactIndex hashes = let n = VU.length hashes- sorted = runST $ do- mv <- VU.thaw (VU.zip hashes (VU.enumFromN 0 n))- VA.sortBy (\(h1, _) (h2, _) -> compare h1 h2) mv- VU.unsafeFreeze mv- sortedHashes = VU.map fst sorted- sortedIndices = VU.map snd sorted+ (sortedHashes, sortedIndices) = sortWithIndices hashes !offsets = buildOffsets sortedHashes n 0 HM.empty in CompactIndex sortedIndices offsets where@@ -102,15 +98,19 @@ | otherwise = j {-# INLINE findGroupEnd #-} --- | Sort a hash vector, returning sorted hashes and corresponding original indices.+{- | Sort a hash vector, returning sorted hashes and corresponding original indices.+Sorts an index array using hash values as the comparison key, avoiding the+intermediate pair vector used by the naive zip-then-sort approach.+-} sortWithIndices :: VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortWithIndices hashes =+sortWithIndices hashes = runST $ do let n = VU.length hashes- sorted = runST $ do- mv <- VU.thaw (VU.zip hashes (VU.enumFromN 0 n))- VA.sortBy (\(h1, _) (h2, _) -> compare h1 h2) mv- VU.unsafeFreeze mv- in (VU.map fst sorted, VU.map snd sorted)+ mv <- VU.thaw (VU.enumFromN 0 n)+ VA.sortBy+ (\i j -> compare (hashes `VU.unsafeIndex` i) (hashes `VU.unsafeIndex` j))+ mv+ sortedIdxs <- VU.unsafeFreeze mv+ return (VU.unsafeBackpermute hashes sortedIdxs, sortedIdxs) -- | Write the cross product of two index ranges into mutable vectors. fillCrossProduct ::@@ -170,126 +170,284 @@ @ -} innerJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-innerJoin cs right left =- let csSet = S.fromList cs- leftRows = fst (D.dimensions left)- rightRows = fst (D.dimensions right)+innerJoin cs left right+ | D.null right || D.null left = D.empty+ | otherwise =+ let+ csSet = S.fromList cs+ leftRows = fst (D.dimensions left)+ rightRows = fst (D.dimensions right) - leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right+ leftKeyIdxs = keyColIndices csSet left+ rightKeyIdxs = keyColIndices csSet right+ leftHashes = D.computeRowHashes leftKeyIdxs left+ rightHashes = D.computeRowHashes rightKeyIdxs right - buildRows = min leftRows rightRows- (leftIxs, rightIxs)- | buildRows > joinStrategyThreshold =- sortMergeInnerKernel leftHashes rightHashes- | rightRows <= leftRows =- -- Build on right (smaller or equal), probe with left- hashInnerKernel leftHashes rightHashes- | otherwise =- -- Build on left (smaller), probe with right, swap result- let (!rIxs, !lIxs) = hashInnerKernel rightHashes leftHashes- in (lIxs, rIxs)- in assembleInner csSet left right leftIxs rightIxs+ buildRows = min leftRows rightRows+ (leftIxs, rightIxs)+ | buildRows > joinStrategyThreshold =+ sortMergeInnerKernel leftHashes rightHashes+ | rightRows <= leftRows =+ -- Build on right (smaller or equal), probe with left+ hashInnerKernel leftHashes rightHashes+ | otherwise =+ -- Build on left (smaller), probe with right, swap result+ let (!rIxs, !lIxs) = hashInnerKernel rightHashes leftHashes+ in (lIxs, rIxs)+ in+ assembleInner csSet left right leftIxs rightIxs +-- | Compute hashes for the given key column names in a DataFrame.+buildHashColumn :: [T.Text] -> DataFrame -> VU.Vector Int+buildHashColumn keys df =+ let csSet = S.fromList keys+ keyIdxs = keyColIndices csSet df+ in D.computeRowHashes keyIdxs df++{- | Probe one batch of rows against a pre-built 'CompactIndex'.+Returns @(probeExpandedIxs, buildExpandedIxs)@.+Unlike 'hashInnerKernel', does not build the index (it is pre-built once)+and has no cross-product row guard — the caller controls probe batch size.+-}+hashProbeKernel ::+ -- | Built once from the full right\/build side.+ CompactIndex ->+ -- | Probe hashes (one batch).+ VU.Vector Int ->+ (VU.Vector Int, VU.Vector Int)+hashProbeKernel ci probeHashes =+ let ciIxs = ciSortedIndices ci+ ciOff = ciOffsets ci+ (pFrozen, bFrozen) = runST $ do+ let !probeN = VU.length probeHashes+ initCap = max 1 (min probeN 1_000_000)++ initPv <- VUM.unsafeNew initCap+ initBv <- VUM.unsafeNew initCap+ pvRef <- newSTRef initPv+ bvRef <- newSTRef initBv+ capRef <- newSTRef initCap+ posRef <- newSTRef (0 :: Int)++ let ensureCapacity needed = do+ cap <- readSTRef capRef+ when (needed > cap) $ do+ let newCap = max needed (cap * 2)+ delta = newCap - cap+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ newPv <- VUM.unsafeGrow pv delta+ newBv <- VUM.unsafeGrow bv delta+ writeSTRef pvRef newPv+ writeSTRef bvRef newBv+ writeSTRef capRef newCap++ go !i+ | i >= probeN = return ()+ | otherwise = do+ let !h = probeHashes `VU.unsafeIndex` i+ case HM.lookup h ciOff of+ Nothing -> go (i + 1)+ Just (!start, !len) -> do+ !p <- readSTRef posRef+ ensureCapacity (p + len)+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ fillBuild i start len p 0 pv bv+ writeSTRef posRef (p + len)+ go (i + 1)+ fillBuild !probeIdx !start !len !p !j !pv !bv+ | j >= len = return ()+ | otherwise = do+ VUM.unsafeWrite pv (p + j) probeIdx+ VUM.unsafeWrite bv (p + j) (ciIxs `VU.unsafeIndex` (start + j))+ fillBuild probeIdx start len p (j + 1) pv bv+ go 0++ !total <- readSTRef posRef+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ (,)+ <$> VU.unsafeFreeze (VUM.slice 0 total pv)+ <*> VU.unsafeFreeze (VUM.slice 0 total bv)+ in (VU.force pFrozen, VU.force bFrozen)+ {- | Hash-based inner join kernel. Builds compact index on @buildHashes@ (second arg), probes with @probeHashes@ (first arg). Returns @(probeExpandedIndices, buildExpandedIndices)@.+Uses a dynamically growing output buffer to avoid pre-allocating the full+cross-product size (which can be astronomically large for low-cardinality keys). -}++{- | Maximum number of output rows allowed from a join kernel.+Exceeding this limit indicates a cross-product explosion (e.g. low-cardinality keys).+-}+maxJoinOutputRows :: Int+maxJoinOutputRows = 500_000_000+ hashInnerKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-hashInnerKernel probeHashes buildHashes = runST $ do- let ci = buildCompactIndex buildHashes- ciIxs = ciSortedIndices ci- ciOff = ciOffsets ci- !probeN = VU.length probeHashes+hashInnerKernel probeHashes buildHashes =+ let (pFrozen, bFrozen) = runST $ do+ let ci = buildCompactIndex buildHashes+ ciIxs = ciSortedIndices ci+ ciOff = ciOffsets ci+ !probeN = VU.length probeHashes+ !buildN = VU.length buildHashes+ initCap = max 1 (min (probeN + buildN) 1_000_000) - -- Pass 1: count total output rows- let !totalRows =- VU.foldl'- ( \acc h ->- case HM.lookup h ciOff of- Nothing -> acc- Just (_, len) -> acc + len- )- 0- probeHashes+ initPv <- VUM.unsafeNew initCap+ initBv <- VUM.unsafeNew initCap+ pvRef <- newSTRef initPv+ bvRef <- newSTRef initBv+ capRef <- newSTRef initCap+ posRef <- newSTRef (0 :: Int) - -- Pass 2: fill output vectors- pv <- VUM.unsafeNew totalRows- bv <- VUM.unsafeNew totalRows- posRef <- newSTRef (0 :: Int)+ let ensureCapacity needed = do+ cap <- readSTRef capRef+ when (needed > cap) $ do+ let newCap = max needed (cap * 2)+ delta = newCap - cap+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ newPv <- VUM.unsafeGrow pv delta+ newBv <- VUM.unsafeGrow bv delta+ writeSTRef pvRef newPv+ writeSTRef bvRef newBv+ writeSTRef capRef newCap - let go !i- | i >= probeN = return ()- | otherwise = do- let !h = probeHashes `VU.unsafeIndex` i- case HM.lookup h ciOff of- Nothing -> go (i + 1)- Just (!start, !len) -> do- !p <- readSTRef posRef- fillBuild i start len p 0- writeSTRef posRef (p + len)- go (i + 1)- fillBuild !probeIdx !start !len !p !j- | j >= len = return ()- | otherwise = do- VUM.unsafeWrite pv (p + j) probeIdx- VUM.unsafeWrite bv (p + j) (ciIxs `VU.unsafeIndex` (start + j))- fillBuild probeIdx start len p (j + 1)- go 0+ go !i+ | i >= probeN = return ()+ | otherwise = do+ let !h = probeHashes `VU.unsafeIndex` i+ case HM.lookup h ciOff of+ Nothing -> go (i + 1)+ Just (!start, !len) -> do+ !p <- readSTRef posRef+ when (p + len > maxJoinOutputRows) $+ error $+ "Join output would exceed "+ ++ show maxJoinOutputRows+ ++ " rows (cross-product explosion). "+ ++ "Consider filtering or using higher-cardinality join keys or using the lazy API."+ ensureCapacity (p + len)+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ fillBuild i start len p 0 pv bv+ writeSTRef posRef (p + len)+ go (i + 1)+ fillBuild !probeIdx !start !len !p !j !pv !bv+ | j >= len = return ()+ | otherwise = do+ VUM.unsafeWrite pv (p + j) probeIdx+ VUM.unsafeWrite bv (p + j) (ciIxs `VU.unsafeIndex` (start + j))+ fillBuild probeIdx start len p (j + 1) pv bv+ go 0 - (,) <$> VU.unsafeFreeze pv <*> VU.unsafeFreeze bv+ !total <- readSTRef posRef+ pv <- readSTRef pvRef+ bv <- readSTRef bvRef+ (,)+ <$> VU.unsafeFreeze (VUM.slice 0 total pv)+ <*> VU.unsafeFreeze (VUM.slice 0 total bv)+ in -- VU.force copies the slice into a compact array, releasing the oversized+ -- backing buffer allocated by the doubling strategy.+ (VU.force pFrozen, VU.force bFrozen) {- | Sort-merge inner join kernel. Sorts both sides by hash, walks in lockstep. Returns @(leftExpandedIndices, rightExpandedIndices)@.+Uses a dynamically growing output buffer instead of a two-pass count-then-allocate+strategy, which OOMs when low-cardinality keys produce large cross products. -} sortMergeInnerKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)-sortMergeInnerKernel leftHashes rightHashes = runST $ do- let (leftSH, leftSI) = sortWithIndices leftHashes- (rightSH, rightSI) = sortWithIndices rightHashes- !leftN = VU.length leftHashes- !rightN = VU.length rightHashes+sortMergeInnerKernel leftHashes rightHashes =+ let (lFrozen, rFrozen) = runST $ do+ let (leftSH, leftSI) = sortWithIndices leftHashes+ (rightSH, rightSI) = sortWithIndices rightHashes+ !leftN = VU.length leftHashes+ !rightN = VU.length rightHashes+ initCap = max 1 (min (leftN + rightN) 1_000_000) - -- Pass 1: count- let countLoop !li !ri !c- | li >= leftN || ri >= rightN = c- | lh < rh = countLoop (li + 1) ri c- | lh > rh = countLoop li (ri + 1) c- | otherwise =- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- in countLoop lEnd rEnd (c + (lEnd - li) * (rEnd - ri))- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri- !totalRows = countLoop 0 0 0+ initLv <- VUM.unsafeNew initCap+ initRv <- VUM.unsafeNew initCap+ lvRef <- newSTRef initLv+ rvRef <- newSTRef initRv+ capRef <- newSTRef initCap+ posRef <- newSTRef (0 :: Int) - -- Pass 2: fill- lv <- VUM.unsafeNew totalRows- rv <- VUM.unsafeNew totalRows+ let ensureCapacity needed = do+ cap <- readSTRef capRef+ when (needed > cap) $ do+ let newCap = max needed (cap * 2)+ delta = newCap - cap+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ newLv <- VUM.unsafeGrow lv delta+ newRv <- VUM.unsafeGrow rv delta+ writeSTRef lvRef newLv+ writeSTRef rvRef newRv+ writeSTRef capRef newCap - let fill !li !ri !pos- | li >= leftN || ri >= rightN = return ()- | lh < rh = fill (li + 1) ri pos- | lh > rh = fill li (ri + 1) pos- | otherwise = do- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- !groupSize = (lEnd - li) * (rEnd - ri)- fillCrossProduct leftSI rightSI li lEnd ri rEnd lv rv pos- fill lEnd rEnd (pos + groupSize)- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri+ fillGroup !li !lEnd !ri !rEnd = do+ let !lLen = lEnd - li+ !rLen = rEnd - ri+ !groupSize = lLen * rLen+ !p <- readSTRef posRef+ when (p + groupSize > maxJoinOutputRows) $+ error $+ "Join output would exceed "+ ++ show maxJoinOutputRows+ ++ " rows (cross-product explosion with group sizes "+ ++ show lLen+ ++ " × "+ ++ show rLen+ ++ "). Consider filtering or using higher-cardinality join keys."+ ensureCapacity (p + groupSize)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ let goL !lIdx !pos+ | lIdx >= lEnd = return ()+ | otherwise = do+ let !lOrig = leftSI `VU.unsafeIndex` lIdx+ goR lOrig ri pos+ goL (lIdx + 1) (pos + rLen)+ goR !lOrig !rIdx !pos+ | rIdx >= rEnd = return ()+ | otherwise = do+ VUM.unsafeWrite lv pos lOrig+ VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` rIdx)+ goR lOrig (rIdx + 1) (pos + 1)+ goL li p+ writeSTRef posRef (p + groupSize) - fill 0 0 0- (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv+ fill !li !ri+ | li >= leftN || ri >= rightN = return ()+ | lh < rh = fill (li + 1) ri+ | lh > rh = fill li (ri + 1)+ | otherwise = do+ let !lEnd = findGroupEnd leftSH lh (li + 1) leftN+ !rEnd = findGroupEnd rightSH rh (ri + 1) rightN+ fillGroup li lEnd ri rEnd+ fill lEnd rEnd+ where+ !lh = leftSH `VU.unsafeIndex` li+ !rh = rightSH `VU.unsafeIndex` ri + fill 0 0++ !total <- readSTRef posRef+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ (,)+ <$> VU.unsafeFreeze (VUM.slice 0 total lv)+ <*> VU.unsafeFreeze (VUM.slice 0 total rv)+ in -- VU.force copies the slice into a compact array, releasing the oversized+ -- backing buffer allocated by the doubling strategy.+ (VU.force lFrozen, VU.force rFrozen)+ -- | Assemble the result DataFrame for an inner join from expanded index vectors. assembleInner :: S.Set T.Text ->@@ -369,27 +527,34 @@ @ -} leftJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-leftJoin cs right left =- let csSet = S.fromList cs- rightRows = fst (D.dimensions right)+leftJoin cs left right+ | D.null right || D.nRows right == 0 = left+ | D.null left || D.nRows left == 0 = D.empty+ | otherwise =+ let+ csSet = S.fromList cs+ rightRows = fst (D.dimensions right) - leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right+ leftKeyIdxs = keyColIndices csSet left+ rightKeyIdxs = keyColIndices csSet right+ leftHashes = D.computeRowHashes leftKeyIdxs left+ rightHashes = D.computeRowHashes rightKeyIdxs right - -- Right is always the build side for left join- (leftIxs, rightIxs)- | rightRows > joinStrategyThreshold =- sortMergeLeftKernel leftHashes rightHashes- | otherwise =- hashLeftKernel leftHashes rightHashes- in -- rightIxs uses -1 as sentinel for "no match"- assembleLeft csSet left right leftIxs rightIxs+ -- Right is always the build side for left join+ (leftIxs, rightIxs)+ | rightRows > joinStrategyThreshold =+ sortMergeLeftKernel leftHashes rightHashes+ | otherwise =+ hashLeftKernel leftHashes rightHashes+ in+ -- rightIxs uses -1 as sentinel for "no match"+ assembleLeft csSet left right leftIxs rightIxs {- | Hash-based left join kernel. Returns @(leftExpandedIndices, rightExpandedIndices)@ where right indices use @-1@ as sentinel for unmatched rows.+Uses a dynamically growing output buffer to avoid pre-allocating the full+cross-product size (which can be astronomically large for low-cardinality keys). -} hashLeftKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)@@ -398,49 +563,68 @@ ciIxs = ciSortedIndices ci ciOff = ciOffsets ci !leftN = VU.length leftHashes-- -- Pass 1: count- let !totalRows =- VU.foldl'- ( \acc h ->- case HM.lookup h ciOff of- Nothing -> acc + 1 -- Unmatched left row still produces one output row- Just (_, len) -> acc + len- )- 0- leftHashes+ !rightN = VU.length rightHashes+ initCap = max 1 (min (leftN + rightN) 1_000_000) - -- Pass 2: fill- lv <- VUM.unsafeNew totalRows- rv <- VUM.unsafeNew totalRows+ initLv <- VUM.unsafeNew initCap+ initRv <- VUM.unsafeNew initCap+ lvRef <- newSTRef initLv+ rvRef <- newSTRef initRv+ capRef <- newSTRef initCap posRef <- newSTRef (0 :: Int) - let go !i+ let ensureCapacity needed = do+ cap <- readSTRef capRef+ when (needed > cap) $ do+ let newCap = max needed (cap * 2)+ delta = newCap - cap+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ newLv <- VUM.unsafeGrow lv delta+ newRv <- VUM.unsafeGrow rv delta+ writeSTRef lvRef newLv+ writeSTRef rvRef newRv+ writeSTRef capRef newCap++ go !i | i >= leftN = return () | otherwise = do let !h = leftHashes `VU.unsafeIndex` i !p <- readSTRef posRef case HM.lookup h ciOff of Nothing -> do+ ensureCapacity (p + 1)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef VUM.unsafeWrite lv p i VUM.unsafeWrite rv p (-1) writeSTRef posRef (p + 1) Just (!start, !len) -> do- fillBuild i start len p 0+ ensureCapacity (p + len)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ fillBuild i start len p 0 lv rv writeSTRef posRef (p + len) go (i + 1)- fillBuild !leftIdx !start !len !p !j+ fillBuild !leftIdx !start !len !p !j !lv !rv | j >= len = return () | otherwise = do VUM.unsafeWrite lv (p + j) leftIdx VUM.unsafeWrite rv (p + j) (ciIxs `VU.unsafeIndex` (start + j))- fillBuild leftIdx start len p (j + 1)+ fillBuild leftIdx start len p (j + 1) lv rv go 0 - (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv+ !total <- readSTRef posRef+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ (,)+ <$> VU.unsafeFreeze (VUM.slice 0 total lv)+ <*> VU.unsafeFreeze (VUM.slice 0 total rv) {- | Sort-merge left join kernel. Returns @(leftExpandedIndices, rightExpandedIndices)@ with @-1@ sentinel.+Uses a dynamically growing output buffer instead of a two-pass count-then-allocate+strategy, which OOMs when low-cardinality keys produce large cross products. -} sortMergeLeftKernel :: VU.Vector Int -> VU.Vector Int -> (VU.Vector Int, VU.Vector Int)@@ -449,54 +633,98 @@ (rightSH, rightSI) = sortWithIndices rightHashes !leftN = VU.length leftHashes !rightN = VU.length rightHashes+ initCap = max 1 (min (leftN + rightN) 1_000_000) - -- Pass 1: count- let countLoop !li !ri !c- | li >= leftN = c- | ri >= rightN = c + (leftN - li)- | lh < rh = countLoop (li + 1) ri (c + 1)- | lh > rh = countLoop li (ri + 1) c- | otherwise =- let !lEnd = findGroupEnd leftSH lh (li + 1) leftN- !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- in countLoop lEnd rEnd (c + (lEnd - li) * (rEnd - ri))- where- !lh = leftSH `VU.unsafeIndex` li- !rh = rightSH `VU.unsafeIndex` ri- !totalRows = countLoop 0 0 0+ initLv <- VUM.unsafeNew initCap+ initRv <- VUM.unsafeNew initCap+ lvRef <- newSTRef initLv+ rvRef <- newSTRef initRv+ capRef <- newSTRef initCap+ posRef <- newSTRef (0 :: Int) - -- Pass 2: fill- lv <- VUM.unsafeNew totalRows- rv <- VUM.unsafeNew totalRows+ let ensureCapacity needed = do+ cap <- readSTRef capRef+ when (needed > cap) $ do+ let newCap = max needed (cap * 2)+ delta = newCap - cap+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ newLv <- VUM.unsafeGrow lv delta+ newRv <- VUM.unsafeGrow rv delta+ writeSTRef lvRef newLv+ writeSTRef rvRef newRv+ writeSTRef capRef newCap - let fill !li !ri !pos+ fillGroup !li !lEnd !ri !rEnd = do+ let !lLen = lEnd - li+ !rLen = rEnd - ri+ !groupSize = lLen * rLen+ !p <- readSTRef posRef+ ensureCapacity (p + groupSize)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ let goL !lIdx !pos+ | lIdx >= lEnd = return ()+ | otherwise = do+ let !lOrig = leftSI `VU.unsafeIndex` lIdx+ goR lOrig ri pos+ goL (lIdx + 1) (pos + rLen)+ goR !lOrig !rIdx !pos+ | rIdx >= rEnd = return ()+ | otherwise = do+ VUM.unsafeWrite lv pos lOrig+ VUM.unsafeWrite rv pos (rightSI `VU.unsafeIndex` rIdx)+ goR lOrig (rIdx + 1) (pos + 1)+ goL li p+ writeSTRef posRef (p + groupSize)++ fill !li !ri | li >= leftN = return ()- | ri >= rightN = fillRemainingLeft li pos+ | ri >= rightN = fillRemainingLeft li | lh < rh = do- VUM.unsafeWrite lv pos (leftSI `VU.unsafeIndex` li)- VUM.unsafeWrite rv pos (-1)- fill (li + 1) ri (pos + 1)- | lh > rh = fill li (ri + 1) pos+ !p <- readSTRef posRef+ ensureCapacity (p + 1)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ VUM.unsafeWrite lv p (leftSI `VU.unsafeIndex` li)+ VUM.unsafeWrite rv p (-1)+ writeSTRef posRef (p + 1)+ fill (li + 1) ri+ | lh > rh = fill li (ri + 1) | otherwise = do let !lEnd = findGroupEnd leftSH lh (li + 1) leftN !rEnd = findGroupEnd rightSH rh (ri + 1) rightN- !groupSize = (lEnd - li) * (rEnd - ri)- fillCrossProduct leftSI rightSI li lEnd ri rEnd lv rv pos- fill lEnd rEnd (pos + groupSize)+ fillGroup li lEnd ri rEnd+ fill lEnd rEnd where !lh = leftSH `VU.unsafeIndex` li !rh = rightSH `VU.unsafeIndex` ri - fillRemainingLeft !i !pos- | i >= leftN = return ()- | otherwise = do- VUM.unsafeWrite lv pos (leftSI `VU.unsafeIndex` i)- VUM.unsafeWrite rv pos (-1)- fillRemainingLeft (i + 1) (pos + 1)+ fillRemainingLeft !i = do+ let !remaining = leftN - i+ when (remaining > 0) $ do+ !p <- readSTRef posRef+ ensureCapacity (p + remaining)+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ let go !j+ | j >= remaining = return ()+ | otherwise = do+ VUM.unsafeWrite lv (p + j) (leftSI `VU.unsafeIndex` (i + j))+ VUM.unsafeWrite rv (p + j) (-1)+ go (j + 1)+ go 0+ writeSTRef posRef (p + remaining) - fill 0 0 0- (,) <$> VU.unsafeFreeze lv <*> VU.unsafeFreeze rv+ fill 0 0 + !total <- readSTRef posRef+ lv <- readSTRef lvRef+ rv <- readSTRef rvRef+ (,)+ <$> VU.unsafeFreeze (VUM.slice 0 total lv)+ <*> VU.unsafeFreeze (VUM.slice 0 total rv)+ {- | Assemble the result DataFrame for a left join. Right index vectors use @-1@ sentinel, gathered via 'gatherWithSentinel'. -}@@ -574,24 +802,29 @@ fullOuterJoin :: [T.Text] -> DataFrame -> DataFrame -> DataFrame-fullOuterJoin cs right left =- let csSet = S.fromList cs- leftRows = fst (D.dimensions left)- rightRows = fst (D.dimensions right)+fullOuterJoin cs left right+ | D.null right || D.nRows right == 0 = left+ | D.null left || D.nRows left == 0 = right+ | otherwise =+ let+ csSet = S.fromList cs+ leftRows = fst (D.dimensions left)+ rightRows = fst (D.dimensions right) - leftKeyIdxs = keyColIndices csSet left- rightKeyIdxs = keyColIndices csSet right- leftHashes = D.computeRowHashes leftKeyIdxs left- rightHashes = D.computeRowHashes rightKeyIdxs right+ leftKeyIdxs = keyColIndices csSet left+ rightKeyIdxs = keyColIndices csSet right+ leftHashes = D.computeRowHashes leftKeyIdxs left+ rightHashes = D.computeRowHashes rightKeyIdxs right - -- Both sides can have nulls in full outer- (leftIxs, rightIxs)- | max leftRows rightRows > joinStrategyThreshold =- sortMergeFullOuterKernel leftHashes rightHashes- | otherwise =- hashFullOuterKernel leftHashes rightHashes- in -- Both index vectors use -1 as sentinel- assembleFullOuter csSet left right leftIxs rightIxs+ -- Both sides can have nulls in full outer+ (leftIxs, rightIxs)+ | max leftRows rightRows > joinStrategyThreshold =+ sortMergeFullOuterKernel leftHashes rightHashes+ | otherwise =+ hashFullOuterKernel leftHashes rightHashes+ in+ -- Both index vectors use -1 as sentinel+ assembleFullOuter csSet left right leftIxs rightIxs {- | Hash-based full outer join kernel. Builds compact indices on both sides.
src/DataFrame/Typed.hs view
@@ -192,9 +192,6 @@ -- * Constraints KnownSchema (..), AllKnownSymbol (..),-- -- * Pipe operator- (|>), ) where import Prelude hiding (drop, filter, take)
src/DataFrame/Typed/Expr.hs view
@@ -86,7 +86,6 @@ import Data.Proxy (Proxy (..)) import Data.String (IsString (..)) import qualified Data.Text as T-import qualified Data.Vector.Unboxed as VU import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import DataFrame.Internal.Column (Columnable)@@ -94,11 +93,12 @@ AggStrategy (..), BinaryOp (..), Expr (..),+ MeanAcc (..), NamedExpr, UExpr (..), UnaryOp (..), )-import DataFrame.Internal.Statistics+ import DataFrame.Typed.Schema (AssertPresent, Lookup) import DataFrame.Typed.Types (TExpr (..), TSortOrder (..)) import Prelude hiding (maximum, minimum, sum)@@ -233,11 +233,22 @@ sum :: (Columnable a, Num a) => TExpr cols a -> TExpr cols a sum (TExpr e) = TExpr (Agg (FoldAgg "sum" Nothing (+)) e) -mean :: (Columnable a, Real a, VU.Unbox a) => TExpr cols a -> TExpr cols Double-mean (TExpr e) = TExpr (Agg (CollectAgg "mean" mean') e)+mean :: (Columnable a, Real a) => TExpr cols a -> TExpr cols Double+mean (TExpr e) =+ TExpr+ ( Agg+ ( MergeAgg+ "mean"+ (MeanAcc 0.0 0)+ (\(MeanAcc s c) x -> MeanAcc (s + realToFrac x) (c + 1))+ (\(MeanAcc s1 c1) (MeanAcc s2 c2) -> MeanAcc (s1 + s2) (c1 + c2))+ (\(MeanAcc s c) -> if c == 0 then 0 / 0 else s / fromIntegral c)+ )+ e+ ) count :: (Columnable a) => TExpr cols a -> TExpr cols Int-count (TExpr e) = TExpr (Agg (FoldAgg "count" (Just 0) (\acc _ -> acc + 1)) e)+count (TExpr e) = TExpr (Agg (MergeAgg "count" (0 :: Int) (\c _ -> c + 1) (+) id) e) minimum :: (Columnable a, Ord a) => TExpr cols a -> TExpr cols a minimum (TExpr e) = TExpr (Agg (FoldAgg "minimum" Nothing min) e)
src/DataFrame/Typed/Join.hs view
@@ -43,7 +43,7 @@ TypedDataFrame right -> TypedDataFrame (LeftJoinSchema keys left right) leftJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.leftJoin keyNames r l)+ unsafeFreeze (DJ.leftJoin keyNames l r) where keyNames = symbolVals @keys @@ -55,7 +55,7 @@ TypedDataFrame right -> TypedDataFrame (RightJoinSchema keys left right) rightJoin (TDF l) (TDF r) =- unsafeFreeze (DJ.rightJoin keyNames r l)+ unsafeFreeze (DJ.rightJoin keyNames l r) where keyNames = symbolVals @keys
src/DataFrame/Typed/Operations.hs view
@@ -50,12 +50,8 @@ -- * Vertical merge append,-- -- * Pipe operator- (|>), ) where -import Data.Function ((&)) import Data.Proxy (Proxy (..)) import qualified Data.Text as T import qualified Data.Vector as V@@ -73,19 +69,11 @@ import qualified DataFrame.Operations.Subset as D import qualified DataFrame.Operations.Transformations as D --- Semigroup instance- import DataFrame.Typed.Freeze (unsafeFreeze) import DataFrame.Typed.Schema import DataFrame.Typed.Types (TExpr (..), TSortOrder (..), TypedDataFrame (..)) import qualified DataFrame.Typed.Types as T --- | Pipe operator, re-exported for convenience.-(|>) :: a -> (a -> b) -> b-(|>) = (&)--infixl 1 |>- ------------------------------------------------------------------------------- -- Schema-preserving operations -------------------------------------------------------------------------------@@ -226,6 +214,7 @@ forall name a cols. ( KnownSymbol name , Columnable a+ , Maybe a ~ Lookup name cols ) => a -> TypedDataFrame cols ->
+ tests/IO/JSON.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE OverloadedStrings #-}++module IO.JSON where++import qualified Data.ByteString.Lazy.Char8 as LBSC+import DataFrame.IO.JSON (readJSONEither)+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Internal.DataFrame as DI+import qualified DataFrame.Operations.Core as D+import Test.HUnit (+ Test (TestCase, TestLabel),+ assertBool,+ assertEqual,+ assertFailure,+ )++-- | Happy path: array of objects with string and number columns.+jsonHappyPath :: Test+jsonHappyPath =+ TestCase+ ( case readJSONEither+ (LBSC.pack "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]") of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "Happy path: 2 rows" 2 (D.nRows df)+ assertEqual "Happy path: 2 columns" 2 (D.nColumns df)+ )++-- | Boolean column is preserved correctly.+jsonBoolColumn :: Test+jsonBoolColumn =+ TestCase+ ( case readJSONEither (LBSC.pack "[{\"flag\":true},{\"flag\":false}]") of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "Bool column: 2 rows" 2 (D.nRows df)+ assertEqual "Bool column: 1 column" 1 (D.nColumns df)+ )++-- | A key absent from some objects produces an Optional column.+jsonMissingKeyBecomesOptional :: Test+jsonMissingKeyBecomesOptional =+ TestCase+ ( case readJSONEither (LBSC.pack "[{\"a\":1,\"b\":2},{\"a\":3}]") of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "Missing key: 2 rows" 2 (D.nRows df)+ assertEqual "Missing key: 2 columns" 2 (D.nColumns df)+ -- 'b' is absent from the second row, so must have a missing value+ assertBool+ "b column should have missing values"+ (maybe False DI.hasMissing (DI.getColumn "b" df))+ )++-- | When column values are different types across rows, the column becomes generic.+jsonMixedTypeColumn :: Test+jsonMixedTypeColumn =+ TestCase+ ( case readJSONEither (LBSC.pack "[{\"x\":1},{\"x\":\"hello\"}]") of+ Left err -> assertFailure $ "Unexpected Left: " ++ err+ Right df -> do+ assertEqual "Mixed type: 2 rows" 2 (D.nRows df)+ assertEqual "Mixed type: 1 column" 1 (D.nColumns df)+ )++-- | An empty top-level JSON array is rejected.+jsonEmptyArray :: Test+jsonEmptyArray =+ TestCase+ ( case readJSONEither (LBSC.pack "[]") of+ Left _ -> return ()+ Right _ -> assertFailure "Expected Left for empty array"+ )++-- | A non-array top-level JSON value is rejected.+jsonNonArray :: Test+jsonNonArray =+ TestCase+ ( case readJSONEither (LBSC.pack "{\"name\":\"Alice\"}") of+ Left _ -> return ()+ Right _ -> assertFailure "Expected Left for non-array top-level value"+ )++-- | Array elements that are not objects are rejected.+jsonNonObjectElement :: Test+jsonNonObjectElement =+ TestCase+ ( case readJSONEither (LBSC.pack "[1, 2, 3]") of+ Left _ -> return ()+ Right _ -> assertFailure "Expected Left for non-object array elements"+ )++-- | Completely invalid JSON is rejected.+jsonInvalidJSON :: Test+jsonInvalidJSON =+ TestCase+ ( case readJSONEither (LBSC.pack "not valid json at all") of+ Left _ -> return ()+ Right _ -> assertFailure "Expected Left for invalid JSON"+ )++tests :: [Test]+tests =+ [ TestLabel "jsonHappyPath" jsonHappyPath+ , TestLabel "jsonBoolColumn" jsonBoolColumn+ , TestLabel "jsonMissingKeyBecomesOptional" jsonMissingKeyBecomesOptional+ , TestLabel "jsonMixedTypeColumn" jsonMixedTypeColumn+ , TestLabel "jsonEmptyArray" jsonEmptyArray+ , TestLabel "jsonNonArray" jsonNonArray+ , TestLabel "jsonNonObjectElement" jsonNonObjectElement+ , TestLabel "jsonInvalidJSON" jsonInvalidJSON+ ]
+ tests/Internal/Parsing.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE OverloadedStrings #-}++module Internal.Parsing where++import DataFrame.Internal.Parsing+import Test.HUnit++-- isNullish: recognized null strings++isNullishEmptyString :: Test+isNullishEmptyString =+ TestCase (assertBool "empty string is nullish" (isNullish ""))++isNullishNA :: Test+isNullishNA = TestCase (assertBool "NA is nullish" (isNullish "NA"))++isNullishNULL :: Test+isNullishNULL = TestCase (assertBool "NULL is nullish" (isNullish "NULL"))++isNullishNull :: Test+isNullishNull = TestCase (assertBool "null is nullish" (isNullish "null"))++isNullishNaN :: Test+isNullishNaN = TestCase (assertBool "nan is nullish" (isNullish "nan"))++isNullishNaNMixed :: Test+isNullishNaNMixed = TestCase (assertBool "NaN is nullish" (isNullish "NaN"))++isNullishNANUpper :: Test+isNullishNANUpper = TestCase (assertBool "NAN is nullish" (isNullish "NAN"))++isNullishNothing :: Test+isNullishNothing =+ TestCase (assertBool "Nothing is nullish" (isNullish "Nothing"))++isNullishSpace :: Test+isNullishSpace =+ TestCase (assertBool "single space is nullish" (isNullish " "))++isNullishNSlashA :: Test+isNullishNSlashA = TestCase (assertBool "N/A is nullish" (isNullish "N/A"))++-- isNullish: values that are NOT null++notNullishNumber :: Test+notNullishNumber =+ TestCase (assertBool "\"42\" is not nullish" (not (isNullish "42")))++notNullishText :: Test+notNullishText =+ TestCase (assertBool "\"hello\" is not nullish" (not (isNullish "hello")))++notNullishTrue :: Test+notNullishTrue =+ TestCase (assertBool "\"True\" is not nullish" (not (isNullish "True")))++notNullishDouble :: Test+notNullishDouble =+ TestCase (assertBool "\"3.14\" is not nullish" (not (isNullish "3.14")))++notNullishZero :: Test+notNullishZero =+ TestCase (assertBool "\"0\" is not nullish" (not (isNullish "0")))++-- readBool: positive cases++readBoolTrue :: Test+readBoolTrue =+ TestCase (assertEqual "readBool \"True\"" (Just True) (readBool "True"))++readBoolTrueLower :: Test+readBoolTrueLower =+ TestCase (assertEqual "readBool \"true\"" (Just True) (readBool "true"))++readBoolTrueUpper :: Test+readBoolTrueUpper =+ TestCase (assertEqual "readBool \"TRUE\"" (Just True) (readBool "TRUE"))++readBoolFalse :: Test+readBoolFalse =+ TestCase (assertEqual "readBool \"False\"" (Just False) (readBool "False"))++readBoolFalseLower :: Test+readBoolFalseLower =+ TestCase (assertEqual "readBool \"false\"" (Just False) (readBool "false"))++readBoolFalseUpper :: Test+readBoolFalseUpper =+ TestCase (assertEqual "readBool \"FALSE\"" (Just False) (readBool "FALSE"))++-- readBool: values that are not booleans++readBoolDigit :: Test+readBoolDigit =+ TestCase (assertEqual "readBool \"1\" is Nothing" Nothing (readBool "1"))++readBoolYes :: Test+readBoolYes =+ TestCase (assertEqual "readBool \"yes\" is Nothing" Nothing (readBool "yes"))++readBoolEmpty :: Test+readBoolEmpty =+ TestCase (assertEqual "readBool \"\" is Nothing" Nothing (readBool ""))++readBoolPartialTrue :: Test+readBoolPartialTrue =+ TestCase (assertEqual "readBool \"Tru\" is Nothing" Nothing (readBool "Tru"))++-- readInt++readIntPositive :: Test+readIntPositive =+ TestCase (assertEqual "readInt \"42\"" (Just 42) (readInt "42"))++readIntNegative :: Test+readIntNegative =+ TestCase (assertEqual "readInt \"-17\"" (Just (-17)) (readInt "-17"))++readIntZero :: Test+readIntZero =+ TestCase (assertEqual "readInt \"0\"" (Just 0) (readInt "0"))++-- readInt strips whitespace before parsing+readIntLeadingSpace :: Test+readIntLeadingSpace =+ TestCase+ ( assertEqual+ "readInt \" 5 \" (strips whitespace)"+ (Just 5)+ (readInt " 5 ")+ )++readIntFloat :: Test+readIntFloat =+ TestCase+ (assertEqual "readInt \"3.14\" is Nothing" Nothing (readInt "3.14"))++readIntText :: Test+readIntText =+ TestCase (assertEqual "readInt \"abc\" is Nothing" Nothing (readInt "abc"))++readIntEmpty :: Test+readIntEmpty =+ TestCase (assertEqual "readInt \"\" is Nothing" Nothing (readInt ""))++-- trailing non-digits must make the parse fail+readIntPartialSuffix :: Test+readIntPartialSuffix =+ TestCase+ ( assertEqual+ "readInt \"42abc\" is Nothing"+ Nothing+ (readInt "42abc")+ )++-- readDouble++readDoublePositive :: Test+readDoublePositive =+ TestCase+ (assertEqual "readDouble \"3.14\"" (Just 3.14) (readDouble "3.14"))++readDoubleNegative :: Test+readDoubleNegative =+ TestCase+ (assertEqual "readDouble \"-1.5\"" (Just (-1.5)) (readDouble "-1.5"))++readDoubleWholeNumber :: Test+readDoubleWholeNumber =+ TestCase+ ( assertEqual+ "readDouble \"42\" parses as 42.0"+ (Just 42.0)+ (readDouble "42")+ )++readDoubleText :: Test+readDoubleText =+ TestCase+ (assertEqual "readDouble \"abc\" is Nothing" Nothing (readDouble "abc"))++readDoubleEmpty :: Test+readDoubleEmpty =+ TestCase+ (assertEqual "readDouble \"\" is Nothing" Nothing (readDouble ""))++readDoublePartialSuffix :: Test+readDoublePartialSuffix =+ TestCase+ ( assertEqual+ "readDouble \"3.14abc\" is Nothing"+ Nothing+ (readDouble "3.14abc")+ )++tests :: [Test]+tests =+ [ TestLabel "isNullishEmptyString" isNullishEmptyString+ , TestLabel "isNullishNA" isNullishNA+ , TestLabel "isNullishNULL" isNullishNULL+ , TestLabel "isNullishNull" isNullishNull+ , TestLabel "isNullishNaN" isNullishNaN+ , TestLabel "isNullishNaNMixed" isNullishNaNMixed+ , TestLabel "isNullishNANUpper" isNullishNANUpper+ , TestLabel "isNullishNothing" isNullishNothing+ , TestLabel "isNullishSpace" isNullishSpace+ , TestLabel "isNullishNSlashA" isNullishNSlashA+ , TestLabel "notNullishNumber" notNullishNumber+ , TestLabel "notNullishText" notNullishText+ , TestLabel "notNullishTrue" notNullishTrue+ , TestLabel "notNullishDouble" notNullishDouble+ , TestLabel "notNullishZero" notNullishZero+ , TestLabel "readBoolTrue" readBoolTrue+ , TestLabel "readBoolTrueLower" readBoolTrueLower+ , TestLabel "readBoolTrueUpper" readBoolTrueUpper+ , TestLabel "readBoolFalse" readBoolFalse+ , TestLabel "readBoolFalseLower" readBoolFalseLower+ , TestLabel "readBoolFalseUpper" readBoolFalseUpper+ , TestLabel "readBoolDigit" readBoolDigit+ , TestLabel "readBoolYes" readBoolYes+ , TestLabel "readBoolEmpty" readBoolEmpty+ , TestLabel "readBoolPartialTrue" readBoolPartialTrue+ , TestLabel "readIntPositive" readIntPositive+ , TestLabel "readIntNegative" readIntNegative+ , TestLabel "readIntZero" readIntZero+ , TestLabel "readIntLeadingSpace" readIntLeadingSpace+ , TestLabel "readIntFloat" readIntFloat+ , TestLabel "readIntText" readIntText+ , TestLabel "readIntEmpty" readIntEmpty+ , TestLabel "readIntPartialSuffix" readIntPartialSuffix+ , TestLabel "readDoublePositive" readDoublePositive+ , TestLabel "readDoubleNegative" readDoubleNegative+ , TestLabel "readDoubleWholeNumber" readDoubleWholeNumber+ , TestLabel "readDoubleText" readDoubleText+ , TestLabel "readDoubleEmpty" readDoubleEmpty+ , TestLabel "readDoublePartialSuffix" readDoublePartialSuffix+ ]
+ tests/LazyParquet.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module LazyParquet where++import Data.Int (Int32, Int64)+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import Data.Time (UTCTime)+import qualified DataFrame as D+import qualified DataFrame.Functions as F+import DataFrame.Internal.Schema (Schema (..), schemaType)+import qualified DataFrame.Lazy as L+import DataFrame.Lazy.Internal.LogicalPlan (SortOrder (..))+import Test.HUnit++-- | Schema matching all columns in alltypes_plain.parquet.+allTypesSchema :: Schema+allTypesSchema =+ Schema $+ M.fromList+ [ ("id", schemaType @Int32)+ , ("bool_col", schemaType @Bool)+ , ("tinyint_col", schemaType @Int32)+ , ("smallint_col", schemaType @Int32)+ , ("int_col", schemaType @Int32)+ , ("bigint_col", schemaType @Int64)+ , ("float_col", schemaType @Float)+ , ("double_col", schemaType @Double)+ , ("date_string_col", schemaType @T.Text)+ , ("string_col", schemaType @T.Text)+ , ("timestamp_col", schemaType @UTCTime)+ ]++plainPath :: FilePath+plainPath = "./tests/data/alltypes_plain.parquet"++-- Test 1: basic scan — dimensions match eager reader.+basicScan :: Test+basicScan =+ TestCase+ ( do+ eager <- D.readParquet plainPath+ actual <- L.runDataFrame (L.scanParquet allTypesSchema (T.pack plainPath))+ assertEqual "basicScan dimensions" (D.dimensions eager) (D.dimensions actual)+ )++-- Test 2: column projection — select 2 columns.+columnProjection :: Test+columnProjection =+ TestCase+ ( do+ actual <-+ L.runDataFrame+ (L.select ["id", "bool_col"] (L.scanParquet allTypesSchema (T.pack plainPath)))+ assertEqual "columnProjection" (8, 2) (D.dimensions actual)+ )++-- Test 3: filter pushdown — id >= 6 gives 2 rows.+filterPushdown :: Test+filterPushdown =+ TestCase+ ( do+ actual <-+ L.runDataFrame+ ( L.filter+ (F.geq (F.col @Int32 "id") (F.lit 6))+ (L.scanParquet allTypesSchema (T.pack plainPath))+ )+ assertEqual "filterPushdown" (2, 11) (D.dimensions actual)+ )++-- Test 4: filter + project combined.+filterAndProject :: Test+filterAndProject =+ TestCase+ ( do+ actual <-+ L.runDataFrame+ ( L.select+ ["id", "bool_col"]+ ( L.filter+ (F.geq (F.col @Int32 "id") (F.lit 6))+ (L.scanParquet allTypesSchema (T.pack plainPath))+ )+ )+ assertEqual "filterAndProject" (2, 2) (D.dimensions actual)+ )++-- Test 5: multi-file glob — alltypes_plain*.parquet matches plain + snappy = 10 rows.+multiFileGlob :: Test+multiFileGlob =+ TestCase+ ( do+ actual <-+ L.runDataFrame+ ( L.scanParquet+ allTypesSchema+ "./tests/data/alltypes_plain*.parquet"+ )+ let (rows, cols) = D.dimensions actual+ assertEqual "multiFileGlob cols" 11 cols+ assertBool "multiFileGlob rows >= 8" (rows >= 8)+ )++-- Test 6: sort + limit — get 3 smallest ids.+sortAndLimit :: Test+sortAndLimit =+ TestCase+ ( do+ actual <-+ L.runDataFrame+ ( L.limit+ 3+ ( L.sortBy+ [("id", Ascending)]+ (L.scanParquet allTypesSchema (T.pack plainPath))+ )+ )+ assertEqual "sortAndLimit rows" 3 (fst (D.dimensions actual))+ )++tests :: [Test]+tests =+ [ basicScan+ , columnProjection+ , filterPushdown+ , filterAndProject+ , multiFileGlob+ , sortAndLimit+ ]
tests/Main.hs view
@@ -1,5151 +1,81 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Main where--import qualified Data.Text as T-import qualified Data.Vector as V-import qualified Data.Vector.Unboxed as VU-import qualified DataFrame as D-import qualified DataFrame.Internal.Column as DI-import qualified DataFrame.Operations.Typing as D-import qualified System.Exit as Exit--import Data.Time-import GenDataFrame ()-import Test.HUnit-import Test.QuickCheck--import qualified Functions-import qualified Monad-import qualified Operations.Aggregations-import qualified Operations.Apply-import qualified Operations.Core-import qualified Operations.Derive-import qualified Operations.Filter-import qualified Operations.GroupBy-import qualified Operations.InsertColumn-import qualified Operations.Join-import qualified Operations.Merge-import qualified Operations.ReadCsv-import qualified Operations.Shuffle-import qualified Operations.Sort-import qualified Operations.Statistics-import qualified Operations.Subset-import qualified Operations.Take-import qualified Parquet--testData :: D.DataFrame-testData =- D.fromNamedColumns- [ ("test1", DI.fromList ([1 .. 26] :: [Int]))- , ("test2", DI.fromList ['a' .. 'z'])- ]---- Dimensions-correctDimensions :: Test-correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))--emptyDataframeDimensions :: Test-emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))--dimensionsTest :: [Test]-dimensionsTest =- [ TestLabel "dimensions_correctDimensions" correctDimensions- , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions- ]---- PARSING TESTS-------- 1. SIMPLE CASES-parseBools :: Test-parseBools =- let afterParse :: [Bool]- afterParse = [True, True, True] ++ [False, False, False]- 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- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Bools"- expected- actual- )--parseInts :: Test-parseInts =- let afterParse :: [Int]- afterParse = [1 .. 50]- 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- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints"- expected- actual- )--parseDoubles :: Test-parseDoubles =- let afterParse :: [Double]- afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]- beforeParse :: [T.Text]- beforeParse =- 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- in TestCase- ( assertEqual- "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"- expected- actual- )--parseDates :: Test-parseDates =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days"- expected- actual- )--parseTexts :: Test-parseTexts =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Text without missing values as BoxedColumn of Text"- expected- actual- )----- 2. COMBINATION CASES-parseBoolsAndIntsAsTexts :: Test-parseBoolsAndIntsAsTexts =- let afterParse :: [T.Text]- afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]- 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- in TestCase- ( assertEqual- "Correctly parses mixture of Bools and Ints as Text"- expected- actual- )--parseIntsAndDoublesAsDoubles :: Test-parseIntsAndDoublesAsDoubles =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"- expected- actual- )--parseIntsAndDatesAsTexts :: Test-parseIntsAndDatesAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"- expected- actual- )--parseTextsAndDoublesAsTexts :: Test-parseTextsAndDoublesAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"- expected- actual- )--parseDatesAndTextsAsTexts :: Test-parseDatesAndTextsAsTexts =- let afterParse :: [T.Text]- afterParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , "Jessie"- , "James"- , "Meowth"- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , "Jessie"- , "James"- , "Meowth"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"- expected- actual- )---- 3A. PARSING WITH SAFEREAD OFF--parseBoolsWithoutSafeRead :: Test-parseBoolsWithoutSafeRead =- let afterParse :: [Bool]- afterParse = replicate 10 True ++ replicate 10 False- beforeParse :: [T.Text]- 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- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"- expected- actual- )--parseIntsWithoutSafeRead :: Test-parseIntsWithoutSafeRead =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"- expected- actual- )--parseDoublesWithoutSafeRead :: Test-parseDoublesWithoutSafeRead =- let afterParse :: [Double]- afterParse =- [ 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"- expected- actual- )--parseDatesWithoutSafeRead :: Test-parseDatesWithoutSafeRead =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days"- expected- actual- )--parseTextsWithoutSafeRead :: Test-parseTextsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , "Surrender now or prepare to fight!"- , "Meowth, that's right!"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Text without missing values as BoxedColumn of Text"- expected- actual- )--parseBoolsAndEmptyStringsWithoutSafeRead :: Test-parseBoolsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Bool]- afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)- beforeParse :: [T.Text]- 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- in TestCase- ( assertEqual- "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"- expected- actual- )--parseIntsAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , ""- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , ""- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , ""- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , ""- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ 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"- expected- actual- )--parseDatesAndEmptyStringsWithoutSafeRead :: Test-parseDatesAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe Day]- afterParse =- [ Just $ fromGregorian 2020 02 12- , Just $ fromGregorian 2020 02 13- , Just $ fromGregorian 2020 02 14- , Nothing- , Just $ fromGregorian 2020 02 15- , Just $ fromGregorian 2020 02 16- , Just $ fromGregorian 2020 02 17- , Nothing- , Just $ fromGregorian 2020 02 18- , Just $ fromGregorian 2020 02 19- , Just $ fromGregorian 2020 02 20- , Nothing- , Just $ fromGregorian 2020 02 21- , Just $ fromGregorian 2020 02 22- , Just $ fromGregorian 2020 02 23- , Nothing- , Just $ fromGregorian 2020 02 24- , Just $ fromGregorian 2020 02 25- , Just $ fromGregorian 2020 02 26- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , ""- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , ""- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , ""- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , ""- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"- expected- actual- )--parseTextsAndEmptyStringsWithoutSafeRead :: Test-parseTextsAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )--parseBoolsAndNullishStringsWithoutSafeRead :: Test-parseBoolsAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"- beforeParse :: [T.Text]- 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- in TestCase- ( assertEqual- "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"- expected- actual- )--parseIntsAndNullishStringsWithoutSafeRead :: Test-parseIntsAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"- expected- actual- )--parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithoutSafeRead =- let afterParse :: [T.Text]- afterParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ 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"- expected- actual- )--parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Just "N/A"- , Just "N/A"- , Just "N/A"- , Just "N/A"- , Just "N/A"- , Nothing- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Nothing- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Nothing- , Just "21"- , Just "22"- , Just "23"- , Just "24"- , Just "25"- , Just "26"- , Just "27"- , Just "28"- , Just "29"- , Just "30"- , Nothing- , Just "31"- , Just "32"- , Just "33"- , Just "34"- , Just "35"- , Just "36"- , Just "37"- , Just "38"- , Just "39"- , Just "40"- , Nothing- , Just "41"- , Just "42"- , Just "43"- , Just "44"- , Just "45"- , Just "46"- , Just "47"- , Just "48"- , Just "49"- , Just "50"- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , ""- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ 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"- expected- actual- )--parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithoutSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- , Just "NaN"- , Just "Nothing"- , Just "N/A"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- , "NaN"- , "Nothing"- , "N/A"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )---- 3B. PARSING WITH SAFEREAD ON-parseBoolsAndEmptyStringsWithSafeRead :: Test-parseBoolsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Bool]- afterParse = replicate 10 Nothing ++ replicate 10 (Just True)- 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- in TestCase- ( assertEqual- "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"- expected- actual- )--parseIntsAndEmptyStringsWithSafeRead :: Test-parseIntsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , ""- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , ""- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , ""- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , ""- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"- expected- actual- )--parseDatesAndEmptyStringsWithSafeRead :: Test-parseDatesAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Day]- afterParse =- [ Just $ fromGregorian 2020 02 12- , Just $ fromGregorian 2020 02 13- , Just $ fromGregorian 2020 02 14- , Nothing- , Just $ fromGregorian 2020 02 15- , Just $ fromGregorian 2020 02 16- , Just $ fromGregorian 2020 02 17- , Nothing- , Just $ fromGregorian 2020 02 18- , Just $ fromGregorian 2020 02 19- , Just $ fromGregorian 2020 02 20- , Nothing- , Just $ fromGregorian 2020 02 21- , Just $ fromGregorian 2020 02 22- , Just $ fromGregorian 2020 02 23- , Nothing- , Just $ fromGregorian 2020 02 24- , Just $ fromGregorian 2020 02 25- , Just $ fromGregorian 2020 02 26- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , ""- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , ""- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , ""- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , ""- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"- expected- actual- )--parseTextsAndEmptyStringsWithSafeRead :: Test-parseTextsAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"- expected- actual- )--parseIntsAndNullishStringsWithSafeRead :: Test-parseIntsAndNullishStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"- expected- actual- )--parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Nothing- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Nothing- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Nothing- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Nothing- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Nothing- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.03- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "Nothing"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "N/A"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "NULL"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "null"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "NAN"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.03"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"- expected- actual- )--parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndNullishAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Int]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Nothing- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Nothing- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Nothing- , Just 31- , Just 32- , Just 33- , Just 34- , Just 35- , Just 36- , Just 37- , Just 38- , Just 39- , Just 40- , Nothing- , Just 41- , Just 42- , Just 43- , Just 44- , Just 45- , Just 46- , Just 47- , Just 48- , Just 49- , Just 50- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "Nothing"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , ""- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , ""- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"- expected- actual- )--parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test-parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1- , Just 2- , Just 3- , Just 4- , Just 5- , Just 6- , Just 7- , Just 8- , Just 9- , Just 10- , Nothing- , Just 11- , Just 12- , Just 13- , Just 14- , Just 15- , Just 16- , Just 17- , Just 18- , Just 19- , Just 20- , Nothing- , Just 21- , Just 22- , Just 23- , Just 24- , Just 25- , Just 26- , Just 27- , Just 28- , Just 29- , Just 30- , Nothing- , Just 3.14- ]- beforeParse :: [T.Text]- beforeParse =- [ "N/A"- , "N/A"- , "N/A"- , "N/A"- , "Nothing"- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , ""- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , ""- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , ""- , "3.14"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"- expected- actual- )--parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test-parseTextsAndEmptyAndNullishStringsWithSafeRead =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Just "To"- , Just "protect"- , Just "the"- , Just "world"- , Just "from"- , Just "devastation"- , Nothing- , Just "To"- , Just "unite"- , Just "all"- , Just "people"- , Just "within"- , Just "our"- , Just "nation"- , Nothing- , Just "To"- , Just "denounce"- , Just "the"- , Just "evils"- , Just "of"- , Just "truth"- , Just "and"- , Just "love"- , Nothing- , Just "To"- , Just "extend"- , Just "our"- , Just "reach"- , Just "to"- , Just "the"- , Just "stars"- , Just "above"- , Nothing- , Just "JESSIE!"- , Just "JAMES!"- , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , Nothing- , Just "Surrender now or prepare to fight!"- , Nothing- , Just "Meowth, that's right!"- , Nothing- , Nothing- , Nothing- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , "To"- , "protect"- , "the"- , "world"- , "from"- , "devastation"- , ""- , "To"- , "unite"- , "all"- , "people"- , "within"- , "our"- , "nation"- , ""- , "To"- , "denounce"- , "the"- , "evils"- , "of"- , "truth"- , "and"- , "love"- , ""- , "To"- , "extend"- , "our"- , "reach"- , "to"- , "the"- , "stars"- , "above"- , ""- , "JESSIE!"- , "JAMES!"- , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"- , ""- , "Surrender now or prepare to fight!"- , ""- , "Meowth, that's right!"- , "NaN"- , "Nothing"- , "N/A"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"- expected- actual- )---- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.-parseBoolsWithOneExample :: Test-parseBoolsWithOneExample =- let afterParse :: [Bool]- afterParse = False : replicate 50 True- 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- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseBoolsWithManyExamples :: Test-parseBoolsWithManyExamples =- let afterParse :: [Bool]- afterParse = False : replicate 50 True- 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- in TestCase- ( assertEqual- "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseIntsWithOneExample :: Test-parseIntsWithOneExample =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"- expected- actual- )--parseIntsWithTwentyFiveExamples :: Test-parseIntsWithTwentyFiveExamples =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"- expected- actual- )--parseIntsWithFortyNineExamples :: Test-parseIntsWithFortyNineExamples =- let afterParse :: [Int]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"- expected- actual- )--parseDatesWithOneExample :: Test-parseDatesWithOneExample =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"- expected- actual- )--parseDatesWithFifteenExamples :: Test-parseDatesWithFifteenExamples =- let afterParse :: [Day]- afterParse =- [ fromGregorian 2020 02 12- , fromGregorian 2020 02 13- , fromGregorian 2020 02 14- , fromGregorian 2020 02 15- , fromGregorian 2020 02 16- , fromGregorian 2020 02 17- , fromGregorian 2020 02 18- , fromGregorian 2020 02 19- , fromGregorian 2020 02 20- , fromGregorian 2020 02 21- , fromGregorian 2020 02 22- , fromGregorian 2020 02 23- , fromGregorian 2020 02 24- , fromGregorian 2020 02 25- , fromGregorian 2020 02 26- ]- beforeParse :: [T.Text]- beforeParse =- [ "2020-02-12"- , "2020-02-13"- , "2020-02-14"- , "2020-02-15"- , "2020-02-16"- , "2020-02-17"- , "2020-02-18"- , "2020-02-19"- , "2020-02-20"- , "2020-02-21"- , "2020-02-22"- , "2020-02-23"- , "2020-02-24"- , "2020-02-25"- , "2020-02-26"- ]- expected = DI.BoxedColumn $ V.fromList afterParse- actual = D.parseDefault [] 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"- expected- actual- )--parseIntsAndDoublesAsDoublesWithOneExample :: Test-parseIntsAndDoublesAsDoublesWithOneExample =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAsDoublesWithManyExamples :: Test-parseIntsAndDoublesAsDoublesWithManyExamples =- let afterParse :: [Double]- afterParse =- [ 1- , 2- , 3- , 4- , 5- , 6- , 7- , 8- , 9- , 10- , 11- , 12- , 13- , 14- , 15- , 16- , 17- , 18- , 19- , 20- , 21- , 22- , 23- , 24- , 25- , 26- , 27- , 28- , 29- , 30- , 31- , 32- , 33- , 34- , 35- , 36- , 37- , 38- , 39- , 40- , 41- , 42- , 43- , 44- , 45- , 46- , 47- , 48- , 49- , 50- , 1.0- , 2.0- , 3.0- , 4.0- , 5.0- , 6.0- , 7.0- , 8.0- , 9.0- , 10.0- , 11.0- , 12.0- , 13.0- , 14.0- , 15.0- , 16.0- , 17.0- , 18.0- , 19.0- , 20.0- , 21.0- , 22.0- , 23.0- , 24.0- , 25.0- , 26.0- , 27.0- , 28.0- , 29.0- , 30.0- , 31.0- , 32.0- , 33.0- , 34.0- , 35.0- , 36.0- , 37.0- , 38.0- , 39.0- , 40.0- , 41.0- , 42.0- , 43.0- , 44.0- , 45.0- , 46.0- , 47.0- , 48.0- , 49.0- , 50.0- , 3.14- , 2.22- , 8.55- , 23.3- , 12.22222235049450945049504950- ]- beforeParse :: [T.Text]- beforeParse =- [ "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.UnboxedColumn $ VU.fromList afterParse- actual = D.parseDefault [] 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =- let afterParse :: [Maybe Double]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 1.0- , Just 2.0- , Just 3.0- , Just 4.0- , Just 5.0- , Just 6.0- , Just 7.0- , Just 8.0- , Just 9.0- , Just 10.0- , Just 11.0- , Just 12.0- , Just 13.0- , Just 14.0- , Just 15.0- , Just 16.0- , Just 17.0- , Just 18.0- , Just 19.0- , Just 20.0- , Just 21.0- , Just 22.0- , Just 23.0- , Just 24.0- , Just 25.0- , Just 26.0- , Just 27.0- , Just 28.0- , Just 29.0- , Just 30.0- , Just 31.0- , Just 32.0- , Just 33.0- , Just 34.0- , Just 35.0- , Just 36.0- , Just 37.0- , Just 38.0- , Just 39.0- , Just 40.0- , Just 41.0- , Just 42.0- , Just 43.0- , Just 44.0- , Just 45.0- , Just 46.0- , Just 47.0- , Just 48.0- , Just 49.0- , Just 50.0- , Just 3.14- , Just 2.22- , Just 8.55- , Just 23.3- , Just 12.22222235049451- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "21"- , "22"- , "23"- , "24"- , "25"- , "26"- , "27"- , "28"- , "29"- , "30"- , "31"- , "32"- , "33"- , "34"- , "35"- , "36"- , "37"- , "38"- , "39"- , "40"- , "41"- , "42"- , "43"- , "44"- , "45"- , "46"- , "47"- , "48"- , "49"- , "50"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "31.0"- , "32.0"- , "33.0"- , "34.0"- , "35.0"- , "36.0"- , "37.0"- , "38.0"- , "39.0"- , "40.0"- , "41.0"- , "42.0"- , "43.0"- , "44.0"- , "45.0"- , "46.0"- , "47.0"- , "48.0"- , "49.0"- , "50.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse- in TestCase- ( assertEqual- "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Just "1.0"- , Just "2.0"- , Just "3.0"- , Just "4.0"- , Just "5.0"- , Just "6.0"- , Just "7.0"- , Just "8.0"- , Just "9.0"- , Just "10.0"- , Just "11.0"- , Just "12.0"- , Just "13.0"- , Just "14.0"- , Just "15.0"- , Just "16.0"- , Just "17.0"- , Just "18.0"- , Just "19.0"- , Just "20.0"- , Just "21.0"- , Just "22.0"- , Just "23.0"- , Just "24.0"- , Just "25.0"- , Just "26.0"- , Just "27.0"- , Just "28.0"- , Just "29.0"- , Just "30.0"- , Just "3.14"- , Just "2.22"- , Just "8.55"- , Just "23.3"- , Just "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual = D.parseDefault [] 1 False "%Y-%m-%d" $ 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"- expected- actual- )--parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::- Test-parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =- let afterParse :: [Maybe T.Text]- afterParse =- [ Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Nothing- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "NaN"- , Just "N/A"- , Just "1"- , Just "2"- , Just "3"- , Just "4"- , Just "5"- , Just "6"- , Just "7"- , Just "8"- , Just "9"- , Just "10"- , Just "11"- , Just "12"- , Just "13"- , Just "14"- , Just "15"- , Just "16"- , Just "17"- , Just "18"- , Just "19"- , Just "20"- , Just "1.0"- , Just "2.0"- , Just "3.0"- , Just "4.0"- , Just "5.0"- , Just "6.0"- , Just "7.0"- , Just "8.0"- , Just "9.0"- , Just "10.0"- , Just "11.0"- , Just "12.0"- , Just "13.0"- , Just "14.0"- , Just "15.0"- , Just "16.0"- , Just "17.0"- , Just "18.0"- , Just "19.0"- , Just "20.0"- , Just "21.0"- , Just "22.0"- , Just "23.0"- , Just "24.0"- , Just "25.0"- , Just "26.0"- , Just "27.0"- , Just "28.0"- , Just "29.0"- , Just "30.0"- , Just "3.14"- , Just "2.22"- , Just "8.55"- , Just "23.3"- , Just "12.22222235049451"- ]- beforeParse :: [T.Text]- beforeParse =- [ ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , ""- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "NaN"- , "N/A"- , "1"- , "2"- , "3"- , "4"- , "5"- , "6"- , "7"- , "8"- , "9"- , "10"- , "11"- , "12"- , "13"- , "14"- , "15"- , "16"- , "17"- , "18"- , "19"- , "20"- , "1.0"- , "2.0"- , "3.0"- , "4.0"- , "5.0"- , "6.0"- , "7.0"- , "8.0"- , "9.0"- , "10.0"- , "11.0"- , "12.0"- , "13.0"- , "14.0"- , "15.0"- , "16.0"- , "17.0"- , "18.0"- , "19.0"- , "20.0"- , "21.0"- , "22.0"- , "23.0"- , "24.0"- , "25.0"- , "26.0"- , "27.0"- , "28.0"- , "29.0"- , "30.0"- , "3.14"- , "2.22"- , "8.55"- , "23.3"- , "12.22222235049451"- ]- expected = DI.OptionalColumn $ V.fromList afterParse- actual =- D.parseDefault [] 30 False "%Y-%m-%d" $ 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"- expected- actual- )---- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY--parseManyNullishAndOneInt :: Test-parseManyNullishAndOneInt =- let afterParse :: [Maybe Int]- afterParse = replicate 100 Nothing ++ [Just 100000]- 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- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDouble :: Test-parseManyNullishAndOneDouble =- let afterParse :: [Maybe Double]- afterParse = replicate 100 Nothing ++ [Just 3.14]- 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- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndOneDate :: Test-parseManyNullishAndOneDate =- let afterParse :: [Maybe Day]- afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]- 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- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseManyNullishAndIncorrectDates :: Test-parseManyNullishAndIncorrectDates =- let afterParse :: [Maybe T.Text]- afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]- 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- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseRepeatedNullish :: Test-parseRepeatedNullish =- let afterParse :: [Maybe T.Text]- afterParse = replicate 100 Nothing- 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- in TestCase- (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)--parseTests :: [Test]-parseTests =- [ -- 1. SIMPLE CASES- TestLabel "parseBools" parseBools- , TestLabel "parseInts" parseInts- , TestLabel "parseDoubles" parseDoubles- , TestLabel "parseDates" parseDates- , TestLabel "parseTexts" parseTexts- , -- 2. COMBINATION CASES- TestLabel "parseBoolsAndIntsAsTexts" parseBoolsAndIntsAsTexts- , TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles- , TestLabel "parseIntsAndDatesAsTexts" parseIntsAndDatesAsTexts- , TestLabel "parseTextsAndDoublesAsTexts" parseTextsAndDoublesAsTexts- , TestLabel "parseDatesAndTextsAsTexts" parseDatesAndTextsAsTexts- , -- 3A. PARSING WITH SAFEREAD OFF- TestLabel "parseBoolsWithoutSafeRead" parseBoolsWithoutSafeRead- , TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead- , TestLabel "parseDoublesWithoutSafeRead" parseDoublesWithoutSafeRead- , TestLabel "parseDatesWithoutSafeRead" parseDatesWithoutSafeRead- , TestLabel "parseTextsWithoutSafeRead" parseTextsWithoutSafeRead- , TestLabel- "parseBoolsAndEmptyStringsWithoutSafeRead"- parseBoolsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseIntsAndEmptyStringsWithoutSafeRead"- parseIntsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseIntsAndDoublesAndEmptyStringsWithoutSafeRead"- parseIntsAndDoublesAndEmptyStringsWithoutSafeRead- , TestLabel- "parseDatesAndEmptyStringsWithoutSafeRead"- parseDatesAndEmptyStringsWithoutSafeRead- , TestLabel- "parseTextsAndEmptyStringsWithoutSafeRead"- parseTextsAndEmptyStringsWithoutSafeRead- , TestLabel- "parseBoolsAndNullishStringsWithoutSafeRead"- parseBoolsAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndNullishStringsWithoutSafeRead"- parseIntsAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishStringsWithoutSafeRead"- parseIntsAndDoublesAndNullishStringsWithoutSafeRead- , TestLabel- "parseIntsAndNullishAndEmptyStringsWithoutSafeRead"- parseIntsAndNullishAndEmptyStringsWithoutSafeRead- , TestLabel- "parseTextsAndEmptyAndNullishStringsWithoutSafeRead"- parseTextsAndEmptyAndNullishStringsWithoutSafeRead- , -- 3B. PARSING WITH SAFEREAD ON- TestLabel- "parseBoolsAndEmptyStringsWithSafeRead"- parseBoolsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndEmptyStringsWithSafeRead"- parseIntsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndEmptyStringsWithSafeRead"- parseIntsAndDoublesAndEmptyStringsWithSafeRead- , TestLabel- "parseDatesAndEmptyStringsWithSafeRead"- parseDatesAndEmptyStringsWithSafeRead- , TestLabel- "parseTextsAndEmptyStringsWithSafeRead"- parseTextsAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndNullishStringsWithSafeRead"- parseIntsAndNullishStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishStringsWithSafeRead"- parseIntsAndDoublesAndNullishStringsWithSafeRead- , TestLabel- "parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead"- parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead- , TestLabel- "parseIntsAndNullishAndEmptyStringsWithSafeRead"- parseIntsAndNullishAndEmptyStringsWithSafeRead- , TestLabel- "parseTextsAndEmptyAndNullishStringsWithSafeRead"- parseTextsAndEmptyAndNullishStringsWithSafeRead- , -- 4. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES- TestLabel "parseBoolsWithOneExample" parseBoolsWithOneExample- , TestLabel "parseBoolsWithManyExamples" parseBoolsWithManyExamples- , TestLabel "parseIntsWithOneExample" parseIntsWithOneExample- , TestLabel "parseIntsWithTwentyFiveExamples" parseIntsWithTwentyFiveExamples- , TestLabel "parseIntsWithFortyNineExamples" parseIntsWithFortyNineExamples- , TestLabel "parseDatesWithOneExample" parseDatesWithOneExample- , TestLabel "parseDatesWithFifteenExamples" parseDatesWithFifteenExamples- , TestLabel- "parseIntsAndDoublesAsDoublesWithOneExample"- parseIntsAndDoublesAsDoublesWithOneExample- , TestLabel- "parseIntsAndDoublesAsDoublesWithManyExamples"- parseIntsAndDoublesAsDoublesWithManyExamples- , TestLabel- "parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff"- parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff- , TestLabel- "parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff"- parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff- , TestLabel- "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff"- parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff- , TestLabel- "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff"- parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff- , -- 5. EDGE CASES THAT HAVE TO BE PARSED CORRECTLY- TestLabel "parseManyNullishAndOneInt" parseManyNullishAndOneInt- , TestLabel "parseManyNullishAndOneDouble" parseManyNullishAndOneDouble- , TestLabel "parseRepeatedNullish" parseRepeatedNullish- ]--tests :: Test-tests =- TestList $- dimensionsTest- ++ Operations.Aggregations.tests- ++ Operations.Apply.tests- ++ Operations.Core.tests- ++ Operations.Derive.tests- ++ Operations.Filter.tests- ++ Operations.GroupBy.tests- ++ Operations.InsertColumn.tests- ++ Operations.Join.tests- ++ Operations.Merge.tests- ++ Operations.ReadCsv.tests- ++ Operations.Shuffle.tests- ++ Operations.Sort.tests- ++ Operations.Statistics.tests- ++ Operations.Take.tests- ++ Functions.tests- ++ Parquet.tests- ++ parseTests--isSuccessful :: Result -> Bool-isSuccessful (Success{..}) = True-isSuccessful _ = False--main :: IO ()-main = do- result <- runTestTT tests- if failures result > 0 || errors result > 0- then Exit.exitFailure- else do- -- Property tests- propRes <-- mapM- (quickCheckWithResult stdArgs)- Operations.Subset.tests- monadRes <- mapM (quickCheckWithResult stdArgs) Monad.tests- if not (all isSuccessful propRes)- || not (all isSuccessful monadRes)+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main where++import qualified System.Exit as Exit++import GenDataFrame ()+import Test.HUnit+import Test.QuickCheck++import qualified Functions+import qualified IO.JSON+import qualified Internal.Parsing+import qualified LazyParquet+import qualified Monad+import qualified Operations.Aggregations+import qualified Operations.Apply+import qualified Operations.Core+import qualified Operations.Derive+import qualified Operations.Filter+import qualified Operations.GroupBy+import qualified Operations.InsertColumn+import qualified Operations.Join+import qualified Operations.Merge+import qualified Operations.ReadCsv+import qualified Operations.Shuffle+import qualified Operations.Sort+import qualified Operations.Statistics+import qualified Operations.Subset+import qualified Operations.Take+import qualified Operations.Typing+import qualified Parquet+import qualified Properties++tests :: Test+tests =+ TestList $+ Internal.Parsing.tests+ ++ Operations.Aggregations.tests+ ++ Operations.Apply.tests+ ++ Operations.Core.tests+ ++ Operations.Derive.tests+ ++ Operations.Filter.tests+ ++ Operations.GroupBy.tests+ ++ Operations.InsertColumn.tests+ ++ Operations.Join.tests+ ++ Operations.Merge.tests+ ++ Operations.ReadCsv.tests+ ++ Operations.Shuffle.tests+ ++ Operations.Sort.tests+ ++ Operations.Statistics.tests+ ++ Operations.Take.tests+ ++ Operations.Typing.tests+ ++ Functions.tests+ ++ IO.JSON.tests+ ++ Parquet.tests+ ++ LazyParquet.tests++isSuccessful :: Result -> Bool+isSuccessful (Success{..}) = True+isSuccessful _ = False++main :: IO ()+main = do+ result <- runTestTT tests+ if failures result > 0 || errors result > 0+ then Exit.exitFailure+ else do+ -- Property tests+ propRes <-+ mapM+ (quickCheckWithResult stdArgs)+ Operations.Subset.tests+ monadRes <- mapM (quickCheckWithResult stdArgs) Monad.tests+ propsRes <- mapM (quickCheckWithResult stdArgs) Properties.tests+ if not (all isSuccessful propRes)+ || not (all isSuccessful monadRes)+ || not (all isSuccessful propsRes) then Exit.exitFailure else Exit.exitSuccess
tests/Operations/Aggregations.hs view
@@ -207,6 +207,79 @@ ) ) +-- distinct++distinctRemovesDuplicates :: Test+distinctRemovesDuplicates =+ TestCase+ ( assertEqual+ "distinct reduces duplicate rows to one representative each"+ 3+ ( D.nRows+ ( D.distinct+ ( D.fromNamedColumns+ [ ("x", DI.fromList [1 :: Int, 1, 2, 2, 3])+ , ("y", DI.fromList [10 :: Int, 10, 20, 20, 30])+ ]+ )+ )+ )+ )++distinctNoDuplicates :: Test+distinctNoDuplicates =+ TestCase+ ( assertEqual+ "distinct on a DataFrame with no duplicates preserves all rows"+ 3+ ( D.nRows+ ( D.distinct+ ( D.fromNamedColumns+ [ ("x", DI.fromList [1 :: Int, 2, 3])+ , ("y", DI.fromList [10 :: Int, 20, 30])+ ]+ )+ )+ )+ )++distinctAllSameRows :: Test+distinctAllSameRows =+ TestCase+ ( assertEqual+ "distinct on all-identical rows leaves exactly one row"+ 1+ ( D.nRows+ ( D.distinct+ ( D.fromNamedColumns+ [("x", DI.fromList [42 :: Int, 42, 42, 42])]+ )+ )+ )+ )++-- groupBy on an Optional (nullable) column: Nothing values form their own group.+optGroupByDf :: D.DataFrame+optGroupByDf =+ D.fromNamedColumns+ [ ("key", DI.fromList [Just 1 :: Maybe Int, Just 1, Just 2, Nothing, Nothing])+ , ("val", DI.fromList [10 :: Int, 20, 30, 40, 50])+ ]++groupByOptionalColumn :: Test+groupByOptionalColumn =+ TestCase+ ( assertEqual+ "groupBy on an Optional column groups Nothing values together"+ 3 -- groups: Just 1, Just 2, Nothing+ ( D.nRows+ ( optGroupByDf+ & D.groupBy ["key"]+ & D.aggregate [F.count (F.col @Int "val") `as` "val"]+ )+ )+ )+ tests :: [Test] tests = [ TestLabel "foldAggregation" foldAggregation@@ -228,4 +301,8 @@ , TestLabel "aggregationOnNoRows" aggregationOnNoRows+ , TestLabel "distinctRemovesDuplicates" distinctRemovesDuplicates+ , TestLabel "distinctNoDuplicates" distinctNoDuplicates+ , TestLabel "distinctAllSameRows" distinctAllSameRows+ , TestLabel "groupByOptionalColumn" groupByOptionalColumn ]
tests/Operations/Apply.hs view
@@ -9,9 +9,11 @@ import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D import qualified DataFrame as DE+import qualified DataFrame.Functions as F import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.DataFrame as D import qualified DataFrame.Internal.DataFrame as DI+import DataFrame.Operations.Transformations (impute) import Assertions import Test.HUnit@@ -208,6 +210,40 @@ ) ) +imputeData :: D.DataFrame+imputeData =+ D.fromNamedColumns+ [ ("opt", DI.fromList [Just (1 :: Int), Nothing, Just 3])+ , ("plain", DI.fromList [10 :: Int, 20, 30])+ ]++imputeHappyPath :: Test+imputeHappyPath =+ TestCase+ ( assertEqual+ "impute fills Nothing with the given value"+ (Just $ DI.UnboxedColumn (VU.fromList [1 :: Int, 0, 3]))+ (DI.getColumn "opt" $ impute (F.col @(Maybe Int) "opt") 0 imputeData)+ )++imputeColumnNotFound :: Test+imputeColumnNotFound =+ TestCase+ ( assertExpectException+ "[Error Case]"+ (DE.columnNotFound "missing" "impute" (D.columnNames imputeData))+ (print $ impute (F.col @(Maybe Int) "missing") 0 imputeData)+ )++imputeOnNonOptional :: Test+imputeOnNonOptional =+ TestCase+ ( assertExpectException+ "[Error Case]"+ "Cannot impute to a non-Empty column: plain"+ (print $ impute (F.col @(Maybe Int) "plain") 0 imputeData)+ )+ tests :: [Test] tests = [ TestLabel "applyBoxedToUnboxed" applyBoxedToUnboxed@@ -224,4 +260,7 @@ , TestLabel "applyWhereConditionColumnNotFound" applyWhereConditionColumnNotFound , TestLabel "applyWhereTargetColumnNotFound" applyWhereTargetColumnNotFound , TestLabel "applyWhereWAI" applyWhereWAI+ , TestLabel "imputeHappyPath" imputeHappyPath+ , TestLabel "imputeColumnNotFound" imputeColumnNotFound+ , TestLabel "imputeOnNonOptional" imputeOnNonOptional ]
tests/Operations/GroupBy.hs view
@@ -28,7 +28,7 @@ ( assertEqual "Groups by single column" -- We don't yet compare offsets and indices- (D.Grouped testData ["test1"] VU.empty VU.empty)+ (D.Grouped testData ["test1"] VU.empty VU.empty VU.empty) (D.groupBy ["test1"] testData) ) @@ -38,7 +38,7 @@ ( assertEqual "Groups by single column" -- We don't yet compare offsets and indices- (D.Grouped testData ["test1", "test2"] VU.empty VU.empty)+ (D.Grouped testData ["test1", "test2"] VU.empty VU.empty VU.empty) (D.groupBy ["test1", "test2"] testData) )
tests/Operations/Join.hs view
@@ -51,7 +51,7 @@ , ("B", D.fromList [Just "B0", Just "B1" :: Maybe Text, Just "B2"]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] df2 df1))+ (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] df1 df2)) ) testRightJoin :: Test@@ -65,7 +65,7 @@ , ("B", D.fromList ["B0" :: Text, "B1", "B2"]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] df2 df1))+ (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] df1 df2)) ) tdf1 :: DT.TypedDataFrame [DT.Column "key" Text, DT.Column "A" Text]@@ -191,7 +191,7 @@ , ("Ronly", D.fromList [10 :: Int, 11]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (innerJoin ["key"] dfR dfL))+ (D.sortBy [D.Asc (F.col @Text "key")] (innerJoin ["key"] dfL dfR)) ) testLeftJoinWithCollisions :: Test@@ -209,7 +209,7 @@ , ("Ronly", D.fromList [Just 10 :: Maybe Int, Just 11, Nothing]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] dfR dfL))+ (D.sortBy [D.Asc (F.col @Text "key")] (leftJoin ["key"] dfL dfR)) ) testRightJoinWithCollisions :: Test@@ -227,7 +227,7 @@ , ("Lonly", D.fromList [Just "L0" :: Maybe Text, Just "L1", Nothing]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] dfR dfL))+ (D.sortBy [D.Asc (F.col @Text "key")] (rightJoin ["key"] dfL dfR)) ) testOuterJoinWithCollisions :: Test@@ -250,9 +250,96 @@ , ("Ronly", D.fromList [Just 10 :: Maybe Int, Just 11, Nothing, Just 13]) ] )- (D.sortBy [D.Asc (F.col @Text "key")] (fullOuterJoin ["key"] dfR dfL))+ (D.sortBy [D.Asc (F.col @Text "key")] (fullOuterJoin ["key"] dfL dfR)) ) +-- Empty DataFrame fixtures: same schema as df1/df2 but zero rows.+emptyDf1 :: D.DataFrame+emptyDf1 =+ D.fromNamedColumns+ [ ("key", D.fromList ([] :: [Text]))+ , ("A", D.fromList ([] :: [Text]))+ ]++emptyDf2 :: D.DataFrame+emptyDf2 =+ D.fromNamedColumns+ [ ("key", D.fromList ([] :: [Text]))+ , ("B", D.fromList ([] :: [Text]))+ ]++testInnerJoinBothEmpty :: Test+testInnerJoinBothEmpty =+ TestCase+ ( assertEqual+ "Inner join of two empty DataFrames produces 0 rows"+ 0+ (D.nRows (innerJoin ["key"] emptyDf1 emptyDf2))+ )++testInnerJoinLeftEmpty :: Test+testInnerJoinLeftEmpty =+ TestCase+ ( assertEqual+ "Inner join with empty left produces 0 rows"+ 0+ (D.nRows (innerJoin ["key"] emptyDf1 df2))+ )++testInnerJoinRightEmpty :: Test+testInnerJoinRightEmpty =+ TestCase+ ( assertEqual+ "Inner join with empty right produces 0 rows"+ 0+ (D.nRows (innerJoin ["key"] df1 emptyDf2))+ )++testLeftJoinRightEmpty :: Test+testLeftJoinRightEmpty =+ TestCase+ ( assertEqual+ "Left join with empty right returns left"+ 6+ (D.nRows (leftJoin ["key"] df1 emptyDf2))+ )++-- Many-to-many: duplicate keys on both sides produce the cross-product.+-- manyLeft: K0->A0, K1->A1, K1->A2+-- manyRight: K0->B0, K1->B1, K1->B2+-- Expected inner join: 1 K0 pair + 4 K1 pairs = 5 rows+manyLeft :: D.DataFrame+manyLeft =+ D.fromNamedColumns+ [ ("key", D.fromList ["K0" :: Text, "K1", "K1"])+ , ("A", D.fromList ["A0" :: Text, "A1", "A2"])+ ]++manyRight :: D.DataFrame+manyRight =+ D.fromNamedColumns+ [ ("key", D.fromList ["K0" :: Text, "K1", "K1"])+ , ("B", D.fromList ["B0" :: Text, "B1", "B2"])+ ]++testManyToManyInnerJoin :: Test+testManyToManyInnerJoin =+ TestCase+ ( assertEqual+ "Many-to-many inner join produces cross-product row count"+ 5+ (D.nRows (innerJoin ["key"] manyLeft manyRight))+ )++testManyToManyLeftJoin :: Test+testManyToManyLeftJoin =+ TestCase+ ( assertEqual+ "Many-to-many left join includes all cross-product rows"+ 5+ (D.nRows (leftJoin ["key"] manyLeft manyRight))+ )+ tests :: [Test] tests = [ TestLabel "innerJoin" testInnerJoin@@ -266,4 +353,10 @@ , TestLabel "leftJoinWithCollisions" testLeftJoinWithCollisions , TestLabel "rightJoinWithCollisions" testRightJoinWithCollisions , TestLabel "outerJoinWithCollisions" testOuterJoinWithCollisions+ , TestLabel "innerJoinBothEmpty" testInnerJoinBothEmpty+ , TestLabel "innerJoinLeftEmpty" testInnerJoinLeftEmpty+ , TestLabel "innerJoinRightEmpty" testInnerJoinRightEmpty+ , TestLabel "leftJoinRightEmpty" testLeftJoinRightEmpty+ , TestLabel "manyToManyInnerJoin" testManyToManyInnerJoin+ , TestLabel "manyToManyLeftJoin" testManyToManyLeftJoin ]
tests/Operations/Statistics.hs view
@@ -6,6 +6,7 @@ import qualified Data.Vector.Unboxed as VU import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI import qualified DataFrame.Internal.Statistics as D import Assertions@@ -197,6 +198,62 @@ ) ) +-- correlation++correlationDf :: D.DataFrame+correlationDf =+ D.fromNamedColumns+ [ ("x", DI.fromList [1 :: Int, 2, 3, 4, 5])+ , ("y_pos", DI.fromList [1 :: Int, 2, 3, 4, 5])+ , ("y_neg", DI.fromList [5 :: Int, 4, 3, 2, 1])+ ]++-- x perfectly predicts y_pos → r = 1.0+correlationPerfectPositive :: Test+correlationPerfectPositive =+ TestCase+ ( case D.correlation "x" "y_pos" correlationDf of+ Nothing -> assertFailure "Expected Just 1.0, got Nothing"+ Just r ->+ assertBool+ "Perfect positive correlation should be 1.0"+ (abs (r - 1.0) < 1e-10)+ )++-- x perfectly anti-predicts y_neg → r = -1.0+correlationPerfectNegative :: Test+correlationPerfectNegative =+ TestCase+ ( case D.correlation "x" "y_neg" correlationDf of+ Nothing -> assertFailure "Expected Just (-1.0), got Nothing"+ Just r ->+ assertBool+ "Perfect negative correlation should be -1.0"+ (abs (r + 1.0) < 1e-10)+ )++-- Correlation of a column with itself is 1.0+correlationSelfIdentity :: Test+correlationSelfIdentity =+ TestCase+ ( case D.correlation "x" "x" correlationDf of+ Nothing -> assertFailure "Expected Just 1.0, got Nothing"+ Just r ->+ assertBool+ "Correlation of a column with itself should be 1.0"+ (abs (r - 1.0) < 1e-10)+ )++-- Requesting a missing column should throw ColumnNotFoundException+correlationMissingColumn :: Test+correlationMissingColumn =+ TestCase+ ( assertExpectException+ "[Error Case]"+ "missingcol"+ (print $ D.correlation "x" "missingcol" correlationDf)+ )+ tests :: [Test] tests = [ TestLabel "medianOfOddLengthDataSet" medianOfOddLengthDataSet@@ -220,4 +277,8 @@ , TestLabel "wrongQuantileNumber" wrongQuantileNumber , TestLabel "wrongQuantileIndex" wrongQuantileIndex , TestLabel "summarizeOptional" summarizeOptional+ , TestLabel "correlationPerfectPositive" correlationPerfectPositive+ , TestLabel "correlationPerfectNegative" correlationPerfectNegative+ , TestLabel "correlationSelfIdentity" correlationSelfIdentity+ , TestLabel "correlationMissingColumn" correlationMissingColumn ]
+ tests/Operations/Typing.hs view
@@ -0,0 +1,5087 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}++module Operations.Typing where++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified DataFrame as D+import qualified DataFrame.Internal.Column as DI+import qualified DataFrame.Operations.Typing as D++import Data.Time (Day, fromGregorian)+import Test.HUnit (Test (TestCase, TestLabel), assertEqual)++testData :: D.DataFrame+testData =+ D.fromNamedColumns+ [ ("test1", DI.fromList ([1 .. 26] :: [Int]))+ , ("test2", DI.fromList ['a' .. 'z'])+ ]++-- Dimensions+correctDimensions :: Test+correctDimensions = TestCase (assertEqual "should be (26, 2)" (26, 2) (D.dimensions testData))++emptyDataframeDimensions :: Test+emptyDataframeDimensions = TestCase (assertEqual "should be (0, 0)" (0, 0) (D.dimensions D.empty))++dimensionsTest :: [Test]+dimensionsTest =+ [ TestLabel "dimensions_correctDimensions" correctDimensions+ , TestLabel "dimensions_emptyDataframeDimensions" emptyDataframeDimensions+ ]++-- PARSING TESTS+------- 1. SIMPLE CASES+parseBools :: Test+parseBools =+ let afterParse :: [Bool]+ afterParse = [True, True, True] ++ [False, False, False]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as UnboxedColumn of Bools"+ expected+ actual+ )++parseInts :: Test+parseInts =+ let afterParse :: [Int]+ afterParse = [1 .. 50]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints"+ expected+ actual+ )++parseDoubles :: Test+parseDoubles =+ let afterParse :: [Double]+ afterParse = [1.0 .. 50.0] ++ [3.14, 2.22, 8.55, 23.3, 12.22222235049450945049504950]+ beforeParse :: [T.Text]+ beforeParse =+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Doubles without missing values as UnboxedColumn of Doubles"+ expected+ actual+ )++parseDates :: Test+parseDates =+ let afterParse :: [Day]+ afterParse =+ [ fromGregorian 2020 02 12+ , fromGregorian 2020 02 13+ , fromGregorian 2020 02 14+ , fromGregorian 2020 02 15+ , fromGregorian 2020 02 16+ , fromGregorian 2020 02 17+ , fromGregorian 2020 02 18+ , fromGregorian 2020 02 19+ , fromGregorian 2020 02 20+ , fromGregorian 2020 02 21+ , fromGregorian 2020 02 22+ , fromGregorian 2020 02 23+ , fromGregorian 2020 02 24+ , fromGregorian 2020 02 25+ , fromGregorian 2020 02 26+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as BoxedColumn of Days"+ expected+ actual+ )++parseTexts :: Test+parseTexts =+ let afterParse :: [T.Text]+ afterParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , "Surrender now or prepare to fight!"+ , "Meowth, that's right!"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , "Surrender now or prepare to fight!"+ , "Meowth, that's right!"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Text without missing values as BoxedColumn of Text"+ expected+ actual+ )++--- 2. COMBINATION CASES+parseBoolsAndIntsAsTexts :: Test+parseBoolsAndIntsAsTexts =+ let afterParse :: [T.Text]+ afterParse = ["True", "true", "TRUE"] ++ ["False", "false", "FALSE"] ++ ["1", "0", "1"]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses mixture of Bools and Ints as Text"+ expected+ actual+ )++parseIntsAndDoublesAsDoubles :: Test+parseIntsAndDoublesAsDoubles =+ let afterParse :: [Double]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ , 1.0+ , 2.0+ , 3.0+ , 4.0+ , 5.0+ , 6.0+ , 7.0+ , 8.0+ , 9.0+ , 10.0+ , 11.0+ , 12.0+ , 13.0+ , 14.0+ , 15.0+ , 16.0+ , 17.0+ , 18.0+ , 19.0+ , 20.0+ , 21.0+ , 22.0+ , 23.0+ , 24.0+ , 25.0+ , 26.0+ , 27.0+ , 28.0+ , 29.0+ , 30.0+ , 31.0+ , 32.0+ , 33.0+ , 34.0+ , 35.0+ , 36.0+ , 37.0+ , 38.0+ , 39.0+ , 40.0+ , 41.0+ , 42.0+ , 43.0+ , 44.0+ , 45.0+ , 46.0+ , 47.0+ , 48.0+ , 49.0+ , 50.0+ , 3.14+ , 2.22+ , 8.55+ , 23.3+ , 12.22222235049450945049504950+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles"+ expected+ actual+ )++parseIntsAndDatesAsTexts :: Test+parseIntsAndDatesAsTexts =+ let afterParse :: [T.Text]+ afterParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Dates as BoxedColumn of Texts"+ expected+ actual+ )++parseTextsAndDoublesAsTexts :: Test+parseTextsAndDoublesAsTexts =+ let afterParse :: [T.Text]+ afterParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Texts and Doubles as BoxedColumn of Texts"+ expected+ actual+ )++parseDatesAndTextsAsTexts :: Test+parseDatesAndTextsAsTexts =+ let afterParse :: [T.Text]+ afterParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ , "Jessie"+ , "James"+ , "Meowth"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ , "Jessie"+ , "James"+ , "Meowth"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Dates and Texts as BoxedColumn of Texts"+ expected+ actual+ )++-- 3A. PARSING WITH SAFEREAD OFF++parseBoolsWithoutSafeRead :: Test+parseBoolsWithoutSafeRead =+ let afterParse :: [Bool]+ afterParse = replicate 10 True ++ replicate 10 False+ beforeParse :: [T.Text]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as UnboxedColumn of Bools, when safeRead is off"+ expected+ actual+ )++parseIntsWithoutSafeRead :: Test+parseIntsWithoutSafeRead =+ let afterParse :: [Int]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints, when safeRead is off"+ expected+ actual+ )++parseDoublesWithoutSafeRead :: Test+parseDoublesWithoutSafeRead =+ let afterParse :: [Double]+ afterParse =+ [ 1.0+ , 2.0+ , 3.0+ , 4.0+ , 5.0+ , 6.0+ , 7.0+ , 8.0+ , 9.0+ , 10.0+ , 11.0+ , 12.0+ , 13.0+ , 14.0+ , 15.0+ , 16.0+ , 17.0+ , 18.0+ , 19.0+ , 20.0+ , 21.0+ , 22.0+ , 23.0+ , 24.0+ , 25.0+ , 26.0+ , 27.0+ , 28.0+ , 29.0+ , 30.0+ , 31.0+ , 32.0+ , 33.0+ , 34.0+ , 35.0+ , 36.0+ , 37.0+ , 38.0+ , 39.0+ , 40.0+ , 41.0+ , 42.0+ , 43.0+ , 44.0+ , 45.0+ , 46.0+ , 47.0+ , 48.0+ , 49.0+ , 50.0+ , 3.14+ , 2.22+ , 8.55+ , 23.3+ , 12.22222235049450945049504950+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Doubles without missing values as UnboxedColumn of Doubles, when safeRead is off"+ expected+ actual+ )++parseDatesWithoutSafeRead :: Test+parseDatesWithoutSafeRead =+ let afterParse :: [Day]+ afterParse =+ [ fromGregorian 2020 02 12+ , fromGregorian 2020 02 13+ , fromGregorian 2020 02 14+ , fromGregorian 2020 02 15+ , fromGregorian 2020 02 16+ , fromGregorian 2020 02 17+ , fromGregorian 2020 02 18+ , fromGregorian 2020 02 19+ , fromGregorian 2020 02 20+ , fromGregorian 2020 02 21+ , fromGregorian 2020 02 22+ , fromGregorian 2020 02 23+ , fromGregorian 2020 02 24+ , fromGregorian 2020 02 25+ , fromGregorian 2020 02 26+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as BoxedColumn of Days"+ expected+ actual+ )++parseTextsWithoutSafeRead :: Test+parseTextsWithoutSafeRead =+ let afterParse :: [T.Text]+ afterParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , "Surrender now or prepare to fight!"+ , "Meowth, that's right!"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , "Surrender now or prepare to fight!"+ , "Meowth, that's right!"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Text without missing values as BoxedColumn of Text"+ expected+ actual+ )++parseBoolsAndEmptyStringsWithoutSafeRead :: Test+parseBoolsAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe Bool]+ afterParse = replicate 10 Nothing ++ replicate 10 (Just True) ++ replicate 10 (Just False)+ beforeParse :: [T.Text]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools and empty Strings as OptionalColumn of Bools, when safeRead is off"+ expected+ actual+ )++parseIntsAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe Int]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1+ , Just 2+ , Just 3+ , Just 4+ , Just 5+ , Just 6+ , Just 7+ , Just 8+ , Just 9+ , Just 10+ , Just 11+ , Just 12+ , Just 13+ , Just 14+ , Just 15+ , Just 16+ , Just 17+ , Just 18+ , Just 19+ , Just 20+ , Just 21+ , Just 22+ , Just 23+ , Just 24+ , Just 25+ , Just 26+ , Just 27+ , Just 28+ , Just 29+ , Just 30+ , Just 31+ , Just 32+ , Just 33+ , Just 34+ , Just 35+ , Just 36+ , Just 37+ , Just 38+ , Just 39+ , Just 40+ , Just 41+ , Just 42+ , Just 43+ , Just 44+ , Just 45+ , Just 46+ , Just 47+ , Just 48+ , Just 49+ , Just 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is off"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Nothing+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Nothing+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Nothing+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Nothing+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Nothing+ , Just 3.14+ , Just 2.22+ , Just 8.55+ , Just 23.3+ , Just 12.22222235049451+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , ""+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , ""+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , ""+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , ""+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , ""+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseDatesAndEmptyStringsWithoutSafeRead :: Test+parseDatesAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe Day]+ afterParse =+ [ Just $ fromGregorian 2020 02 12+ , Just $ fromGregorian 2020 02 13+ , Just $ fromGregorian 2020 02 14+ , Nothing+ , Just $ fromGregorian 2020 02 15+ , Just $ fromGregorian 2020 02 16+ , Just $ fromGregorian 2020 02 17+ , Nothing+ , Just $ fromGregorian 2020 02 18+ , Just $ fromGregorian 2020 02 19+ , Just $ fromGregorian 2020 02 20+ , Nothing+ , Just $ fromGregorian 2020 02 21+ , Just $ fromGregorian 2020 02 22+ , Just $ fromGregorian 2020 02 23+ , Nothing+ , Just $ fromGregorian 2020 02 24+ , Just $ fromGregorian 2020 02 25+ , Just $ fromGregorian 2020 02 26+ , Nothing+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , ""+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , ""+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , ""+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , ""+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ , ""+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead off"+ expected+ actual+ )++parseTextsAndEmptyStringsWithoutSafeRead :: Test+parseTextsAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Just "To"+ , Just "protect"+ , Just "the"+ , Just "world"+ , Just "from"+ , Just "devastation"+ , Nothing+ , Just "To"+ , Just "unite"+ , Just "all"+ , Just "people"+ , Just "within"+ , Just "our"+ , Just "nation"+ , Nothing+ , Just "To"+ , Just "denounce"+ , Just "the"+ , Just "evils"+ , Just "of"+ , Just "truth"+ , Just "and"+ , Just "love"+ , Nothing+ , Just "To"+ , Just "extend"+ , Just "our"+ , Just "reach"+ , Just "to"+ , Just "the"+ , Just "stars"+ , Just "above"+ , Nothing+ , Just "JESSIE!"+ , Just "JAMES!"+ , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , Nothing+ , Just "Surrender now or prepare to fight!"+ , Nothing+ , Just "Meowth, that's right!"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , ""+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , ""+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , ""+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , ""+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , ""+ , "Surrender now or prepare to fight!"+ , ""+ , "Meowth, that's right!"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual+ )++parseBoolsAndNullishStringsWithoutSafeRead :: Test+parseBoolsAndNullishStringsWithoutSafeRead =+ let afterParse :: [T.Text]+ afterParse = replicate 10 "N/A" ++ replicate 10 "True" ++ replicate 10 "False"+ beforeParse :: [T.Text]+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools with nullish values as BoxedColumn of Texts, when safeRead is off"+ expected+ actual+ )++parseIntsAndNullishStringsWithoutSafeRead :: Test+parseIntsAndNullishStringsWithoutSafeRead =+ let afterParse :: [T.Text]+ afterParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values as BoxedColumn of Texts, when safeRead is off"+ expected+ actual+ )++parseIntsAndDoublesAndNullishStringsWithoutSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithoutSafeRead =+ let afterParse :: [T.Text]+ afterParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "Nothing"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "N/A"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "NULL"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "null"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "NAN"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "Nothing"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "N/A"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "NULL"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "null"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "NAN"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseIntsAndNullishAndEmptyStringsWithoutSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithoutSafeRead =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Just "N/A"+ , Just "N/A"+ , Just "N/A"+ , Just "N/A"+ , Just "N/A"+ , Nothing+ , Just "1"+ , Just "2"+ , Just "3"+ , Just "4"+ , Just "5"+ , Just "6"+ , Just "7"+ , Just "8"+ , Just "9"+ , Just "10"+ , Nothing+ , Just "11"+ , Just "12"+ , Just "13"+ , Just "14"+ , Just "15"+ , Just "16"+ , Just "17"+ , Just "18"+ , Just "19"+ , Just "20"+ , Nothing+ , Just "21"+ , Just "22"+ , Just "23"+ , Just "24"+ , Just "25"+ , Just "26"+ , Just "27"+ , Just "28"+ , Just "29"+ , Just "30"+ , Nothing+ , Just "31"+ , Just "32"+ , Just "33"+ , Just "34"+ , Just "35"+ , Just "36"+ , Just "37"+ , Just "38"+ , Just "39"+ , Just "40"+ , Nothing+ , Just "41"+ , Just "42"+ , Just "43"+ , Just "44"+ , Just "45"+ , Just "46"+ , Just "47"+ , Just "48"+ , Just "49"+ , Just "50"+ , Nothing+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , ""+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , ""+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , ""+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , ""+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , ""+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseTextsAndEmptyAndNullishStringsWithoutSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithoutSafeRead =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Just "To"+ , Just "protect"+ , Just "the"+ , Just "world"+ , Just "from"+ , Just "devastation"+ , Nothing+ , Just "To"+ , Just "unite"+ , Just "all"+ , Just "people"+ , Just "within"+ , Just "our"+ , Just "nation"+ , Nothing+ , Just "To"+ , Just "denounce"+ , Just "the"+ , Just "evils"+ , Just "of"+ , Just "truth"+ , Just "and"+ , Just "love"+ , Nothing+ , Just "To"+ , Just "extend"+ , Just "our"+ , Just "reach"+ , Just "to"+ , Just "the"+ , Just "stars"+ , Just "above"+ , Nothing+ , Just "JESSIE!"+ , Just "JAMES!"+ , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , Nothing+ , Just "Surrender now or prepare to fight!"+ , Nothing+ , Just "Meowth, that's right!"+ , Just "NaN"+ , Just "Nothing"+ , Just "N/A"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , ""+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , ""+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , ""+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , ""+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , ""+ , "Surrender now or prepare to fight!"+ , ""+ , "Meowth, that's right!"+ , "NaN"+ , "Nothing"+ , "N/A"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 10 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual+ )++-- 3B. PARSING WITH SAFEREAD ON+parseBoolsAndEmptyStringsWithSafeRead :: Test+parseBoolsAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Bool]+ afterParse = replicate 10 Nothing ++ replicate 10 (Just True)+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools and empty strings as OptionalColumn of Bools, when safeRead is on"+ expected+ actual+ )++parseIntsAndEmptyStringsWithSafeRead :: Test+parseIntsAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Int]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1+ , Just 2+ , Just 3+ , Just 4+ , Just 5+ , Just 6+ , Just 7+ , Just 8+ , Just 9+ , Just 10+ , Just 11+ , Just 12+ , Just 13+ , Just 14+ , Just 15+ , Just 16+ , Just 17+ , Just 18+ , Just 19+ , Just 20+ , Just 21+ , Just 22+ , Just 23+ , Just 24+ , Just 25+ , Just 26+ , Just 27+ , Just 28+ , Just 29+ , Just 30+ , Just 31+ , Just 32+ , Just 33+ , Just 34+ , Just 35+ , Just 36+ , Just 37+ , Just 38+ , Just 39+ , Just 40+ , Just 41+ , Just 42+ , Just 43+ , Just 44+ , Just 45+ , Just 46+ , Just 47+ , Just 48+ , Just 49+ , Just 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints and empty strings as OptionalColumn of Ints, when safeRead is on"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Nothing+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Nothing+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Nothing+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Nothing+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Nothing+ , Just 3.14+ , Just 2.22+ , Just 8.55+ , Just 23.3+ , Just 12.22222235049451+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , ""+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , ""+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , ""+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , ""+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , ""+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseDatesAndEmptyStringsWithSafeRead :: Test+parseDatesAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Day]+ afterParse =+ [ Just $ fromGregorian 2020 02 12+ , Just $ fromGregorian 2020 02 13+ , Just $ fromGregorian 2020 02 14+ , Nothing+ , Just $ fromGregorian 2020 02 15+ , Just $ fromGregorian 2020 02 16+ , Just $ fromGregorian 2020 02 17+ , Nothing+ , Just $ fromGregorian 2020 02 18+ , Just $ fromGregorian 2020 02 19+ , Just $ fromGregorian 2020 02 20+ , Nothing+ , Just $ fromGregorian 2020 02 21+ , Just $ fromGregorian 2020 02 22+ , Just $ fromGregorian 2020 02 23+ , Nothing+ , Just $ fromGregorian 2020 02 24+ , Just $ fromGregorian 2020 02 25+ , Just $ fromGregorian 2020 02 26+ , Nothing+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , ""+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , ""+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , ""+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , ""+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ , ""+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates and Empty Strings as OptionalColumn of Dates, with safeRead on"+ expected+ actual+ )++parseTextsAndEmptyStringsWithSafeRead :: Test+parseTextsAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Just "To"+ , Just "protect"+ , Just "the"+ , Just "world"+ , Just "from"+ , Just "devastation"+ , Nothing+ , Just "To"+ , Just "unite"+ , Just "all"+ , Just "people"+ , Just "within"+ , Just "our"+ , Just "nation"+ , Nothing+ , Just "To"+ , Just "denounce"+ , Just "the"+ , Just "evils"+ , Just "of"+ , Just "truth"+ , Just "and"+ , Just "love"+ , Nothing+ , Just "To"+ , Just "extend"+ , Just "our"+ , Just "reach"+ , Just "to"+ , Just "the"+ , Just "stars"+ , Just "above"+ , Nothing+ , Just "JESSIE!"+ , Just "JAMES!"+ , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , Nothing+ , Just "Surrender now or prepare to fight!"+ , Nothing+ , Just "Meowth, that's right!"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , ""+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , ""+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , ""+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , ""+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , ""+ , "Surrender now or prepare to fight!"+ , ""+ , "Meowth, that's right!"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead on"+ expected+ actual+ )++parseIntsAndNullishStringsWithSafeRead :: Test+parseIntsAndNullishStringsWithSafeRead =+ let afterParse :: [Maybe Int]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1+ , Just 2+ , Just 3+ , Just 4+ , Just 5+ , Just 6+ , Just 7+ , Just 8+ , Just 9+ , Just 10+ , Just 11+ , Just 12+ , Just 13+ , Just 14+ , Just 15+ , Just 16+ , Just 17+ , Just 18+ , Just 19+ , Just 20+ , Just 21+ , Just 22+ , Just 23+ , Just 24+ , Just 25+ , Just 26+ , Just 27+ , Just 28+ , Just 29+ , Just 30+ , Just 31+ , Just 32+ , Just 33+ , Just 34+ , Just 35+ , Just 36+ , Just 37+ , Just 38+ , Just 39+ , Just 40+ , Just 41+ , Just 42+ , Just 43+ , Just 44+ , Just 45+ , Just 46+ , Just 47+ , Just 48+ , Just 49+ , Just 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints with nullish values as OptionalColumn of Ints, when safeRead is on"+ expected+ actual+ )++parseIntsAndDoublesAndNullishStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishStringsWithSafeRead =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Nothing+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Nothing+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Nothing+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Nothing+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Nothing+ , Just 3.14+ , Just 2.22+ , Just 8.55+ , Just 23.3+ , Just 12.03+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "Nothing"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "N/A"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "NULL"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "null"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "NAN"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.03"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseIntsAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndNullishAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Int]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1+ , Just 2+ , Just 3+ , Just 4+ , Just 5+ , Just 6+ , Just 7+ , Just 8+ , Just 9+ , Just 10+ , Nothing+ , Just 11+ , Just 12+ , Just 13+ , Just 14+ , Just 15+ , Just 16+ , Just 17+ , Just 18+ , Just 19+ , Just 20+ , Nothing+ , Just 21+ , Just 22+ , Just 23+ , Just 24+ , Just 25+ , Just 26+ , Just 27+ , Just 28+ , Just 29+ , Just 30+ , Nothing+ , Just 31+ , Just 32+ , Just 33+ , Just 34+ , Just 35+ , Just 36+ , Just 37+ , Just 38+ , Just 39+ , Just 40+ , Nothing+ , Just 41+ , Just 42+ , Just 43+ , Just 44+ , Just 45+ , Just 46+ , Just 47+ , Just 48+ , Just 49+ , Just 50+ , Nothing+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "Nothing"+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , ""+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , ""+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , ""+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , ""+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , ""+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead :: Test+parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1+ , Just 2+ , Just 3+ , Just 4+ , Just 5+ , Just 6+ , Just 7+ , Just 8+ , Just 9+ , Just 10+ , Nothing+ , Just 11+ , Just 12+ , Just 13+ , Just 14+ , Just 15+ , Just 16+ , Just 17+ , Just 18+ , Just 19+ , Just 20+ , Nothing+ , Just 21+ , Just 22+ , Just 23+ , Just 24+ , Just 25+ , Just 26+ , Just 27+ , Just 28+ , Just 29+ , Just 30+ , Nothing+ , Just 3.14+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "N/A"+ , "N/A"+ , "N/A"+ , "N/A"+ , "Nothing"+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , ""+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , ""+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , ""+ , "3.14"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseTextsAndEmptyAndNullishStringsWithSafeRead :: Test+parseTextsAndEmptyAndNullishStringsWithSafeRead =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Just "To"+ , Just "protect"+ , Just "the"+ , Just "world"+ , Just "from"+ , Just "devastation"+ , Nothing+ , Just "To"+ , Just "unite"+ , Just "all"+ , Just "people"+ , Just "within"+ , Just "our"+ , Just "nation"+ , Nothing+ , Just "To"+ , Just "denounce"+ , Just "the"+ , Just "evils"+ , Just "of"+ , Just "truth"+ , Just "and"+ , Just "love"+ , Nothing+ , Just "To"+ , Just "extend"+ , Just "our"+ , Just "reach"+ , Just "to"+ , Just "the"+ , Just "stars"+ , Just "above"+ , Nothing+ , Just "JESSIE!"+ , Just "JAMES!"+ , Just "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , Nothing+ , Just "Surrender now or prepare to fight!"+ , Nothing+ , Just "Meowth, that's right!"+ , Nothing+ , Nothing+ , Nothing+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , "To"+ , "protect"+ , "the"+ , "world"+ , "from"+ , "devastation"+ , ""+ , "To"+ , "unite"+ , "all"+ , "people"+ , "within"+ , "our"+ , "nation"+ , ""+ , "To"+ , "denounce"+ , "the"+ , "evils"+ , "of"+ , "truth"+ , "and"+ , "love"+ , ""+ , "To"+ , "extend"+ , "our"+ , "reach"+ , "to"+ , "the"+ , "stars"+ , "above"+ , ""+ , "JESSIE!"+ , "JAMES!"+ , "TEAM ROCKET BLASTS OFF AT THE SPEED OF LIGHT!"+ , ""+ , "Surrender now or prepare to fight!"+ , ""+ , "Meowth, that's right!"+ , "NaN"+ , "Nothing"+ , "N/A"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 10 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Texts and Empty Strings as OptionalColumn of Text, with safeRead off"+ expected+ actual+ )++-- 4. PARSING SHOULD NOT DEPEND ON THE NUMBER OF EXAMPLES.+parseBoolsWithOneExample :: Test+parseBoolsWithOneExample =+ let afterParse :: [Bool]+ afterParse = False : replicate 50 True+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"+ expected+ actual+ )++parseBoolsWithManyExamples :: Test+parseBoolsWithManyExamples =+ let afterParse :: [Bool]+ afterParse = False : replicate 50 True+ 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+ in TestCase+ ( assertEqual+ "Correctly parses Bools without missing values as UnboxedColumn of Ints with only one example"+ expected+ actual+ )++parseIntsWithOneExample :: Test+parseIntsWithOneExample =+ let afterParse :: [Int]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints with only one example"+ expected+ actual+ )++parseIntsWithTwentyFiveExamples :: Test+parseIntsWithTwentyFiveExamples =+ let afterParse :: [Int]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 25 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints with some examples"+ expected+ actual+ )++parseIntsWithFortyNineExamples :: Test+parseIntsWithFortyNineExamples =+ let afterParse :: [Int]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 49 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Ints without missing values as UnboxedColumn of Ints with many examples"+ expected+ actual+ )++parseDatesWithOneExample :: Test+parseDatesWithOneExample =+ let afterParse :: [Day]+ afterParse =+ [ fromGregorian 2020 02 12+ , fromGregorian 2020 02 13+ , fromGregorian 2020 02 14+ , fromGregorian 2020 02 15+ , fromGregorian 2020 02 16+ , fromGregorian 2020 02 17+ , fromGregorian 2020 02 18+ , fromGregorian 2020 02 19+ , fromGregorian 2020 02 20+ , fromGregorian 2020 02 21+ , fromGregorian 2020 02 22+ , fromGregorian 2020 02 23+ , fromGregorian 2020 02 24+ , fromGregorian 2020 02 25+ , fromGregorian 2020 02 26+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as BoxedColumn of Days with only one example"+ expected+ actual+ )++parseDatesWithFifteenExamples :: Test+parseDatesWithFifteenExamples =+ let afterParse :: [Day]+ afterParse =+ [ fromGregorian 2020 02 12+ , fromGregorian 2020 02 13+ , fromGregorian 2020 02 14+ , fromGregorian 2020 02 15+ , fromGregorian 2020 02 16+ , fromGregorian 2020 02 17+ , fromGregorian 2020 02 18+ , fromGregorian 2020 02 19+ , fromGregorian 2020 02 20+ , fromGregorian 2020 02 21+ , fromGregorian 2020 02 22+ , fromGregorian 2020 02 23+ , fromGregorian 2020 02 24+ , fromGregorian 2020 02 25+ , fromGregorian 2020 02 26+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "2020-02-12"+ , "2020-02-13"+ , "2020-02-14"+ , "2020-02-15"+ , "2020-02-16"+ , "2020-02-17"+ , "2020-02-18"+ , "2020-02-19"+ , "2020-02-20"+ , "2020-02-21"+ , "2020-02-22"+ , "2020-02-23"+ , "2020-02-24"+ , "2020-02-25"+ , "2020-02-26"+ ]+ expected = DI.BoxedColumn $ V.fromList afterParse+ actual = D.parseDefault [] 15 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses Dates without missing values as BoxedColumn of Days with many examples"+ expected+ actual+ )++parseIntsAndDoublesAsDoublesWithOneExample :: Test+parseIntsAndDoublesAsDoublesWithOneExample =+ let afterParse :: [Double]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ , 1.0+ , 2.0+ , 3.0+ , 4.0+ , 5.0+ , 6.0+ , 7.0+ , 8.0+ , 9.0+ , 10.0+ , 11.0+ , 12.0+ , 13.0+ , 14.0+ , 15.0+ , 16.0+ , 17.0+ , 18.0+ , 19.0+ , 20.0+ , 21.0+ , 22.0+ , 23.0+ , 24.0+ , 25.0+ , 26.0+ , 27.0+ , 28.0+ , 29.0+ , 30.0+ , 31.0+ , 32.0+ , 33.0+ , 34.0+ , 35.0+ , 36.0+ , 37.0+ , 38.0+ , 39.0+ , 40.0+ , 41.0+ , 42.0+ , 43.0+ , 44.0+ , 45.0+ , 46.0+ , 47.0+ , 48.0+ , 49.0+ , 50.0+ , 3.14+ , 2.22+ , 8.55+ , 23.3+ , 12.22222235049450945049504950+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 1 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAsDoublesWithManyExamples :: Test+parseIntsAndDoublesAsDoublesWithManyExamples =+ let afterParse :: [Double]+ afterParse =+ [ 1+ , 2+ , 3+ , 4+ , 5+ , 6+ , 7+ , 8+ , 9+ , 10+ , 11+ , 12+ , 13+ , 14+ , 15+ , 16+ , 17+ , 18+ , 19+ , 20+ , 21+ , 22+ , 23+ , 24+ , 25+ , 26+ , 27+ , 28+ , 29+ , 30+ , 31+ , 32+ , 33+ , 34+ , 35+ , 36+ , 37+ , 38+ , 39+ , 40+ , 41+ , 42+ , 43+ , 44+ , 45+ , 46+ , 47+ , 48+ , 49+ , 50+ , 1.0+ , 2.0+ , 3.0+ , 4.0+ , 5.0+ , 6.0+ , 7.0+ , 8.0+ , 9.0+ , 10.0+ , 11.0+ , 12.0+ , 13.0+ , 14.0+ , 15.0+ , 16.0+ , 17.0+ , 18.0+ , 19.0+ , 20.0+ , 21.0+ , 22.0+ , 23.0+ , 24.0+ , 25.0+ , 26.0+ , 27.0+ , 28.0+ , 29.0+ , 30.0+ , 31.0+ , 32.0+ , 33.0+ , 34.0+ , 35.0+ , 36.0+ , 37.0+ , 38.0+ , 39.0+ , 40.0+ , 41.0+ , 42.0+ , 43.0+ , 44.0+ , 45.0+ , 46.0+ , 47.0+ , 48.0+ , 49.0+ , 50.0+ , 3.14+ , 2.22+ , 8.55+ , 23.3+ , 12.22222235049450945049504950+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.UnboxedColumn $ VU.fromList afterParse+ actual = D.parseDefault [] 50 True "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff :: Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Just 3.14+ , Just 2.22+ , Just 8.55+ , Just 23.3+ , Just 12.22222235049451+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 1 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff =+ let afterParse :: [Maybe Double]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Just 1.0+ , Just 2.0+ , Just 3.0+ , Just 4.0+ , Just 5.0+ , Just 6.0+ , Just 7.0+ , Just 8.0+ , Just 9.0+ , Just 10.0+ , Just 11.0+ , Just 12.0+ , Just 13.0+ , Just 14.0+ , Just 15.0+ , Just 16.0+ , Just 17.0+ , Just 18.0+ , Just 19.0+ , Just 20.0+ , Just 21.0+ , Just 22.0+ , Just 23.0+ , Just 24.0+ , Just 25.0+ , Just 26.0+ , Just 27.0+ , Just 28.0+ , Just 29.0+ , Just 30.0+ , Just 31.0+ , Just 32.0+ , Just 33.0+ , Just 34.0+ , Just 35.0+ , Just 36.0+ , Just 37.0+ , Just 38.0+ , Just 39.0+ , Just 40.0+ , Just 41.0+ , Just 42.0+ , Just 43.0+ , Just 44.0+ , Just 45.0+ , Just 46.0+ , Just 47.0+ , Just 48.0+ , Just 49.0+ , Just 50.0+ , Just 3.14+ , Just 2.22+ , Just 8.55+ , Just 23.3+ , Just 12.22222235049451+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "21"+ , "22"+ , "23"+ , "24"+ , "25"+ , "26"+ , "27"+ , "28"+ , "29"+ , "30"+ , "31"+ , "32"+ , "33"+ , "34"+ , "35"+ , "36"+ , "37"+ , "38"+ , "39"+ , "40"+ , "41"+ , "42"+ , "43"+ , "44"+ , "45"+ , "46"+ , "47"+ , "48"+ , "49"+ , "50"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "31.0"+ , "32.0"+ , "33.0"+ , "34.0"+ , "35.0"+ , "36.0"+ , "37.0"+ , "38.0"+ , "39.0"+ , "40.0"+ , "41.0"+ , "42.0"+ , "43.0"+ , "44.0"+ , "45.0"+ , "46.0"+ , "47.0"+ , "48.0"+ , "49.0"+ , "50.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 30 False "%Y-%m-%d" $ DI.fromVector $ V.fromList beforeParse+ in TestCase+ ( assertEqual+ "Correctly parses a mixture of Ints and Doubles as UnboxedColumn of Doubles with just one example"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "1"+ , Just "2"+ , Just "3"+ , Just "4"+ , Just "5"+ , Just "6"+ , Just "7"+ , Just "8"+ , Just "9"+ , Just "10"+ , Just "11"+ , Just "12"+ , Just "13"+ , Just "14"+ , Just "15"+ , Just "16"+ , Just "17"+ , Just "18"+ , Just "19"+ , Just "20"+ , Just "1.0"+ , Just "2.0"+ , Just "3.0"+ , Just "4.0"+ , Just "5.0"+ , Just "6.0"+ , Just "7.0"+ , Just "8.0"+ , Just "9.0"+ , Just "10.0"+ , Just "11.0"+ , Just "12.0"+ , Just "13.0"+ , Just "14.0"+ , Just "15.0"+ , Just "16.0"+ , Just "17.0"+ , Just "18.0"+ , Just "19.0"+ , Just "20.0"+ , Just "21.0"+ , Just "22.0"+ , Just "23.0"+ , Just "24.0"+ , Just "25.0"+ , Just "26.0"+ , Just "27.0"+ , Just "28.0"+ , Just "29.0"+ , Just "30.0"+ , Just "3.14"+ , Just "2.22"+ , Just "8.55"+ , Just "23.3"+ , Just "12.22222235049451"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual = D.parseDefault [] 1 False "%Y-%m-%d" $ 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"+ expected+ actual+ )++parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff ::+ Test+parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff =+ let afterParse :: [Maybe T.Text]+ afterParse =+ [ Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Nothing+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "NaN"+ , Just "N/A"+ , Just "1"+ , Just "2"+ , Just "3"+ , Just "4"+ , Just "5"+ , Just "6"+ , Just "7"+ , Just "8"+ , Just "9"+ , Just "10"+ , Just "11"+ , Just "12"+ , Just "13"+ , Just "14"+ , Just "15"+ , Just "16"+ , Just "17"+ , Just "18"+ , Just "19"+ , Just "20"+ , Just "1.0"+ , Just "2.0"+ , Just "3.0"+ , Just "4.0"+ , Just "5.0"+ , Just "6.0"+ , Just "7.0"+ , Just "8.0"+ , Just "9.0"+ , Just "10.0"+ , Just "11.0"+ , Just "12.0"+ , Just "13.0"+ , Just "14.0"+ , Just "15.0"+ , Just "16.0"+ , Just "17.0"+ , Just "18.0"+ , Just "19.0"+ , Just "20.0"+ , Just "21.0"+ , Just "22.0"+ , Just "23.0"+ , Just "24.0"+ , Just "25.0"+ , Just "26.0"+ , Just "27.0"+ , Just "28.0"+ , Just "29.0"+ , Just "30.0"+ , Just "3.14"+ , Just "2.22"+ , Just "8.55"+ , Just "23.3"+ , Just "12.22222235049451"+ ]+ beforeParse :: [T.Text]+ beforeParse =+ [ ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , ""+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "NaN"+ , "N/A"+ , "1"+ , "2"+ , "3"+ , "4"+ , "5"+ , "6"+ , "7"+ , "8"+ , "9"+ , "10"+ , "11"+ , "12"+ , "13"+ , "14"+ , "15"+ , "16"+ , "17"+ , "18"+ , "19"+ , "20"+ , "1.0"+ , "2.0"+ , "3.0"+ , "4.0"+ , "5.0"+ , "6.0"+ , "7.0"+ , "8.0"+ , "9.0"+ , "10.0"+ , "11.0"+ , "12.0"+ , "13.0"+ , "14.0"+ , "15.0"+ , "16.0"+ , "17.0"+ , "18.0"+ , "19.0"+ , "20.0"+ , "21.0"+ , "22.0"+ , "23.0"+ , "24.0"+ , "25.0"+ , "26.0"+ , "27.0"+ , "28.0"+ , "29.0"+ , "30.0"+ , "3.14"+ , "2.22"+ , "8.55"+ , "23.3"+ , "12.22222235049451"+ ]+ expected = DI.OptionalColumn $ V.fromList afterParse+ actual =+ D.parseDefault [] 30 False "%Y-%m-%d" $ 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"+ expected+ actual+ )++-- 5. EDGE CASES THAT HAVE TO BE INTERPRETED CORRECTLY++parseManyNullishAndOneInt :: Test+parseManyNullishAndOneInt =+ let afterParse :: [Maybe Int]+ afterParse = replicate 100 Nothing ++ [Just 100000]+ 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+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDouble :: Test+parseManyNullishAndOneDouble =+ let afterParse :: [Maybe Double]+ afterParse = replicate 100 Nothing ++ [Just 3.14]+ 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+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndOneDate :: Test+parseManyNullishAndOneDate =+ let afterParse :: [Maybe Day]+ afterParse = replicate 100 Nothing ++ [Just $ fromGregorian 2024 12 25]+ 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+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseManyNullishAndIncorrectDates :: Test+parseManyNullishAndIncorrectDates =+ let afterParse :: [Maybe T.Text]+ afterParse = replicate 100 Nothing ++ [Just "2024-12-25", Just "2024-12-w6"]+ 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+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++parseRepeatedNullish :: Test+parseRepeatedNullish =+ let afterParse :: [Maybe T.Text]+ afterParse = replicate 100 Nothing+ 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+ in TestCase+ (assertEqual "Correctly parses many Nulls followed by one Int" expected actual)++tests :: [Test]+tests =+ dimensionsTest+ ++ [+ -- 1. SIMPLE CASES+ TestLabel "parseBools" parseBools+ , TestLabel "parseInts" parseInts+ , TestLabel "parseDoubles" parseDoubles+ , TestLabel "parseDates" parseDates+ , TestLabel "parseTexts" parseTexts+ , -- 2. COMBINATION CASES+ TestLabel "parseBoolsAndIntsAsTexts" parseBoolsAndIntsAsTexts+ , TestLabel "parseIntsAndDoublesAsDoubles" parseIntsAndDoublesAsDoubles+ , TestLabel "parseIntsAndDatesAsTexts" parseIntsAndDatesAsTexts+ , TestLabel "parseTextsAndDoublesAsTexts" parseTextsAndDoublesAsTexts+ , TestLabel "parseDatesAndTextsAsTexts" parseDatesAndTextsAsTexts+ , -- 3A. PARSING WITH SAFEREAD OFF+ TestLabel "parseBoolsWithoutSafeRead" parseBoolsWithoutSafeRead+ , TestLabel "parseIntsWithoutSafeRead" parseIntsWithoutSafeRead+ , TestLabel "parseDoublesWithoutSafeRead" parseDoublesWithoutSafeRead+ , TestLabel "parseDatesWithoutSafeRead" parseDatesWithoutSafeRead+ , TestLabel "parseTextsWithoutSafeRead" parseTextsWithoutSafeRead+ , TestLabel+ "parseBoolsAndEmptyStringsWithoutSafeRead"+ parseBoolsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndEmptyStringsWithoutSafeRead"+ parseIntsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsWithoutSafeRead"+ parseIntsAndDoublesAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseDatesAndEmptyStringsWithoutSafeRead"+ parseDatesAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseTextsAndEmptyStringsWithoutSafeRead"+ parseTextsAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseBoolsAndNullishStringsWithoutSafeRead"+ parseBoolsAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndNullishStringsWithoutSafeRead"+ parseIntsAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishStringsWithoutSafeRead"+ parseIntsAndDoublesAndNullishStringsWithoutSafeRead+ , TestLabel+ "parseIntsAndNullishAndEmptyStringsWithoutSafeRead"+ parseIntsAndNullishAndEmptyStringsWithoutSafeRead+ , TestLabel+ "parseTextsAndEmptyAndNullishStringsWithoutSafeRead"+ parseTextsAndEmptyAndNullishStringsWithoutSafeRead+ , -- 3B. PARSING WITH SAFEREAD ON+ TestLabel+ "parseBoolsAndEmptyStringsWithSafeRead"+ parseBoolsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndEmptyStringsWithSafeRead"+ parseIntsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsWithSafeRead"+ parseIntsAndDoublesAndEmptyStringsWithSafeRead+ , TestLabel+ "parseDatesAndEmptyStringsWithSafeRead"+ parseDatesAndEmptyStringsWithSafeRead+ , TestLabel+ "parseTextsAndEmptyStringsWithSafeRead"+ parseTextsAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndNullishStringsWithSafeRead"+ parseIntsAndNullishStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishStringsWithSafeRead"+ parseIntsAndDoublesAndNullishStringsWithSafeRead+ , TestLabel+ "parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead"+ parseIntsAndDoublesAndNullishAndEmptyStringsWithSafeRead+ , TestLabel+ "parseIntsAndNullishAndEmptyStringsWithSafeRead"+ parseIntsAndNullishAndEmptyStringsWithSafeRead+ , TestLabel+ "parseTextsAndEmptyAndNullishStringsWithSafeRead"+ parseTextsAndEmptyAndNullishStringsWithSafeRead+ , -- 4. PARSING MUST NOT DEPEND ON THE NUMBER OF EXAMPLES+ TestLabel "parseBoolsWithOneExample" parseBoolsWithOneExample+ , TestLabel "parseBoolsWithManyExamples" parseBoolsWithManyExamples+ , TestLabel "parseIntsWithOneExample" parseIntsWithOneExample+ , TestLabel "parseIntsWithTwentyFiveExamples" parseIntsWithTwentyFiveExamples+ , TestLabel "parseIntsWithFortyNineExamples" parseIntsWithFortyNineExamples+ , TestLabel "parseDatesWithOneExample" parseDatesWithOneExample+ , TestLabel "parseDatesWithFifteenExamples" parseDatesWithFifteenExamples+ , TestLabel+ "parseIntsAndDoublesAsDoublesWithOneExample"+ parseIntsAndDoublesAsDoublesWithOneExample+ , TestLabel+ "parseIntsAndDoublesAsDoublesWithManyExamples"+ parseIntsAndDoublesAsDoublesWithManyExamples+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff"+ parseIntsAndDoublesAndEmptyStringsAsDoublesWithOneExampleWithSafeReadOff+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff"+ parseIntsAndDoublesAndEmptyStringsAsDoublesWithManyExamplesWithSafeReadOff+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff"+ parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithOneExampleWithSafeReadOff+ , TestLabel+ "parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff"+ parseIntsAndDoublesAndEmptyStringsAndNullishAsStringssWithManyExamplesWithSafeReadOff+ , -- 5. EDGE CASES THAT HAVE TO BE PARSED CORRECTLY+ TestLabel "parseManyNullishAndOneInt" parseManyNullishAndOneInt+ , TestLabel "parseManyNullishAndOneDouble" parseManyNullishAndOneDouble+ , TestLabel "parseRepeatedNullish" parseRepeatedNullish+ ]
tests/Parquet.hs view
@@ -926,6 +926,691 @@ (unsafePerformIO (D.readParquet "./tests/data/mtcars.parquet")) ) +-- ---------------------------------------------------------------------------+-- Group 1: Plain variant+-- ---------------------------------------------------------------------------++allTypesTinyPagesPlain :: Test+allTypesTinyPagesPlain =+ TestCase+ ( assertEqual+ "alltypes_tiny_pages_plain dimensions"+ (7300, 13)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/alltypes_tiny_pages_plain.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 2: Compression codecs (unsupported → error tests)+-- ---------------------------------------------------------------------------++hadoopLz4Compressed :: Test+hadoopLz4Compressed =+ TestCase+ ( assertExpectException+ "hadoopLz4Compressed"+ "LZ4"+ (D.readParquet "./tests/data/hadoop_lz4_compressed.parquet")+ )++hadoopLz4CompressedLarger :: Test+hadoopLz4CompressedLarger =+ TestCase+ ( assertExpectException+ "hadoopLz4CompressedLarger"+ "LZ4"+ (D.readParquet "./tests/data/hadoop_lz4_compressed_larger.parquet")+ )++nonHadoopLz4Compressed :: Test+nonHadoopLz4Compressed =+ TestCase+ ( assertExpectException+ "nonHadoopLz4Compressed"+ "LZ4"+ (D.readParquet "./tests/data/non_hadoop_lz4_compressed.parquet")+ )++lz4RawCompressed :: Test+lz4RawCompressed =+ TestCase+ ( assertExpectException+ "lz4RawCompressed"+ "LZ4_RAW"+ (D.readParquet "./tests/data/lz4_raw_compressed.parquet")+ )++lz4RawCompressedLarger :: Test+lz4RawCompressedLarger =+ TestCase+ ( assertExpectException+ "lz4RawCompressedLarger"+ "LZ4_RAW"+ (D.readParquet "./tests/data/lz4_raw_compressed_larger.parquet")+ )++concatenatedGzipMembers :: Test+concatenatedGzipMembers =+ TestCase+ ( assertExpectException+ "concatenatedGzipMembers"+ "12"+ (D.readParquet "./tests/data/concatenated_gzip_members.parquet")+ )++largeBrotliMap :: Test+largeBrotliMap =+ TestCase+ ( assertExpectException+ "largeBrotliMap"+ "BROTLI"+ (D.readParquet "./tests/data/large_string_map.brotli.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 3: Delta / RLE encodings (unsupported → error tests)+-- ---------------------------------------------------------------------------++deltaBinaryPacked :: Test+deltaBinaryPacked =+ TestCase+ ( assertExpectException+ "deltaBinaryPacked"+ "EDELTA_BINARY_PACKED"+ (D.readParquet "./tests/data/delta_binary_packed.parquet")+ )++deltaByteArray :: Test+deltaByteArray =+ TestCase+ ( assertExpectException+ "deltaByteArray"+ "EDELTA_BYTE_ARRAY"+ (D.readParquet "./tests/data/delta_byte_array.parquet")+ )++deltaEncodingOptionalColumn :: Test+deltaEncodingOptionalColumn =+ TestCase+ ( assertExpectException+ "deltaEncodingOptionalColumn"+ "EDELTA_BINARY_PACKED"+ (D.readParquet "./tests/data/delta_encoding_optional_column.parquet")+ )++deltaEncodingRequiredColumn :: Test+deltaEncodingRequiredColumn =+ TestCase+ ( assertExpectException+ "deltaEncodingRequiredColumn"+ "EDELTA_BINARY_PACKED"+ (D.readParquet "./tests/data/delta_encoding_required_column.parquet")+ )++deltaLengthByteArray :: Test+deltaLengthByteArray =+ TestCase+ ( assertExpectException+ "deltaLengthByteArray"+ "ZSTD"+ (D.readParquet "./tests/data/delta_length_byte_array.parquet")+ )++rleBooleanEncoding :: Test+rleBooleanEncoding =+ TestCase+ ( assertExpectException+ "rleBooleanEncoding"+ "Zlib"+ (D.readParquet "./tests/data/rle_boolean_encoding.parquet")+ )++dictPageOffsetZero :: Test+dictPageOffsetZero =+ TestCase+ ( assertExpectException+ "dictPageOffsetZero"+ "Unknown kv"+ (D.readParquet "./tests/data/dict-page-offset-zero.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 4: Data Page V2 (unsupported → error tests)+-- ---------------------------------------------------------------------------++datapageV2Snappy :: Test+datapageV2Snappy =+ TestCase+ ( assertExpectException+ "datapageV2Snappy"+ "InvalidOffset"+ (D.readParquet "./tests/data/datapage_v2.snappy.parquet")+ )++datapageV2EmptyDatapage :: Test+datapageV2EmptyDatapage =+ TestCase+ ( assertExpectException+ "datapageV2EmptyDatapage"+ "UnexpectedEOF"+ (D.readParquet "./tests/data/datapage_v2_empty_datapage.snappy.parquet")+ )++pageV2EmptyCompressed :: Test+pageV2EmptyCompressed =+ TestCase+ ( assertExpectException+ "pageV2EmptyCompressed"+ "10"+ (D.readParquet "./tests/data/page_v2_empty_compressed.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 5: Checksum files (all read successfully)+-- ---------------------------------------------------------------------------++datapageV1UncompressedChecksum :: Test+datapageV1UncompressedChecksum =+ TestCase+ ( assertEqual+ "datapageV1UncompressedChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-uncompressed-checksum.parquet")+ )+ )+ )++datapageV1SnappyChecksum :: Test+datapageV1SnappyChecksum =+ TestCase+ ( assertEqual+ "datapageV1SnappyChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-snappy-compressed-checksum.parquet")+ )+ )+ )++plainDictUncompressedChecksum :: Test+plainDictUncompressedChecksum =+ TestCase+ ( assertEqual+ "plainDictUncompressedChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/plain-dict-uncompressed-checksum.parquet")+ )+ )+ )++rleDictSnappyChecksum :: Test+rleDictSnappyChecksum =+ TestCase+ ( assertEqual+ "rleDictSnappyChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/rle-dict-snappy-checksum.parquet")+ )+ )+ )++datapageV1CorruptChecksum :: Test+datapageV1CorruptChecksum =+ TestCase+ ( assertEqual+ "datapageV1CorruptChecksum"+ (5120, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/datapage_v1-corrupt-checksum.parquet")+ )+ )+ )++rleDictUncompressedCorruptChecksum :: Test+rleDictUncompressedCorruptChecksum =+ TestCase+ ( assertEqual+ "rleDictUncompressedCorruptChecksum"+ (1000, 2)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/rle-dict-uncompressed-corrupt-checksum.parquet")+ )+ )+ )++-- ---------------------------------------------------------------------------+-- Group 6: NULL handling+-- ---------------------------------------------------------------------------++nullsSnappy :: Test+nullsSnappy =+ TestCase+ ( assertEqual+ "nullsSnappy"+ (8, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nulls.snappy.parquet"))+ )+ )++int32WithNullPages :: Test+int32WithNullPages =+ TestCase+ ( assertEqual+ "int32WithNullPages"+ (1000, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int32_with_null_pages.parquet"))+ )+ )++nullableImpala :: Test+nullableImpala =+ TestCase+ ( assertEqual+ "nullableImpala"+ (7, 13)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nullable.impala.parquet"))+ )+ )++nonnullableImpala :: Test+nonnullableImpala =+ TestCase+ ( assertEqual+ "nonnullableImpala"+ (1, 13)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nonnullable.impala.parquet"))+ )+ )++singleNan :: Test+singleNan =+ TestCase+ ( assertEqual+ "singleNan"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/single_nan.parquet"))+ )+ )++nanInStats :: Test+nanInStats =+ TestCase+ ( assertEqual+ "nanInStats"+ (2, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nan_in_stats.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 7: Decimal types+-- ---------------------------------------------------------------------------++int32Decimal :: Test+int32Decimal =+ TestCase+ ( assertEqual+ "int32Decimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int32_decimal.parquet"))+ )+ )++int64Decimal :: Test+int64Decimal =+ TestCase+ ( assertEqual+ "int64Decimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int64_decimal.parquet"))+ )+ )++byteArrayDecimal :: Test+byteArrayDecimal =+ TestCase+ ( assertEqual+ "byteArrayDecimal"+ (24, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/byte_array_decimal.parquet"))+ )+ )++fixedLengthDecimal :: Test+fixedLengthDecimal =+ TestCase+ ( assertExpectException+ "fixedLengthDecimal"+ "FIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/fixed_length_decimal.parquet")+ )++fixedLengthDecimalLegacy :: Test+fixedLengthDecimalLegacy =+ TestCase+ ( assertExpectException+ "fixedLengthDecimalLegacy"+ "FIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/fixed_length_decimal_legacy.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 8: Binary / fixed-length bytes+-- ---------------------------------------------------------------------------++binaryFile :: Test+binaryFile =+ TestCase+ ( assertEqual+ "binaryFile"+ (12, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/binary.parquet"))+ )+ )++binaryTruncatedMinMax :: Test+binaryTruncatedMinMax =+ TestCase+ ( assertEqual+ "binaryTruncatedMinMax"+ (12, 6)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/binary_truncated_min_max.parquet")+ )+ )+ )++fixedLengthByteArray :: Test+fixedLengthByteArray =+ TestCase+ ( assertExpectException+ "fixedLengthByteArray"+ "FIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/fixed_length_byte_array.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 9: INT96 timestamps+-- ---------------------------------------------------------------------------++int96FromSpark :: Test+int96FromSpark =+ TestCase+ ( assertEqual+ "int96FromSpark"+ (6, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/int96_from_spark.parquet"))+ )+ )++-- ---------------------------------------------------------------------------+-- Group 10: Metadata / index / bloom filters+-- ---------------------------------------------------------------------------++columnChunkKeyValueMetadata :: Test+columnChunkKeyValueMetadata =+ TestCase+ ( assertExpectException+ "columnChunkKeyValueMetadata"+ "Unknown page header field"+ (D.readParquet "./tests/data/column_chunk_key_value_metadata.parquet")+ )++dataIndexBloomEncodingStats :: Test+dataIndexBloomEncodingStats =+ TestCase+ ( assertEqual+ "dataIndexBloomEncodingStats"+ (14, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/data_index_bloom_encoding_stats.parquet")+ )+ )+ )++dataIndexBloomEncodingWithLength :: Test+dataIndexBloomEncodingWithLength =+ TestCase+ ( assertEqual+ "dataIndexBloomEncodingWithLength"+ (14, 1)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/data_index_bloom_encoding_with_length.parquet")+ )+ )+ )++sortColumns :: Test+sortColumns =+ TestCase+ ( assertEqual+ "sortColumns"+ (3, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/sort_columns.parquet"))+ )+ )++overflowI16PageCnt :: Test+overflowI16PageCnt =+ TestCase+ ( assertExpectException+ "overflowI16PageCnt"+ "UNIMPLEMENTED"+ (D.readParquet "./tests/data/overflow_i16_page_cnt.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 11: Nested / complex types and byte-stream-split+-- ---------------------------------------------------------------------------++byteStreamSplitZstd :: Test+byteStreamSplitZstd =+ TestCase+ ( assertExpectException+ "byteStreamSplitZstd"+ "EBYTE_STREAM_SPLIT"+ (D.readParquet "./tests/data/byte_stream_split.zstd.parquet")+ )++byteStreamSplitExtendedGzip :: Test+byteStreamSplitExtendedGzip =+ TestCase+ ( assertExpectException+ "byteStreamSplitExtendedGzip"+ "FIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/byte_stream_split_extended.gzip.parquet")+ )++float16NonzerosAndNans :: Test+float16NonzerosAndNans =+ TestCase+ ( assertExpectException+ "float16NonzerosAndNans"+ "PFIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/float16_nonzeros_and_nans.parquet")+ )++float16ZerosAndNans :: Test+float16ZerosAndNans =+ TestCase+ ( assertExpectException+ "float16ZerosAndNans"+ "PFIXED_LEN_BYTE_ARRAY"+ (D.readParquet "./tests/data/float16_zeros_and_nans.parquet")+ )++nestedListsSnappy :: Test+nestedListsSnappy =+ TestCase+ ( assertEqual+ "nestedListsSnappy"+ (3, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_lists.snappy.parquet"))+ )+ )++nestedMapsSnappy :: Test+nestedMapsSnappy =+ TestCase+ ( assertEqual+ "nestedMapsSnappy"+ (6, 5)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_maps.snappy.parquet"))+ )+ )++nestedStructsRust :: Test+nestedStructsRust =+ TestCase+ ( assertEqual+ "nestedStructsRust"+ (1, 216)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/nested_structs.rust.parquet"))+ )+ )++listColumns :: Test+listColumns =+ TestCase+ ( assertEqual+ "listColumns"+ (3, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/list_columns.parquet"))+ )+ )++oldListStructure :: Test+oldListStructure =+ TestCase+ ( assertEqual+ "oldListStructure"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/old_list_structure.parquet"))+ )+ )++nullList :: Test+nullList =+ TestCase+ ( assertEqual+ "nullList"+ (1, 1)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/null_list.parquet"))+ )+ )++mapNoValue :: Test+mapNoValue =+ TestCase+ ( assertEqual+ "mapNoValue"+ (3, 4)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/map_no_value.parquet"))+ )+ )++incorrectMapSchema :: Test+incorrectMapSchema =+ TestCase+ ( assertEqual+ "incorrectMapSchema"+ (1, 2)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/incorrect_map_schema.parquet"))+ )+ )++repeatedNoAnnotation :: Test+repeatedNoAnnotation =+ TestCase+ ( assertEqual+ "repeatedNoAnnotation"+ (6, 3)+ ( unsafePerformIO+ (fmap D.dimensions (D.readParquet "./tests/data/repeated_no_annotation.parquet"))+ )+ )++repeatedPrimitiveNoList :: Test+repeatedPrimitiveNoList =+ TestCase+ ( assertEqual+ "repeatedPrimitiveNoList"+ (4, 4)+ ( unsafePerformIO+ ( fmap+ D.dimensions+ (D.readParquet "./tests/data/repeated_primitive_no_list.parquet")+ )+ )+ )++unknownLogicalType :: Test+unknownLogicalType =+ TestCase+ ( assertExpectException+ "unknownLogicalType"+ "Unknown logical type"+ (D.readParquet "./tests/data/unknown-logical-type.parquet")+ )++-- ---------------------------------------------------------------------------+-- Group 12: Malformed files+-- ---------------------------------------------------------------------------++nationDictMalformed :: Test+nationDictMalformed =+ TestCase+ ( assertExpectException+ "nationDictMalformed"+ "dict index count mismatch"+ (D.readParquet "./tests/data/nation.dict-malformed.parquet")+ )+ tests :: [Test] tests = [ allTypesPlain@@ -941,4 +1626,76 @@ , allTypesTinyPagesLastFew , allTypesTinyPagesDimensions , transactionsTest+ , -- Group 1+ allTypesTinyPagesPlain+ , -- Group 2: compression codecs+ hadoopLz4Compressed+ , hadoopLz4CompressedLarger+ , nonHadoopLz4Compressed+ , lz4RawCompressed+ , lz4RawCompressedLarger+ , concatenatedGzipMembers+ , largeBrotliMap+ , -- Group 3: delta / rle encodings+ deltaBinaryPacked+ , deltaByteArray+ , deltaEncodingOptionalColumn+ , deltaEncodingRequiredColumn+ , deltaLengthByteArray+ , rleBooleanEncoding+ , dictPageOffsetZero+ , -- Group 4: Data Page V2+ datapageV2Snappy+ , datapageV2EmptyDatapage+ , pageV2EmptyCompressed+ , -- Group 5: checksum files+ datapageV1UncompressedChecksum+ , datapageV1SnappyChecksum+ , plainDictUncompressedChecksum+ , rleDictSnappyChecksum+ , datapageV1CorruptChecksum+ , rleDictUncompressedCorruptChecksum+ , -- Group 6: NULL handling+ nullsSnappy+ , int32WithNullPages+ , nullableImpala+ , nonnullableImpala+ , singleNan+ , nanInStats+ , -- Group 7: decimal types+ int32Decimal+ , int64Decimal+ , byteArrayDecimal+ , fixedLengthDecimal+ , fixedLengthDecimalLegacy+ , -- Group 8: binary / fixed-length bytes+ binaryFile+ , binaryTruncatedMinMax+ , fixedLengthByteArray+ , -- Group 9: INT96 timestamps+ int96FromSpark+ , -- Group 10: metadata / bloom filters+ columnChunkKeyValueMetadata+ , dataIndexBloomEncodingStats+ , dataIndexBloomEncodingWithLength+ , sortColumns+ , overflowI16PageCnt+ , -- Group 11: nested / complex types+ byteStreamSplitZstd+ , byteStreamSplitExtendedGzip+ , float16NonzerosAndNans+ , float16ZerosAndNans+ , nestedListsSnappy+ , nestedMapsSnappy+ , nestedStructsRust+ , listColumns+ , oldListStructure+ , nullList+ , mapNoValue+ , incorrectMapSchema+ , repeatedNoAnnotation+ , repeatedPrimitiveNoList+ , unknownLogicalType+ , -- Group 12: malformed files+ nationDictMalformed ]
+ tests/Properties.hs view
@@ -0,0 +1,37 @@+module Properties where++import qualified DataFrame as D+import DataFrame.Internal.DataFrame (DataFrame, dataframeDimensions)++-- | distinct never increases row count.+prop_distinctMonotone :: DataFrame -> Bool+prop_distinctMonotone df =+ fst (dataframeDimensions (D.distinct df)) <= fst (dataframeDimensions df)++-- | distinct is idempotent.+prop_distinctIdempotent :: DataFrame -> Bool+prop_distinctIdempotent df = D.distinct (D.distinct df) == D.distinct df++-- | Merging empty on the left is identity for row count.+prop_mergeEmptyLeft :: DataFrame -> Bool+prop_mergeEmptyLeft df =+ fst (dataframeDimensions (D.empty <> df)) == fst (dataframeDimensions df)++-- | Merging empty on the right is identity for row count.+prop_mergeEmptyRight :: DataFrame -> Bool+prop_mergeEmptyRight df =+ fst (dataframeDimensions (df <> D.empty)) == fst (dataframeDimensions df)++-- | Merging a DataFrame with itself doubles the row count.+prop_mergeSelfDoubles :: DataFrame -> Bool+prop_mergeSelfDoubles df =+ fst (dataframeDimensions (df <> df)) == 2 * fst (dataframeDimensions df)++tests :: [DataFrame -> Bool]+tests =+ [ prop_distinctMonotone+ , prop_distinctIdempotent+ , prop_mergeEmptyLeft+ , prop_mergeEmptyRight+ , prop_mergeSelfDoubles+ ]