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 +3/−0
- dataframe-persistent.cabal +4/−4
- src/DataFrame/IO/Persistent.hs +8/−9
- src/DataFrame/IO/Persistent/Read.hs +18/−59
- src/DataFrame/IO/Persistent/Read/Columns.hs +5/−18
- src/DataFrame/IO/Persistent/Read/Sqlite.hs +3/−14
- src/DataFrame/IO/Persistent/Schema.hs +7/−42
- src/DataFrame/IO/Persistent/Schema/Common.hs +8/−24
- src/DataFrame/IO/Persistent/Schema/Introspect.hs +6/−28
- src/DataFrame/IO/Persistent/TH.hs +1/−2
- tests/PersistentTests.hs +1/−1
- tests/SchemaTHTests.hs +1/−12
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