packages feed

dataframe-persistent 0.5.0.0 → 0.5.0.1

raw patch · 12 files changed

+65/−213 lines, 12 filesdep ~dataframe-coredep ~dataframe-operations

Dependency ranges changed: dataframe-core, dataframe-operations

Files

README.md view
@@ -1,3 +1,5 @@+<!-- scripths: 0.5.3.0 -->+ <!--   This file is the runnable scripths source for dataframe-persistent's README.   Every ```haskell block executes in order in one shared session against@@ -35,6 +37,7 @@ and a table.  ```haskell+-- cabal: packages: ../../.., ../.., ../../../dataframe-core, ../../../dataframe-learn, ../../../dataframe-parsing, ../../../dataframe-operations, ../../../dataframe-csv, ../../../dataframe-json, ../../../dataframe-parquet, ../../../dataframe-lazy, ../../../dataframe-viz, ../../../dataframe-expr-serializer, ../../../dataframe-th, ../../../dataframe-csv-th, ../../../dataframe-parquet-th, ../../../dataframe-huggingface -- cabal: build-depends: dataframe, dataframe-persistent, text -- cabal: default-extensions: OverloadedStrings, TemplateHaskell, DataKinds -- cabal: default-extensions: TypeApplications, TypeOperators, FlexibleContexts
dataframe-persistent.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               dataframe-persistent-version:            0.5.0.0+version:            0.5.0.1 synopsis: Persistent database integration for the dataframe library  description: This package provides integration between the dataframe library and the Persistent database library, allowing you to load database entities into DataFrames and save DataFrames back to the database.@@ -43,8 +43,8 @@                       bytestring >= 0.10 && < 0.14,                       conduit >= 1.3 && < 1.5,                       containers >= 0.6.7 && < 0.10,-                      dataframe-core ^>= 2.2,-                      dataframe-operations ^>= 2.2,+                      dataframe-core ^>= 2.6,+                      dataframe-operations ^>= 2.6,                       persistent >= 2.14 && < 3,                       persistent-sqlite >= 2.13 && < 3,                       resourcet >= 1.2 && < 2,@@ -64,7 +64,7 @@                    SchemaTHTests     build-depends:  base >= 4 && < 5,                     dataframe >= 1 && < 4,-                    dataframe-operations ^>= 2.2,+                    dataframe-operations ^>= 2.6,                     dataframe-persistent,                     HUnit >= 1.6 && < 1.8,                     monad-logger >= 0.3 && < 0.5,
src/DataFrame/IO/Persistent.hs view
@@ -176,7 +176,7 @@     let columns = map (createColumnFromEntityData allColumnData) allColNames     return columns --- | Create a single column by extracting and combining data for a specific column name+-- | Create a single column by combining the data for one column name createColumnFromEntityData :: [[(Text, SomeColumn)]] -> Text -> DFCol.Column createColumnFromEntityData allEntityData colName =     -- Find all matching columns for this name@@ -194,8 +194,7 @@ concatSomeColumns :: [SomeColumn] -> DFCol.Column concatSomeColumns [] = DFCol.fromList ([] :: [Text]) concatSomeColumns (SomeColumn (firstVec :: V.Vector a) : rest) =-    -- Since all columns for a given field should have the same type,-    -- we can safely extract and concatenate them+    -- All columns for a field share a type, so concatenating is safe.     let vectors = firstVec : map extractSameType rest         extractSameType (SomeColumn v) = unsafeCoerce v :: V.Vector a         combined = V.concat vectors@@ -231,7 +230,7 @@     (PersistEntity record) => record -> [(Text, SomeColumn)] persistFieldsToColumns record =     let persistValues = toPersistFields record-        -- Get entity definition using proxy - pass the record value to get the correct type+        -- Entity definition, taken from the record value's type         fieldNames =             map (unFieldNameHS . fieldHaskell) $ getEntityFields $ entityDef (Just record)         fieldPairs = zip fieldNames persistValues@@ -252,11 +251,11 @@     PersistDay d -> SomeColumn (V.singleton d)     PersistTimeOfDay tod -> SomeColumn (V.singleton tod)     PersistUTCTime utc -> SomeColumn (V.singleton utc)-    PersistNull -> SomeColumn (V.singleton ("" :: Text)) -- Handle nulls as empty text-    PersistList vals -> SomeColumn (V.singleton (T.pack $ show vals)) -- Convert list to string representation-    PersistMap m -> SomeColumn (V.singleton (T.pack $ show m)) -- Convert map to string representation-    PersistObjectId oid -> SomeColumn (V.singleton (T.pack $ show oid)) -- Convert ObjectId to string-    PersistArray vals -> SomeColumn (V.singleton (T.pack $ show vals)) -- Convert array to string representation+    PersistNull -> SomeColumn (V.singleton ("" :: Text)) -- nulls as empty text+    PersistList vals -> SomeColumn (V.singleton (T.pack $ show vals))+    PersistMap m -> SomeColumn (V.singleton (T.pack $ show m))+    PersistObjectId oid -> SomeColumn (V.singleton (T.pack $ show oid))+    PersistArray vals -> SomeColumn (V.singleton (T.pack $ show vals))     PersistLiteral_ _ bs -> SomeColumn (V.singleton bs) -- Use the raw bytes  -- | Convert columns back to entity (helper for implementations)
src/DataFrame/IO/Persistent/Read.hs view
@@ -6,18 +6,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} -{- |-Module      : DataFrame.IO.Persistent.Read-License     : MIT--The runtime front door for loading SQLite into a 'DataFrame'. Mirrors the-ergonomics of @readCsv@: point at a file and a table and get a 'DataFrame', with-column types inferred from the SQLite schema. No Template Haskell, no entity-boilerplate, and it works directly in GHCi.--For compile-time typed schemas use "DataFrame.IO.Persistent.Schema"; for the-@persistent@ entity path use "DataFrame.IO.Persistent".--}+-- | Runtime front door for loading SQLite tables into a 'DataFrame'. module DataFrame.IO.Persistent.Read (     -- * Reading a whole table (types from the schema)     readTable,@@ -75,16 +64,14 @@ import DataFrame.IO.Persistent.Read.Sqlite (runNamedQuery) import DataFrame.IO.Persistent.Schema.Introspect import DataFrame.Internal.Column (fromList)-import DataFrame.Internal.DataFrame (DataFrame, fromNamedColumns)+import DataFrame.Internal.DataFrame (DataFrame, fromColumns) import DataFrame.Typed.Schema (KnownSchema) import DataFrame.Typed.Types (TypedDataFrame) -{- | A SQLite-side filter pushed into the generated @SELECT@. Build with-'allRows' and the '&'-friendly combinators 'where_', 'limit', 'orderBy'.--}+-- | A SQLite-side filter pushed into the generated @SELECT@. data ReadQuery = ReadQuery     { rqWhere :: Maybe Text-    -- ^ A @WHERE@ body (without the @WHERE@ keyword); may contain @?@ placeholders.+    -- ^ A @WHERE@ body (without the keyword); may contain @?@ placeholders.     , rqParams :: [PersistValue]     -- ^ Values bound to the placeholders in 'rqWhere'.     , rqOrderBy :: Maybe Text@@ -110,9 +97,7 @@ orderBy :: Text -> ReadQuery -> ReadQuery orderBy clause q = q{rqOrderBy = Just clause} -{- | Read an entire table into a 'DataFrame', inferring column types (and-nullability) from the SQLite schema. The @pd.read_sql_table@ analogue.--}+-- | Read an entire table into a 'DataFrame', inferring column types. readTable :: FilePath -> Text -> IO DataFrame readTable path table = readTableWith path table allRows @@ -122,9 +107,7 @@     readers <- columnReadersFor path table     runSqlite (T.pack path) (readColumnsConn table readers q) -{- | 'readTable' over an existing @persistent@ connection (any 'SqlBackend',-so it shares a transaction with @fromPersistent@ / @insert@).--}+-- | 'readTable' over an existing @persistent@ connection. readTableConn :: (MonadResource m) => Text -> ReaderT SqlBackend m DataFrame readTableConn table = readTableConnWith table allRows @@ -143,9 +126,7 @@ columnInfoReader ci =     (ciName ci, columnReaderFor (inferType (ciDeclType ci)) (not (ciNotNull ci))) -{- | Run an arbitrary SQL query into a 'DataFrame'. Result columns keep their-query names (aliases included); types are inferred from the returned values.--}+-- | Run an arbitrary SQL query into a 'DataFrame' with inferred types. readSql :: FilePath -> Text -> IO DataFrame readSql path sql = readSqlWith path sql [] @@ -155,17 +136,7 @@     (names, rows) <- runNamedQuery (T.pack path) sql params     pure (assembleInferred names rows) -{- | Read a whole table straight into a typed 'TypedDataFrame', validating the-schema at the boundary. The schema is a /type argument/ while the database and-table are ordinary values, so the same table name in two different databases is-just two calls with the same @\@Schema@, no generated names to collide:--@-a <- readTableTyped \@ArtistsSchema "db1.sqlite" "artists"-b <- readTableTyped \@ArtistsSchema "db2.sqlite" "artists"--- ... then join a and b-@--}+-- | Read a whole table into a typed dataframe, checking the schema. readTableTyped ::     forall cols. (KnownSchema cols) => FilePath -> Text -> IO (TypedDataFrame cols) readTableTyped path table = readTable path table >>= freezeOrThrow@@ -177,7 +148,7 @@     Text -> ReaderT SqlBackend m (TypedDataFrame cols) readTableTypedConn table = readTableConn table >>= freezeOrThrow --- | Read an arbitrary query (e.g. a join) straight into a typed 'TypedDataFrame'.+-- | Read an arbitrary query (e.g. a join) straight into a 'TypedDataFrame'. readSqlTyped ::     forall cols. (KnownSchema cols) => FilePath -> Text -> IO (TypedDataFrame cols) readSqlTyped path sql = readSql path sql >>= freezeOrThrow@@ -186,15 +157,11 @@ listTables :: FilePath -> IO [Text] listTables = introspectTableNames -{- | 'listTables' over an existing connection (works on any backend, e.g. a-PostgreSQL 'SqlBackend' opened with @withPostgresqlConn@).--}+-- | 'listTables' over an existing connection, on any backend. listTablesConn :: (MonadResource m) => ReaderT SqlBackend m [Text] listTablesConn = introspectTableNamesConn -{- | A @describeColumns@-style summary of a table's schema (one row per column),-read from the schema without scanning the data.--}+-- | Summarise a table's schema, one row per column, without scanning data. describeTable :: FilePath -> Text -> IO DataFrame describeTable path table = schemaFrame <$> introspectTable path (T.unpack table) @@ -204,7 +171,7 @@  schemaFrame :: [ColumnInfo] -> DataFrame schemaFrame cols =-    fromNamedColumns+    fromColumns         [ ("Column Name", fromList (map ciName cols))         , ("Type", fromList (map columnHaskellType cols))         , ("SQLite Type", fromList (map ciDeclType cols))@@ -217,10 +184,7 @@     let base = haskellTypeName (inferType (ciDeclType c))      in if ciNotNull c then base else "Maybe " <> base -{- | Load any @persistent@ entity into a 'DataFrame' without deriving an-instance: filters give you the typed @persistent@ DSL, the rows are decoded-generically. Powers the @declareEntity@ workflow.--}+-- | Load any @persistent@ entity into a 'DataFrame' without a derived instance. selectToDataFrame ::     forall r m.     ( MonadIO m@@ -235,8 +199,7 @@ entitiesToDataFrame ents =     assembleInferred ("id" : entityFieldNames @r) (map entityRow ents) --- @keyToValues@ decodes any key (single or backend) without a @ToBackendKey@--- constraint; single-column keys (the common case) yield the @id@ column.+-- @keyToValues@ decodes any key without a @ToBackendKey@ constraint. entityRow :: (PersistEntity r) => Entity r -> [PersistValue] entityRow (Entity k v) = keyToValues k ++ toPersistFields v @@ -246,9 +209,7 @@         (unFieldNameHS . fieldHaskell)         (getEntityFields (entityDef (Nothing :: Maybe r))) -{- | The streaming engine behind every table reader: build the @SELECT@, fetch-rows, transpose into columns, and apply each column's reader.--}+-- | Engine behind every table reader: build the @SELECT@, fetch, and decode. readColumnsConn ::     (MonadResource m) =>     Text -> [(Text, ColumnReader)] -> ReadQuery -> ReaderT SqlBackend m DataFrame@@ -258,17 +219,15 @@  assembleColumns :: [(Text, ColumnReader)] -> [[PersistValue]] -> DataFrame assembleColumns cols rows =-    fromNamedColumns (zipWith apply cols (cells (length cols) rows))+    fromColumns (zipWith apply cols (cells (length cols) rows))   where     apply (name, rdr) pvs = (name, rdr pvs)  assembleInferred :: [Text] -> [[PersistValue]] -> DataFrame assembleInferred names rows =-    fromNamedColumns (zip names (map inferColumn (cells (length names) rows)))+    fromColumns (zip names (map inferColumn (cells (length names) rows))) -{- | Transpose rows into per-column value lists, padding to @n@ empty columns-when there are no rows (so column structure survives an empty result).--}+-- | Transpose rows into per-column lists, padding to @n@ columns when empty. cells :: Int -> [[PersistValue]] -> [[PersistValue]] cells n [] = replicate n [] cells _ rows = transpose rows
src/DataFrame/IO/Persistent/Read/Columns.hs view
@@ -3,18 +3,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}--- 'buildNullableColumn' deliberately constrains @Columnable (Maybe a)@ (needed to--- build a bitmap-backed nullable column); GHC flags that as simplifiable.+-- buildNullableColumn needs @Columnable (Maybe a)@; GHC flags it simplifiable. {-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-} -{- |-Module      : DataFrame.IO.Persistent.Read.Columns-License     : MIT--Turning rows of 'PersistValue's into typed dataframe 'Column's: decoders driven-by a known 'HaskellType', value-sniffing inference for unknown columns, and the-typed-schema boundary. Shared by the runtime readers and the generated readers.--}+-- | Turn rows of 'PersistValue's into typed dataframe 'Column's. module DataFrame.IO.Persistent.Read.Columns (     ColumnReader,     buildColumn,@@ -40,7 +32,7 @@ import DataFrame.Typed.Schema (KnownSchema) import DataFrame.Typed.Types (TypedDataFrame) --- | A function that turns one column's worth of 'PersistValue's into a 'Column'.+-- | Turns one column's worth of 'PersistValue's into a 'Column'. type ColumnReader = [PersistValue] -> Column  -- | Build a non-null column, decoding each value with 'fromPersistValue'.@@ -56,9 +48,7 @@     Left err -> error (decodeError (typeName @(Maybe a)) err)     Right (xs :: [Maybe a]) -> fromList xs -{- | Infer a column's type by sniffing the first non-null value; nullable if any-value is @PersistNull@. Used for arbitrary queries and generic entity loads.--}+-- | Infer a column's type from its first non-null value. inferColumn :: [PersistValue] -> Column inferColumn pvs = columnReaderFor (inferHaskellType pvs) (any isNull pvs) pvs @@ -109,10 +99,7 @@ decodeError :: String -> Text -> String decodeError ty err = "buildColumn: failed to decode column as " <> ty <> ": " <> T.unpack err -{- | Validate an untyped 'DataFrame' against a typed schema and wrap it, throwing-an 'IOError' on mismatch. The generated @declareTable@ readers call this; the-schema came from the same database, so success is the normal case.--}+-- | Validate an untyped 'DataFrame' against a typed schema, throwing on error. freezeOrThrow ::     forall cols m.     (KnownSchema cols, MonadIO m) =>
src/DataFrame/IO/Persistent/Read/Sqlite.hs view
@@ -1,12 +1,4 @@-{- |-Module      : DataFrame.IO.Persistent.Read.Sqlite-License     : MIT--A minimal direct-SQLite query that returns result column /names/ alongside the-values. @persistent@'s @rawQuery@ drops the names, so for arbitrary @readSql@-queries we prepare the statement ourselves and read @sqlite3_column_name@, which-works even for aliased expressions and empty result sets.--}+-- | Direct SQLite query that returns result column names with the values. module DataFrame.IO.Persistent.Read.Sqlite (     runNamedQuery, ) where@@ -31,17 +23,14 @@ import Foreign.C.Types (CInt (..)) import Foreign.Ptr (Ptr) --- @sqlite3_column_count@ / @sqlite3_column_name@ are not re-exported by--- @Database.Sqlite@, so we bind them directly against the statement pointer.+-- @Database.Sqlite@ does not re-export these, so they are bound directly. foreign import ccall unsafe "sqlite3_column_count"     c_sqlite3_column_count :: Ptr () -> IO CInt  foreign import ccall unsafe "sqlite3_column_name"     c_sqlite3_column_name :: Ptr () -> CInt -> IO CString -{- | Run @sql@ (with bound @params@) against the database at @dbPath@ and return-the result column names together with every row's values.--}+-- | Run @sql@ with bound params and return column names and every row. runNamedQuery :: Text -> Text -> [PersistValue] -> IO ([Text], [[PersistValue]]) runNamedQuery dbPath sql params =     bracket (open dbPath) close $ \conn ->
src/DataFrame/IO/Persistent/Schema.hs view
@@ -1,31 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskellQuotes #-} -{- |-Module      : DataFrame.IO.Persistent.Schema-License     : MIT--Template Haskell that reads a SQLite table's schema __at compile time__ and-generates either a typed-schema type synonym ('declareTable') or a @persistent@-entity ('declareEntity'). Both replace the hand-written @persistLowerCase@ +-@derivePersistentDataFrame@ boilerplate.--'declareTable' emits only the schema /type/. You read into it with the generic-'DataFrame.IO.Persistent.Read.readTableTyped', so the database and table are-runtime values and the same schema can be reused across databases.--@--- Typed schema, checked at compile time:-\$('declareTable' "chinook.db" "artists")--- generates: type ArtistsSchema = '[ '("ArtistId", Int), '("Name", Maybe Text)]--- read any database/table into it:---   readTableTyped \@ArtistsSchema "chinook.db" "artists" :: IO (TypedDataFrame ArtistsSchema)---- Persistent entity: full Filter DSL, write-back, no hand-written schema.-\$('declareEntity' "chinook.db" "artists")--- then: runSqlite "chinook.db" (selectToDataFrame [ArtistsName ==. Just "Accept"] [])-@--}+-- | Template Haskell that reads a SQLite schema at compile time. module DataFrame.IO.Persistent.Schema (     -- * Typed schema     declareTable,@@ -55,10 +31,7 @@ import DataFrame.IO.Persistent.Schema.Common import DataFrame.IO.Persistent.Schema.Introspect -{- | Read @table@ from the SQLite database at @path@ and emit a typed schema-synonym, @type \<Table\>Schema = '[ '(name, ty), ...]@. Read into it with-'DataFrame.IO.Persistent.Read.readTableTyped'.--}+-- | Emit a typed schema synonym for a SQLite table. declareTable :: FilePath -> String -> Q [Dec] declareTable = declareTableWith defaultDeclareOptions @@ -69,14 +42,12 @@     warnOnNumeric cols     schemaDecl opts (T.pack table) cols -{- | Like 'declareTable' but from an explicit column list, so __no database is-opened at build time__ (use in CI / cross-compilation / sdist consumers).--}+-- | Like 'declareTable' but from an explicit column list, opening no database. declareTableFromSchema :: [ColumnInfo] -> String -> Q [Dec] declareTableFromSchema cols name =     schemaDecl defaultDeclareOptions (T.pack name) cols --- | Emit @type \<Table\>Schema = '[ '(name, ty), ...]@ for the selected columns.+-- | Emit @type \<Table\>Schema = '[ '(name, ty), ...]@ for chosen columns. schemaDecl :: DeclareOptions -> Text -> [ColumnInfo] -> Q [Dec] schemaDecl opts table cols0 = do     let cols = selectColumns opts cols0@@ -109,10 +80,7 @@ wrapMaybe True ty = ConT ''Maybe `AppT` ty wrapMaybe False ty = ty -{- | Read @table@ at compile time and generate a @persistent@ entity for it-(via 'mkPersist'), giving the full @persistent@ typed-'Filter' DSL with no-hand-written schema. Load it with 'DataFrame.IO.Persistent.Read.selectToDataFrame'.--}+-- | Generate a @persistent@ entity for a SQLite table at compile time. declareEntity :: FilePath -> String -> Q [Dec] declareEntity = declareEntityWith defaultDeclareOptions @@ -130,16 +98,13 @@     Left errs -> fail ("declareEntity: could not build entity definition:\n" <> show errs)     Right defs -> pure defs -{- | Render the @persistLowerCase@ DSL text for an entity. An integer primary key-becomes the @Id@ field; every other column is a field bound to its DB name.--}+-- | Render the @persistLowerCase@ DSL text for an entity. renderEntityDsl :: DeclareOptions -> Text -> Text -> [ColumnInfo] -> Text renderEntityDsl opts ent table cols =     T.unlines ((ent <> " sql=" <> table) : idLines <> map (renderField opts) fields)   where     (pks, others) = partition (isIntPk opts) cols-    -- Rename persistent's default (ToBackendKey-enabled) key column rather than-    -- declaring a natural key, so generated entities keep fromSqlKey/ToBackendKey.+    -- Rename the default key column so entities keep fromSqlKey/ToBackendKey.     (idLines, fields) = case pks of         [pk] -> (["    Id sql=" <> ciName pk], others)         _ -> ([], cols)
src/DataFrame/IO/Persistent/Schema/Common.hs view
@@ -2,14 +2,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskellQuotes #-} -{- |-Module      : DataFrame.IO.Persistent.Schema.Common-License     : MIT--Shared options, name mangling, type mapping, and compile-time introspection used-by both the typed-schema splice (@declareTable@) and the entity splice-(@declareEntity@) in "DataFrame.IO.Persistent.Schema".--}+-- | Shared options, name mangling, type mapping, and introspection for splices. module DataFrame.IO.Persistent.Schema.Common (     -- * Options     DeclareOptions (..),@@ -52,30 +45,23 @@ import Data.ByteString (ByteString) import DataFrame.IO.Persistent.Schema.Introspect -{- | Knobs shared by 'DataFrame.IO.Persistent.Schema.declareTable' and-'DataFrame.IO.Persistent.Schema.declareEntity'. Start from-'defaultDeclareOptions' and override fields with record syntax.--}+-- | Options shared by 'declareTable' and 'declareEntity'. data DeclareOptions = DeclareOptions     { honorNotNull :: Bool-    {- ^ @True@ (default): @NOT NULL@ columns are non-null, others @Maybe@.-    @False@: every column is treated as nullable.-    -}+    -- ^ @True@ (default): @NOT NULL@ columns are non-null, others @Maybe@.     , typeOverride :: Text -> Text -> Maybe HaskellType-    {- ^ Given @(rawColumnName, declaredSqliteType)@, optionally override the-    inferred Haskell type. Default: never override.-    -}+    -- ^ Override the inferred Haskell type for a column; default is none.     , includeColumns :: Maybe [Text]     -- ^ Restrict to these raw columns (@Nothing@ = all).     , excludeColumns :: [Text]     -- ^ Drop these raw columns (applied after 'includeColumns').     , schemaTypeName :: Maybe String-    -- ^ Override the generated schema type-synonym name (default @\<Table\>Schema@).+    -- ^ Override the generated schema type name (default @\<Table\>Schema@).     , entityName :: Maybe String     -- ^ Override the generated entity name (default PascalCased table).     } --- | Sensible defaults: honor @NOT NULL@, no overrides, all columns, derived names.+-- | Sensible defaults: honor @NOT NULL@, no overrides, all columns. defaultDeclareOptions :: DeclareOptions defaultDeclareOptions =     DeclareOptions@@ -87,9 +73,7 @@         , entityName = Nothing         } -{- | Introspect a table at compile time: register the DB as a dependency (so a-schema change forces a rebuild), then fail helpfully if it can't be read.--}+-- | Introspect a table at compile time, failing helpfully if unreadable. introspectQ :: FilePath -> String -> Q [ColumnInfo] introspectQ path table = do     addDependentFile path@@ -178,7 +162,7 @@ entityNameOf :: DeclareOptions -> Text -> String entityNameOf opts table = fromMaybe (pascalCase table) (entityName opts) --- | Turn a raw column name into a valid lowercase-initial @persistent@ field name.+-- | Turn a raw column name into a valid lowercase @persistent@ field name. fieldIdentifier :: Text -> Text fieldIdentifier raw = T.pack (lowerFirst (ensureLetter cleaned))   where
src/DataFrame/IO/Persistent/Schema/Introspect.hs view
@@ -1,15 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} -{- |-Module      : DataFrame.IO.Persistent.Schema.Introspect-License     : MIT--Compile-time-safe SQLite schema introspection. Everything here is plain 'IO'-(callable from a Template Haskell @runIO@ in "DataFrame.IO.Persistent.Schema")-plus a pure SQLite-affinity → 'HaskellType' mapping. No Template Haskell and no-DataFrame dependencies live here, so the logic can be unit-tested directly.--}+-- | Compile-time-safe SQLite schema introspection and type mapping. module DataFrame.IO.Persistent.Schema.Introspect (     -- * Column metadata     ColumnInfo (..),@@ -55,10 +47,7 @@     }     deriving (Eq, Show) -{- | The closed set of Haskell element types we map SQLite columns onto. Every-constructor corresponds to a type that is both 'Database.Persist.PersistField'-and 'DataFrame.Internal.Column.Columnable'.--}+-- | The closed set of Haskell element types SQLite columns map onto. data HaskellType     = HTInt     | HTDouble@@ -70,11 +59,7 @@     | HTTimeOfDay     deriving (Eq, Show) -{- | Map a declared SQLite type to a 'HaskellType' using SQLite's own-(case-insensitive, substring) type-affinity rules, with a few semantic-refinements for dates/booleans. Unknown or empty declarations fall back the-same way SQLite does (BLOB affinity for empty, numeric otherwise).--}+-- | Map a declared SQLite type to a 'HaskellType' using SQLite affinity rules. inferType :: Text -> HaskellType inferType declared     | "INT" `inUpper` declared = HTInt@@ -104,17 +89,12 @@ anyIn :: [Text] -> Text -> Bool anyIn needles hay = any (`inUpper` hay) needles -{- | Read the columns of @table@ from the SQLite database at @path@, ordered by-their @PRAGMA table_info@ @cid@ (i.e. declaration order).--}+-- | Read a table's columns from a SQLite database in declaration order. introspectTable :: FilePath -> String -> IO [ColumnInfo] introspectTable path table =     runSqlite (T.pack path) (introspectTableConn (T.pack table)) -{- | 'introspectTable' over an existing connection. Backend-agnostic: SQLite uses-@PRAGMA table_info@; every other backend (PostgreSQL, MySQL, etc.) uses the-standard @information_schema.columns@.--}+-- | 'introspectTable' over an existing, backend-agnostic connection. introspectTableConn ::     (MonadResource m) => Text -> ReaderT SqlBackend m [ColumnInfo] introspectTableConn table = do@@ -186,9 +166,7 @@ parsePragmaRow other =     error ("introspectTable: unexpected PRAGMA table_info row: " <> show other) -{- | Parse an @information_schema.columns@ row @(name, data_type, is_nullable)@,-marking 'ciPk' from the table's primary-key column names.--}+-- | Parse an @information_schema.columns@ row into a 'ColumnInfo'. parseInfoSchemaRow :: [Text] -> [PersistValue] -> ColumnInfo parseInfoSchemaRow pkNames [name, dtype, nullable] =     ColumnInfo nm (pvText dtype) (pvText nullable == "NO") (nm `elem` pkNames)
src/DataFrame/IO/Persistent/TH.hs view
@@ -166,8 +166,7 @@ -- | Generate the rowToEntity expression generateRowToEntity :: Name -> [(Name, Bang, Type)] -> Q Exp generateRowToEntity conName fields = do-    -- This is a simplified implementation-    -- In practice, you'd need to extract values from the DataFrame+    -- Simplified: a real version must extract values from the DataFrame     [|Left "Auto-generated rowToEntity not fully implemented"|]  -- | Derive both instances at once
tests/PersistentTests.hs view
@@ -257,7 +257,7 @@     assertBool "Has age column" ("age" `elem` columnNames)     assertBool "Has active column" ("active" `elem` columnNames) -    -- Check that each column has exactly one value (since we're testing a single record)+    -- Each column should hold exactly one value (a single record)     let nameCol = lookup "name" columns         ageCol = lookup "age" columns         activeCol = lookup "active" columns
tests/SchemaTHTests.hs view
@@ -14,10 +14,7 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -{- |-End-to-end tests for the compile-time SQLite schema splices and the runtime-readers, exercised against the committed @data/chinook.db@ fixture.--}+-- | Tests for the SQLite schema splices and runtime readers on chinook.db. module SchemaTHTests (schemaThTests) where  import Control.Monad.IO.Class (liftIO)@@ -45,8 +42,6 @@ chinook :: T.Text chinook = "./data/chinook.db" --- Tier 0: runtime readers --------------------------------------------------- testReadTable :: Test testReadTable = TestCase $ do     df <- readTable "./data/chinook.db" "artists"@@ -80,8 +75,6 @@         (DF.columnNames d)     assertEqual "one row per column" 2 (nRows d) --- Tier 1: typed splice ------------------------------------------------------ testDeclareTableTyped :: Test testDeclareTableTyped = TestCase $ do     -- declareTable emits only the ArtistsSchema type; read into it generically:@@ -91,8 +84,6 @@     let names = DT.columnAsList @"Name" tdf     assertEqual "first artist" (Just (Just "AC/DC")) (listToMaybe names) --- Tier 2: persistent entity generation -------------------------------------- testDeclareEntity :: Test testDeclareEntity = TestCase $ runSqlite chinook $ do     df <- selectToDataFrame ([] :: [Filter Albums]) []@@ -102,8 +93,6 @@ testDeclareEntityFilter = TestCase $ runSqlite chinook $ do     df <- selectToDataFrame [AlbumsArtistId ==. 1] []     liftIO $ assertEqual "AC/DC albums" 2 (nRows df)---- Hand-off: raw read and entity read share one connection ------------------  testHandOff :: Test testHandOff = TestCase $ runSqlite chinook $ do