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.2.0.0
+version:            2.3.0.0
 synopsis:           Column operations, expression DSL, and statistics for the dataframe ecosystem.
 description:
     Untyped column operations (select, filter, sort, join, groupBy,
@@ -58,7 +58,7 @@
     build-depends:      base >= 4 && < 5,
                         bytestring >= 0.11 && < 0.14,
                         containers >= 0.6.7 && < 0.10,
-                        dataframe-core >= 2.2 && < 2.3,
+                        dataframe-core >= 2.3 && < 2.4,
                         dataframe-parsing >= 2.2 && < 2.3,
                         random >= 1.2 && < 2,
                         regex-tdfa >= 1.3.0 && < 2,
diff --git a/src/DataFrame/Functions.hs b/src/DataFrame/Functions.hs
--- a/src/DataFrame/Functions.hs
+++ b/src/DataFrame/Functions.hs
@@ -11,7 +11,15 @@
 {-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE UndecidableInstances #-}
 
-module DataFrame.Functions (module DataFrame.Functions, module DataFrame.Operators) where
+module DataFrame.Functions (
+    module DataFrame.Functions,
+    module DataFrame.Operators,
+    add,
+    sub,
+    mult,
+    divide,
+    prettyPrint,
+) where
 
 import DataFrame.Internal.Column
 import DataFrame.Internal.Expression
@@ -476,6 +484,16 @@
 {-# SPECIALIZE isNothing :: Expr (Maybe Double) -> Expr Bool #-}
 {-# SPECIALIZE isNothing :: Expr (Maybe Int) -> Expr Bool #-}
 {-# INLINEABLE isNothing #-}
+
+-- | SQL spelling of 'isNothing'.
+isNull :: (Columnable a) => Expr (Maybe a) -> Expr Bool
+isNull = isNothing
+{-# INLINEABLE isNull #-}
+
+-- | SQL spelling of 'isJust'.
+isNotNull :: (Columnable a) => Expr (Maybe a) -> Expr Bool
+isNotNull = isJust
+{-# INLINEABLE isNotNull #-}
 
 fromJust :: (Columnable a) => Expr (Maybe a) -> Expr a
 fromJust = liftDecorated Maybe.fromJust "fromJust" Nothing
diff --git a/src/DataFrame/Monad.hs b/src/DataFrame/Monad.hs
--- a/src/DataFrame/Monad.hs
+++ b/src/DataFrame/Monad.hs
@@ -24,6 +24,10 @@
     sampleM,
     takeM,
     dropM,
+    selectM,
+    excludeM,
+    sortByM,
+    SortOrder (..),
     columnAsListM,
     filterJustM,
     imputeM,
@@ -43,6 +47,8 @@
 import DataFrame.Internal.Expression (Expr (..), UExpr (..), prettyPrint)
 import DataFrame.Internal.Nullable (BaseType)
 import qualified DataFrame.Operations.Core as D
+import DataFrame.Operations.Permutation (SortOrder)
+import qualified DataFrame.Operations.Permutation as D
 import qualified DataFrame.Operations.Subset as D
 import DataFrame.Operations.Transformations (ImputeOp)
 import qualified DataFrame.Operations.Transformations as D
@@ -110,6 +116,17 @@
 
 dropM :: Int -> FrameM ()
 dropM n = modifyM (D.drop n)
+
+-- | Keep only the named columns. 'dropM' drops rows; this drops columns.
+selectM :: [T.Text] -> FrameM ()
+selectM names = modifyM (D.select names)
+
+-- | Drop the named columns.
+excludeM :: [T.Text] -> FrameM ()
+excludeM names = modifyM (D.exclude names)
+
+sortByM :: [SortOrder] -> FrameM ()
+sortByM orders = modifyM (D.sortBy orders)
 
 columnAsListM :: (Columnable a) => Expr a -> FrameM [a]
 columnAsListM c = inspectM (D.columnAsList c)
diff --git a/src/DataFrame/Operations/Core.hs b/src/DataFrame/Operations/Core.hs
--- a/src/DataFrame/Operations/Core.hs
+++ b/src/DataFrame/Operations/Core.hs
@@ -665,9 +665,9 @@
 fromRows :: [T.Text] -> [[Any]] -> DataFrame
 fromRows names rows =
     L.foldl'
-        (\df i -> insertColumn (names !! i) (mkColumnFromRow i rows) df)
+        (\df (i, name) -> insertColumn name (mkColumnFromRow name i rows) df)
         empty
-        [0 .. length names - 1]
+        (zip [0 ..] names)
 
 {- | O (k * n) Counts the occurences of each value in a given column.
 
diff --git a/src/DataFrame/Operations/Statistics.hs b/src/DataFrame/Operations/Statistics.hs
--- a/src/DataFrame/Operations/Statistics.hs
+++ b/src/DataFrame/Operations/Statistics.hs
@@ -327,7 +327,7 @@
                     if all (== h) (toList @b value)
                         then imputeCore col h df
                         else error "Impute expression returned more than one value"
-    runImputeWith _ _ df = df
+    runImputeWith _ expr _ = throw (NonColumnReferenceException (T.pack (show expr)))
 
 imputeWith ::
     forall a.
diff --git a/src/DataFrame/Operations/Transformations.hs b/src/DataFrame/Operations/Transformations.hs
--- a/src/DataFrame/Operations/Transformations.hs
+++ b/src/DataFrame/Operations/Transformations.hs
@@ -1,5 +1,5 @@
 {-# LANGUAGE ConstrainedClassMethods #-}
-{-# LANGUAGE ExplicitNamespaces #-}
+{-# LANGUAGE DataKinds #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GADTs #-}
@@ -7,6 +7,7 @@
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE UndecidableInstances #-}
 {-# LANGUAGE UndecidableSuperClasses #-}
 
@@ -40,8 +41,10 @@
 import Data.Maybe
 import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))
 import DataFrame.Internal.Column (
+    Column,
     Columnable,
     TypedColumn (..),
+    columnTypeString,
     hasMissing,
     ifoldrColumn,
     imapColumn,
@@ -52,6 +55,8 @@
 import DataFrame.Internal.Interpreter
 import DataFrame.Internal.Nullable (BaseType)
 import DataFrame.Operations.Core
+import GHC.TypeLits (ErrorMessage (..), TypeError)
+import Type.Reflection (typeRep)
 
 -- | O(k) Apply a function to a given column in a dataframe.
 apply ::
@@ -218,7 +223,9 @@
         Left e -> throw e
         Right column' -> insertColumn columnName column' df
 
--- | Core impute implementation for nullable columns. Silently no-ops on non-nullable columns.
+{- | Core impute implementation for nullable columns. A column with no null
+bitmap cannot hold a missing value, so imputing it is a mistake, not a no-op.
+-}
 imputeCore ::
     forall b.
     (Columnable b) =>
@@ -234,9 +241,24 @@
         Left (TypeMismatchException context) -> throw $ TypeMismatchException (context{callingFunctionName = Just "impute"})
         Left exception -> throw exception
         Right res -> res
-    _ -> df
-imputeCore _ _ df = df
+    Just col ->
+        throw
+            (nonNullableColumnError columnName ("Maybe " ++ show (typeRep @b)) col)
+imputeCore expr _ _ = throw (NonColumnReferenceException (T.pack (show expr)))
 
+{- | 'impute' was handed a column that carries no null bitmap, so there is
+nothing it could replace.
+-}
+nonNullableColumnError :: T.Text -> String -> Column -> DataFrameException
+nonNullableColumnError columnName wanted col =
+    TypeMismatchException @() @()
+        MkTypeErrorContext
+            { userType = Left wanted
+            , expectedType = Left (columnTypeString col)
+            , errorColumnName = Just (T.unpack columnName)
+            , callingFunctionName = Just "impute"
+            }
+
 class (Columnable a) => ImputeOp a where
     runImpute :: Expr a -> BaseType a -> DataFrame -> DataFrame
     runImputeWith ::
@@ -246,12 +268,24 @@
         DataFrame ->
         DataFrame
 
-instance {-# OVERLAPPABLE #-} (Columnable a) => ImputeOp a where
-    runImpute _ _ df = df
-    runImputeWith _ _ df = df
+instance
+    {-# OVERLAPPABLE #-}
+    ( Columnable a
+    , TypeError
+        ( 'Text "impute needs a nullable column, but was given Expr "
+            ':<>: 'ShowType a
+            ':$$: 'Text "Try F.col @(Maybe "
+            ':<>: 'ShowType a
+            ':<>: 'Text ") instead."
+        )
+    ) =>
+    ImputeOp a
+    where
+    runImpute = errorWithoutStackTrace "impute: unreachable"
+    runImputeWith = errorWithoutStackTrace "impute: unreachable"
 
 {- | Replace all instances of `Nothing` in a column with the given value.
-When the column is already non-nullable, this is a silent no-op.
+Throws when the column carries no nulls to replace.
 -}
 impute ::
     forall a.
