packages feed

dataframe-fastcsv-1.1.1.0: src/DataFrame/IO/CSV/Fast/Core.hs

{- | The shared read pipeline behind every "DataFrame.IO.CSV.Fast" entry
point: scan for delimiters, classify rows, then build every column with
the typed extraction passes — chunk-parallel when the input is large and
capabilities allow ('autoChunkCount'), unless the caller pins a chunk
count. The result is fully forced before it is returned.
-}
module DataFrame.IO.CSV.Fast.Core (
    readSeparatedCore,
) where

import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as Text
import qualified Data.Vector as Vector
import qualified Data.Vector.Storable as VS

import Control.Exception (throwIO)
import Control.Monad (when)
import Data.Word (Word8)
import Debug.Trace (traceMarkerIO)

import DataFrame.IO.CSV (
    HeaderSpec (..),
    RaggedRowPolicy (..),
    ReadOptions (..),
    UnclosedQuotePolicy (..),
    schemaTypeMap,
 )
import DataFrame.IO.CSV.Fast.Index (
    CsvParseError (..),
    byteStringView,
    checkNoRaggedRows,
    classifyRowEnds,
    collectDataRows,
    fieldsInRow,
    getDelimiterIndicesPolicy,
 )
import DataFrame.IO.CSV.Fast.IndexPar (
    ParIndex (..),
    getDelimiterIndicesPar,
 )
import DataFrame.IO.CSV.Fast.Parallel (autoChunkCount, buildAllColumns)
import DataFrame.IO.CSV.Fast.Passes (ColumnEnv (..))
import DataFrame.IO.CSV.Fast.Slice (FieldCtx (..), extractField, stripBom)
import DataFrame.Internal.DataFrame (DataFrame (..), forceDataFrame)
import DataFrame.Operations.Typing (effectiveSafeRead, parseWithTypes)

{- | @readSeparatedCore chunkSpec separator opts buffer@; @chunkSpec@
pins the chunk count ('Nothing' = capability\/size-gated automatic).
-}
readSeparatedCore ::
    Maybe Int ->
    Word8 ->
    ReadOptions ->
    VS.Vector Word8 ->
    IO DataFrame
readSeparatedCore chunkSpec separator opts fileRaw = do
    let (file, _bomLen) = stripBom fileRaw
        contentLen = VS.length file
    nChunks <- maybe (autoChunkCount contentLen) pure chunkSpec
    -- Parallel reads scan in parallel too; sequential reads (or builds
    -- without SIMD) keep the original scan + row classification.
    mPar <-
        if nChunks > 1
            then getDelimiterIndicesPar nChunks separator contentLen file
            else pure Nothing
    (indices, rowEnds) <- case mPar of
        Just par -> do
            when
                ( piEndsInQuote par
                    && fastCsvOnUnclosedQuote opts == RaiseOnUnclosedQuote
                )
                (throwIO CsvUnclosedQuote)
            pure (piDelims par, piRowEnds par)
        Nothing -> do
            is <-
                getDelimiterIndicesPolicy
                    (fastCsvOnUnclosedQuote opts)
                    separator
                    contentLen
                    file
            pure (is, classifyRowEnds file contentLen is)
    -- Eventlog phase markers (visible under +RTS -l; no-ops otherwise).
    traceMarkerIO "fastcsv:scan-done"
    let totalRows = VS.length rowEnds
    if totalRows == 0
        then return emptyDataFrame
        else do
            let ctx =
                    FieldCtx
                        { fcFile = file
                        , fcBS = byteStringView contentLen file
                        , fcDelims = indices
                        , fcRowEnds = rowEnds
                        , fcContentLen = contentLen
                        , fcTrim = fastCsvTrimUnquoted opts
                        }
                headerNumCol = fieldsInRow rowEnds 0
                (columnNames, dataStartRow, numCol) = case headerSpec opts of
                    NoHeader ->
                        ( Vector.fromList $
                            map (Text.pack . show) [0 .. headerNumCol - 1]
                        , 0
                        , headerNumCol
                        )
                    UseFirstRow ->
                        ( Vector.fromList $
                            map (extractField ctx 0) [0 .. headerNumCol - 1]
                        , 1
                        , headerNumCol
                        )
                    ProvideNames ns ->
                        (Vector.fromList ns, 0, length ns)
            if numCol == 0
                then return emptyDataFrame
                else do
                    let dataRows =
                            collectDataRows
                                file
                                indices
                                rowEnds
                                contentLen
                                dataStartRow
                                totalRows
                        numRow = VS.length dataRows
                    when (fastCsvOnRaggedRow opts == RaiseOnRagged) $
                        checkNoRaggedRows rowEnds dataRows numCol
                    traceMarkerIO "fastcsv:rows-done"
                    VS.unsafeWith file $ \filePtr -> do
                        let env =
                                ColumnEnv
                                    { ceCtx = ctx
                                    , ceRows = dataRows
                                    , ceNumRow = numRow
                                    , cePtr = filePtr
                                    , ceTextHint =
                                        max 64 (contentLen `div` numCol)
                                    , ceOpts = opts
                                    }
                        results <- buildAllColumns nChunks env columnNames numCol
                        traceMarkerIO "fastcsv:build-done"
                        let cols = Vector.fromListN numCol (map fst results)
                            legacySchema =
                                S.fromList
                                    [ columnNames Vector.! c
                                    | (c, (_, True)) <- zip [0 ..] results
                                    ]
                            colIndices =
                                M.fromList $
                                    zip (Vector.toList columnNames) [0 ..]
                            df =
                                DataFrame
                                    cols
                                    colIndices
                                    (numRow, numCol)
                                    M.empty
                            resolveMode =
                                effectiveSafeRead
                                    (safeRead opts)
                                    (safeReadOverrides opts)
                            df' =
                                if S.null legacySchema
                                    then df
                                    else
                                        parseWithTypes
                                            resolveMode
                                            ( M.restrictKeys
                                                (schemaTypeMap (typeSpec opts))
                                                legacySchema
                                            )
                                            df
                        -- The parallel merge already forced every column
                        -- payload, so only the column thunks themselves
                        -- need forcing; the deep walk would re-chase 6M+
                        -- pointers per boxed column for nothing.
                        if nChunks > 1 && S.null legacySchema
                            then do
                                Vector.foldl' (\() c -> c `seq` ()) () cols `seq`
                                    return df
                            else return $! forceDataFrame df'

{- | An empty 'DataFrame' — returned when the input has no delimiters
(empty file or single line with no separator and no newline). Guards
against a divide-by-zero in the row-stride math when 'numCol == 0'.
-}
emptyDataFrame :: DataFrame
emptyDataFrame = DataFrame Vector.empty M.empty (0, 0) M.empty