diff --git a/dataframe-operations.cabal b/dataframe-operations.cabal
--- a/dataframe-operations.cabal
+++ b/dataframe-operations.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-operations
-version:            2.1.0.0
+version:            2.2.0.0
 synopsis:           Column operations, expression DSL, and statistics for the dataframe ecosystem.
 description:
     Untyped column operations (select, filter, sort, join, groupBy,
@@ -58,8 +58,8 @@
     build-depends:      base >= 4 && < 5,
                         bytestring >= 0.11 && < 0.14,
                         containers >= 0.6.7 && < 0.10,
-                        dataframe-core >= 2.1 && < 2.2,
-                        dataframe-parsing >= 2.1 && < 2.2,
+                        dataframe-core >= 2.2 && < 2.3,
+                        dataframe-parsing >= 2.2 && < 2.3,
                         random >= 1.2 && < 2,
                         regex-tdfa >= 1.3.0 && < 2,
                         text >= 2.1 && < 3,
diff --git a/src/DataFrame/Typed/Aggregate.hs b/src/DataFrame/Typed/Aggregate.hs
--- a/src/DataFrame/Typed/Aggregate.hs
+++ b/src/DataFrame/Typed/Aggregate.hs
@@ -83,7 +83,7 @@
     (KnownSymbol name, Columnable a) =>
     TExpr cols a ->
     TAgg keys cols aggs ->
-    TAgg keys cols (Column name a ': aggs)
+    TAgg keys cols ('(name, a) ': aggs)
 as = TAggCons (T.pack (symbolVal (Proxy @name)))
 
 {- | Run a typed aggregation against a grouped DataFrame.
@@ -99,9 +99,9 @@
     . as \@\"orders\" (count (col \@\"order_id\"))
     )
 -- result :: TypedDataFrame
---     '[ Column \"region\" Text
---      , Column \"total\"  Double
---      , Column \"orders\" Int
+--     '[ '(\"region\", Text)
+--      , '(\"total\", Double)
+--      , '(\"orders\", Int)
 --      ]
 @
 -}
diff --git a/src/DataFrame/Typed/Apply.hs b/src/DataFrame/Typed/Apply.hs
--- a/src/DataFrame/Typed/Apply.hs
+++ b/src/DataFrame/Typed/Apply.hs
@@ -48,7 +48,7 @@
     Snoc,
     symbolVals,
  )
-import DataFrame.Typed.Types (Column, TExpr (..), TypedDataFrame (..))
+import DataFrame.Typed.Types (TExpr (..), TypedDataFrame (..))
 
 {- | Map a function over a column, rewriting its element type from @a@ to @b@.
 The schema's entry for @name@ is updated via 'SetColumnType'.
@@ -156,8 +156,8 @@
     ) =>
     TExpr cols a ->
     TypedDataFrame cols ->
