diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/dataframe-persistent.cabal b/dataframe-persistent.cabal
--- a/dataframe-persistent.cabal
+++ b/dataframe-persistent.cabal
@@ -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,
diff --git a/src/DataFrame/IO/Persistent.hs b/src/DataFrame/IO/Persistent.hs
--- a/src/DataFrame/IO/Persistent.hs
+++ b/src/DataFrame/IO/Persistent.hs
@@ -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)
diff --git a/src/DataFrame/IO/Persistent/Read.hs b/src/DataFrame/IO/Persistent/Read.hs
--- a/src/DataFrame/IO/Persistent/Read.hs
+++ b/src/DataFrame/IO/Persistent/Read.hs
@@ -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
diff --git a/src/DataFrame/IO/Persistent/Read/Columns.hs b/src/DataFrame/IO/Persistent/Read/Columns.hs
--- a/src/DataFrame/IO/Persistent/Read/Columns.hs
+++ b/src/DataFrame/IO/Persistent/Read/Columns.hs
@@ -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) =>
diff --git a/src/DataFrame/IO/Persistent/Read/Sqlite.hs b/src/DataFrame/IO/Persistent/Read/Sqlite.hs
--- a/src/DataFrame/IO/Persistent/Read/Sqlite.hs
+++ b/src/DataFrame/IO/Persistent/Read/Sqlite.hs
@@ -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 ->
diff --git a/src/DataFrame/IO/Persistent/Schema.hs b/src/DataFrame/IO/Persistent/Schema.hs
--- a/src/DataFrame/IO/Persistent/Schema.hs
+++ b/src/DataFrame/IO/Persistent/Schema.hs
@@ -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)
diff --git a/src/DataFrame/IO/Persistent/Schema/Common.hs b/src/DataFrame/IO/Persistent/Schema/Common.hs
--- a/src/DataFrame/IO/Persistent/Schema/Common.hs
+++ b/src/DataFrame/IO/Persistent/Schema/Common.hs
@@ -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
diff --git a/src/DataFrame/IO/Persistent/Schema/Introspect.hs b/src/DataFrame/IO/Persistent/Schema/Introspect.hs
--- a/src/DataFrame/IO/Persistent/Schema/Introspect.hs
+++ b/src/DataFrame/IO/Persistent/Schema/Introspect.hs
@@ -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)
diff --git a/src/DataFrame/IO/Persistent/TH.hs b/src/DataFrame/IO/Persistent/TH.hs
--- a/src/DataFrame/IO/Persistent/TH.hs
+++ b/src/DataFrame/IO/Persistent/TH.hs
@@ -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
diff --git a/tests/PersistentTests.hs b/tests/PersistentTests.hs
--- a/tests/PersistentTests.hs
+++ b/tests/PersistentTests.hs
@@ -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
diff --git a/tests/SchemaTHTests.hs b/tests/SchemaTHTests.hs
--- a/tests/SchemaTHTests.hs
+++ b/tests/SchemaTHTests.hs
@@ -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
