diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for selda
+
+## 0.1.0.0  -- 2017-04-14
+
+* Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Anton Ekblad
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,500 @@
+What is Selda?
+==============
+[![Build Status](https://travis-ci.org/valderman/selda.svg?branch=master)](https://travis-ci.org/valderman/selda)
+
+Selda is an embedded domain-specific language for interacting with relational
+databases. It was inspired by LINQ and
+[Opaleye](http://hackage.haskell.org/package/opaleye).
+
+
+Features
+========
+
+* Creating, dropping and querying tables using type-safe database schemas.
+* Monadic query language with products, filtering, joins and aggregation.
+* Inserting, updating and deleting rows from tables.
+* Transaction support.
+* Configurable, automatic, consistent in-process caching of query results.
+* Multiple backends: SQLite and PostgreSQL.
+* Lightweight and modular: non-essential features are optional or split into
+  add-on packages.
+
+
+Requirements
+============
+
+Selda requires SQLite 3.7.11+, or PostgreSQL 9+.
+To build the SQLite backend, you need a C compiler installed.
+To build the PostgreSQL backend, you need the `libpq` development libraries
+installed (`libpq-dev` on Debian-based Linux distributions).
+
+
+A brief tutorial
+================
+
+Defining a schema
+-----------------
+
+To work productively with Selda, you will need to enable the `TypeOperators` and
+`OverloadedStrings` extensions.
+
+Table schemas are defined as the product of one or more columns, stitched
+together using the `¤` operator.
+A table is parameterized over the types of its columns, with the column types
+separated by the `:*:` operator. This, by the way, is why you need
+`TypeOperators`.
+
+```
+people :: Table (Text :*: Int :*: Maybe Text)
+people = table "people" $ primary "name" ¤ required "age" ¤ optional "pet"
+
+addresses :: Table (Text :*: Text)
+addresses = table "addresses" $ required "name" ¤ required "city"
+```
+
+Columns may be either `required` or `optional`.
+Although the SQL standard supports nullable primary keys, Selda primary keys
+are always required.
+
+
+Running queries
+---------------
+
+Selda queries are run in the `SeldaT` monad transformer. Any `MonadIO` can be
+extended with database capabilities. Throughout this tutorial, we will simply
+use `SeldaT` on top of the plain `IO` monad.
+`SeldaT` is entered using a backend-specific `withX` function. For instance,
+the SQLite backend uses the `withSQLite` function:
+
+```
+main :: IO ()
+main = withSQLite "my_database.sqlite" $ do
+  people <- getAllPeople
+  liftIO (print people)
+
+getAllPeople :: SeldaT IO [Text :*: Int :*: Maybe Text]
+getAllPeople = query (select people)
+```
+
+This will open the `my_database.sqlite` database for the duration of the
+computation. If the computation terminates normally, or if it raises an
+exception, the database is automatically closed.
+
+Note the somewhat weird return type of `getAllPeople`. In Selda, queries are
+represented using *inductive tuples*: a list of values, separated
+by the `:*:` operator, but where each element can have a different type.
+You can think of them as tuples with a slightly different syntax.
+In this example, `getAllPeople` having a return type of
+`[Text :*: Int :*: Maybe Text]` means that it returns a list of "3-tuples",
+where the three elements have the types `Text`, `Int` and `Maybe Text`
+respectively.
+
+You can pattern match on these values as you would on normal tuples:
+
+```
+firstOfThree :: (a :*: b :*: c) -> a
+firstOfThree (a :*: b :*: c) = a
+```
+
+Since inductive tuples are inductively defined, you may also choose to pattern
+match on just the first few elements:
+
+```
+firstOfN :: (a :*: rest) -> a
+firstOfN (a :*: _) = a
+```
+
+Throughout the rest of this tutorial, we will simply use inductive tuples as if
+they were "normal" tuples.
+
+
+Creating and deleting databases
+-------------------------------
+
+You can use a table definition to create the corresponding table in your
+database backend, as well as delete it.
+
+```
+setup :: SeldaT IO ()
+setup = do
+  createTable people
+  createTable addresses
+
+teardown :: SeldaT IO ()
+teardown = do
+  tryDropTable people
+  tryDropTable addresses
+```
+
+Both creating and deleting tables comes in two variants: the `try` version
+which is a silent no-op when attempting to create a table that already exists
+or delete one that doesn't, and the "plain" version which raises an error.
+
+
+Inserting data
+--------------
+
+Data insertion is done in batches. To insert a batch of rows, pass a list of
+rows where each row is an inductive tuple matching the type of the table.
+Optional values are encoded as `Maybe` values.
+
+```
+populate :: SeldaT IO ()
+populate = do
+  insert_ people
+    [ "Link"      :*: 125 :*: Just "horse"
+    , "Velvet"    :*: 19  :*: Nothing
+    , "Kobayashi" :*: 23  :*: Just "dragon"
+    , "Miyu"      :*: 10  :*: Nothing
+    ]
+  insert_ addresses
+    [ "Link"      :*: "Kakariko"
+    , "Kobayashi" :*: "Tokyo"
+    , "Miyu"      :*: "Fuyukishi"
+    ]
+```
+
+Insertions come in two variants: the "plain" version which reports back the
+number of inserted rows, and one appended with an underscore which returns `()`.
+Use the latter to explicitly indicate your intent to ignore the return value.
+
+There is one gotcha when inserting tuples: auto-incrementing primary keys.
+When inserting data into a table with such a primary key, *the primary key
+column must be omitted from any inserted tuples* to avoid a type error.
+The reason for this is that it's usually a bad idea to set auto-incrementing
+keys manually. The following example inserts a few rows into a table with an
+auto-incrementing primary key:
+
+```
+people' :: Table (Auto Int :*: Text :*: Int :*: Maybe Text)
+people' = table "people_with_ids"
+        $ autoPrimary "id"
+        ¤ required "name"
+        ¤ required "age"
+        ¤ optional "pet"
+
+populate' :: SeldaT IO ()
+populate' = do
+  insert_ people'
+    [ "Link"      :*: 125 :*: Just "horse"
+    , "Velvet"    :*: 19  :*: Nothing
+    , "Kobayashi" :*: 23  :*: Just "dragon"
+    , "Miyu"      :*: 10  :*: Nothing
+    ]
+```
+
+Note that the tuple list passed to `insert_` is the same as for the previous
+example, even though our new table has an additional field `id`.
+Since the `id` field is an auto-incrementing primary key, it will automatically
+be assigned a unique, increasing value.
+Thus, the resulting table would look like this:
+
+```
+id | name      | age | pet
+-----------------------------
+ 0 | Link      | 125 | horse
+ 1 | Velvet    | 19  |
+ 2 | Kobayashi | 23  | dragon
+ 3 | Miyu      | 10  |
+```
+
+If you want manual control over your primary keys, do not use `autoPrimary`.
+
+
+Updating rows
+-------------
+
+To update a table, pass the table and two functions to the `update` function.
+The first is a mapping over table columns, specifying how to update each row.
+The second is a predicate over table columns.
+Only rows satisfying the predicate are updated.
+
+```
+age10Years :: SeldaT IO ()
+age10Years = do
+  update_ people (\(name :*: _ :*: _) -> name ./= "Link")
+                 (\(name :*: age :*: pet) -> name :*: age + 10 :*: pet)
+```
+
+Note that you can use arithmetic, logic and other standard SQL operations on
+the columns in either function. Columns implement the appropriate numeric
+type classes. For operations with less malleable types -- logic and
+comparisons, for instance -- the standard Haskell operators are prefixed
+with a period (`.`).
+
+
+Deleting rows
+-------------
+
+Deleting rows is quite similar to updating them. The only difference is that
+the `delete` operation takes a table and a predicate, specifying which rows
+to delete.
+The following example deletes all minors from the `people` table:
+
+```
+byeMinors :: SeldaT IO ()
+byeMinors = delete_ people (\(_ :*: age :*: _) -> age .< 20)
+```
+
+
+Basic queries
+-------------
+
+Queries are written in the `Query` monad, in which you can query tables,
+restrict the result set, and perform inner, aggregate queries.
+Queries are executed in some Selda monad using the `query` function.
+
+The following example uses the `select` operation to draw each row from the
+`people` table, and the `restrict` operation to remove out all rows except
+those having an `age` column with a value greater than 20.
+
+
+```
+grownups :: Query s (Col s Text)
+grownups = do
+  (name :*: age :*: _) <- select people
+  restrict (age .> 20)
+  return name
+
+printGrownups :: SeldaT IO ()
+printGrownups = do
+  names <- query grownups
+  liftIO (print names)
+```
+
+You may have noticed that in addition to the return type of a query,
+the `Query` type has an additional type parameter `s`.
+We'll cover this parameter in more detail when we get to
+aggregating queries, so for now you can just ignore it.
+
+
+Products and joins
+==================
+
+Of course, data can be drawn from multiple tables. The unfiltered result set
+is essentially the cartesian product of all queried tables.
+For this reason, `restrict` calls should be made as early as possible, to avoid
+creating an unnecessarily large result set.
+
+Arbitrary Haskell values can be injected into queries. As injected values are
+passed as parameters to prepared statements under the hood, there is no need
+to escape data; SQL injection is impossible by construction.
+
+The following example uses data from two tables to find all grown-ups who
+reside in Tokyo. Note the use of the `text` function, to convert a Haskell
+`Text` value into an SQL column literal, as well as the use of `name .== name'`
+to remove all elements from the result set where the name in the `people` table
+does not match the one in the `addresses` table.
+
+```
+grownupsIn :: Text -> Query s (Col s Text)
+grownupsIn city = do
+  (name :*: age :*: _) <- select people
+  restrict (age .> 20)
+  (name' :*: home) <- select addresses
+  restrict (home .== text city .&& name .== name')
+  return name
+
+printGrownupsInTokyo :: SeldaT IO ()
+printGrownupsInTokyo = do
+  names <- query (grownupsIn "Tokyo")
+  liftIO (print names)
+```
+
+Also note that this is slightly different from an SQL join. If, for instance,
+you wanted to get a list of all people and their addresses, you might do
+something like this:
+
+```
+allPeople :: Query s (Col s Text :*: Col s Text)
+allPeople = do
+  (people_name :*: _ :*: _) <- select people
+  (addresses_name :*: city) <- select addresses
+  restrict (people_name == addresses_name)
+  return (people_name :*: city)
+```
+
+This will give you the list of everyone who has an address, resulting in the
+following result set:
+
+```
+name      | city
+---------------------
+Link      | Kakariko
+Kobayashi | Tokyo
+Miyu      | Fuyukishi
+```
+
+Note the absence of Velvet in this result set. Since there is no entry for
+Velvet in the `addresses` table, there can be no entry in the product table
+`people × addresses` where both `people_name` and `addresses_name` are equal
+to `"Velvet"`. To produce a table like the above but with a `NULL` column for
+Velvet's address (or for anyone else who does not have an entry in the
+`addresses` table), you would have to use a join:
+
+```
+allPeople' :: Query s (Col s Text :*: Col s Maybe Text)
+allPeople' = do
+  (name :*: _ :*: _) <- select people
+  (_ :*: city) <- leftJoin (\(name' :*: _) -> name .== name')
+                           (select addresses)
+  return (name :*: city)
+```
+
+This gives us the result table we want:
+
+```
+name      | city
+---------------------
+Link      | Kakariko
+Velvet    |
+Kobayashi | Tokyo
+Miyu      | Fuyukishi
+
+```
+
+The `leftJoin` function left joins its query argument to the current result set
+for all rows matching its predicate argument.
+Note that all columns returned from the inner (or right) query are converted by
+`leftJoin` into nullable columns. As there may not be a right counter part for
+every element in the result set, SQL and Selda alike set any missing joined
+columns to `NULL`.
+
+
+Aggregate queries, grouping and sorting
+---------------------------------------
+
+You can also perform queries that sum, count, or otherwise aggregate their
+result sets. This is done using the `aggregate` function.
+This is where the additional type parameter to `Query` comes into play.
+When used as an inner query, aggregate queries must not depend on any columns
+from the outer query. To enforce this, the `aggregate` function forces all
+operations to take place in the `Query (Inner s)` monad, if the outer query
+takes place in the `Query s` monad. This ensures that aggregate inner queries
+can only communicate with their outside query by returning some value.
+
+Like in standard SQL, aggregate queries can be grouped by column name or by
+some arbitrary expression.
+An aggregate subquery must return at least one aggregate column, obtained using
+`sum_`, `avg`, `count`, or one of the other provided aggregate functions.
+Note that aggregate columns, having type `Aggr s a`, are different from normal
+columns of type `Col s a`.
+Since SQL does not allow aggregate functions in `WHERE` clauses, Selda prevents
+them from being used in arguments to `restrict`.
+
+The following example uses an aggregate query to calculate how many home each
+person has, and order the result set with the most affluent homeowners at the
+top.
+
+```
+countHomes :: Query s (Col s Text :*: Col s Int)
+countHomes = do
+  (name :*: _ :*: _) <- select people
+  (owner :*: homes) <- aggregate $ do
+    (owner :*: city) <- select addresses
+    owner' <- groupBy owner
+    return (count city :*: owner')
+  restrict (owner .== name)
+  order homes descending
+  return (owner :*: homes)
+```
+
+Note how `groupBy` returns an aggregate version of its argument, which can be
+returned from the aggregate query. In this example, returning `owner` instead of
+`owner'` wouldn't work since the former is a plain column and not an aggregate.
+
+
+Transactions
+------------
+
+All databases supported by Selda guarantee that each query is atomic: either
+the entire query is performed in one go, with no observable intermediate state,
+or the whole query fails without leaving a trace in the database.
+However, sometimes this guarantee is not enough.
+Consider, for instance, a money transfer from Alice's bank account to Bob's.
+This involves at least two queries: one to remove the money from
+Alice's account, and one to add the same amount to Bob's.
+Clearly, it would be *bad* if this operation were to be interrupted after
+withdrawing the money from Alice's account but before depositing it into Bob's.
+
+The solution to this problem is *transactions*: a mechanism by which
+*a list of queries* gain the same atomicity guarantees as a single query always
+enjoys. Using transactions in Selda is super easy:
+
+```
+transferMoney :: Text -> Text -> Double -> SeldaT s ()
+transferMoney from to amount = do
+  transaction $ do
+    update_ accounts (\(owner :*: _) -> owner .== text from)
+                     (\(owner :*: money) -> owner :*: money - float amount)
+    update_ accounts (\(owner :*: _) -> owner .== text to)
+                     (\(owner :*: money) -> owner :*: money + float amount)
+```
+
+This is all there is to it: pass the entire computation to the `transaction`
+function, and the whole computation is guaranteed to either execute atomically,
+or to fail without leaving a trace in the database.
+If an exception is raised during the computation, it will of course be rolled
+back.
+
+Do be careful, however, to avoid performing IO within a query.
+While they will not affect the atomicity of the computation as far as the
+database is concerned, the computations themselves can obviously not be
+rolled back.
+
+
+In-process caching
+------------------
+
+In many applications, read operations are orders of magnitude more common than
+write operations. For such applications, it is often useful to *cache* the
+results of a query, to avoid having the database perform the same, potentially
+heavy, query over and over even though we *know* we'll get the same result
+every time.
+
+Selda supports automatic caching of query results out of the box.
+However, it is turned off by default.
+To enable caching, use the `setLocalCache` function.
+
+```
+main = withPostgreSQL connection_info $ do
+  setLocalCache 1000
+  ...
+```
+
+This will enable local caching of up to 1,000 different results.
+When that limit is reached, the least recently used result will be discarded,
+so the next request for that result will need to actually execute the query
+on the database backend.
+If caching was already enabled, changing the maximum number of cached results
+will discard the cache's previous contents.
+Setting the cache limit to 0 disables caching again.
+
+To make sure that the cache is always consistent with the underlying database,
+Selda keeps track of which tables each query depends on.
+Whenever an insert, update, delete or drop is issued on a table `t`, all cached
+queries that depend on `t` will be discarded.
+
+This guarantees consistency between cache and database, but *only* under the
+assumption that *no other process will modify the database*.
+If this assumption does not hold for your application, you should avoid using
+in-process caching.
+It is perfectly fine, however, to have multiple *threads* within the same
+application modifying the same database as long as they're all using Selda
+to do it, as the cache shared between all Selda computations
+running in the same process.
+
+
+TODOs
+=====
+
+Features that would be nice to have but are not yet implemented.
+
+* If/else.
+* Examples.
+* Foreign keys.
+* `WHERE x IN (SELECT ...)`
+* `SELECT INTO`.
+* Constraints other than primary key.
+* Database schema upgrades.
+* Stack build.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/selda.cabal b/selda.cabal
new file mode 100644
--- /dev/null
+++ b/selda.cabal
@@ -0,0 +1,99 @@
+name:                selda
+version:             0.1.0.0
+synopsis:            Type-safe, high-level EDSL for interacting with relational databases.
+description:         This package provides an EDSL for writing portable, type-safe, high-level
+                     database code. Its feature set includes querying and modifying databases,
+                     automatic, in-process caching with consistency guarantees, and transaction
+                     support.
+
+                     See the package readme for a brief usage tutorial.
+                     
+                     To use this package you need at least one backend package, in addition to
+                     this package. There are currently two different backend packages:
+                     selda-sqlite and selda-postgresql.
+homepage:            https://github.com/valderman/selda
+license:             MIT
+license-file:        LICENSE
+author:              Anton Ekblad
+maintainer:          anton@ekblad.cc
+category:            Database
+build-type:          Simple
+extra-source-files:  ChangeLog.md
+cabal-version:       >=1.10
+tested-with:         GHC == 7.10.3, GHC == 8.0.2
+
+extra-source-files:
+  README.md
+
+flag localcache
+  default: True
+  description:
+    Enable process-local cache support. Even when supported, caching is turned
+    off by default until enabled by the application.
+    When unsupported, the relevant APIs are still available, but the cache will
+    act as if every update is a no-op and every lookup a cache miss.
+
+flag haste
+  default: False
+  description:
+    Automatically set when installing for the Haste compiler.
+
+source-repository head
+  type:     git
+  location: https://github.com/valderman/selda.git
+
+library
+  exposed-modules:
+    Database.Selda
+    Database.Selda.Backend
+    Database.Selda.Unsafe
+  other-modules:
+    Database.Selda.Caching
+    Database.Selda.Column
+    Database.Selda.Compile
+    Database.Selda.Frontend
+    Database.Selda.Inner
+    Database.Selda.Query
+    Database.Selda.Query.Type
+    Database.Selda.SQL
+    Database.Selda.SQL.Print
+    Database.Selda.SqlType
+    Database.Selda.Table
+    Database.Selda.Table.Compile
+    Database.Selda.Transform
+    Database.Selda.Types
+  other-extensions:
+    OverloadedStrings
+    GADTs
+    CPP
+    MultiParamTypeClasses
+    UndecidableInstances
+    ScopedTypeVariables
+    RankNTypes
+    TypeFamilies
+    FlexibleInstances
+    GeneralizedNewtypeDeriving
+    FlexibleContexts
+  build-depends:
+      base       >=4.8 && <5
+    , exceptions >=0.8 && <0.9
+    , mtl        >=2.0 && <2.3
+    , text       >=1.0 && <1.3
+    , time       >=1.6 && <1.9
+  if impl(ghc < 7.11)
+    build-depends:
+      transformers  >=0.4 && <0.6
+  if !flag(haste) && flag(localcache)
+    build-depends:
+        hashable             >=1.1 && <1.3
+      , psqueues             >=0.2 && <0.3
+      , unordered-containers >=0.2 && <0.3
+  else
+    cpp-options:
+      -DNO_LOCALCACHE
+  hs-source-dirs:
+    src
+  default-language:
+    Haskell2010
+  ghc-options:
+    -Wall
diff --git a/src/Database/Selda.hs b/src/Database/Selda.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda.hs
@@ -0,0 +1,189 @@
+{-# LANGUAGE OverloadedStrings, FlexibleInstances, UndecidableInstances #-}
+-- | Selda is not LINQ, but they're definitely related.
+--
+--   Selda is a high-level EDSL for interacting with relational databases.
+--   Please see <https://github.com/valderman/selda/> for a brief tutorial.
+module Database.Selda
+  ( -- * Running queries
+    MonadIO (..), MonadSelda
+  , SeldaT, Table, Query, Col, Res, Result
+  , query, transaction, setLocalCache
+    -- * Constructing queries
+  , SqlType
+  , Text, Cols, Columns
+  , Order (..)
+  , (:*:)(..)
+  , select, restrict, limit, order
+  , ascending, descending
+    -- * Expressions over columns
+  , (.==), (./=), (.>), (.<), (.>=), (.<=), like
+  , (.&&), (.||), not_
+  , literal, int, float, text, true, false, null_
+  , roundTo, length_, isNull
+    -- * Converting between column types
+  , round_, just, fromBool, fromInt, toString
+    -- * Inner queries
+  , Aggr, Aggregates, OuterCols, JoinCols, Inner, MinMax
+  , leftJoin
+  , aggregate, groupBy
+  , count, avg, sum_, max_, min_
+    -- * Modifying tables
+  , Insert, InsertCols, HasAutoPrimary
+  , insert, insert_, insertWithPK
+  , update, update_
+  , deleteFrom, deleteFrom_
+    -- * Defining schemas
+  , ColSpec, TableName, ColName
+  , NonNull, IsNullable, Nullable, NotNullable
+  , Auto
+  , table, (¤), required, optional
+  , primary, autoPrimary
+    -- * Combining schemas
+  , ComposeSpec, (:+++:), (+++)
+    -- * Creating and dropping tables
+  , createTable, tryCreateTable
+  , dropTable, tryDropTable
+    -- * Compiling and inspecting queries
+  , OnError (..)
+  , compile
+  , compileCreateTable, compileDropTable
+  , compileInsert, compileUpdate
+    -- * Tuple convenience functions
+  , Tup, Head
+  , first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth
+  ) where
+import Data.Text (Text)
+import Database.Selda.Backend
+import Database.Selda.Column
+import Database.Selda.Compile
+import Database.Selda.Frontend
+import Database.Selda.Inner
+import Database.Selda.Query
+import Database.Selda.Query.Type
+import Database.Selda.SQL (Order (..))
+import Database.Selda.SqlType
+import Database.Selda.Table
+import Database.Selda.Table.Compile
+import Database.Selda.Types
+import Database.Selda.Unsafe
+
+-- | Any column type that can be used with the 'min_' and 'max_' functions.
+class MinMax a
+instance {-# OVERLAPPABLE #-} Num a => MinMax a
+instance MinMax Text
+instance MinMax a => MinMax (Maybe a)
+
+(.==), (./=), (.>), (.<), (.>=), (.<=) :: Col s a -> Col s a -> Col s Bool
+(.==) = liftC2 $ BinOp Eq
+(./=) = liftC2 $ BinOp Neq
+(.>)  = liftC2 $ BinOp Gt
+(.<)  = liftC2 $ BinOp Lt
+(.>=) = liftC2 $ BinOp Gte
+(.<=) = liftC2 $ BinOp Lte
+infixl 4 .==
+infixl 4 .>
+infixl 4 .<
+infixl 4 .>=
+infixl 4 .<=
+
+-- | Is the given column null?
+isNull :: Col s (Maybe a) -> Col s Bool
+isNull = liftC $ UnOp IsNull
+
+(.&&), (.||) :: Col s Bool -> Col s Bool -> Col s Bool
+(.&&) = liftC2 $ BinOp And
+(.||) = liftC2 $ BinOp Or
+infixr 3 .&&
+infixr 2 .||
+
+-- | Ordering for 'order'.
+ascending, descending :: Order
+ascending = Asc
+descending = Desc
+
+-- | Lift a non-nullable column to a nullable one.
+--   Useful for creating expressions over optional columns:
+--
+-- > people :: Table (Text :*: Int :*: Maybe Text)
+-- > people = table "people" $ required "name" ¤ required "age" ¤ optional "pet"
+-- >
+-- > peopleWithCats = do
+-- >   name :*: _ :*: pet <- select people
+-- >   restrict (pet .== just "cat")
+-- >   return name
+just :: Col s a -> Col s (Maybe a)
+just = cast
+
+-- | SQL NULL, at any type you like.
+null_ :: SqlType a => Col s (Maybe a)
+null_ = literal Nothing
+
+-- | Specialization of 'literal' for integers.
+int :: Int -> Col s Int
+int = literal
+
+-- | Specialization of 'literal' for doubles.
+float :: Double -> Col s Double
+float = literal
+
+-- | Specialization of 'literal' for text.
+text :: Text -> Col s Text
+text = literal
+
+-- | True and false boolean literals.
+true, false :: Col s Bool
+true = literal True
+false = literal False
+
+-- | The SQL @LIKE@ operator; matches strings with wildcards.
+like :: Col s Text -> Col s Text -> Col s Bool
+like = liftC2 $ BinOp Like
+infixl 4 `like`
+
+-- | The number of non-null values in the given column.
+count :: Col s a -> Aggr s Int
+count = aggr "COUNT"
+
+-- | The average of all values in the given column.
+avg :: Num a => Col s a -> Aggr s a
+avg = aggr "AVG"
+
+-- | The greatest value in the given column. Texts are compared lexically.
+max_ :: MinMax a => Col s a -> Aggr s a
+max_ = aggr "MAX"
+
+-- | The smallest value in the given column. Texts are compared lexically.
+min_  :: MinMax a => Col s a -> Aggr s a
+min_ = aggr "MIN"
+
+-- | Sum all values in the given column.
+sum_ :: Num a => Col s a -> Aggr s a
+sum_ = aggr "SUM"
+
+-- | Round a value to the nearest integer. Equivalent to @roundTo 0@.
+round_ :: Num a => Col s Double -> Col s a
+round_ = fun "ROUND"
+
+-- | Round a column to the given number of decimals places.
+roundTo :: Col s Int -> Col s Double -> Col s Double
+roundTo = flip $ fun2 "ROUND"
+
+-- | Calculate the length of a string column.
+length_ :: Col s Text -> Col s Int
+length_ = fun "LENGTH"
+
+-- | Boolean negation.
+not_ :: Col s Bool -> Col s Bool
+not_ = liftC $ UnOp Not
+
+-- | Convert a boolean column to any numeric type.
+fromBool :: Num a => Col s Bool -> Col s a
+fromBool = cast
+
+-- | Convert an integer column to any numeric type.
+fromInt :: Num a => Col s Int -> Col s a
+fromInt = cast
+
+-- | Convert any column to a string.
+toString :: Col s a -> Col s String
+toString = cast
diff --git a/src/Database/Selda/Backend.hs b/src/Database/Selda/Backend.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Backend.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+-- | API for building Selda backends.
+module Database.Selda.Backend
+  ( MonadIO (..)
+  , QueryRunner, SeldaBackend (..), MonadSelda (..), SeldaT (..)
+  , Param (..), Lit (..), SqlValue (..), ColAttr (..)
+  , compileColAttr
+  , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat
+  , runSeldaT
+  ) where
+import Database.Selda.SQL (Param (..))
+import Database.Selda.SqlType
+import Database.Selda.Table (ColAttr (..))
+import Database.Selda.Table.Compile (compileColAttr)
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+import Control.Monad.State
+import Data.Text (Text)
+
+-- | A function which executes a query and gives back a list of extensible
+--   tuples; one tuple per result row, and one tuple element per column.
+type QueryRunner a = Text -> [Param] -> IO a
+
+-- | A collection of functions making up a Selda backend.
+data SeldaBackend = SeldaBackend
+  { -- | Execute an SQL statement.
+    runStmt       :: QueryRunner (Int, [[SqlValue]])
+
+    -- | Execute an SQL statement and return the last inserted primary key,
+    --   where the primary key is auto-incrementing.
+    --   Backends must take special care to make this thread-safe.
+  , runStmtWithPK :: QueryRunner Int
+
+    -- | Generate a custom column type for the column having the given Selda
+    --   type and list of attributes.
+  , customColType :: Text -> [ColAttr] -> Maybe Text
+  }
+
+-- | Some monad with Selda SQL capabilitites.
+class MonadIO m => MonadSelda m where
+  -- | Get the backend in use by the computation.
+  seldaBackend :: m SeldaBackend
+
+-- | Monad transformer adding Selda SQL capabilities.
+newtype SeldaT m a = S {unS :: StateT SeldaBackend m a}
+  deriving ( Functor, Applicative, Monad, MonadIO
+           , MonadThrow, MonadCatch, MonadMask, MonadTrans
+           )
+
+instance MonadIO m => MonadSelda (SeldaT m) where
+  seldaBackend = S get
+
+-- | Run a Selda transformer. Backends should use this to implement their
+--   @withX@ functions.
+runSeldaT :: MonadIO m => SeldaT m a -> SeldaBackend -> m a
+runSeldaT m b = fst <$> runStateT (unS m) b
diff --git a/src/Database/Selda/Caching.hs b/src/Database/Selda/Caching.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Caching.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE GADTs, ScopedTypeVariables, CPP #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Database.Selda.Caching
+   ( CacheKey
+    , cache, cached, invalidate, setMaxItems
+   ) where
+import Prelude hiding (lookup)
+import Data.Dynamic
+#ifndef NO_LOCALCACHE
+import Data.Hashable
+import Data.HashPSQ
+import qualified Data.HashMap.Strict as M
+import qualified Data.HashSet as S
+import Data.List (foldl')
+import Database.Selda.SqlType
+import Data.IORef
+import System.IO.Unsafe
+#endif
+import Data.Text (Text)
+import Database.Selda.SQL (Param (..))
+import Database.Selda.Types (TableName)
+
+type CacheKey = (Text, [Param])
+
+#ifdef NO_LOCALCACHE
+
+cache :: Typeable a => [TableName] -> CacheKey -> a -> IO ()
+cache _ _ _ = return ()
+
+cached :: forall a. Typeable a => CacheKey -> IO (Maybe a)
+cached _ = return Nothing
+
+invalidate :: TableName -> IO ()
+invalidate _ = return ()
+
+setMaxItems :: Int -> IO ()
+setMaxItems _ = return ()
+
+#else
+
+instance Hashable Param where
+  hashWithSalt s (Param x) = hashWithSalt s x
+instance Hashable (Lit a) where
+  hashWithSalt s (LitS x)    = hashWithSalt s x
+  hashWithSalt s (LitI x)    = hashWithSalt s x
+  hashWithSalt s (LitD x)    = hashWithSalt s x
+  hashWithSalt s (LitB x)    = hashWithSalt s x
+  hashWithSalt s (LitTS x)   = hashWithSalt s x
+  hashWithSalt s (LitDate x) = hashWithSalt s x
+  hashWithSalt s (LitTime x) = hashWithSalt s x
+  hashWithSalt s (LitJust x) = hashWithSalt s x
+  hashWithSalt _ (LitNull)   = 0
+
+data ResultCache = ResultCache
+  { -- | Query to result mapping.
+    results  :: !(HashPSQ CacheKey Int Dynamic)
+    -- | Table to query mapping, for keeping track of which queries depend on
+    --   which tables.
+  , deps     :: !(M.HashMap TableName (S.HashSet CacheKey))
+    -- | Items currently in cache.
+  , items    :: !Int
+    -- | Max number of items in cache.
+  , maxItems :: !Int
+    -- | Next cache prio to use.
+  , nextPrio :: !Int
+  } deriving Show
+
+emptyCache :: ResultCache
+emptyCache = ResultCache
+  { results  = empty
+  , deps     = M.empty
+  , items    = 0
+  , maxItems = 0
+  , nextPrio = 0
+  }
+
+{-# NOINLINE theCache #-}
+theCache :: IORef ResultCache
+theCache = unsafePerformIO $ newIORef emptyCache
+
+-- | Cache the given value, with the given table dependencies.
+cache :: Typeable a => [TableName] -> CacheKey -> a -> IO ()
+cache tns k v = atomicModifyIORef' theCache $ \c -> (cache' tns k v c, ())
+
+cache' :: Typeable a => [TableName] -> CacheKey -> a -> ResultCache -> ResultCache
+cache' tns k v rc
+  | maxItems rc == 0 = rc
+  | prio + 1 < prio  = cache' tns k v (emptyCache {maxItems = maxItems rc})
+  | otherwise        = rc
+    { results  = insert k prio v' results'
+    , deps     = foldl' (\m tn -> M.alter (addTbl k) tn m) (deps rc) tns
+    , nextPrio = prio + 1
+    , items    = items'
+    }
+  where
+    v' = toDyn v
+    prio = nextPrio rc
+    -- evict LRU element before inserting if full
+    (items', results')
+      | items rc >= maxItems rc = (items rc, deleteMin $ results rc)
+      | otherwise               = (items rc + 1, results rc)
+    addTbl key (Just ks) = Just (S.insert key ks)
+    addTbl key Nothing   = Just (S.singleton key)
+
+-- | Get the cached value for the given key, if it exists.
+cached :: forall a. Typeable a => CacheKey -> IO (Maybe a)
+cached k = atomicModifyIORef' theCache $ cached' k
+
+cached' :: forall a. Typeable a => CacheKey -> ResultCache -> (ResultCache, Maybe a)
+cached' k rc = do
+  case (maxItems rc, alter updatePrio k (results rc)) of
+    (0, _)                  -> (rc, Nothing)
+    (_, (Just x, results')) -> (rc' results', fromDynamic x)
+    _                       -> (rc, Nothing)
+  where
+    rc' rs = rc
+      { results = rs
+      , nextPrio = nextPrio rc + 1
+      }
+    updatePrio (Just (_, v)) = (Just v, Just (nextPrio rc, v))
+    updatePrio _             = (Nothing, Nothing)
+
+-- | Invalidate all items in cache that depend on the given table.
+invalidate :: TableName -> IO ()
+invalidate tn = atomicModifyIORef' theCache $ \c -> (invalidate' tn c, ())
+
+invalidate' :: TableName -> ResultCache -> ResultCache
+invalidate' tbl rc
+  | maxItems rc == 0 = rc
+  | otherwise        = rc
+    { results = results'
+    , deps    = deps'
+    , items   = items rc - length ks
+    }
+  where
+    ks = maybe S.empty id $ M.lookup tbl (deps rc)
+    results' = S.foldl' (flip delete) (results rc) ks
+    deps' = M.delete tbl (deps rc)
+
+-- | Set the maximum number of items allowed in the cache.
+setMaxItems :: Int -> IO ()
+setMaxItems n = atomicModifyIORef' theCache $ \c -> (setMaxItems' n c, ())
+
+-- | The the maximum number of items for the cache.
+getMaxItems :: IO Int
+getMaxItems = maxItems <$> readIORef theCache
+
+setMaxItems' :: Int -> ResultCache -> ResultCache
+setMaxItems' 0 _  = emptyCache
+setMaxItems' n rc = emptyCache {maxItems = n}
+#endif
diff --git a/src/Database/Selda/Column.hs b/src/Database/Selda/Column.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Column.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE GADTs, TypeFamilies, TypeOperators, PolyKinds, FlexibleInstances #-}
+-- | Columns and associated utility functions.
+module Database.Selda.Column where
+import Database.Selda.SqlType
+import Database.Selda.Table (Auto)
+import Database.Selda.Types
+import Data.String
+import Data.Text (Text)
+
+-- | Convert a tuple of Haskell types to a tuple of column types.
+type family Cols s a where
+  Cols s (Auto a :*: b) = Col s a :*: Cols s b
+  Cols s (a :*: b)      = Col s a :*: Cols s b
+  Cols s (Auto a)       = Col s a
+  Cols s a              = Col s a
+
+-- | Any column tuple.
+class Columns a where
+  toTup :: [ColName] -> a
+  fromTup :: a -> [SomeCol]
+
+instance Columns b => Columns (Col s a :*: b) where
+  toTup (x:xs) = C (Col x) :*: toTup xs
+  toTup _      = error "too few elements to toTup"
+  fromTup (C x :*: xs) = Some x : fromTup xs
+
+instance Columns (Col s a) where
+  toTup [x] = C (Col x)
+  toTup []  = error "too few elements to toTup"
+  toTup _   = error "too many elements to toTup"
+  fromTup (C x) = [Some x]
+
+-- | A type-erased column, which may also be renamed.
+--   Only for internal use.
+data SomeCol where
+  Some  :: !(Exp a) -> SomeCol
+  Named :: !ColName -> !(Exp a) -> SomeCol
+
+-- | A database column. A column is often a literal column table, but can also
+--   be an expression over such a column or a constant expression.
+newtype Col s a = C {unC :: Exp a}
+
+-- | A literal expression.
+literal :: SqlType a => a -> Col s a
+literal = C . Lit . mkLit
+
+-- | A unary operation. Note that the provided function name is spliced
+--   directly into the resulting SQL query. Thus, this function should ONLY
+--   be used to implement well-defined functions that are missing from Selda's
+--   standard library, and NOT in an ad hoc manner during queries.
+fun :: Text -> Col s a -> Col s b
+fun f = liftC $ UnOp (Fun f)
+
+-- | Like 'fun', but with two arguments.
+fun2 :: Text -> Col s a -> Col s b -> Col s c
+fun2 f = liftC2 (Fun2 f)
+
+-- | Underlying column expression type, not tied to any particular query.
+data Exp a where
+  Col    :: !ColName -> Exp a
+  Lit    :: !(Lit a) -> Exp a
+  BinOp  :: !(BinOp a b) -> !(Exp a) -> !(Exp a) -> Exp b
+  UnOp   :: !(UnOp a b) -> !(Exp a) -> Exp b
+  Fun2   :: !Text -> !(Exp a) -> !(Exp b) -> Exp c
+  Cast   :: !(Exp a) -> Exp b
+  AggrEx :: !Text -> !(Exp a) -> Exp b
+
+-- | Get all column names in the given expression.
+allNamesIn :: Exp a -> [ColName]
+allNamesIn (Col n)       = [n]
+allNamesIn (Lit _)       = []
+allNamesIn (BinOp _ a b) = allNamesIn a ++ allNamesIn b
+allNamesIn (UnOp _ a)    = allNamesIn a
+allNamesIn (Fun2 _ a b)  = allNamesIn a ++ allNamesIn b
+allNamesIn (Cast x)      = allNamesIn x
+allNamesIn (AggrEx _ x)  = allNamesIn x
+
+data UnOp a b where
+  Abs    :: UnOp a a
+  Not    :: UnOp Bool Bool
+  Neg    :: UnOp a a
+  Sgn    :: UnOp a a
+  IsNull :: UnOp (Maybe a) Bool
+  Fun    :: Text -> UnOp a b
+
+data BinOp a b where
+  Gt    :: BinOp a Bool
+  Lt    :: BinOp a Bool
+  Gte   :: BinOp a Bool
+  Lte   :: BinOp a Bool
+  Eq    :: BinOp a Bool
+  Neq   :: BinOp a Bool
+  And   :: BinOp Bool Bool
+  Or    :: BinOp Bool Bool
+  Add   :: BinOp a a
+  Sub   :: BinOp a a
+  Mul   :: BinOp a a
+  Div   :: BinOp a a
+  Like  :: BinOp Text Bool
+
+instance IsString (Col s Text) where
+  fromString = literal . fromString
+
+liftC2 :: (Exp a -> Exp b -> Exp c) -> Col s a -> Col s b -> Col s c
+liftC2 f (C a) (C b) = C (f a b)
+
+liftC :: (Exp a -> Exp b) -> Col s a -> Col s b
+liftC f = C . f . unC
+
+instance (SqlType a, Num a) => Num (Col s a) where
+  fromInteger = literal . fromInteger
+  (+) = liftC2 $ BinOp Add
+  (-) = liftC2 $ BinOp Sub
+  (*) = liftC2 $ BinOp Mul
+  negate = liftC $ UnOp Neg
+  abs = liftC $ UnOp Abs
+  signum = liftC $ UnOp Sgn
+
+instance {-# OVERLAPPING #-} (SqlType a, Num a) => Num (Col s (Maybe a)) where
+  fromInteger = literal . Just . fromInteger
+  (+) = liftC2 $ BinOp Add
+  (-) = liftC2 $ BinOp Sub
+  (*) = liftC2 $ BinOp Mul
+  negate = liftC $ UnOp Neg
+  abs = liftC $ UnOp Abs
+  signum = liftC $ UnOp Sgn
+
+instance Fractional (Col s Double) where
+  fromRational = literal . fromRational
+  (/) = liftC2 $ BinOp Div  
+
+instance Fractional (Col s (Maybe Double)) where
+  fromRational = literal . Just . fromRational
+  (/) = liftC2 $ BinOp Div  
+
+instance Fractional (Col s Int) where
+  fromRational = literal . (truncate :: Double -> Int) . fromRational
+  (/) = liftC2 $ BinOp Div
+
+instance Fractional (Col s (Maybe Int)) where
+  fromRational = literal . Just . (truncate :: Double -> Int) . fromRational
+  (/) = liftC2 $ BinOp Div
diff --git a/src/Database/Selda/Compile.hs b/src/Database/Selda/Compile.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Compile.hs
@@ -0,0 +1,102 @@
+{-# LANGUAGE GADTs, TypeOperators, TypeFamilies, ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleInstances, FlexibleContexts, UndecidableInstances #-}
+-- | Selda SQL compilation.
+module Database.Selda.Compile where
+import Database.Selda.Column
+import Database.Selda.Query.Type
+import Database.Selda.SQL
+import Database.Selda.SQL.Print
+import Database.Selda.SqlType
+import Database.Selda.Table
+import Database.Selda.Table.Compile
+import Database.Selda.Transform
+import Database.Selda.Types
+import Data.Proxy
+import Data.Text (Text, empty)
+import Data.Typeable
+
+-- | Compile a query into a parameterised SQL statement.
+compile :: Result a => Query s a -> (Text, [Param])
+compile = snd . compileWithTables
+
+-- | Compile a query into a parameterised SQL statement. Also returns all
+--   tables depended on by the query.
+compileWithTables :: Result a => Query s a -> ([TableName], (Text, [Param]))
+compileWithTables = compSql . snd . compQuery
+
+-- | Compile an @INSERT@ query.
+compileInsert :: Insert (InsertCols a)
+              => Table a -> [InsertCols a] -> (Text, [Param])
+compileInsert _ []     = (empty, [])
+compileInsert tbl rows = (compInsert tbl nrows, concat ps)
+  where ps = map params rows
+        nrows = length rows
+
+-- | Compile an @UPDATE@ query.
+compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a))
+              => Table a                  -- ^ The table to update.
+              -> (Cols s a -> Cols s a)   -- ^ Update function.
+              -> (Cols s a -> Col s Bool) -- ^ Predicate: update only when true.
+              -> (Text, [Param])
+compileUpdate tbl upd check =
+    compUpdate (tableName tbl) predicate updated
+  where
+    names = map colName (tableCols tbl)
+    cs = toTup names
+    updated = zip names (finalCols (upd cs))
+    C predicate = check cs
+
+-- | Compile a @DELETE FROM@ query.
+compileDelete :: Columns (Cols s a)
+              => Table a -> (Cols s a -> Col s Bool) -> (Text, [Param])
+compileDelete tbl check = compDelete (tableName tbl) predicate
+  where C predicate = check $ toTup $ map colName $ tableCols tbl
+
+-- | Compile a query to an SQL AST.
+--   Groups are ignored, as they are only used by 'aggregate'.
+compQuery :: Result a => Query s a -> (Int, SQL)
+compQuery q =
+    (nameSupply st, SQL final (Product [srcs]) [] [] [] Nothing)
+  where
+    (cs, st) = runQueryM q
+    final = finalCols cs
+    sql = state2sql st
+    live = colNames final ++ allNonOutputColNames sql
+    srcs = removeDeadCols live sql
+
+-- | An extensible tuple of Haskell-level values (i.e. @Int :*: Maybe Text@)
+--   which can be inserted into a table.
+class Insert a where
+  params :: a -> [Param]
+instance (SqlType a, Insert b) => Insert (a :*: b) where
+  params (a :*: b) = Param (mkLit a) : params b
+instance {-# OVERLAPPABLE #-} SqlType a => Insert a where
+  params a = [Param (mkLit a) ]
+
+-- | An acceptable query result type; one or more columns stitched together
+--   with @:*:@.
+class Typeable (Res r) => Result r where
+  type Res r
+  -- | Converts the given list of @SqlValue@s into an tuple of well-typed
+  --   results.
+  --   See 'querySQLite' for example usage.
+  --   The given list must contain exactly as many elements as dictated by
+  --   the @Res r@. If the result is @a :*: b :*: c@, then the list must
+  --   contain exactly three values, for instance.
+  toRes :: Proxy r -> [SqlValue] -> Res r
+
+  -- | Produce a list of all columns present in the result.
+  finalCols :: r -> [SomeCol]
+
+instance (Typeable a, SqlType a, Result b) => Result (Col s a :*: b) where
+  type Res (Col s a :*: b) = a :*: Res b
+  toRes _ (x:xs) = fromSql x :*: toRes (Proxy :: Proxy b) xs
+  toRes _ _      = error "backend bug: too few result columns to toRes"
+  finalCols (a :*: b) = finalCols a ++ finalCols b
+
+instance (Typeable a, SqlType a) => Result (Col s a) where
+  type Res (Col s a) = a
+  toRes _ [x] = fromSql x
+  toRes _ []  = error "backend bug: too few result columns to toRes"
+  toRes _ _   = error "backend bug: too many result columns to toRes"
+  finalCols (C c) = [Some c]
diff --git a/src/Database/Selda/Frontend.hs b/src/Database/Selda/Frontend.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Frontend.hs
@@ -0,0 +1,192 @@
+{-# LANGUAGE ScopedTypeVariables, FlexibleContexts, OverloadedStrings #-}
+-- | API for running Selda operations over databases.
+module Database.Selda.Frontend
+  ( Result, Res, MonadIO (..), MonadSelda (..), SeldaT
+  , query
+  , insert, insert_, insertWithPK
+  , update, update_
+  , deleteFrom, deleteFrom_
+  , createTable, tryCreateTable
+  , dropTable, tryDropTable
+  , transaction, setLocalCache
+  ) where
+import Database.Selda.Backend
+import Database.Selda.Caching
+import Database.Selda.Column
+import Database.Selda.Compile
+import Database.Selda.Query.Type
+import Database.Selda.Table
+import Database.Selda.Table.Compile
+import Data.Proxy
+import Data.Text (Text)
+import Control.Monad
+import Control.Monad.Catch
+
+-- | Run a query within a Selda transformer.
+--   Selda transformers are entered using backend-specific @withX@ functions,
+--   such as 'withSQLite' from the SQLite backend.
+query :: (MonadSelda m, Result a) => Query s a -> m [Res a]
+query q = do
+  backend <- seldaBackend
+  queryWith (runStmt backend) q
+
+-- | Insert the given values into the given table. All columns of the table
+--   must be present, EXCEPT any auto-incrementing primary keys ('autoPrimary'
+--   columns), which are always assigned their default value.
+--   Returns the number of rows that were inserted.
+--
+--   To insert a list of tuples into a table with auto-incrementing primary key:
+--
+-- > people :: Table (Auto Int :*: Text :*: Int :*: Maybe Text)
+-- > people = table "ppl"
+-- >        $ autoPrimary "id"
+-- >        ¤ required "name"
+-- >        ¤ required "age"
+-- >        ¤ optional "pet"
+-- >
+-- > main = withSQLite "my_database.sqlite" $ do
+-- >   insert_ people
+-- >     [ "Link"  :*: 125 :*: Just "horse"
+-- >     , "Zelda" :*: 119 :*: Nothing
+-- >     , ...
+-- >     ]
+--
+--   Again, note that ALL non-auto-incrementing fields must be present in the
+--   tuples to be inserted, including primary keys without the auto-increment
+--   attribute.
+insert :: (MonadSelda m, Insert (InsertCols a))
+       => Table a -> [InsertCols a] -> m Int
+insert _ [] = do
+  return 0
+insert t cs = do
+  liftIO $ invalidate (tableName t)
+  uncurry exec $ compileInsert t cs
+
+-- | Like 'insert', but does not return anything.
+--   Use this when you really don't care about how many rows were inserted.
+insert_ :: (MonadSelda m, Insert (InsertCols a))
+        => Table a -> [InsertCols a] -> m ()
+insert_ t cs = void $ insert t cs
+
+-- | Like 'insert', but returns the primary key of the last inserted row.
+--   Attempting to run this operation on a table without an auto-incrementing
+--   primary key is a type error.
+insertWithPK :: (MonadSelda m, HasAutoPrimary a, Insert (InsertCols a))
+                => Table a -> [InsertCols a] -> m Int
+insertWithPK t cs = do
+  backend <- seldaBackend
+  liftIO $ invalidate (tableName t)
+  liftIO . uncurry (runStmtWithPK backend) $ compileInsert t cs
+
+-- | Update the given table using the given update function, for all rows
+--   matching the given predicate. Returns the number of updated rows.
+update :: (MonadSelda m, Columns (Cols s a), Result (Cols s a))
+       => Table a                  -- ^ The table to update.
+       -> (Cols s a -> Col s Bool) -- ^ Predicate.
+       -> (Cols s a -> Cols s a)   -- ^ Update function.
+       -> m Int
+update tbl check upd = do
+  liftIO $ invalidate (tableName tbl)
+  uncurry exec $ compileUpdate tbl upd check
+
+-- | Like 'update', but doesn't return the number of updated rows.
+update_ :: (MonadSelda m, Columns (Cols s a), Result (Cols s a))
+       => Table a
+       -> (Cols s a -> Col s Bool)
+       -> (Cols s a -> Cols s a)
+       -> m ()
+update_ tbl check upd = void $ update tbl check upd
+
+-- | From the given table, delete all rows matching the given predicate.
+--   Returns the number of deleted rows.
+deleteFrom :: (MonadSelda m, Columns (Cols s a))
+           => Table a -> (Cols s a -> Col s Bool) -> m Int
+deleteFrom tbl f = do
+  liftIO $ invalidate (tableName tbl)
+  uncurry exec $ compileDelete tbl f
+
+-- | Like 'deleteFrom', but does not return the number of deleted rows.
+deleteFrom_ :: (MonadSelda m, Columns (Cols s a))
+            => Table a -> (Cols s a -> Col s Bool) -> m ()
+deleteFrom_ tbl f = void $ deleteFrom tbl f
+
+-- | Create a table from the given schema.
+createTable :: MonadSelda m => Table a -> m ()
+createTable tbl = do
+  cct <- customColType <$> seldaBackend
+  void . flip exec [] $ compileCreateTable cct Fail tbl
+
+-- | Create a table from the given schema, unless it already exists.
+tryCreateTable :: MonadSelda m => Table a -> m ()
+tryCreateTable tbl = do
+  cct <- customColType <$> seldaBackend
+  void . flip exec [] $ compileCreateTable cct Ignore tbl
+
+-- | Drop the given table.
+dropTable :: MonadSelda m => Table a -> m ()
+dropTable = withInval $ void . flip exec [] . compileDropTable Fail
+
+-- | Drop the given table, if it exists.
+tryDropTable :: MonadSelda m => Table a -> m ()
+tryDropTable = withInval $ void . flip exec [] . compileDropTable Ignore
+
+-- | Perform the given computation atomically.
+--   If an exception is raised during its execution, the enture transaction
+--   will be rolled back, and the exception re-thrown.
+transaction :: (MonadSelda m, MonadThrow m, MonadCatch m) => m a -> m a
+transaction m = do
+  void $ exec "BEGIN TRANSACTION" []
+  res <- try m
+  case res of
+    Left (SomeException e) -> do
+      void $ exec "ROLLBACK" []
+      throwM e
+    Right x -> do
+      void $ exec "COMMIT" []
+      return x
+
+-- | Set the maximum local cache size to @n@. A cache size of zero disables
+--   local cache altogether. Changing the cache size will also flush all
+--   entries.
+--
+--   By default, local caching is turned off.
+--
+--   WARNING: local caching is guaranteed to be consistent with the underlying
+--   database, ONLY under the assumption that no other process will modify it.
+--   Also note that the cache is shared between ALL Selda computations running
+--   within the same process.
+setLocalCache :: MonadSelda m => Int -> m ()
+setLocalCache = liftIO . setMaxItems
+
+-- | Build the final result from a list of result columns.
+queryWith :: forall s m a. (MonadSelda m, Result a)
+          => QueryRunner (Int, [[SqlValue]]) -> Query s a -> m [Res a]
+queryWith qr q = do
+    mres <- liftIO $ cached qry
+    case mres of
+      Just res -> do
+        return res
+      _        -> do
+        res <- fmap snd . liftIO $ uncurry qr qry
+        let res' = mkResults (Proxy :: Proxy a) res
+        liftIO $ cache tables qry res'
+        return res'
+  where
+    (tables, qry) = compileWithTables q
+
+-- | Generate the final result of a query from a list of untyped result rows.
+mkResults :: Result a => Proxy a -> [[SqlValue]] -> [Res a]
+mkResults p = map (toRes p)
+
+-- | Run the given computation over a table after invalidating all cached
+--   results depending on that table.
+withInval :: MonadSelda m => (Table a -> m b) -> Table a -> m b
+withInval f t = do
+  liftIO $ invalidate $ tableName t
+  f t
+
+-- | Execute a statement without a result.
+exec :: MonadSelda m => Text -> [Param] -> m Int
+exec q ps = do
+  backend <- seldaBackend
+  fmap fst . liftIO $ runStmt backend q ps
diff --git a/src/Database/Selda/Inner.hs b/src/Database/Selda/Inner.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Inner.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE TypeOperators, TypeFamilies, FlexibleInstances, FlexibleContexts #-}
+-- | Helpers for working with inner queries.
+module Database.Selda.Inner where
+import Database.Selda.Column
+import Database.Selda.Types
+import Data.Text (Text)
+
+-- | A single aggregate column.
+--   Aggregate columns may not be used to restrict queries.
+--   When returned from an 'aggregate' subquery, an aggregate column is
+--   converted into a non-aggregate column.
+newtype Aggr s a = Aggr {unAggr :: Exp a}
+
+-- | Denotes an inner query.
+--   For aggregation, treating sequencing as the cartesian product of queries
+--   does not work well.
+--   Instead, we treat the sequencing of 'aggregate' with other
+--   queries as the cartesian product of the aggregated result of the query,
+--   a small but important difference.
+--
+--   However, for this to work, the aggregate query must not depend on any
+--   columns in the outer product. Therefore, we let the aggregate query be
+--   parameterized over @Inner s@ if the parent query is parameterized over @s@,
+--   to enforce this separation.
+data Inner s
+
+-- | Create a named aggregate function.
+--   Like 'fun', this function is generally unsafe and should ONLY be used
+--   to implement missing backend-specific functionality.
+aggr :: Text -> Col s a -> Aggr s b
+aggr f = Aggr . AggrEx f . unC
+
+-- | Convert one or more inner column to equivalent columns in the outer query.
+--   @OuterCols (Aggr (Inner s) a :*: Aggr (Inner s) b) = Col s a :*: Col s b@,
+--   for instance.
+type family OuterCols a where
+  OuterCols (t (Inner s) a :*: b) = Col s a :*: OuterCols b
+  OuterCols (t (Inner s) a)       = Col s a
+
+-- | The results of a join are always nullable, as there is no guarantee that
+--   all joined columns will be non-null.
+--   @JoinCols a@ where @a@ is an extensible tuple is that same tuple, but in
+--   the outer query and with all elements nullable.
+--   For instance:
+--
+-- >  JoinCols (Col (Inner s) Int :*: Col (Inner s) Text)
+-- >    = Col s (Maybe Int) :*: Col s (Maybe Text)
+type family JoinCols a where
+  JoinCols (Col (Inner s) (Maybe a) :*: b) = Col s (Maybe a) :*: JoinCols b
+  JoinCols (Col (Inner s) a :*: b)         = Col s (Maybe a) :*: JoinCols b
+  JoinCols (Col (Inner s) (Maybe a))       = Col s (Maybe a)
+  JoinCols (Col (Inner s) a)               = Col s (Maybe a)
+
+-- | One or more aggregate columns.
+class Aggregates a where
+  unAggrs :: a -> [SomeCol]
+instance Aggregates (Aggr (Inner s) a) where
+  unAggrs (Aggr x) = [Some x]
+instance Aggregates b => Aggregates (Aggr (Inner s) a :*: b) where
+  unAggrs (Aggr a :*: b) = Some a : unAggrs b
diff --git a/src/Database/Selda/Query.hs b/src/Database/Selda/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Query.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE FlexibleContexts #-}
+-- | Query monad and primitive operations.
+module Database.Selda.Query where
+import Database.Selda.Column
+import Database.Selda.Inner
+import Database.Selda.Query.Type
+import Database.Selda.SQL
+import Database.Selda.Table
+import Database.Selda.Transform
+import Control.Monad.State.Strict
+
+-- | Query the given table. Result is returned as an inductive tuple, i.e.
+--   @first :*: second :*: third <- query tableOfThree@.
+select :: Columns (Cols s a) => Table a -> Query s (Cols s a)
+select (Table name cs) = Query $ do
+    rns <- mapM (rename . Some . Col) cs'
+    st <- get
+    put $ st {sources = SQL rns (TableName name) [] [] [] Nothing : sources st}
+    return $ toTup [n | Named n _ <- rns]
+  where
+    cs' = map colName cs
+
+-- | Restrict the query somehow. Roughly equivalent to @WHERE@.
+restrict :: Col s Bool -> Query s ()
+restrict (C p) = Query $ do
+    st <- get
+    put $ case sources st of
+      [] ->
+        st {staticRestricts = p : staticRestricts st}
+      -- PostgreSQL doesn't put renamed columns in scope in the WHERE clause
+      -- of the query where they are renamed, so if the restrict predicate
+      -- contains any vars renamed in this query, we must add another query
+      -- just for the restrict.
+      [SQL cs s ps gs os lim] | not $ p `wasRenamedIn` cs ->
+        st {sources = [SQL cs s (p : ps) gs os lim]}
+      ss ->
+        st {sources = [SQL (allCols ss) (Product ss) [p] [] [] Nothing]}
+  where
+    wasRenamedIn predicate cs =
+      let cs' = [n | Named n _ <- cs]
+      in  any (`elem` cs') (colNames [Some predicate])
+
+-- | Execute a query, returning an aggregation of its results.
+--   The query must return an inductive tuple of 'Aggregate' columns.
+--   When @aggregate@ returns, those columns are converted into non-aggregate
+--   columns, which may then be used to further restrict the query.
+--
+--   Note that aggregate queries must not depend on outer queries, nor must
+--   they return any non-aggregate columns. Attempting to do either results in
+--   a type error.
+--
+--   The SQL @HAVING@ keyword can be implemented by combining @aggregte@
+--   and 'restrict':
+--
+-- > -- Find the number of people living on every address, for all addresses
+-- < -- with more than one tenant:
+-- > -- SELECT COUNT(name) AS c, address FROM housing GROUP BY name HAVING c > 1
+-- >
+-- > numPpl = do
+-- >   num_tenants :*: address <- aggregate $ do
+-- >     _ :*: address <- select housing
+-- >     groupBy address
+-- >     return (count address :*: some address)
+-- >  restrict (num_tenants .> 1)
+-- >  return (num_tenants :*: address)
+aggregate :: (Columns (OuterCols a), Aggregates a)
+          => Query (Inner s) a
+          -> Query s (OuterCols a)
+aggregate q = Query $ do
+  -- Run query in isolation, then rename the remaining vars and generate outer
+  -- query.
+  st <- get
+  (gst, aggrs) <- isolate q
+  cs <- mapM rename $ unAggrs aggrs
+  let sql = state2sql gst
+      sql' = SQL cs (Product [sql]) [] (groupCols gst) [] Nothing
+  put $ st {sources = sql' : sources st}
+  pure $ toTup [n | Named n _ <- cs]
+
+-- | Perform a @LEFT JOIN@ with the current result set (i.e. the outer query)
+--   as the left hand side, and the given query as the right hand side.
+--   Like with 'aggregate', the inner (or right) query must not depend on the
+--   outer (or right) one.
+--
+--   The given predicate over the values returned by the inner query determines
+--   for each row whether to join or not. This predicate may depend on any
+--   values from the outer query.
+--
+--   For instance, the following will list everyone in the @people@ table
+--   together with their address if they have one; if they don't, the address
+--   field will be @NULL@.
+--
+-- > getAddresses :: Query s (Col s Text :*: Col s (Maybe Text))
+-- > getAddresses = do
+-- >   name :*: _ <- select people
+-- >   _ :*: address <- leftJoin (\(n :*: _) -> n .== name)
+-- >                             (select addresses)
+-- >   return (name :*: address)
+leftJoin :: (Columns a, Columns (OuterCols a), Columns (JoinCols a))
+            -- | Predicate determining which lines to join.
+         => (OuterCols a -> Col s Bool)
+            -- | Right-hand query to join.
+         -> Query (Inner s) a
+         -> Query s (JoinCols a)
+leftJoin check q = Query $ do
+  (join_st, res) <- isolate q
+  cs <- mapM rename $ fromTup res
+  st <- get
+  let nameds = [n | Named n _ <- cs]
+      left = state2sql st
+      right = SQL cs (Product [state2sql join_st]) [] [] [] Nothing
+      C on = check $ toTup nameds
+      outCols = [Some $ Col n | Named n _ <- cs] ++ allCols [left]
+      sql = SQL outCols (LeftJoin on left right) [] [] [] Nothing
+  put $ st {sources = [sql]}
+  pure $ toTup nameds
+
+-- | Group an aggregate query by a column.
+--   Attempting to group a non-aggregate query is a type error.
+--   An aggregate representing the grouped-by column is returned, which can be
+--   returned from the aggregate query. For instance, if you want to find out
+--   how many people have a pet at home:
+--
+-- > aggregate $ do
+-- >   name :*: pet_name <- select people
+-- >   name' <- groupBy name
+-- >   return (name' :*: count(pet_name) > 0)
+groupBy :: Col (Inner s) a -> Query (Inner s) (Aggr (Inner s) a)
+groupBy (C c) = Query $ do
+  st <- get
+  put $ st {groupCols = Some c : groupCols st}
+  return (Aggr c)
+
+-- | Drop the first @m@ rows, then get at most @n@ of the remaining rows.
+limit :: Int -> Int -> Query s ()
+limit from to = Query $ do
+  st <- get
+  put $ case sources st of
+    [SQL cs s ps gs os Nothing] ->
+      st {sources = [SQL cs s ps gs os (Just (from, to))]}
+    ss ->
+      st {sources = [SQL (allCols ss) (Product ss) [] [] [] (Just (from, to))]}
+
+-- | Sort the result rows in ascending or descending order on the given row.
+order :: Col s a -> Order -> Query s ()
+order (C c) o = Query $ do
+  st <- get
+  put $ case sources st of
+    [SQL cs s ps gs os lim] ->
+      st {sources = [SQL cs s ps gs ((o, Some c):os) lim]}
+    ss ->
+      st {sources = [SQL (allCols ss) (Product ss) [] [] [(o, Some c)] Nothing]}
diff --git a/src/Database/Selda/Query/Type.hs b/src/Database/Selda/Query/Type.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Query/Type.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}
+module Database.Selda.Query.Type where
+import Control.Monad.State.Strict
+import Data.Monoid
+import Data.Text (pack)
+import Database.Selda.SQL
+import Database.Selda.Column
+
+-- | An SQL query.
+newtype Query s a = Query {unQ :: State GenState a}
+  deriving (Functor, Applicative, Monad)
+
+-- | Run a query computation from an initial state.
+runQueryM :: Query s a -> (a, GenState)
+runQueryM = flip runState initState . unQ
+
+-- | Run a query computation in isolation, but reusing the current name supply.
+isolate :: Query s a -> State GenState (GenState, a)
+isolate (Query q) = do
+  st <- get
+  put $ initState {nameSupply = nameSupply st}
+  x <- q
+  st' <- get
+  put $ st {nameSupply = nameSupply st'}
+  return (st', x)
+
+-- | SQL generation internal state.
+--   Contains the subqueries and static (i.e. not dependent on any subqueries)
+--   restrictions of the query currently being built, as well as a name supply
+--   for column renaming.
+data GenState = GenState
+  { sources         :: ![SQL]
+  , staticRestricts :: ![Exp Bool]
+  , groupCols       :: ![SomeCol]
+  , nameSupply      :: !Int
+  }
+
+-- | Initial state: no subqueries, no restrictions.
+initState :: GenState
+initState = GenState
+  { sources = []
+  , staticRestricts = []
+  , groupCols = []
+  , nameSupply = 0
+  }
+
+-- | Generate a unique name for the given column.
+rename :: SomeCol -> State GenState SomeCol
+rename (Some col) = do
+    st <- get
+    put $ st {nameSupply = succ $ nameSupply st}
+    return $ Named (newName $ nameSupply st) col
+  where
+    newName ns =
+      case col of
+        Col n -> n <> "_" <> pack (show ns)
+        _     -> "tmp_" <> pack (show ns)
+rename col@(Named _ _) = do
+  return col
diff --git a/src/Database/Selda/SQL.hs b/src/Database/Selda/SQL.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/SQL.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE GADTs, OverloadedStrings #-}
+-- | SQL AST and pretty-printing.
+module Database.Selda.SQL where
+import Database.Selda.Column
+import Database.Selda.SqlType
+import Database.Selda.Types (TableName)
+import Data.Monoid
+
+-- | A source for an SQL query.
+data SqlSource
+ = TableName !TableName
+ | Product ![SQL]
+ | LeftJoin !(Exp Bool) !SQL !SQL
+
+-- | AST for SQL queries.
+data SQL = SQL
+  { cols      :: ![SomeCol]
+  , source    :: !SqlSource
+  , restricts :: ![Exp Bool]
+  , groups    :: ![SomeCol]
+  , ordering  :: ![(Order, SomeCol)]
+  , limits    :: !(Maybe (Int, Int))
+  }
+
+-- | The order in which to sort result rows.
+data Order = Asc | Desc
+  deriving (Show, Ord, Eq)
+
+-- | A parameter to a prepared SQL statement.
+data Param where
+  Param :: !(Lit a) -> Param
+
+instance Show Param where
+  show (Param l) = "Param " <> show l
+
+instance Eq Param where
+  Param a == Param b = compLit a b == EQ
+instance Ord Param where
+  compare (Param a) (Param b) = compLit a b
diff --git a/src/Database/Selda/SQL/Print.hs b/src/Database/Selda/SQL/Print.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/SQL/Print.hs
@@ -0,0 +1,216 @@
+{-# LANGUAGE GADTs, OverloadedStrings #-}
+-- | Pretty-printing for SQL queries. For some values of pretty.
+module Database.Selda.SQL.Print where
+import Database.Selda.Column
+import Database.Selda.SQL
+import Database.Selda.SqlType
+import Database.Selda.Types
+import Control.Monad.State
+import Data.List
+import Data.Monoid hiding (Product)
+import Data.Text (Text)
+import qualified Data.Text as Text
+
+-- | O(n log n) equivalent of @nub . sort@
+snub :: (Ord a, Eq a) => [a] -> [a]
+snub = map head . group . sort
+
+-- | SQL pretty-printer. The state is the list of SQL parameters to the
+--   prepared statement.
+type PP = State PPState
+
+data PPState = PPState
+  { ppParams  :: [Param]
+  , ppTables  :: [TableName]
+  , ppParamNS :: Int
+  , ppQueryNS :: Int
+  }
+
+-- | Run a pretty-printer.
+runPP :: PP Text -> ([TableName], (Text, [Param]))
+runPP pp =
+  case runState pp (PPState [] [] 1 0) of
+    (q, st) -> (snub $ ppTables st, (q, reverse (ppParams st)))
+
+-- | Compile an SQL AST into a parameterized SQL query.
+compSql :: SQL -> ([TableName], (Text, [Param]))
+compSql = runPP . ppSql
+
+-- | Compile a single column expression.
+compExp :: Exp a -> (Text, [Param])
+compExp = snd . runPP . ppCol
+
+-- | Compile an @UPATE@ statement.
+compUpdate :: TableName -> Exp Bool -> [(ColName, SomeCol)] -> (Text, [Param])
+compUpdate tbl p cs = snd $ runPP ppUpd
+  where
+    ppUpd = do
+      updates <- mapM ppUpdate cs
+      check <- ppCol p
+      pure $ Text.unwords
+        [ "UPDATE", tbl
+        , "SET", Text.intercalate ", " $ filter (not . Text.null) updates
+        , "WHERE", check
+        ]
+    ppUpdate (n, c) = do
+      c' <- ppSomeCol c
+      if n == c'
+        then pure ""
+        else pure $ Text.unwords [n, "=", c']
+
+-- | Compile a @DELETE@ statement.
+compDelete :: TableName -> Exp Bool -> (Text, [Param])
+compDelete tbl p = snd $ runPP ppDelete
+  where
+    ppDelete = do
+      c' <- ppCol p
+      pure $ Text.unwords ["DELETE FROM", tbl, "WHERE", c']
+
+-- | Pretty-print a literal as a named parameter and save the
+--   name-value binding in the environment.
+ppLit :: Lit a -> PP Text
+ppLit LitNull     = pure "NULL"
+ppLit (LitJust l) = ppLit l
+ppLit l           = do
+  PPState ps ts ns qns <- get
+  put $ PPState (Param l : ps) ts (succ ns) qns
+  return $ Text.pack ('$':show ns)
+
+dependOn :: TableName -> PP ()
+dependOn t = do
+  PPState ps ts ns qns <- get
+  put $ PPState ps (t:ts) ns qns
+
+-- | Generate a unique name for a subquery.
+freshQueryName :: PP Text
+freshQueryName = do
+  PPState ps ts ns qns <- get
+  put $ PPState ps ts ns (succ qns)
+  return $ Text.pack ('q':show qns)
+
+-- | Pretty-print an SQL AST.
+ppSql :: SQL -> PP Text
+ppSql (SQL cs src r gs ord lim) = do
+  cs' <- mapM ppSomeCol cs
+  src' <- ppSrc src
+  r' <- ppRestricts r
+  gs' <- ppGroups gs
+  ord' <- ppOrder ord
+  lim' <- ppLimit lim
+  pure $ mconcat
+    [ "SELECT ", result cs'
+    , src'
+    , r'
+    , gs'
+    , ord'
+    , lim'
+    ]
+  where
+    result []  = "1"
+    result cs' = Text.intercalate "," cs'
+
+    ppSrc (TableName n)  = do
+      dependOn n
+      pure $ " FROM " <> n
+    ppSrc (Product [])   = do
+      pure ""
+    ppSrc (Product sqls) = do
+      srcs <- mapM ppSql (reverse sqls)
+      qs <- flip mapM ["(" <> s <> ")" | s <- srcs] $ \q -> do
+        qn <- freshQueryName
+        pure (q <> " AS " <> qn)
+      pure $ " FROM " <> Text.intercalate "," qs
+    ppSrc (LeftJoin on left right) = do
+      l' <- ppSql left
+      r' <- ppSql right
+      on' <- ppCol on
+      lqn <- freshQueryName
+      rqn <- freshQueryName
+      pure $ mconcat
+        [ " FROM (", l', ") AS ", lqn
+        , " LEFT JOIN (", r', ") AS ", rqn
+        , " ON ", on'
+        ]
+
+    ppRestricts [] = pure ""
+    ppRestricts rs = ppCols rs >>= \rs' -> pure $ " WHERE " <> rs'
+
+    ppGroups [] = pure ""
+    ppGroups grps = do
+      cls <- sequence [ppCol c | Some c <- grps]
+      pure $ " GROUP BY " <> Text.intercalate ", " cls
+
+    ppOrder [] = pure ""
+    ppOrder os = do
+      os' <- sequence [(<> (" " <> ppOrd o)) <$> ppCol c | (o, Some c) <- os]
+      pure $ " ORDER BY " <> Text.intercalate ", " os'
+
+    ppOrd Asc = "ASC"
+    ppOrd Desc = "DESC"
+
+    ppLimit Nothing =
+      pure ""
+    ppLimit (Just (off, limit)) =
+      pure $ " LIMIT " <> ppInt limit <> " OFFSET " <> ppInt off
+
+    ppInt = Text.pack . show
+
+ppSomeCol :: SomeCol -> PP Text
+ppSomeCol (Some c)    = ppCol c
+ppSomeCol (Named n c) = do
+  c' <- ppCol c
+  pure $ c' <> " AS " <> n
+
+ppCols :: [Exp Bool] -> PP Text
+ppCols cs = do
+  cs' <- mapM ppCol (reverse cs)
+  pure $ "(" <> Text.intercalate ") AND (" cs' <> ")"
+
+ppCol :: Exp a -> PP Text
+ppCol (Col name)     = pure name
+ppCol (Lit l)        = ppLit l
+ppCol (BinOp op a b) = ppBinOp op a b
+ppCol (UnOp op a)    = ppUnOp op a
+ppCol (Fun2 f a b)   = do
+  a' <- ppCol a
+  b' <- ppCol b
+  pure $ mconcat [f, "(", a', ", ", b', ")"]
+ppCol (AggrEx f x)   = ppUnOp (Fun f) x
+ppCol (Cast x)       = ppCol x
+
+ppUnOp :: UnOp a b -> Exp a -> PP Text
+ppUnOp op c = do
+  c' <- ppCol c
+  pure $ case op of
+    Abs    -> "ABS(" <> c' <> ")"
+    Sgn    -> "SIGN(" <> c' <> ")"
+    Neg    -> "-(" <> c' <> ")"
+    Not    -> "NOT(" <> c' <> ")"
+    IsNull -> "(" <> c' <> ") IS NULL"
+    Fun f  -> f <> "(" <> c' <> ")"
+
+ppBinOp :: BinOp a b -> Exp a -> Exp a -> PP Text
+ppBinOp op a b = do
+    a' <- ppCol a
+    b' <- ppCol b
+    pure $ paren a a' <> " " <> ppOp op <> " " <> paren b b'
+  where
+    paren :: Exp a -> Text -> Text
+    paren (Col{}) c = c
+    paren (Lit{}) c = c
+    paren _ c       = "(" <> c <> ")"
+
+    ppOp :: BinOp a b -> Text
+    ppOp Gt    = ">"
+    ppOp Lt    = "<"
+    ppOp Gte   = ">="
+    ppOp Lte   = "<="
+    ppOp Eq    = "="
+    ppOp Neq   = "!="
+    ppOp And   = "AND"
+    ppOp Or    = "OR"
+    ppOp Add   = "+"
+    ppOp Sub   = "-"
+    ppOp Mul   = "*"
+    ppOp Div   = "/"
+    ppOp Like  = "LIKE"
diff --git a/src/Database/Selda/SqlType.hs b/src/Database/Selda/SqlType.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/SqlType.hs
@@ -0,0 +1,148 @@
+{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables #-}
+-- | Types representable in Selda's subset of SQL.
+module Database.Selda.SqlType where
+import Data.Text (Text, pack, unpack)
+import Data.Time
+import Data.Proxy
+
+-- | Format string used to represent date and time when
+--   talking to the database backend.
+sqlDateTimeFormat :: String
+sqlDateTimeFormat = "%F %H:%M:%S%Q"
+
+-- | Format string used to represent date when
+--   talking to the database backend.
+sqlDateFormat :: String
+sqlDateFormat = "%F"
+
+-- | Format string used to represent time of day when
+--   talking to the database backend.
+sqlTimeFormat :: String
+sqlTimeFormat = "%H:%M:%S%Q"
+
+-- | Any datatype representable in (Selda's subset of) SQL.
+class SqlType a where
+  mkLit :: a -> Lit a
+  sqlType :: Proxy a -> Text
+  fromSql :: SqlValue -> a
+
+-- | An SQL mkLit.
+data Lit a where
+  LitS    :: !Text    -> Lit Text
+  LitI    :: !Int     -> Lit Int
+  LitD    :: !Double  -> Lit Double
+  LitB    :: !Bool    -> Lit Bool
+  LitTS   :: !Text    -> Lit UTCTime
+  LitDate :: !Text    -> Lit Day
+  LitTime :: !Text    -> Lit TimeOfDay
+  LitJust :: !(Lit a) -> Lit (Maybe a)
+  LitNull :: Lit (Maybe a)
+
+instance Eq (Lit a) where
+  a == b = compLit a b == EQ
+
+instance Ord (Lit a) where
+  compare = compLit
+
+-- | Constructor tag for all literals. Used for Ord instance.
+litConTag :: Lit a -> Int
+litConTag (LitS{})    = 0
+litConTag (LitI{})    = 1
+litConTag (LitD{})    = 2
+litConTag (LitB{})    = 3
+litConTag (LitTS{})   = 4
+litConTag (LitDate{}) = 5
+litConTag (LitTime{}) = 6
+litConTag (LitJust{}) = 7
+litConTag (LitNull)   = 8
+
+-- | Compare two literals of different type for equality.
+compLit :: Lit a -> Lit b -> Ordering
+compLit (LitS x)    (LitS x')    = x `compare` x'
+compLit (LitI x)    (LitI x')    = x `compare` x'
+compLit (LitD x)    (LitD x')    = x `compare` x'
+compLit (LitB x)    (LitB x')    = x `compare` x'
+compLit (LitTS x)   (LitTS x')   = x `compare` x'
+compLit (LitDate x) (LitDate x') = x `compare` x'
+compLit (LitTime x) (LitTime x') = x `compare` x'
+compLit (LitJust x) (LitJust x') = x `compLit` x'
+compLit a           b            = litConTag a `compare` litConTag b
+
+-- | Some value that is representable in SQL.
+data SqlValue where
+  SqlInt    :: !Int    -> SqlValue
+  SqlFloat  :: !Double -> SqlValue
+  SqlString :: !Text   -> SqlValue
+  SqlBool   :: !Bool   -> SqlValue
+  SqlNull   :: SqlValue
+
+instance Show SqlValue where
+  show (SqlInt n)    = "SqlInt " ++ show n
+  show (SqlFloat f)  = "SqlFloat " ++ show f
+  show (SqlString s) = "SqlString " ++ show s
+  show (SqlBool b)   = "SqlBool " ++ show b
+  show (SqlNull)     = "SqlNull"
+
+instance Show (Lit a) where
+  show (LitS s)    = show s
+  show (LitI i)    = show i
+  show (LitD d)    = show d
+  show (LitB b)    = show b
+  show (LitTS s)   = show s
+  show (LitDate s) = show s
+  show (LitTime s) = show s
+  show (LitJust x) = "Just " ++ show x
+  show (LitNull)   = "Nothing"
+
+instance SqlType Int where
+  mkLit = LitI
+  sqlType _ = "INTEGER"
+  fromSql (SqlInt x) = x
+  fromSql v          = error $ "fromSql: int column with non-int value: " ++ show v
+instance SqlType Double where
+  mkLit = LitD
+  sqlType _ = "DOUBLE"
+  fromSql (SqlFloat x) = x
+  fromSql v            = error $ "fromSql: float column with non-float value: " ++ show v
+instance SqlType Text where
+  mkLit = LitS
+  sqlType _ = "TEXT"
+  fromSql (SqlString x) = x
+  fromSql v             = error $ "fromSql: text column with non-text value: " ++ show v
+instance SqlType Bool where
+  mkLit = LitB
+  sqlType _ = "INT"
+  fromSql (SqlBool x) = x
+  fromSql (SqlInt 0)  = False
+  fromSql (SqlInt _)  = True
+  fromSql v           = error $ "fromSql: bool column with non-bool value: " ++ show v
+instance SqlType UTCTime where
+  mkLit = LitTS . pack . formatTime defaultTimeLocale sqlDateTimeFormat
+  sqlType _             = "DATETIME"
+  fromSql (SqlString s) =
+    case parseTimeM True defaultTimeLocale sqlDateTimeFormat (unpack s) of
+      Just t -> t
+      _      -> error $ "fromSql: bad datetime string: " ++ unpack s
+  fromSql v             = error $ "fromSql: datetime column with non-datetime value: " ++ show v
+instance SqlType Day where
+  mkLit = LitDate . pack . formatTime defaultTimeLocale sqlDateFormat
+  sqlType _             = "DATE"
+  fromSql (SqlString s) =
+    case parseTimeM True defaultTimeLocale sqlDateFormat (unpack s) of
+      Just t -> t
+      _      -> error $ "fromSql: bad date string: " ++ unpack s
+  fromSql v             = error $ "fromSql: date column with non-date value: " ++ show v
+instance SqlType TimeOfDay where
+  mkLit = LitTime . pack . formatTime defaultTimeLocale sqlTimeFormat
+  sqlType _             = "TIME"
+  fromSql (SqlString s) =
+    case parseTimeM True defaultTimeLocale sqlTimeFormat (unpack s) of
+      Just t -> t
+      _      -> error $ "fromSql: bad time string: " ++ unpack s
+  fromSql v             = error $ "fromSql: time column with non-time value: " ++ show v
+instance SqlType a => SqlType (Maybe a) where
+  mkLit (Just x) = LitJust $ mkLit x
+  mkLit Nothing  = LitNull
+  sqlType _ = sqlType (Proxy :: Proxy a)
+  fromSql (SqlNull) = Nothing
+  fromSql x         = Just $ fromSql x
diff --git a/src/Database/Selda/Table.hs b/src/Database/Selda/Table.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Table.hs
@@ -0,0 +1,196 @@
+{-# LANGUAGE GADTs, TypeOperators, OverloadedStrings #-}
+{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, RankNTypes #-}
+{-# LANGUAGE UndecidableInstances, FlexibleInstances, ScopedTypeVariables #-}
+-- | Selda table definition language.
+module Database.Selda.Table where
+import Database.Selda.Types
+import Database.Selda.SqlType
+import Data.Text (Text, unpack, intercalate)
+import Data.Proxy
+import Data.List (sort, group)
+import Data.Monoid
+
+type family a :+++: b where
+  (a :*: b) :+++: c = a :*: (b :+++: c)
+  a :+++: b         = a :*: b
+infixr 5 :+++:
+infixr 5 +++
+
+class ComposeSpec t a b where
+  -- | Combine the given tables or column specifications into a new
+  --   column specification which can be used to create a new table.
+  --   Useful for building composable table specifications.
+  --
+  --   Note that this function is only suitable for combining specifications
+  --   which have a concrete type. To build a column specification from scratch,
+  --   use '(¤)' instead.
+  (+++) :: t a -> t b -> ColSpec (a :+++: b)
+
+instance (ComposeSpec Table a b, ComposeSpec Table b c) =>
+         ComposeSpec Table (a :*: b) c where
+  a +++ b = ColSpec $ tableCols a ++ tableCols b
+
+instance {-# OVERLAPPABLE #-} ((a :+++: b) ~ (a :*: b)) =>
+         ComposeSpec Table a b where
+  a +++ b = ColSpec $ tableCols a ++ tableCols b
+
+instance (ComposeSpec ColSpec a b, ComposeSpec ColSpec b c) =>
+         ComposeSpec ColSpec (a :*: b) c where
+  ColSpec a +++ ColSpec b = ColSpec $ a ++ b
+
+instance {-# OVERLAPPABLE #-} ((a :+++: b) ~ (a :*: b)) =>
+         ComposeSpec ColSpec a b where
+  ColSpec a +++ ColSpec b = ColSpec $ a ++ b
+
+-- | Insertion over all non-autoincrementing required columns.
+--   Autoincrementing primary keys are automatically assigned their value.
+type family InsertCols a where
+  InsertCols (Auto a :*: b)  = InsertCols b
+  InsertCols (a :*: Auto b)  = a
+  InsertCols (a :*: b)       = a :*: InsertCols b
+  InsertCols a               = a
+
+-- | A database table.
+--   Tables are parameterized over their column types. For instance, a table
+--   containing one string and one integer, in that order, would have the type
+--   @Table (Text :*: Int)@, and a table containing only a single string column
+--   would have the type @Table Text@.
+data Table a = Table
+  { -- | Name of the table. NOT guaranteed to be a valid SQL name.
+    tableName :: !TableName
+    -- | All table columns.
+    --   Invariant: the 'colAttrs' list of each column is sorted and contains
+    --   no duplicates.
+  , tableCols :: ![ColInfo]
+  }
+
+data ColInfo = ColInfo
+  { colName  :: !ColName
+  , colType  :: !Text
+  , colAttrs :: ![ColAttr]
+  }
+
+newCol :: forall a. SqlType a => ColName -> ColSpec a
+newCol name = ColSpec [ColInfo
+  { colName  = name
+  , colType  = sqlType (Proxy :: Proxy a)
+  , colAttrs = []
+  }]
+
+-- | A table column specification.
+newtype ColSpec a = ColSpec [ColInfo]
+
+-- | Combine two column specifications.
+--   Table descriptions are built by chaining columns using this operator:
+--
+-- > people :: Table (Text :*: Int :*: Maybe Text)
+-- > people = table "people" $ required "name" ¤ required "age" ¤ optional "pet"
+--
+--   To combine two pre-built tables into a table comprised of both tables'
+--   fields, see '(+++)'.
+(¤) :: ColSpec a -> ColSpec b -> ColSpec (a :*: b)
+ColSpec a ¤ ColSpec b = ColSpec (a ++ b)
+infixr 1 ¤
+
+-- | Indicates an automatically incrementing column.
+--   Auto columns are usually not touched in @INSERT@ queries.
+data Auto a
+
+-- | Used by 'IsNullable' to indicate a nullable type.
+data Nullable
+
+-- | Used by 'IsNullable' to indicate a nullable type.
+data NotNullable
+
+-- | Is the given type nullable?
+type family IsNullable a where
+  IsNullable (Maybe a) = Nullable
+  IsNullable a         = NotNullable
+
+-- | Any table type that has an auto-incrementing primary key.
+class HasAutoPrimary a
+instance HasAutoPrimary (Auto a)
+instance HasAutoPrimary (Auto a :*: b)
+instance {-# OVERLAPPABLE #-} HasAutoPrimary b => HasAutoPrimary (a :*: b)
+
+-- | Any SQL type which is NOT nullable.
+class SqlType a => NonNull a
+instance (SqlType a, IsNullable a ~ NotNullable) => NonNull a
+
+-- | Column attributes such as nullability, auto increment, etc.
+--   When adding elements, make sure that they are added in the order
+--   required by SQL syntax, as this list is only sorted before being
+--   pretty-printed.
+data ColAttr = Primary | AutoIncrement | Required | Optional
+  deriving (Show, Eq, Ord)
+
+-- | A non-nullable column with the given name.
+required :: NonNull a => ColName -> ColSpec a
+required = addAttr Required . newCol
+
+-- | A nullable column with the given name.
+optional :: SqlType a => ColName -> ColSpec (Maybe a)
+optional = addAttr Optional . newCol
+
+-- | Marks the given column as the table's primary key.
+--   A table may only have one primary key; marking more than one key as
+--   primary will result in a run-time error.
+primary :: NonNull a => ColName -> ColSpec a
+primary = addAttr Primary . required
+
+-- | Automatically increment the given attribute if not specified during insert.
+--   Also adds the @PRIMARY KEY@ attribute on the column.
+autoPrimary :: ColName -> ColSpec (Auto Int)
+autoPrimary n = ColSpec [c {colAttrs = [Primary, AutoIncrement, Required]}]
+  where ColSpec [c] = newCol n :: ColSpec Int
+
+-- | Add an attribute to a column. Not for public consumption.
+addAttr :: SqlType a => ColAttr -> ColSpec a -> ColSpec a
+addAttr attr (ColSpec [ci]) = ColSpec [ci {colAttrs = attr : colAttrs ci}]
+addAttr _ _                 = error "impossible: SqlType ColSpec with several columns"
+
+-- | A table with the given name and columns.
+table :: TableName -> ColSpec a -> Table a
+table name (ColSpec cs) = Table
+  { tableName = name
+  , tableCols = validate name $ map tidy cs
+  }
+
+-- | Remove duplicate attributes.
+tidy :: ColInfo -> ColInfo
+tidy ci = ci {colAttrs = snub $ colAttrs ci}
+
+-- | Sort a list and remove all duplicates from it.
+snub :: (Ord a, Eq a) => [a] -> [a]
+snub = map head . soup
+
+-- | Sort a list, then group all identical elements.
+soup :: Ord a => [a] -> [[a]]
+soup = group . sort
+
+-- | Ensure that there are no duplicate column names or primary keys.
+validate :: TableName -> [ColInfo] -> [ColInfo]
+validate name cis
+  | null errs = cis
+  | otherwise = error $ concat
+      [ "validation of table ", unpack name, " failed:"
+      , "\n  "
+      , unpack $ intercalate "\n  " errs
+      ]
+  where
+    errs = concat
+      [ dupes
+      , pkDupes
+      , optionalRequiredMutex
+      ]
+    dupes =
+      ["duplicate column: " <> x | (x:_:_) <- soup $ map colName cis]
+    pkDupes =
+      ["multiple primary keys" | (Primary:_:_) <- soup $ concatMap colAttrs cis]
+
+    -- This should be impossible, but...
+    optionalRequiredMutex =
+      [ "BUG: column " <> colName ci <> " is both optional and required"
+      | ci <- cis
+      , Optional `elem` colAttrs ci && Required `elem` colAttrs ci
+      ]
diff --git a/src/Database/Selda/Table/Compile.hs b/src/Database/Selda/Table/Compile.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Table/Compile.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Generating SQL for creating and deleting tables.
+module Database.Selda.Table.Compile where
+import Database.Selda.Table
+import Data.Monoid
+import Data.Text (Text, intercalate, pack)
+import qualified Data.Text as Text
+
+data OnError = Fail | Ignore
+  deriving (Eq, Ord, Show)
+
+-- | Compile a @CREATAE TABLE@ query from a table definition.
+compileCreateTable :: (Text -> [ColAttr] -> Maybe Text) -> OnError -> Table a -> Text
+compileCreateTable customColType ifex tbl = mconcat
+  [ "CREATE TABLE ", ifNotExists ifex, tableName tbl, "("
+  , intercalate ", " (map (compileTableCol customColType) (tableCols tbl))
+  , ")"
+  ]
+  where
+    ifNotExists Fail   = ""
+    ifNotExists Ignore = "IF NOT EXISTS "
+
+-- | Compile a table column.
+compileTableCol :: (Text -> [ColAttr] -> Maybe Text) -> ColInfo -> Text
+compileTableCol customColType ci = Text.unwords
+  [ colName ci
+  , case customColType typ attrs of
+      Just s -> s
+      _      -> typ <> " " <> Text.unwords (map compileColAttr attrs)
+  ]
+  where
+    typ = colType ci
+    attrs = colAttrs ci
+
+-- | Compile a @DROP TABLE@ query.
+compileDropTable :: OnError -> Table a -> Text
+compileDropTable Fail t = Text.unwords ["DROP TABLE",tableName t]
+compileDropTable _ t    = Text.unwords ["DROP TABLE IF EXISTS",tableName t]
+
+-- | Compile an @INSERT INTO@ query inserting @m@ rows with @n@ cols each.
+--   Note that backends expect insertions to NOT have a semicolon at the end.
+compInsert :: Table a -> Int -> Text
+compInsert tbl mrows =
+    Text.unwords ["INSERT INTO", tableName tbl, names, "VALUES", vals]
+  where
+    nonAutos =
+      [ colName c
+      | c <- tableCols tbl
+      , not (AutoIncrement `elem` colAttrs c)
+      ]
+    ncols = length nonAutos
+    names = "(" <>  Text.intercalate ", " nonAutos <> ")"
+    cols n = "(" <> Text.intercalate ", " (mkParams ncols n) <> ")"
+    vals = Text.intercalate ", " $ zipWith (\f n -> f n)
+                                           (replicate mrows cols)
+                                           [0, ncols ..]
+    mkParams cs n = map (pack . ('$':) . show . (+n)) [1..cs]
+
+-- | Compile a column attribute.
+compileColAttr :: ColAttr -> Text
+compileColAttr Primary       = "PRIMARY KEY"
+compileColAttr AutoIncrement = "AUTOINCREMENT"
+compileColAttr Required      = "NOT NULL"
+compileColAttr Optional      = "NULL"
diff --git a/src/Database/Selda/Transform.hs b/src/Database/Selda/Transform.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Transform.hs
@@ -0,0 +1,76 @@
+-- | Analysis and transformation of SQL queries.
+module Database.Selda.Transform where
+import Database.Selda.Column
+import Database.Selda.SQL
+import Database.Selda.Query.Type
+import Database.Selda.Types
+
+-- | Remove all dead columns recursively, assuming that the given list of
+--   column names contains all names present in the final result.
+removeDeadCols :: [ColName] -> SQL -> SQL
+removeDeadCols live sql =
+    case source sql' of
+      TableName _     -> sql'
+      Product qs      -> sql' {source = Product $ map noDead qs}
+      LeftJoin on l r -> sql' {source = LeftJoin on (noDead l) (noDead r)}
+  where
+    noDead = removeDeadCols live'
+    sql' = keepCols (allNonOutputColNames sql ++ live) sql
+    live' = allColNames sql'
+
+-- | Return the names of all columns in the given top-level query.
+--   Subqueries are not traversed.
+allColNames :: SQL -> [ColName]
+allColNames sql = colNames (cols sql) ++ allNonOutputColNames sql
+
+-- | Return the names of all non-output (i.e. 'cols') columns in the given
+--   top-level query. Subqueries are not traversed.
+allNonOutputColNames :: SQL -> [ColName]
+allNonOutputColNames sql = concat
+  [ concatMap allNamesIn (restricts sql)
+  , colNames (groups sql)
+  , colNames (map snd $ ordering sql)
+  , case source sql of
+      LeftJoin on _ _ -> allNamesIn on
+      _               -> []
+  ]
+
+-- | Get all column names appearing in the given list of (possibly complex)
+--   columns.
+colNames :: [SomeCol] -> [ColName]
+colNames cs = concat
+  [ [n | Some c <- cs, n <- allNamesIn c]
+  , [n | Named _ c <- cs, n <- allNamesIn c]
+  , [n | Named n _ <- cs]
+  ]
+
+-- | Remove all columns but the given, named ones and aggregates, from a query's
+--   list of outputs.
+--   If we want to refer to a column in an outer query, it must have a name.
+--   If it doesn't, then it's either not referred to by an outer query, or
+--   the outer query duplicates the expression, thereby referring directly
+--   to the names of its components.
+keepCols :: [ColName] -> SQL -> SQL
+keepCols live sql = sql {cols = filtered}
+  where
+    filtered = filter (`oneOf` live) (cols sql)
+    oneOf (Some (AggrEx _ _)) _    = True
+    oneOf (Named _ (AggrEx _ _)) _ = True
+    oneOf (Some (Col n)) ns        = n `elem` ns
+    oneOf (Named n _) ns           = n `elem` ns
+    oneOf _ _                      = False
+
+-- | Build the outermost query from the SQL generation state.
+--   Groups are ignored, as they are only used by 'aggregate'.
+state2sql :: GenState -> SQL
+state2sql (GenState [sql] srs _ _) =
+  sql {restricts = restricts sql ++ srs}
+state2sql (GenState ss srs _ _) =
+  SQL (allCols ss) (Product ss) srs [] [] Nothing
+
+-- | Get all output columns from a list of SQL ASTs.
+allCols :: [SQL] -> [SomeCol]
+allCols sqls = [outCol col | sql <- sqls, col <- cols sql]
+  where
+    outCol (Named n _) = Some (Col n)
+    outCol c           = c
diff --git a/src/Database/Selda/Types.hs b/src/Database/Selda/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Types.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE GADTs, TypeOperators, TypeFamilies, FlexibleInstances #-}
+-- | Basic Selda types.
+module Database.Selda.Types where
+import Data.Text (Text)
+import Data.Typeable
+
+-- | Name of a database column.
+type ColName = Text
+
+-- | Name of a database table.
+type TableName = Text
+
+-- | An inductively defined "tuple", or heterogeneous, non-empty list.
+data a :*: b where
+  (:*:) :: a -> b -> a :*: b
+  deriving Typeable
+infixr 1 :*:
+
+instance (Show a, Show b) => Show (a :*: b) where
+  show (a :*: b) = show a ++ " :*: " ++ show b
+
+instance (Eq a, Eq b) => Eq (a :*: b) where
+  (a :*: b) == (a' :*: b') = a == a' && b == b'
+
+instance (Ord a, Ord b) => Ord (a :*: b) where
+  (a :*: b) `compare` (a' :*: b') =
+    case a `compare` a' of
+      EQ -> b `compare` b'
+      o  -> o
+
+type family Head a where
+  Head (a :*: b) = a
+  Head a         = a
+
+class Tup a where
+  tupHead :: a -> Head a
+
+instance {-# OVERLAPPING #-} Tup (a :*: b) where
+  tupHead (a :*: _) = a
+
+instance Head a ~ a => Tup a where
+  tupHead a = a
+
+-- | Get the first element of an inductive tuple.
+first :: Tup a => a -> Head a
+first = tupHead
+
+-- | Get the second element of an inductive tuple.
+second :: Tup b => (a :*: b) -> Head b
+second (_ :*: b) = tupHead b
+
+-- | Get the third element of an inductive tuple.
+third :: Tup c => (a :*: b :*: c) -> Head c
+third (_ :*: _ :*: c) = tupHead c
+
+-- | Get the fourth element of an inductive tuple.
+fourth :: Tup d => (a :*: b :*: c :*: d) -> Head d
+fourth (_ :*: _ :*: _ :*: d) = tupHead d
+
+-- | Get the fifth element of an inductive tuple.
+fifth :: Tup e => (a :*: b :*: c :*: d :*: e) -> Head e
+fifth (_ :*: _ :*: _ :*: _ :*: e) = tupHead e
+
+-- | Get the sixth element of an inductive tuple.
+sixth :: Tup f => (a :*: b :*: c :*: d :*: e :*: f) -> Head f
+sixth (_ :*: _ :*: _ :*: _ :*: _ :*: f) = tupHead f
+
+-- | Get the seventh element of an inductive tuple.
+seventh :: Tup g => (a :*: b :*: c :*: d :*: e :*: f :*: g) -> Head g
+seventh (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: g) = tupHead g
+
+-- | Get the eighth element of an inductive tuple.
+eighth :: Tup h => (a :*: b :*: c :*: d :*: e :*: f :*: g :*: h) -> Head h
+eighth (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: h) = tupHead h
+
+-- | Get the ninth element of an inductive tuple.
+ninth :: Tup i => (a :*: b :*: c :*: d :*: e :*: f :*: h :*: h :*: i) -> Head i
+ninth (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: i) = tupHead i
+
+-- | Get the tenth element of an inductive tuple.
+tenth :: Tup j => (a :*: b :*: c :*: d :*: e :*: f :*: g :*: h :*: i :*: j)
+      -> Head j
+tenth (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: j) = tupHead j
diff --git a/src/Database/Selda/Unsafe.hs b/src/Database/Selda/Unsafe.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Selda/Unsafe.hs
@@ -0,0 +1,14 @@
+-- | Unsafe operations giving the user unchecked low-level control over
+--   the generated SQL.
+module Database.Selda.Unsafe
+  ( fun, fun2
+  , aggr
+  , cast
+  ) where
+import Database.Selda.Column
+import Database.Selda.Inner (aggr)
+
+-- | Cast a column to another type, using whichever coercion semantics are used
+--   by the underlying SQL implementation.
+cast :: Col s a -> Col s b
+cast = liftC Cast