-    ( TExpr (Snoc cols (Column name a)) a
-    , TypedDataFrame (Snoc cols (Column name a))
+    ( TExpr (Snoc cols '(name, a)) a
+    , TypedDataFrame (Snoc cols '(name, a))
     )
 deriveWithExpr (TExpr expr) (TDF df) =
     let (e', df') = D.deriveWithExpr colName expr df
@@ -173,7 +173,7 @@
     , Foldable t
     , AssertAbsent name cols
     ) =>
-    a -> t a -> TypedDataFrame cols -> TypedDataFrame (Column name a ': cols)
+    a -> t a -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insertWithDefault def xs (TDF df) =
     unsafeFreeze (D.insertWithDefault def colName xs df)
   where
@@ -186,7 +186,7 @@
     , Columnable a
     , AssertAbsent name cols
     ) =>
-    a -> V.Vector a -> TypedDataFrame cols -> TypedDataFrame (Column name a ': cols)
+    a -> V.Vector a -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insertVectorWithDefault def vec (TDF df) =
     unsafeFreeze (D.insertVectorWithDefault def colName vec df)
   where
@@ -200,7 +200,7 @@
     , VU.Unbox a
     , AssertAbsent name cols
     ) =>
-    VU.Vector a -> TypedDataFrame cols -> TypedDataFrame (Column name a ': cols)
+    VU.Vector a -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insertUnboxedVector vec (TDF df) =
     unsafeFreeze (D.insertUnboxedVector colName vec df)
   where
diff --git a/src/DataFrame/Typed/Expr.hs b/src/DataFrame/Typed/Expr.hs
--- a/src/DataFrame/Typed/Expr.hs
+++ b/src/DataFrame/Typed/Expr.hs
@@ -25,7 +25,7 @@
 == Example
 
 @
-type Schema = '[Column \"age\" Int, Column \"salary\" Double]
+type Schema = '[ '(\"age\", Int), '(\"salary\", Double)]
 
 -- This compiles:
 goodExpr :: TExpr Schema Double
@@ -178,7 +178,7 @@
 Both checks happen at compile time via type families.
 
 @
-salary :: TExpr '[Column \"salary\" Double] Double
+salary :: TExpr '[(\"salary\", Double)] Double
 salary = col \@\"salary\"
 @
 -}
@@ -221,10 +221,6 @@
     TExpr cols Bool -> TExpr cols a -> TExpr cols a -> TExpr cols a
 ifThenElse (TExpr c) (TExpr t) (TExpr e) = TExpr (If c t e)
 
--------------------------------------------------------------------------------
--- Numeric instances (mirror Expr's instances)
--------------------------------------------------------------------------------
-
 instance (Num a, Columnable a) => Num (TExpr cols a) where
     (TExpr a) + (TExpr b) = TExpr (a + b)
     (TExpr a) - (TExpr b) = TExpr (a - b)
@@ -260,10 +256,6 @@
 instance (IsString a, Columnable a) => IsString (TExpr cols a) where
     fromString = TExpr . fromString
 
--------------------------------------------------------------------------------
--- Lifting arbitrary functions
--------------------------------------------------------------------------------
-
 -- | Lift a unary function into a typed expression.
 lift ::
     (Columnable a, Columnable b) => (a -> b) -> TExpr cols a -> TExpr cols b
@@ -381,10 +373,6 @@
 (.||) (TExpr a) (TExpr b) =
     TExpr (Binary (MkBinaryOp (nullCmpOp (||)) "nullor" (Just ".||") True 2) a b)
 
--------------------------------------------------------------------------------
--- Nullable-aware arithmetic operators
--------------------------------------------------------------------------------
-
 infixl 6 .+, .-
 infixl 7 .*, ./
 
@@ -485,10 +473,6 @@
     TExpr cols a -> TExpr cols b -> TExpr cols a
 (.^) (TExpr a) (TExpr b) =
     TExpr (Binary (MkBinaryOp (applyNull2 (^)) "pow" (Just ".^") False 8) a b)
-
--------------------------------------------------------------------------------
--- Nullable-aware comparison operators (three-valued logic)
--------------------------------------------------------------------------------
 
 {- | Nullable-aware equality. Widens numeric operands to their common type,
 so @TExpr cols Double .== TExpr cols Int@ typechecks. Returns @Maybe Bool@
diff --git a/src/DataFrame/Typed/Expr/Extra.hs b/src/DataFrame/Typed/Expr/Extra.hs
--- a/src/DataFrame/Typed/Expr/Extra.hs
+++ b/src/DataFrame/Typed/Expr/Extra.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MonoLocalBinds #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 
 {- | Typed counterparts of the remaining "DataFrame.Functions" expression
diff --git a/src/DataFrame/Typed/Operations.hs b/src/DataFrame/Typed/Operations.hs
--- a/src/DataFrame/Typed/Operations.hs
+++ b/src/DataFrame/Typed/Operations.hs
@@ -85,12 +85,7 @@
 import DataFrame.Typed.Freeze (unsafeFreeze)
 import DataFrame.Typed.Schema
 import DataFrame.Typed.Types (TExpr (..), TSortOrder (..), TypedDataFrame (..))
-import qualified DataFrame.Typed.Types as T
 
--------------------------------------------------------------------------------
--- Schema-preserving operations
--------------------------------------------------------------------------------
-
 {- | Filter rows where a boolean expression evaluates to True.
 The expression is validated against the schema at compile time.
 -}
@@ -113,8 +108,8 @@
 Strips 'Maybe' from all column types in the result schema.
 
 @
-df :: TDF '[Column \"x\" (Maybe Double), Column \"y\" Int]
-filterAllJust df :: TDF '[Column \"x\" Double, Column \"y\" Int]
+df :: TDF '[ '(\"x\", Maybe Double), '(\"y\", Int)]
+filterAllJust df :: TDF '[ '(\"x\", Double), '(\"y\", Int)]
 @
 -}
 filterAllJust :: TypedDataFrame cols -> TypedDataFrame (StripAllMaybe cols)
@@ -203,17 +198,13 @@
 shuffle :: (RandomGen g) => g -> TypedDataFrame cols -> TypedDataFrame cols
 shuffle g (TDF df) = TDF (D.shuffle g df)
 
--------------------------------------------------------------------------------
--- Schema-modifying operations
--------------------------------------------------------------------------------
-
 {- | Derive a new column from a typed expression. The column name must NOT
 already exist in the schema (enforced at compile time via 'AssertAbsent').
 The expression is validated against the current schema.
 
 @
 df' = derive \@\"total\" (col \@\"price\" * col \@\"qty\") df
--- df' :: TDF (Column \"total\" Double ': originalCols)
+-- df' :: TDF ('(\"total\", Double ': originalCols))
 @
 -}
 derive ::
@@ -224,7 +215,7 @@
     ) =>
     TExpr cols a ->
     TypedDataFrame cols ->
-    TypedDataFrame (Snoc cols (T.Column name a))
+    TypedDataFrame (Snoc cols '(name, a))
 derive (TExpr expr) (TDF df) = unsafeFreeze (D.derive colName expr df)
   where
     colName = T.pack (symbolVal (Proxy @name))
@@ -286,7 +277,7 @@
     , Foldable t
     , AssertAbsent name cols
     ) =>
-    t a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
+    t a -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insert xs (TDF df) = unsafeFreeze (D.insert colName xs df)
   where
     colName = T.pack (symbolVal (Proxy @name))
@@ -298,7 +289,7 @@
     , Columnable a
     , AssertAbsent name cols
     ) =>
-    C.Column -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
+    C.Column -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insertColumn col (TDF df) = unsafeFreeze (D.insertColumn colName col df)
   where
     colName = T.pack (symbolVal (Proxy @name))
@@ -310,7 +301,7 @@
     , Columnable a
     , AssertAbsent name cols
     ) =>
-    V.Vector a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
+    V.Vector a -> TypedDataFrame cols -> TypedDataFrame ('(name, a) ': cols)
 insertVector vec (TDF df) = unsafeFreeze (D.insertVector colName vec df)
   where
     colName = T.pack (symbolVal (Proxy @name))
@@ -323,7 +314,7 @@
     , AssertPresent old cols
     , AssertAbsent new cols
     ) =>
-    TypedDataFrame cols -> TypedDataFrame (T.Column new (Lookup old cols) ': cols)
+    TypedDataFrame cols -> TypedDataFrame ('(new, Lookup old cols) ': cols)
 cloneColumn (TDF df) = unsafeFreeze (D.cloneColumn oldName newName df)
   where
     oldName = T.pack (symbolVal (Proxy @old))
@@ -358,13 +349,6 @@
 -- | Vertically merge two DataFrames with the same schema.
 append :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols
 append (TDF a) (TDF b) = TDF (a <> b)
-
--------------------------------------------------------------------------------
--- Set algebra (topos operations)
---
--- Each treats a DataFrame as a /set/ of rows and is schema-preserving:
--- the output type equals the input type; only which rows are present changes.
--------------------------------------------------------------------------------
 
 -- | Rows appearing in either DataFrame, deduplicated (set union).
 union :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols
