diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright (c) 2017 Morphism, LLC
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the names of the copyright holders nor the names of the
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# squeal
+
+![squeal-icon](http://www.emoticonswallpapers.com/emotion/cute-big-pig/cute-pig-smiley-046.gif)
+
+[![CircleCI](https://circleci.com/gh/echatav/squeal.svg?style=svg&circle-token=a699a654ef50db2c3744fb039cf2087c484d1226)](https://circleci.com/gh/echatav/squeal)
+
+Main repository for the squeal database library.
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/exe/Example.hs b/exe/Example.hs
new file mode 100644
--- /dev/null
+++ b/exe/Example.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE
+    DataKinds
+  , DeriveGeneric
+  , OverloadedLabels
+  , OverloadedStrings
+  , TypeApplications
+  , TypeOperators
+#-}
+
+module Main (main) where
+
+import Control.Monad.Base (liftBase)
+import Data.Int (Int32)
+import Data.Monoid ((<>))
+import Data.Text (Text)
+
+import Squeal.PostgreSQL
+
+import qualified Data.ByteString.Char8 as Char8
+import qualified Generics.SOP as SOP
+import qualified GHC.Generics as GHC
+
+type Schema =
+  '[ "users" :::
+       '[ "id" ::: 'Optional ('NotNull 'PGint4)
+        , "name" ::: 'Required ('NotNull 'PGtext)
+        ]
+   , "emails" :::
+       '[ "id" ::: 'Optional ('NotNull 'PGint4)
+        , "user_id" ::: 'Required ('NotNull 'PGint4)
+        , "email" ::: 'Required ('Null 'PGtext)
+        ]
+   ]
+
+setup :: Definition '[] Schema
+setup = 
+  createTable #users
+    ( serial `As` #id :*
+      (text & notNull) `As` #name :* Nil )
+    [ primaryKey (Column #id :* Nil) ]
+  >>>
+  createTable #emails
+    ( serial `As` #id :*
+      (int & notNull) `As` #user_id :*
+      text `As` #email :* Nil )
+    [ primaryKey (Column #id :* Nil)
+    , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
+      OnDeleteCascade OnUpdateCascade ]
+
+teardown :: Definition Schema '[]
+teardown = dropTable #emails >>> dropTable #users
+
+insertUser :: Manipulation Schema
+  '[ 'Required ('NotNull 'PGtext)]
+  '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
+insertUser = insertInto #users
+  ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
+  OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
+
+insertEmail :: Manipulation Schema
+  '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
+insertEmail = insertInto #emails ( Values
+  ( def `As` #id :*
+    param @1 `As` #user_id :*
+    param @2 `As` #email :* Nil) [] )
+  OnConflictDoNothing (Returning Nil)
+
+getUsers :: Query Schema '[]
+  '[ "userName" ::: 'Required ('NotNull 'PGtext)
+   , "userEmail" ::: 'Required ('Null 'PGtext) ]
+getUsers = select
+  (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
+  ( from (Table (#users `As` #u)
+    & InnerJoin (Table (#emails `As` #e))
+      (#u ! #id .== #e ! #user_id)) )
+
+data User = User { userName :: Text, userEmail :: Maybe Text }
+  deriving (Show, GHC.Generic)
+instance SOP.Generic User
+instance SOP.HasDatatypeInfo User
+
+users :: [User]
+users = 
+  [ User "Alice" (Just "alice@gmail.com")
+  , User "Bob" Nothing
+  , User "Carole" (Just "carole@hotmail.com")
+  ]
+
+main :: IO ()
+main = do
+  Char8.putStrLn "squeal"
+  connectionString <- pure
+    "host=localhost port=5432 dbname=exampledb"
+  Char8.putStrLn $ "connecting to " <> connectionString
+  connection0 <- connectdb connectionString
+  Char8.putStrLn "setting up schema"
+  connection1 <- execPQ (define setup) connection0
+  connection2 <- flip execPQ connection1 $ do
+    liftBase $ Char8.putStrLn "manipulating"
+    idResults <- traversePrepared insertUser (Only . userName <$> users)
+    ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
+    traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
+    liftBase $ Char8.putStrLn "querying"
+    usersResult <- runQuery getUsers
+    usersRows <- getRows usersResult
+    liftBase $ print (usersRows :: [User])
+  Char8.putStrLn "tearing down schema"
+  connection3 <- execPQ (define teardown) connection2
+  finish connection3
diff --git a/squeal-postgresql.cabal b/squeal-postgresql.cabal
new file mode 100644
--- /dev/null
+++ b/squeal-postgresql.cabal
@@ -0,0 +1,93 @@
+name: squeal-postgresql
+version: 0.1.0.0
+synopsis: Squeal PostgreSQL Library
+description: Squeal is a type-safe embedding of PostgreSQL in Haskell
+homepage: https://github.com/morphismtech/squeal
+bug-reports: https://github.com/morphismtech/squeal/issues
+license: BSD3
+license-file: LICENSE
+author: Eitan Chatav
+maintainer: eitan.chatav@gmail.com
+copyright: Copyright (c) 2017 Morphism, LLC
+category: Database
+build-type: Simple
+cabal-version: >=1.18
+extra-doc-files: README.md
+
+source-repository head
+  type: git
+  location: https://github.com/morphismtech/squeal.git
+
+library
+  hs-source-dirs: src
+  exposed-modules:
+    Squeal.PostgreSQL
+    Squeal.PostgreSQL.Binary
+    Squeal.PostgreSQL.Definition
+    Squeal.PostgreSQL.Expression
+    Squeal.PostgreSQL.Manipulation
+    Squeal.PostgreSQL.PQ
+    Squeal.PostgreSQL.Prettyprint
+    Squeal.PostgreSQL.Query
+    Squeal.PostgreSQL.Schema
+  default-language: Haskell2010
+  ghc-options: -Wall -fprint-explicit-kinds
+  build-depends:
+      aeson >= 1.2.1.0 && < 2
+    , base >= 4.10.0.0 && < 5
+    , bytestring >= 0.10.8.2 && < 1
+    , deepseq >= 1.4.3.0 && < 2
+    , generics-sop >= 0.3.1.0 && < 1
+    , lifted-base >= 0.2.3.11 && < 1
+    , monad-control >= 1.0.2.2 && < 2
+    , mtl >= 2.2.1 && < 3
+    , network-ip >= 0.3.0.2 && < 1
+    , postgresql-binary >= 0.12.1 && < 1
+    , postgresql-libpq >= 0.9.3.1 && < 1
+    , scientific >= 0.3.5.1 && < 1
+    , text >= 1.2.2.2 && < 2
+    , time >= 1.8.0.2 && < 2
+    , transformers >= 0.5.2.0 && < 1
+    , transformers-base >= 0.4.4 && < 1
+    , uuid >= 1.3.13 && < 2
+
+test-suite squeal-postgresql-spec
+  default-language: Haskell2010
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  ghc-options: -Wall
+  main-is: Spec.hs
+  other-modules:
+    Squeal.PostgreSQL.DefinitionSpec
+    Squeal.PostgreSQL.ManipulationSpec
+    Squeal.PostgreSQL.QuerySpec
+  build-depends:
+      base >= 4.10.0.0 && < 5
+    , generics-sop >= 0.3.1.0 && < 1
+    , hspec >= 2.4.4 && < 3
+    , squeal-postgresql
+
+test-suite squeal-postgresql-doctest
+  default-language: Haskell2010
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  ghc-options: -Wall
+  main-is: DocTest.hs
+  build-depends:
+      base >= 4.10.0.0 && < 5
+    , doctest >= 0.11.4 && < 1
+
+executable squeal-postgresql-example
+  default-language: Haskell2010
+  hs-source-dirs: exe
+  ghc-options: -Wall
+  main-is: Example.hs
+  build-depends:
+      base >= 4.10.0.0 && < 5
+    , bytestring >= 0.10.8.2 && < 1
+    , generics-sop >= 0.3.1.0 && < 1
+    , mtl >= 2.2.1 && < 3
+    , squeal-postgresql
+    , text >= 1.2.2.2 && < 2
+    , transformers >= 0.5.2.0 && < 1
+    , transformers-base >= 0.4.4 && < 1
diff --git a/src/Squeal/PostgreSQL.hs b/src/Squeal/PostgreSQL.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL.hs
@@ -0,0 +1,184 @@
+-- | Module: Squeal.PostgreSQL
+-- Description: Squeel export module
+-- Copyright: (c) Eitan Chatav, 2017
+-- Maintainer: eitan@morphism.tech
+-- Stability: experimental
+--
+-- Squeal is a deep embedding of PostgreSQL in Haskell. Let's see an example!
+--
+-- First, we need some language extensions because Squeal uses modern GHC
+-- features.
+--
+-- > {-# LANGUAGE
+-- >     DataKinds
+-- >   , DeriveGeneric
+-- >   , OverloadedLabels
+-- >   , OverloadedStrings
+-- >   , TypeApplications
+-- >   , TypeOperators
+-- > #-}
+-- 
+-- Here comes the @Main@ module and imports.
+--
+-- > module Main (main) where
+-- > 
+-- > import Control.Monad.Base (liftBase)
+-- > import Data.Int (Int32)
+-- > import Data.Text (Text)
+-- >
+-- > import Squeal.PostgreSQL
+--
+-- We'll use generics to easily convert between Haskell and PostgreSQL values.
+--
+-- > import qualified Generics.SOP as SOP
+-- > import qualified GHC.Generics as GHC
+--
+-- The first step is to define the schema of our database. This is where
+-- we use @DataKinds@ and @TypeOperators@. The schema consists of a type-level
+-- list of tables, a `:::` pairing of a type level string or
+-- `Data.TypeLit.Symbol` and a list a columns, itself a `:::` pairing of a
+-- `Data.TypeLit.Symbol` and a `ColumnType`. The `ColumnType` describes the
+-- PostgreSQL type of the column as well as whether or not it may contain
+-- @NULL@ and whether or not inserts and updates can use a @DEFAULT@. For our
+-- schema, we'll define two tables, a users table and an emails table.
+--
+-- > type Schema =
+-- >   '[ "users"  :::
+-- >        '[ "id"   ::: 'Optional ('NotNull 'PGint4)
+-- >         , "name" ::: 'Required ('NotNull 'PGtext)
+-- >         ]
+-- >    , "emails" :::
+-- >        '[ "id"      ::: 'Optional ('NotNull 'PGint4)
+-- >         , "user_id" ::: 'Required ('NotNull 'PGint4)
+-- >         , "email"   ::: 'Required ('Null 'PGtext)
+-- >         ]
+-- >    ]
+--
+-- Next, we'll write `Definition`s to set up and tear down the schema. In
+-- Squeal, a `Definition` is a `createTable`, `alterTable` or `dropTable`
+-- command and has two type parameters, corresponding to the schema
+-- before being run and the schema after. We can compose definitions using
+-- `>>>`. Here and in the rest of our commands we make use of overloaded
+-- labels to refer to named tables and columns in our schema.
+--
+-- > setup :: Definition '[] Schema
+-- > setup = 
+-- >   createTable #users
+-- >     ( serial `As` #id :*
+-- >       (text & notNull) `As` #name :* Nil )
+-- >     [ primaryKey (Column #id :* Nil) ]
+-- >   >>>
+-- >   createTable #emails
+-- >     ( serial `As` #id :*
+-- >       (int & notNull) `As` #user_id :*
+-- >       text `As` #email :* Nil )
+-- >     [ primaryKey (Column #id :* Nil)
+-- >     , foreignKey (Column #user_id :* Nil) #users (Column #id :* Nil)
+-- >       OnDeleteCascade OnUpdateCascade ]
+--
+-- Notice that @setup@ starts with an empty schema @'[]@ and produces @Schema@.
+-- In our `createTable` commands we included `TableConstraint`s to define
+-- primary and foreign keys, making them somewhat complex. Our tear down
+-- `Definition` is simpler.
+--
+-- > teardown :: Definition Schema '[]
+-- > teardown = dropTable #emails >>> dropTable #users
+--
+-- Next, we'll write `Manipulation`s to insert data into our two tables.
+-- A `Manipulation` is a `insertInto`, `update` or `deleteFrom` command and
+-- has three type parameters, the schema it refers to, a list of parameters
+-- it can take as input, and a list of columns it produces as output. When
+-- we insert into the users table, we will need a parameter for the @name@
+-- field but not for the @id@ field. Since it's optional, we can use a default
+-- value. However, since the emails table refers to the users table, we will
+-- need to retrieve the user id that the insert generates and insert it into
+-- the emails table. Take a careful look at the type and definition of both
+-- of our inserts.
+--
+-- > insertUser :: Manipulation Schema
+-- >   '[ 'Required ('NotNull 'PGtext)]
+-- >   '[ "fromOnly" ::: 'Required ('NotNull 'PGint4) ]
+-- > insertUser = insertInto #users
+-- >   ( Values (def `As` #id :* param @1 `As` #name :* Nil) [] )
+-- >   OnConflictDoNothing (Returning (#id `As` #fromOnly :* Nil))
+-- > 
+-- > insertEmail :: Manipulation Schema
+-- >   '[ 'Required ('NotNull 'PGint4), 'Required ('Null 'PGtext)] '[]
+-- > insertEmail = insertInto #emails ( Values
+-- >   ( def `As` #id :*
+-- >     param @1 `As` #user_id :*
+-- >     param @2 `As` #email :* Nil) [] )
+-- >   OnConflictDoNothing (Returning Nil)
+--
+-- Next we write a `Query` to retrieve users from the database. We're not
+-- interested in the ids here, just the usernames and email addresses. We
+-- need to use an inner join to get the right result. A `Query` is like a
+-- `Manipulation` with the same kind of type parameters.
+--
+-- > getUsers :: Query Schema '[]
+-- >   '[ "userName" ::: 'Required ('NotNull 'PGtext)
+-- >    , "userEmail" ::: 'Required ('Null 'PGtext) ]
+-- > getUsers = select
+-- >   (#u ! #name `As` #userName :* #e ! #email `As` #userEmail :* Nil)
+-- >   ( from (Table (#users `As` #u)
+-- >     & InnerJoin (Table (#emails `As` #e))
+-- >       (#u ! #id .== #e ! #user_id)) )
+--
+-- Now that we've defined the SQL side of things, we'll need a Haskell type
+-- for users. We give the type `Generics.SOP.Generic` and
+-- `Generics.SOP.HasDatatypeInfo` instances so that we can decode the rows
+-- we receive when we run @getUsers@. Notice that the record fields of the
+-- @User@ type match the column names of @getUsers@.
+-- 
+-- > data User = User { userName :: Text, userEmail :: Maybe Text }
+-- >   deriving (Show, GHC.Generic)
+-- > instance SOP.Generic User
+-- > instance SOP.HasDatatypeInfo User
+--
+-- Let's also create some users to add to the database.
+--
+-- > users :: [User]
+-- > users = 
+-- >   [ User "Alice" (Just "alice@gmail.com")
+-- >   , User "Bob" Nothing
+-- >   , User "Carole" (Just "carole@hotmail.com")
+-- >   ]
+--
+-- Now we can put together all the pieces into a program. The program
+-- connects to the database, sets up the schema, inserts the user data
+-- (using prepared statements as an optimization), queries the user
+-- data and prints it out and finally closes the connection. Since the
+-- `Connection` tracks the schema of the database, we get a new one
+-- when we execute our statements.
+-- 
+-- > main :: IO ()
+-- > main = do
+-- >   connection0 <- connectdb "host=localhost port=5432 dbname=exampledb"
+-- >   connection1 <- execPQ (define setup) connection0
+-- >   connection2 <- flip execPQ connection1 $ do
+-- >     idResults <- traversePrepared insertUser (Only . userName <$> users)
+-- >     ids <- traverse (fmap fromOnly . getRow (RowNumber 0)) idResults
+-- >     traversePrepared_ insertEmail (zip (ids :: [Int32]) (userEmail <$> users))
+-- >     usersResult <- runQuery getUsers
+-- >     usersRows <- getRows usersResult
+-- >     liftBase $ print (usersRows :: [User])
+-- >   connection3 <- execPQ (define teardown) connection2
+-- >   finish connection3
+
+module Squeal.PostgreSQL
+  ( module Squeal.PostgreSQL.Binary
+  , module Squeal.PostgreSQL.Definition
+  , module Squeal.PostgreSQL.Expression
+  , module Squeal.PostgreSQL.Manipulation
+  , module Squeal.PostgreSQL.PQ
+  , module Squeal.PostgreSQL.Query
+  , module Squeal.PostgreSQL.Schema
+  ) where
+
+import Squeal.PostgreSQL.Binary
+import Squeal.PostgreSQL.Definition
+import Squeal.PostgreSQL.Expression
+import Squeal.PostgreSQL.Manipulation
+import Squeal.PostgreSQL.PQ
+import Squeal.PostgreSQL.Query
+import Squeal.PostgreSQL.Schema
diff --git a/src/Squeal/PostgreSQL/Binary.hs b/src/Squeal/PostgreSQL/Binary.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Binary.hs
@@ -0,0 +1,264 @@
+{-|
+Module: Squeal.PostgreSQL.Binary
+Description: Binary encoding and decoding
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Binary encoding and decoding between Haskell and PostgreSQL types.
+-}
+
+{-# LANGUAGE
+    ConstraintKinds
+  , DataKinds
+  , DefaultSignatures
+  , DeriveFoldable
+  , DeriveFunctor
+  , DeriveGeneric
+  , DeriveTraversable
+  , FlexibleContexts
+  , FlexibleInstances
+  , GADTs
+  , LambdaCase
+  , KindSignatures
+  , MultiParamTypeClasses
+  , ScopedTypeVariables
+  , TypeApplications
+  , TypeFamilies
+  , TypeInType
+  , TypeOperators
+  , UndecidableInstances
+#-}
+
+module Squeal.PostgreSQL.Binary
+  ( -- * Encoding
+    ToParam (..)
+  , ToColumnParam (..)
+  , ToParams (..)
+    -- * Decoding
+  , FromValue (..)
+  , FromColumnValue (..)
+  , FromRow (..)
+    -- * Only
+  , Only (..)
+  ) where
+
+import Data.Aeson hiding (Null)
+import Data.Int
+import Data.Kind
+import Data.Scientific
+import Data.Time
+import Data.UUID
+import Data.Word
+import Generics.SOP
+import GHC.TypeLits
+import Network.IP.Addr
+
+import qualified Data.ByteString.Lazy as Lazy
+import qualified Data.ByteString as Strict hiding (unpack)
+import qualified Data.Text.Lazy as Lazy
+import qualified Data.Text as Strict
+import qualified GHC.Generics as GHC
+import qualified PostgreSQL.Binary.Decoding as Decoding
+import qualified PostgreSQL.Binary.Encoding as Encoding
+
+import Squeal.PostgreSQL.Schema
+
+-- | A `ToParam` constraint gives an encoding of a Haskell `Type` into
+-- into the binary format of a PostgreSQL `PGType`.
+class ToParam (x :: Type) (pg :: PGType) where
+  -- | >>> :set -XTypeApplications -XDataKinds
+  -- >>> toParam @Bool @'PGbool False
+  -- K "\NUL"
+  --
+  -- >>> toParam @Int16 @'PGint2 0
+  -- K "\NUL\NUL"
+  --
+  -- >>> toParam @Int32 @'PGint4 0
+  -- K "\NUL\NUL\NUL\NUL"
+  --
+  -- >>> :set -XMultiParamTypeClasses
+  -- >>> newtype Id = Id { getId :: Int16 } deriving Show
+  -- >>> instance ToParam Id 'PGint2 where toParam = toParam . getId
+  -- >>> toParam @Id @'PGint2 (Id 1)
+  -- K "\NUL\SOH"
+  toParam :: x -> K Encoding.Encoding pg
+instance ToParam Bool 'PGbool where toParam = K . Encoding.bool
+instance ToParam Int16 'PGint2 where toParam = K . Encoding.int2_int16
+instance ToParam Word16 'PGint2 where toParam = K . Encoding.int2_word16
+instance ToParam Int32 'PGint4 where toParam = K . Encoding.int4_int32
+instance ToParam Word32 'PGint4 where toParam = K . Encoding.int4_word32
+instance ToParam Int64 'PGint8 where toParam = K . Encoding.int8_int64
+instance ToParam Word64 'PGint8 where toParam = K . Encoding.int8_word64
+instance ToParam Float 'PGfloat4 where toParam = K . Encoding.float4
+instance ToParam Double 'PGfloat8 where toParam = K . Encoding.float8
+instance ToParam Scientific 'PGnumeric where toParam = K . Encoding.numeric
+instance ToParam UUID 'PGuuid where toParam = K . Encoding.uuid
+instance ToParam (NetAddr IP) 'PGinet where toParam = K . Encoding.inet
+instance ToParam Char ('PGchar 1) where toParam = K . Encoding.char_utf8
+instance ToParam Strict.Text 'PGtext where toParam = K . Encoding.text_strict
+instance ToParam Lazy.Text 'PGtext where toParam = K . Encoding.text_lazy
+instance ToParam Strict.ByteString 'PGbytea where
+  toParam = K . Encoding.bytea_strict
+instance ToParam Lazy.ByteString 'PGbytea where
+  toParam = K . Encoding.bytea_lazy
+instance ToParam Day 'PGdate where toParam = K . Encoding.date
+instance ToParam TimeOfDay 'PGtime where toParam = K . Encoding.time_int
+instance ToParam (TimeOfDay, TimeZone) 'PGtimetz where
+  toParam = K . Encoding.timetz_int
+instance ToParam LocalTime 'PGtimestamp where
+  toParam = K . Encoding.timestamp_int
+instance ToParam UTCTime 'PGtimestamptz where
+  toParam = K . Encoding.timestamptz_int
+instance ToParam DiffTime 'PGinterval where toParam = K . Encoding.interval_int
+instance ToParam Value 'PGjson where toParam = K . Encoding.json_ast
+instance ToParam Value 'PGjsonb where toParam = K . Encoding.jsonb_ast
+
+-- | A `ToColumnParam` constraint lifts the `ToParam` encoding 
+-- of a `Type` to a `ColumnType`, encoding `Maybe`s to `Null`s. You should
+-- not define instances of `ToColumnParam`, just use the provided instances.
+class ToColumnParam (x :: Type) (ty :: ColumnType) where
+  -- | >>> toColumnParam @Int16 @('Required ('NotNull 'PGint2)) 0
+  -- K (Just "\NUL\NUL")
+  --
+  -- >>> toColumnParam @(Maybe Int16) @('Required ('Null 'PGint2)) (Just 0)
+  -- K (Just "\NUL\NUL")
+  --
+  -- >>> toColumnParam @(Maybe Int16) @('Required ('Null 'PGint2)) Nothing
+  -- K Nothing
+  toColumnParam :: x -> K (Maybe Strict.ByteString) ty
+instance ToParam x pg => ToColumnParam x (optionality ('NotNull pg)) where
+  toColumnParam = K . Just . Encoding.encodingBytes . unK . toParam @x @pg
+instance ToParam x pg => ToColumnParam (Maybe x) (optionality ('Null pg)) where
+  toColumnParam = K . fmap (Encoding.encodingBytes . unK . toParam @x @pg)
+
+-- | A `ToParams` constraint generically sequences the encodings of `Type`s
+-- of the fields of a tuple or record to a row of `ColumnType`s. You should
+-- not define instances of `ToParams`. Instead define `Generic` instances
+-- which in turn provide `ToParams` instances.
+class SListI tys => ToParams (x :: Type) (tys :: [ColumnType]) where
+  -- | >>> type PGparams = '[ 'Required ('NotNull 'PGbool), 'Required ('Null 'PGint2)]
+  -- >>> toParams @(Bool, Maybe Int16) @PGparams (False, Just 0)
+  -- K (Just "\NUL") :* (K (Just "\NUL\NUL") :* Nil)
+  --
+  -- >>> :set -XDeriveGeneric
+  -- >>> data Hparams = Hparams { col1 :: Bool, col2 :: Maybe Int16} deriving GHC.Generic
+  -- >>> instance Generic Hparams
+  -- >>> toParams @Hparams @PGparams (Hparams False (Just 0))
+  -- K (Just "\NUL") :* (K (Just "\NUL\NUL") :* Nil)
+  toParams :: x -> NP (K (Maybe Strict.ByteString)) tys
+instance (SListI tys, IsProductType x xs, AllZip ToColumnParam xs tys)
+  => ToParams x tys where
+      toParams
+        = htrans (Proxy @ToColumnParam) (toColumnParam . unI)
+        . unZ . unSOP . from
+
+-- | A `FromValue` constraint gives a parser from the binary format of
+-- a PostgreSQL `PGType` into a Haskell `Type`.
+class FromValue (pg :: PGType) (y :: Type) where
+  -- | >>> newtype Id = Id { getId :: Int16 } deriving Show
+  -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
+  fromValue :: proxy pg -> Decoding.Value y
+instance FromValue 'PGbool Bool where fromValue _ = Decoding.bool
+instance FromValue 'PGint2 Int16 where fromValue _ = Decoding.int
+instance FromValue 'PGint4 Int32 where fromValue _ = Decoding.int
+instance FromValue 'PGint8 Int64 where fromValue _ = Decoding.int
+instance FromValue 'PGfloat4 Float where fromValue _ = Decoding.float4
+instance FromValue 'PGfloat8 Double where fromValue _ = Decoding.float8
+instance FromValue 'PGnumeric Scientific where fromValue _ = Decoding.numeric
+instance FromValue 'PGuuid UUID where fromValue _ = Decoding.uuid
+instance FromValue 'PGinet (NetAddr IP) where fromValue _ = Decoding.inet
+instance FromValue ('PGchar 1) Char where fromValue _ = Decoding.char
+instance FromValue 'PGtext Strict.Text where fromValue _ = Decoding.text_strict
+instance FromValue 'PGtext Lazy.Text where fromValue _ = Decoding.text_lazy
+instance FromValue 'PGbytea Strict.ByteString where
+  fromValue _ = Decoding.bytea_strict
+instance FromValue 'PGbytea Lazy.ByteString where
+  fromValue _ = Decoding.bytea_lazy
+instance FromValue 'PGdate Day where fromValue _ = Decoding.date
+instance FromValue 'PGtime TimeOfDay where fromValue _ = Decoding.time_int
+instance FromValue 'PGtimetz (TimeOfDay, TimeZone) where
+  fromValue _ = Decoding.timetz_int
+instance FromValue 'PGtimestamp LocalTime where
+  fromValue _ = Decoding.timestamp_int
+instance FromValue 'PGtimestamptz UTCTime where
+  fromValue _ = Decoding.timestamptz_int
+instance FromValue 'PGinterval DiffTime where
+  fromValue _ = Decoding.interval_int
+instance FromValue 'PGjson Value where fromValue _ = Decoding.json_ast
+instance FromValue 'PGjsonb Value where fromValue _ = Decoding.jsonb_ast
+
+-- | A `FromColumnValue` constraint lifts the `FromValue` parser
+-- to a decoding of a @(Symbol, ColumnType)@ to a `Type`,
+-- decoding `Null`s to `Maybe`s. You should not define instances for
+-- `FromColumnValue`, just use the provided instances.
+class FromColumnValue (colty :: (Symbol,ColumnType)) (y :: Type) where
+  -- | >>> :set -XTypeOperators -XOverloadedStrings
+  -- >>> newtype Id = Id { getId :: Int16 } deriving Show
+  -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
+  -- >>> fromColumnValue @("col" ::: 'Required ('NotNull 'PGint2)) @Id (K (Just "\NUL\SOH"))
+  -- Id {getId = 1}
+  --
+  -- >>> fromColumnValue @("col" ::: 'Required ('Null 'PGint2)) @(Maybe Id) (K (Just "\NUL\SOH"))
+  -- Just (Id {getId = 1})
+  fromColumnValue :: K (Maybe Strict.ByteString) colty -> y
+instance FromValue pg y
+  => FromColumnValue (column ::: ('Required ('NotNull pg))) y where
+    fromColumnValue = \case
+      K Nothing -> error "fromColumnValue: saw NULL when expecting NOT NULL"
+      K (Just bytes) ->
+        let
+          errOrValue =
+            Decoding.valueParser (fromValue @pg @y Proxy) bytes
+          err str = error $ "fromColumnValue: " ++ Strict.unpack str
+        in
+          either err id errOrValue
+instance FromValue pg y
+  => FromColumnValue (column ::: ('Required ('Null pg))) (Maybe y) where
+    fromColumnValue (K nullOrBytes)
+      = either err id
+      . Decoding.valueParser (fromValue @pg @y Proxy)
+      <$> nullOrBytes
+      where
+        err str = error $ "fromColumnValue: " ++ Strict.unpack str
+
+-- | A `FromRow` constraint generically sequences the parsings of the columns
+-- of a `ColumnsType` into the fields of a record `Type` provided they have
+-- the same field names. You should not define instances of `FromRow`.
+-- Instead define `Generic` and `HasDatatypeInfo` instances which in turn
+-- provide `FromRow` instances.
+class SListI results => FromRow (results :: ColumnsType) y where
+  -- | >>> :set -XOverloadedStrings
+  -- >>> import Data.Text
+  -- >>> newtype Id = Id { getId :: Int16 } deriving Show
+  -- >>> instance FromValue 'PGint2 Id where fromValue = fmap Id . fromValue
+  -- >>> data Hrow = Hrow { userId :: Id, userName :: Maybe Text } deriving (Show, GHC.Generic)
+  -- >>> instance Generic Hrow
+  -- >>> instance HasDatatypeInfo Hrow
+  -- >>> type PGrow = '["userId" ::: 'Required ('NotNull 'PGint2), "userName" ::: 'Required ('Null 'PGtext)]
+  -- >>> fromRow @PGrow @Hrow (K (Just "\NUL\SOH") :* K (Just "bloodninja") :* Nil)
+  -- Hrow {userId = Id {getId = 1}, userName = Just "bloodninja"}
+  fromRow :: NP (K (Maybe Strict.ByteString)) results -> y
+instance
+  ( SListI results
+  , IsProductType y ys
+  , AllZip FromColumnValue results ys
+  , SameFields (DatatypeInfoOf y) results
+  ) => FromRow results y where
+    fromRow
+      = to . SOP . Z . htrans (Proxy @FromColumnValue) (I . fromColumnValue)
+
+-- | `Only` is a 1-tuple type, useful for encoding a single parameter with
+-- `toParams` or decoding a single value with `fromRow`.
+--
+-- >>> import Data.Text
+-- >>> toParams @(Only (Maybe Text)) @'[ 'Required ('Null 'PGtext)] (Only (Just "foo"))
+-- K (Just "foo") :* Nil
+--
+-- >>> type PGShortRow = '["fromOnly" ::: 'Required ('Null 'PGtext)]
+-- >>> fromRow @PGShortRow @(Only (Maybe Text)) (K (Just "bar") :* Nil)
+-- Only {fromOnly = Just "bar"}
+newtype Only x = Only { fromOnly :: x }
+  deriving (Functor,Foldable,Traversable,Eq,Ord,Read,Show,GHC.Generic)
+instance Generic (Only x)
+instance HasDatatypeInfo (Only x)
diff --git a/src/Squeal/PostgreSQL/Definition.hs b/src/Squeal/PostgreSQL/Definition.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Definition.hs
@@ -0,0 +1,573 @@
+{-|
+Module: Squeal.PostgreSQL.Definition
+Description: Squeal data definition language
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Squeal data definition language.
+-}
+
+{-# LANGUAGE
+    DataKinds
+  , DeriveDataTypeable
+  , DeriveGeneric
+  , GADTs
+  , GeneralizedNewtypeDeriving
+  , KindSignatures
+  , LambdaCase
+  , OverloadedStrings
+  , RankNTypes
+  , StandaloneDeriving
+  , TypeInType
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.Definition
+  (  -- * Definition
+    Definition (UnsafeDefinition, renderDefinition)
+  , (>>>)
+    -- * Create
+  , createTable
+  , TableConstraint (UnsafeTableConstraint, renderTableConstraint)
+  , check
+  , unique
+  , primaryKey
+  , foreignKey
+  , OnDeleteClause (OnDeleteNoAction, OnDeleteRestrict, OnDeleteCascade)
+  , renderOnDeleteClause
+  , OnUpdateClause (OnUpdateNoAction, OnUpdateRestrict, OnUpdateCascade)
+  , renderOnUpdateClause
+    -- * Drop
+  , dropTable
+    -- * Alter
+  , alterTable
+  , alterTableRename
+  , alterTableAddConstraint
+  , AlterColumns (UnsafeAlterColumns, renderAlterColumns)
+  , addColumnDefault
+  , addColumnNull
+  , dropColumn
+  , dropColumnCascade
+  , renameColumn
+  , alterColumn
+  , AlterColumn (UnsafeAlterColumn, renderAlterColumn)
+  , setDefault
+  , dropDefault
+  , setNotNull
+  , dropNotNull
+  , alterType
+  ) where
+
+import Control.Category
+import Control.DeepSeq
+import Data.ByteString
+import Data.Monoid
+import GHC.TypeLits
+import Prelude hiding ((.), id)
+
+import qualified Generics.SOP as SOP
+import qualified GHC.Generics as GHC
+
+import Squeal.PostgreSQL.Expression
+import Squeal.PostgreSQL.Prettyprint
+import Squeal.PostgreSQL.Schema
+
+{-----------------------------------------
+statements
+-----------------------------------------}
+
+-- | A `Definition` is a statement that changes the schema of the
+-- database, like a `createTable`, `dropTable`, or `alterTable` command.
+-- `Definition`s may be composed using the `>>>` operator.
+newtype Definition
+  (schema0 :: TablesType)
+  (schema1 :: TablesType)
+  = UnsafeDefinition { renderDefinition :: ByteString }
+  deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+instance Category Definition where
+  id = UnsafeDefinition ";"
+  ddl1 . ddl0 = UnsafeDefinition $
+    renderDefinition ddl0 <+> renderDefinition ddl1
+
+{-----------------------------------------
+CREATE statements
+-----------------------------------------}
+
+-- | `createTable` adds a table to the schema.
+--
+-- >>> :set -XOverloadedLabels
+-- >>> :{
+-- renderDefinition $
+--   createTable #tab (int `As` #a :* real `As` #b :* Nil) []
+-- :}
+-- "CREATE TABLE tab (a int, b real);"
+createTable
+  :: (KnownSymbol table, SOP.SListI columns)
+  => Alias table -- ^ the name of the table to add
+  -> NP (Aliased TypeExpression) (column ': columns)
+    -- ^ the names and datatype of each column
+  -> [TableConstraint schema (column ': columns)]
+    -- ^ constraints that must hold for the table
+  -> Definition schema (Create table (column ': columns) schema)
+createTable table columns constraints = UnsafeDefinition $
+  "CREATE TABLE" <+> renderAlias table
+  <+> parenthesized
+    ( renderCommaSeparated renderColumnDef columns
+      <> renderConstraints constraints )
+  <> ";"
+  where
+    renderColumnDef :: Aliased TypeExpression x -> ByteString
+    renderColumnDef (ty `As` column) =
+      renderAlias column <+> renderTypeExpression ty
+    renderConstraints :: [TableConstraint schema columns] -> ByteString
+    renderConstraints = \case
+      [] -> ""
+      _ -> ", " <> commaSeparated (renderTableConstraint <$> constraints)
+
+-- | Data types are a way to limit the kind of data that can be stored in a
+-- table. For many applications, however, the constraint they provide is
+-- too coarse. For example, a column containing a product price should
+-- probably only accept positive values. But there is no standard data type
+-- that accepts only positive numbers. Another issue is that you might want
+-- to constrain column data with respect to other columns or rows.
+-- For example, in a table containing product information,
+-- there should be only one row for each product number.
+-- `TableConstraint`s give you as much control over the data in your tables
+-- as you wish. If a user attempts to store data in a column that would
+-- violate a constraint, an error is raised. This applies
+-- even if the value came from the default value definition.
+newtype TableConstraint
+  (schema :: TablesType)
+  (columns :: ColumnsType)
+    = UnsafeTableConstraint { renderTableConstraint :: ByteString }
+    deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | A `check` constraint is the most generic `TableConstraint` type.
+-- It allows you to specify that the value in a certain column must satisfy
+-- a Boolean (truth-value) expression.
+--
+-- >>> :{
+-- renderDefinition $
+--   createTable #tab
+--     ( (int & notNull) `As` #a :*
+--       (int & notNull) `As` #b :* Nil )
+--     [ check (#a .> #b) ]
+-- :}
+-- "CREATE TABLE tab (a int NOT NULL, b int NOT NULL, CHECK ((a > b)));"
+check
+  :: Condition '[table ::: columns] 'Ungrouped '[]
+  -- ^ condition to check
+  -> TableConstraint schema columns
+check condition = UnsafeTableConstraint $
+  "CHECK" <+> parenthesized (renderExpression condition)
+
+-- | A `unique` constraint ensure that the data contained in a column,
+-- or a group of columns, is unique among all the rows in the table.
+--
+-- >>> :{
+-- renderDefinition $
+--   createTable #tab
+--     ( int `As` #a :*
+--       int `As` #b :* Nil )
+--     [ unique (Column #a :* Column #b :* Nil) ]
+-- :}
+-- "CREATE TABLE tab (a int, b int, UNIQUE (a, b));"
+unique
+  :: SOP.SListI subcolumns
+  => NP (Column columns) subcolumns
+  -- ^ unique column or group of columns
+  -> TableConstraint schema columns
+unique columns = UnsafeTableConstraint $
+  "UNIQUE" <+> parenthesized (renderCommaSeparated renderColumn columns)
+
+-- | A `primaryKey` constraint indicates that a column, or group of columns,
+-- can be used as a unique identifier for rows in the table.
+-- This requires that the values be both unique and not null.
+--
+-- >>> :{
+-- renderDefinition $
+--   createTable #tab
+--     ( serial `As` #id :*
+--       (text & notNull) `As` #name :* Nil )
+--     [ primaryKey (Column #id :* Nil) ]
+-- :}
+-- "CREATE TABLE tab (id serial, name text NOT NULL, PRIMARY KEY (id));"
+primaryKey
+  :: (SOP.SListI subcolumns, AllNotNull subcolumns)
+  => NP (Column columns) subcolumns
+  -- ^ identifying column or group of columns
+  -> TableConstraint schema columns
+primaryKey columns = UnsafeTableConstraint $
+  "PRIMARY KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
+
+-- | A `foreignKey` specifies that the values in a column
+-- (or a group of columns) must match the values appearing in some row of
+-- another table. We say this maintains the referential integrity
+-- between two related tables.
+--
+-- >>> :{
+-- let
+--   definition :: Definition '[]
+--     '[ "users" :::
+--        '[ "id" ::: 'Optional ('NotNull 'PGint4)
+--         , "username" ::: 'Required ('NotNull 'PGtext)
+--         ]
+--      , "emails" :::
+--        '[ "id" ::: 'Optional ('NotNull 'PGint4)
+--         , "userid" ::: 'Required ('NotNull 'PGint4)
+--         , "email" ::: 'Required ('NotNull 'PGtext)
+--         ]
+--      ]
+--   definition =
+--     createTable #users
+--       (serial `As` #id :* (text & notNull) `As` #username :* Nil)
+--       [primaryKey (Column #id :* Nil)] >>>
+--     createTable #emails
+--       ( serial `As` #id :*
+--         (integer & notNull) `As` #userid :*
+--         (text & notNull) `As` #email :* Nil )
+--       [ primaryKey (Column #id :* Nil)
+--       , foreignKey (Column #userid :* Nil) #users (Column #id :* Nil)
+--         OnDeleteCascade OnUpdateRestrict
+--       ]
+-- in renderDefinition definition
+-- :}
+-- "CREATE TABLE users (id serial, username text NOT NULL, PRIMARY KEY (id)); CREATE TABLE emails (id serial, userid integer NOT NULL, email text NOT NULL, PRIMARY KEY (id), FOREIGN KEY (userid) REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT);"
+foreignKey
+  :: ( HasTable reftable schema refcolumns
+     , SameTypes subcolumns refsubcolumns
+     , AllNotNull subcolumns
+     , SOP.SListI subcolumns
+     , SOP.SListI refsubcolumns)
+  => NP (Column columns) subcolumns
+  -- ^ column or columns in the table
+  -> Alias reftable
+  -- ^ reference table
+  -> NP (Column refcolumns) refsubcolumns
+  -- ^ reference column or columns in the reference table
+  -> OnDeleteClause
+  -- ^ what to do when reference is deleted
+  -> OnUpdateClause
+  -- ^ what to do when reference is updated
+  -> TableConstraint schema columns
+foreignKey columns reftable refcolumns onDelete onUpdate =
+  UnsafeTableConstraint $
+    "FOREIGN KEY" <+> parenthesized (renderCommaSeparated renderColumn columns)
+    <+> "REFERENCES" <+> renderAlias reftable
+    <+> parenthesized (renderCommaSeparated renderColumn refcolumns)
+    <+> renderOnDeleteClause onDelete
+    <+> renderOnUpdateClause onUpdate
+
+-- | `OnDelete` indicates what to do with rows that reference a deleted row.
+data OnDeleteClause
+  = OnDeleteNoAction
+    -- ^ if any referencing rows still exist when the constraint is checked,
+    -- an error is raised
+  | OnDeleteRestrict -- ^ prevents deletion of a referenced row
+  | OnDeleteCascade
+    -- ^ specifies that when a referenced row is deleted,
+    -- row(s) referencing it should be automatically deleted as well
+  deriving (GHC.Generic,Show,Eq,Ord)
+instance NFData OnDeleteClause
+
+-- | Render `OnDeleteClause`.
+renderOnDeleteClause :: OnDeleteClause -> ByteString
+renderOnDeleteClause = \case
+  OnDeleteNoAction -> "ON DELETE NO ACTION"
+  OnDeleteRestrict -> "ON DELETE RESTRICT"
+  OnDeleteCascade -> "ON DELETE CASCADE"
+
+-- | Analagous to `OnDelete` there is also `OnUpdate` which is invoked
+-- when a referenced column is changed (updated).
+data OnUpdateClause
+  = OnUpdateNoAction
+  -- ^ if any referencing rows has not changed when the constraint is checked,
+  -- an error is raised
+  | OnUpdateRestrict -- ^ prevents update of a referenced row
+  | OnUpdateCascade
+    -- ^ the updated values of the referenced column(s) should be copied
+    -- into the referencing row(s)
+  deriving (GHC.Generic,Show,Eq,Ord)
+instance NFData OnUpdateClause
+
+-- | Render `OnUpdateClause`.
+renderOnUpdateClause :: OnUpdateClause -> ByteString
+renderOnUpdateClause = \case
+  OnUpdateNoAction -> "ON UPDATE NO ACTION"
+  OnUpdateRestrict -> "ON UPDATE RESTRICT"
+  OnUpdateCascade -> "ON UPDATE CASCADE"
+
+{-----------------------------------------
+DROP statements
+-----------------------------------------}
+
+-- | `dropTable` removes a table from the schema.
+--
+-- >>> renderDefinition $ dropTable #muh_table
+-- "DROP TABLE muh_table;"
+dropTable
+  :: KnownSymbol table
+  => Alias table -- ^ table to remove
+  -> Definition schema (Drop table schema)
+dropTable table = UnsafeDefinition $ "DROP TABLE" <+> renderAlias table <> ";"
+
+{-----------------------------------------
+ALTER statements
+-----------------------------------------}
+
+-- | `alterTable` changes the definition of a table from the schema.
+alterTable
+  :: HasTable table schema columns0
+  => Alias table -- ^ table to alter
+  -> AlterColumns columns0 columns1 -- ^ alteration to perform
+  -> Definition schema (Alter table schema columns1)
+alterTable table alteration = UnsafeDefinition $
+  "ALTER TABLE"
+  <+> renderAlias table
+  <+> renderAlterColumns alteration
+  <> ";"
+
+-- | `alterTableRename` changes the name of a table from the schema.
+--
+-- >>> renderDefinition $ alterTableRename #foo #bar
+-- "ALTER TABLE foo RENAME TO bar;"
+alterTableRename
+  :: (KnownSymbol table0, KnownSymbol table1)
+  => Alias table0 -- ^ table to rename
+  -> Alias table1 -- ^ what to rename it
+  -> Definition schema (Rename table0 table1 schema)
+alterTableRename table0 table1 = UnsafeDefinition $
+  "ALTER TABLE" <+> renderAlias table0
+  <+> "RENAME TO" <+> renderAlias table1 <> ";"
+
+-- | An `alterTableAddConstraint` adds a table constraint.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
+--   definition = alterTableAddConstraint #tab (check (#col .> 0))
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ADD CHECK ((col > 0));"
+alterTableAddConstraint
+  :: HasTable table schema columns
+  => Alias table -- ^ table to constrain
+  -> TableConstraint schema columns -- ^ what constraint to add
+  -> Definition schema schema
+alterTableAddConstraint table constraint = UnsafeDefinition $
+  "ALTER TABLE" <+> renderAlias table
+  <+> "ADD" <+> renderTableConstraint constraint <> ";"
+
+-- | An `AlterColumns` describes the alteration to perform on the columns
+-- of a table.
+newtype AlterColumns
+  (columns0 :: ColumnsType)
+  (columns1 :: ColumnsType) =
+    UnsafeAlterColumns {renderAlterColumns :: ByteString}
+  deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | An `addColumnDefault` adds a new `Optional` column. The new column is
+-- initially filled with whatever default value is given.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
+--     '["tab" :::
+--        '[ "col1" ::: 'Required ('Null 'PGint4)
+--         , "col2" ::: 'Optional ('Null 'PGtext) ]]
+--   definition = alterTable #tab
+--     (addColumnDefault #col2 (text & default_ "foo"))
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ADD COLUMN col2 text DEFAULT E'foo';"
+addColumnDefault
+  :: KnownSymbol column
+  => Alias column -- ^ column to add
+  -> TypeExpression ('Optional ty) -- ^ type of the new column
+  -> AlterColumns columns (Create column ('Optional ty) columns)
+addColumnDefault column ty = UnsafeAlterColumns $
+  "ADD COLUMN" <+> renderAlias column <+> renderTypeExpression ty
+
+-- | An `addColumnDefault` adds a new `Null` column. The new column is
+-- initially filled with %NULL%s.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
+--     '["tab" :::
+--        '[ "col1" ::: 'Required ('Null 'PGint4)
+--         , "col2" ::: 'Required ('Null 'PGtext) ]]
+--   definition = alterTable #tab (addColumnNull #col2 text)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ADD COLUMN col2 text;"
+addColumnNull
+  :: KnownSymbol column
+  => Alias column -- ^ column to add
+  -> TypeExpression ('Required ('Null ty)) -- ^ type of the new column
+  -> AlterColumns columns (Create column ('Required ('Null ty)) columns)
+addColumnNull column ty = UnsafeAlterColumns $
+  "ADD COLUMN" <+> renderAlias column <+> renderTypeExpression ty
+
+-- | A `dropColumn` removes a column. Whatever data was in the column
+-- disappears. Table constraints involving the column are dropped, too.
+-- However, if the column is referenced by a foreign key constraint of
+-- another table, PostgreSQL will not silently drop that constraint.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" :::
+--        '[ "col1" ::: 'Required ('Null 'PGint4)
+--         , "col2" ::: 'Required ('Null 'PGtext) ]]
+--     '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
+--   definition = alterTable #tab (dropColumn #col2)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab DROP COLUMN col2;"
+dropColumn
+  :: KnownSymbol column
+  => Alias column -- ^ column to remove
+  -> AlterColumns columns (Drop column columns)
+dropColumn column = UnsafeAlterColumns $
+  "DROP COLUMN" <+> renderAlias column
+
+-- | Like `dropColumn` but authorizes dropping everything that depends
+-- on the column.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" :::
+--        '[ "col1" ::: 'Required ('Null 'PGint4)
+--         , "col2" ::: 'Required ('Null 'PGtext) ]]
+--     '["tab" ::: '["col1" ::: 'Required ('Null 'PGint4)]]
+--   definition = alterTable #tab (dropColumnCascade #col2)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab DROP COLUMN col2 CASCADE;"
+dropColumnCascade
+  :: KnownSymbol column
+  => Alias column -- ^ column to remove
+  -> AlterColumns columns (Drop column columns)
+dropColumnCascade column = UnsafeAlterColumns $
+  "DROP COLUMN" <+> renderAlias column <+> "CASCADE"
+
+-- | A `renameColumn` renames a column.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["foo" ::: 'Required ('Null 'PGint4)]]
+--     '["tab" ::: '["bar" ::: 'Required ('Null 'PGint4)]]
+--   definition = alterTable #tab (renameColumn #foo #bar)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab RENAME COLUMN foo TO bar;"
+renameColumn
+  :: (KnownSymbol column0, KnownSymbol column1)
+  => Alias column0
+  -> Alias column1
+  -> AlterColumns columns (Rename column0 column1 columns)
+renameColumn column0 column1 = UnsafeAlterColumns $
+  "RENAME COLUMN" <+> renderAlias column0  <+> "TO" <+> renderAlias column1
+
+-- | An `alterColumn` alters a single column.
+alterColumn
+  :: (KnownSymbol column, HasColumn column columns ty0)
+  => Alias column -- ^ column to alter
+  -> AlterColumn ty0 ty1 -- ^ alteration to perform
+  -> AlterColumns columns (Alter column columns ty1)
+alterColumn column alteration = UnsafeAlterColumns $
+  "ALTER COLUMN" <+> renderAlias column <+> renderAlterColumn alteration
+
+-- | An `AlterColumn` describes the alteration to perform on a single column.
+newtype AlterColumn (ty0 :: ColumnType) (ty1 :: ColumnType) =
+  UnsafeAlterColumn {renderAlterColumn :: ByteString}
+  deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | A `setDefault` sets a new default for a column. Note that this doesn't
+-- affect any existing rows in the table, it just changes the default for
+-- future `insertTable` and `updateTable` commands.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Optional ('Null 'PGint4)]]
+--   definition = alterTable #tab (alterColumn #col (setDefault 5))
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ALTER COLUMN col SET DEFAULT 5;"
+setDefault
+  :: Expression '[] 'Ungrouped '[] ('Required ty) -- ^ default value to set
+  -> AlterColumn (optionality ty) ('Optional ty)
+setDefault expression = UnsafeAlterColumn $
+  "SET DEFAULT" <+> renderExpression expression
+
+-- | A `dropDefault` removes any default value for a column.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Optional ('Null 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
+--   definition = alterTable #tab (alterColumn #col dropDefault)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ALTER COLUMN col DROP DEFAULT;"
+dropDefault :: AlterColumn (optionality ty) ('Required ty)
+dropDefault = UnsafeAlterColumn $ "DROP DEFAULT"
+
+-- | A `setNotNull` adds a @NOT NULL@ constraint to a column.
+-- The constraint will be checked immediately, so the table data must satisfy
+-- the constraint before it can be added.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
+--   definition = alterTable #tab (alterColumn #col setNotNull)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ALTER COLUMN col SET NOT NULL;"
+setNotNull :: AlterColumn (optionality ('Null ty)) (optionality ('NotNull ty))
+setNotNull = UnsafeAlterColumn $ "SET NOT NULL"
+
+-- | A `dropNotNull` drops a @NOT NULL@ constraint from a column.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]]
+--   definition = alterTable #tab (alterColumn #col dropNotNull)
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ALTER COLUMN col DROP NOT NULL;"
+dropNotNull :: AlterColumn (optionality ('NotNull ty)) (optionality ('Null ty))
+dropNotNull = UnsafeAlterColumn $ "DROP NOT NULL"
+
+-- | An `alterType` converts a column to a different data type.
+-- This will succeed only if each existing entry in the column can be
+-- converted to the new type by an implicit cast.
+--
+-- >>> :{
+-- let
+--   definition :: Definition
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGint4)]]
+--     '["tab" ::: '["col" ::: 'Required ('NotNull 'PGnumeric)]]
+--   definition =
+--     alterTable #tab (alterColumn #col (alterType (numeric & notNull)))
+-- in renderDefinition definition
+-- :}
+-- "ALTER TABLE tab ALTER COLUMN col TYPE numeric NOT NULL;"
+alterType :: TypeExpression ty -> AlterColumn ty0 ty1
+alterType ty = UnsafeAlterColumn $ "TYPE" <+> renderTypeExpression ty
diff --git a/src/Squeal/PostgreSQL/Expression.hs b/src/Squeal/PostgreSQL/Expression.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Expression.hs
@@ -0,0 +1,1140 @@
+{-|
+Module: Squeal.PostgreSQL.Query
+Description: Squeal expressions
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Squeal expressions are the atoms used to build statements.
+-}
+
+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
+{-# LANGUAGE
+    AllowAmbiguousTypes
+  , DataKinds
+  , DeriveGeneric
+  , DeriveDataTypeable
+  , FlexibleContexts
+  , FlexibleInstances
+  , FunctionalDependencies
+  , GADTs
+  , GeneralizedNewtypeDeriving
+  , LambdaCase
+  , MagicHash
+  , OverloadedStrings
+  , PolyKinds
+  , RankNTypes
+  , ScopedTypeVariables
+  , StandaloneDeriving
+  , TypeApplications
+  , TypeFamilies
+  , TypeInType
+  , TypeOperators
+  , UndecidableInstances
+#-}
+
+module Squeal.PostgreSQL.Expression 
+  ( -- * Expression
+    Expression (UnsafeExpression, renderExpression)
+  , HasParameter (param)
+  , HasColumn (getColumn)
+  , Column (Column)
+  , renderColumn
+  , GroupedBy (getGroup1, getGroup2)
+    -- ** Default
+  , def
+  , unDef
+    -- ** Null
+  , null_
+  , unNull
+  , coalesce
+  , fromNull
+  , isNull
+  , isn'tNull
+  , matchNull
+  , nullIf
+  -- ** Functions
+  , unsafeBinaryOp
+  , unsafeUnaryOp
+  , unsafeFunction
+  , atan2_
+  , cast
+  , quot_
+  , rem_
+  , trunc
+  , round_
+  , ceiling_
+  , greatest
+  , least
+    -- ** Conditions
+  , Condition
+  , true
+  , false
+  , not_
+  , (.&&)
+  , (.||)
+  , caseWhenThenElse
+  , ifThenElse
+  , (.==)
+  , (./=)
+  , (.>=)
+  , (.<)
+  , (.<=)
+  , (.>)
+    -- ** Time
+  , currentDate
+  , currentTime
+  , currentTimestamp
+  , localTime
+  , localTimestamp
+    -- ** Text
+  , lower
+  , upper
+  , charLength
+  , like
+    -- ** Aggregation
+  , unsafeAggregate, unsafeAggregateDistinct
+  , sum_, sumDistinct
+  , PGAvg (avg, avgDistinct)
+  , bitAnd, bitOr, boolAnd, boolOr
+  , bitAndDistinct, bitOrDistinct, boolAndDistinct, boolOrDistinct
+  , countStar
+  , count, countDistinct
+  , every, everyDistinct
+  , max_, maxDistinct, min_, minDistinct
+    -- * Tables
+  , Table (UnsafeTable, renderTable)
+  , HasTable (getTable)
+    -- * TypeExpression
+  , TypeExpression (UnsafeTypeExpression, renderTypeExpression)
+  , PGTyped (pgtype)
+  , bool
+  , int2
+  , smallint
+  , int4
+  , int
+  , integer
+  , int8
+  , bigint
+  , numeric
+  , float4
+  , real
+  , float8
+  , doublePrecision
+  , serial2
+  , smallserial
+  , serial4
+  , serial
+  , serial8
+  , bigserial
+  , text
+  , char
+  , character
+  , varchar
+  , characterVarying
+  , bytea
+  , timestamp
+  , timestampWithTimeZone
+  , date
+  , time
+  , timeWithTimeZone
+  , interval
+  , uuid
+  , inet
+  , json
+  , jsonb
+  , notNull
+  , default_
+    -- * Re-export
+  , (&)
+  , NP ((:*), Nil)
+  ) where
+
+import Control.Category
+import Control.DeepSeq
+import Data.ByteString (ByteString)
+import Data.Function ((&))
+import Data.Monoid hiding (All)
+import Data.Ratio
+import Data.String
+import Generics.SOP hiding (from)
+import GHC.OverloadedLabels
+import GHC.TypeLits
+import Prelude hiding (id, (.))
+
+import qualified GHC.Generics as GHC
+
+import Squeal.PostgreSQL.Prettyprint
+import Squeal.PostgreSQL.Schema
+
+{-----------------------------------------
+column expressions
+-----------------------------------------}
+
+{- | `Expression`s are used in a variety of contexts,
+such as in the target list of the `select` command,
+as new column values in `insertInto` or `update`,
+or in search `Condition`s in a number of commands.
+
+The expression syntax allows the calculation of
+values from primitive expression using arithmetic, logical,
+and other operations.
+-}
+newtype Expression
+  (tables :: TablesType)
+  (grouping :: Grouping)
+  (params :: [ColumnType])
+  (ty :: ColumnType)
+    = UnsafeExpression { renderExpression :: ByteString }
+    deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+{- | A `HasParameter` constraint is used to indicate a value that is
+supplied externally to a SQL statement.
+`Squeal.PostgreSQL.PQ.manipulateParams`,
+`Squeal.PostgreSQL.PQ.queryParams` and
+`Squeal.PostgreSQL.PQ.traversePrepared` support specifying data values
+separately from the SQL command string, in which case `param`s are used to
+refer to the out-of-line data values.
+-}
+class (PGTyped (BaseType ty), KnownNat n)
+  => HasParameter (n :: Nat) params ty | n params -> ty where
+    param :: Expression tables grouping params ty
+    param = UnsafeExpression $ parenthesized $
+      "$" <> renderNat (Proxy @n) <+> "::"
+        <+> renderTypeExpression (pgtype @(BaseType ty))
+instance {-# OVERLAPPING #-} PGTyped (BaseType ty1)
+  => HasParameter 1 (ty1:tys) ty1
+instance {-# OVERLAPPABLE #-} (KnownNat n, HasParameter (n-1) params ty)
+  => HasParameter n (ty' : params) ty
+
+{- | A `HasColumn` constraint indicates an unqualified column reference.
+`getColumn` can only be unambiguous when the `TableExpression` the column
+references is unique, in which case the column may be referenced using
+@-XOverloadedLabels@. Otherwise, combined with a `HasTable` constraint, the
+qualified column reference operator `!` may be used.
+-}
+class KnownSymbol column => HasColumn column columns ty
+  | column columns -> ty where
+    getColumn
+      :: HasUnique table tables columns
+      => Alias column
+      -> Expression tables 'Ungrouped params ty
+    getColumn column = UnsafeExpression $ renderAlias column
+instance {-# OVERLAPPING #-} KnownSymbol column
+  => HasColumn column ((column ::: optionality ty) ': tys) ('Required ty)
+instance {-# OVERLAPPABLE #-} (KnownSymbol column, HasColumn column table ty)
+  => HasColumn column (ty' ': table) ty
+
+instance (HasColumn column columns ty, HasUnique table tables columns)
+  => IsLabel column (Expression tables 'Ungrouped params ty) where
+    fromLabel = getColumn (Alias @column)
+
+instance (HasTable table tables columns, HasColumn column columns ty)
+  => IsTableColumn table column (Expression tables 'Ungrouped params ty) where
+    table ! column = UnsafeExpression $
+      renderAlias table <> "." <> renderAlias column
+
+-- | A `Column` is a witness to a `HasColumn` constraint. It's used
+-- in `Squeel.PostgreSQL.Definition.unique` and other
+-- `Squeel.PostgreSQL.Definition.TableConstraint`s to witness a
+-- subcolumns relationship.
+data Column
+  (columns :: ColumnsType)
+  (columnty :: (Symbol,ColumnType))
+    where
+      Column
+        :: HasColumn column columns ty
+        => Alias column
+        -> Column columns (column ::: ty)
+deriving instance Show (Column columns columnty)
+deriving instance Eq (Column columns columnty)
+deriving instance Ord (Column columns columnty)
+
+-- | Render a `Column`.
+renderColumn :: Column columns columnty -> ByteString
+renderColumn (Column column) = renderAlias column
+
+{- | A `GroupedBy` constraint indicates that a table qualified column is
+a member of the auxiliary namespace created by @GROUP BY@ clauses and thus,
+may be called in an output `Expression` without aggregating.
+-}
+class (KnownSymbol table, KnownSymbol column)
+  => GroupedBy table column bys where
+    getGroup1
+      :: (HasUnique table tables columns, HasColumn column columns ty)
+      => Alias column
+      -> Expression tables ('Grouped bys) params ty
+    getGroup1 column = UnsafeExpression $ renderAlias column
+    getGroup2
+      :: (HasTable table tables columns, HasColumn column columns ty)
+      => Alias table
+      -> Alias column
+      -> Expression tables ('Grouped bys) params ty
+    getGroup2 table column = UnsafeExpression $
+      renderAlias table <> "." <> renderAlias column
+instance {-# OVERLAPPING #-} (KnownSymbol table, KnownSymbol column)
+  => GroupedBy table column ('(table,column) ': bys)
+instance {-# OVERLAPPABLE #-}
+  ( KnownSymbol table
+  , KnownSymbol column
+  , GroupedBy table column bys
+  ) => GroupedBy table column (tabcol ': bys)
+  
+instance
+  ( HasUnique table tables columns
+  , HasColumn column columns ty
+  , GroupedBy table column bys
+  ) => IsLabel column
+    (Expression tables ('Grouped bys) params ty) where
+      fromLabel = getGroup1 (Alias @column)
+  
+instance
+  ( HasTable table tables columns
+  , HasColumn column columns ty
+  , GroupedBy table column bys
+  ) => IsTableColumn table column
+    (Expression tables ('Grouped bys) params ty) where (!) = getGroup2
+
+-- | >>> renderExpression def
+-- "DEFAULT"
+def :: Expression '[] 'Ungrouped params ('Optional (nullity ty))
+def = UnsafeExpression "DEFAULT"
+
+-- | >>> renderExpression $ unDef false
+-- "FALSE"
+unDef
+  :: Expression '[] 'Ungrouped params ('Required (nullity ty))
+  -- ^ not @DEFAULT@
+  -> Expression '[] 'Ungrouped params ('Optional (nullity ty))
+unDef = UnsafeExpression . renderExpression
+
+-- | analagous to `Nothing`
+--
+-- >>> renderExpression $ null_
+-- "NULL"
+null_ :: Expression tables grouping params (optionality ('Null ty))
+null_ = UnsafeExpression "NULL"
+
+-- | analagous to `Just`
+--
+-- >>> renderExpression $ unNull true
+-- "TRUE"
+unNull
+  :: Expression tables grouping params (optionality ('NotNull ty))
+  -- ^ not @NULL@
+  -> Expression tables grouping params (optionality ('Null ty))
+unNull = UnsafeExpression . renderExpression
+
+-- | return the leftmost value which is not NULL
+--
+-- >>> renderExpression $ coalesce [null_, unNull true] false
+-- "COALESCE(NULL, TRUE, FALSE)"
+coalesce
+  :: [Expression tables grouping params ('Required ('Null ty))]
+  -- ^ @NULL@s may be present
+  -> Expression tables grouping params ('Required ('NotNull ty))
+  -- ^ @NULL@ is absent
+  -> Expression tables grouping params ('Required ('NotNull ty))
+coalesce nullxs notNullx = UnsafeExpression $
+  "COALESCE" <> parenthesized (commaSeparated
+    ((renderExpression <$> nullxs) <> [renderExpression notNullx]))
+
+-- | analagous to `fromMaybe` using @COALESCE@
+--
+-- >>> renderExpression $ fromNull true null_
+-- "COALESCE(NULL, TRUE)"
+fromNull
+  :: Expression tables grouping params ('Required ('NotNull ty))
+  -- ^ what to convert @NULL@ to
+  -> Expression tables grouping params ('Required ('Null ty))
+  -> Expression tables grouping params ('Required ('NotNull ty))
+fromNull notNullx nullx = coalesce [nullx] notNullx
+
+-- | >>> renderExpression $ null_ & isNull
+-- "NULL IS NULL"
+isNull
+  :: Expression tables grouping params ('Required ('Null ty))
+  -- ^ possibly @NULL@
+  -> Condition tables grouping params
+isNull x = UnsafeExpression $ renderExpression x <+> "IS NULL"
+
+-- | >>> renderExpression $ null_ & isn'tNull
+-- "NULL IS NOT NULL"
+isn'tNull
+  :: Expression tables grouping params ('Required ('Null ty))
+  -- ^ possibly @NULL@
+  -> Condition tables grouping params
+isn'tNull x = UnsafeExpression $ renderExpression x <+> "IS NOT NULL"
+
+-- | analagous to `maybe` using @IS NULL@
+--
+-- >>> renderExpression $ matchNull true not_ null_
+-- "CASE WHEN NULL IS NULL THEN TRUE ELSE (NOT NULL) END"
+matchNull
+  :: Expression tables grouping params ('Required nullty)
+  -- ^ what to convert @NULL@ to
+  -> ( Expression tables grouping params ('Required ('NotNull ty))
+       -> Expression tables grouping params ('Required nullty) )
+  -- ^ function to perform when @NULL@ is absent
+  -> Expression tables grouping params ('Required ('Null ty))
+  -> Expression tables grouping params ('Required nullty)
+matchNull y f x = ifThenElse (isNull x) y
+  (f (UnsafeExpression (renderExpression x)))
+
+-- | right inverse to `fromNull`, if its arguments are equal then
+-- `nullIf` gives @NULL@.
+--
+-- >>> :set -XTypeApplications -XDataKinds
+-- >>> renderExpression @_ @_ @'[_] $ fromNull false (nullIf false (param @1))
+-- "COALESCE(NULL IF (FALSE, ($1 :: bool)), FALSE)"
+nullIf
+  :: Expression tables grouping params ('Required ('NotNull ty))
+  -- ^ @NULL@ is absent
+  -> Expression tables grouping params ('Required ('NotNull ty))
+  -- ^ @NULL@ is absent
+  -> Expression tables grouping params ('Required ('Null ty))
+nullIf x y = UnsafeExpression $ "NULL IF" <+> parenthesized
+  (renderExpression x <> ", " <> renderExpression y)
+
+-- | >>> renderExpression @_ @_ @'[_] $ greatest currentTimestamp [param @1]
+-- "GREATEST(CURRENT_TIMESTAMP, ($1 :: timestamp with time zone))"
+greatest
+  :: Expression tables grouping params ('Required nullty)
+  -- ^ needs at least 1 argument
+  -> [Expression tables grouping params ('Required nullty)]
+  -- ^ or more
+  -> Expression tables grouping params ('Required nullty)
+greatest x xs = UnsafeExpression $ "GREATEST("
+  <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
+
+-- | >>> renderExpression $ least currentTimestamp [null_]
+-- "LEAST(CURRENT_TIMESTAMP, NULL)"
+least
+  :: Expression tables grouping params ('Required nullty)
+  -- ^ needs at least 1 argument
+  -> [Expression tables grouping params ('Required nullty)]
+  -- ^ or more
+  -> Expression tables grouping params ('Required nullty)
+least x xs = UnsafeExpression $ "LEAST("
+  <> commaSeparated (renderExpression <$> (x:xs)) <> ")"
+
+-- | >>> renderExpression $ unsafeBinaryOp "OR" true false
+-- "(TRUE OR FALSE)"
+unsafeBinaryOp
+  :: ByteString
+  -- ^ operator
+  -> Expression tables grouping params ('Required ty0)
+  -> Expression tables grouping params ('Required ty1)
+  -> Expression tables grouping params ('Required ty2)
+unsafeBinaryOp op x y = UnsafeExpression $ parenthesized $
+  renderExpression x <+> op <+> renderExpression y
+
+-- | >>> renderExpression $ unsafeUnaryOp "NOT" true
+-- "(NOT TRUE)"
+unsafeUnaryOp
+  :: ByteString
+  -- ^ operator
+  -> Expression tables grouping params ('Required ty0)
+  -> Expression tables grouping params ('Required ty1)
+unsafeUnaryOp op x = UnsafeExpression $ parenthesized $
+  op <+> renderExpression x
+
+-- | >>> renderExpression $ unsafeFunction "f" true
+-- "f(TRUE)"
+unsafeFunction
+  :: ByteString
+  -- ^ function
+  -> Expression tables grouping params ('Required xty)
+  -> Expression tables grouping params ('Required yty)
+unsafeFunction fun x = UnsafeExpression $
+  fun <> parenthesized (renderExpression x)
+
+instance PGNum ty
+  => Num (Expression tables grouping params ('Required (nullity ty))) where
+    (+) = unsafeBinaryOp "+"
+    (-) = unsafeBinaryOp "-"
+    (*) = unsafeBinaryOp "*"
+    abs = unsafeFunction "abs"
+    signum = unsafeFunction "sign"
+    fromInteger
+      = UnsafeExpression
+      . fromString
+      . show
+
+instance (PGNum ty, PGFloating ty) => Fractional
+  (Expression tables grouping params ('Required (nullity ty))) where
+    (/) = unsafeBinaryOp "/"
+    fromRational x = fromInteger (numerator x) / fromInteger (denominator x)
+
+instance (PGNum ty, PGFloating ty) => Floating
+  (Expression tables grouping params ('Required (nullity ty))) where
+    pi = UnsafeExpression "pi()"
+    exp = unsafeFunction "exp"
+    log = unsafeFunction "ln"
+    sqrt = unsafeFunction "sqrt"
+    b ** x = UnsafeExpression $
+      "power(" <> renderExpression b <> ", " <> renderExpression x <> ")"
+    logBase b y = log y / log b
+    sin = unsafeFunction "sin"
+    cos = unsafeFunction "cos"
+    tan = unsafeFunction "tan"
+    asin = unsafeFunction "asin"
+    acos = unsafeFunction "acos"
+    atan = unsafeFunction "atan"
+    sinh x = (exp x - exp (-x)) / 2
+    cosh x = (exp x + exp (-x)) / 2
+    tanh x = sinh x / cosh x
+    asinh x = log (x + sqrt (x*x + 1))
+    acosh x = log (x + sqrt (x*x - 1))
+    atanh x = log ((1 + x) / (1 - x)) / 2
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ atan2_ pi 2
+-- "atan2(pi(), 2)"
+atan2_
+  :: PGFloating float
+  => Expression tables grouping params ('Required (nullity float))
+  -- ^ numerator
+  -> Expression tables grouping params ('Required (nullity float))
+  -- ^ denominator
+  -> Expression tables grouping params ('Required (nullity float))
+atan2_ y x = UnsafeExpression $
+  "atan2(" <> renderExpression y <> ", " <> renderExpression x <> ")"
+
+-- When a `cast` is applied to an `Expression` of a known type, it
+-- represents a run-time type conversion. The cast will succeed only if a
+-- suitable type conversion operation has been defined.
+--
+-- | >>> renderExpression $ true & cast int4
+-- "(TRUE :: int4)"
+cast
+  :: TypeExpression ('Required ('Null ty1))
+  -- ^ type to cast as
+  -> Expression tables grouping params ('Required (nullity ty0))
+  -- ^ value to convert
+  -> Expression tables grouping params ('Required (nullity ty1))
+cast ty x = UnsafeExpression $ parenthesized $
+  renderExpression x <+> "::" <+> renderTypeExpression ty
+
+-- | integer division, truncates the result
+--
+-- >>> renderExpression @_ @_ @_ @(_(_ 'PGint2)) $ 5 `quot_` 2
+-- "(5 / 2)"
+quot_
+  :: PGIntegral int
+  => Expression tables grouping params ('Required (nullity int))
+  -- ^ numerator
+  -> Expression tables grouping params ('Required (nullity int))
+  -- ^ denominator
+  -> Expression tables grouping params ('Required (nullity int))
+quot_ = unsafeBinaryOp "/"
+
+-- | remainder upon integer division
+--
+-- >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ 5 `rem_` 2
+-- "(5 % 2)"
+rem_
+  :: PGIntegral int
+  => Expression tables grouping params ('Required (nullity int))
+  -- ^ numerator
+  -> Expression tables grouping params ('Required (nullity int))
+  -- ^ denominator
+  -> Expression tables grouping params ('Required (nullity int))
+rem_ = unsafeBinaryOp "%"
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ trunc pi
+-- "trunc(pi())"
+trunc
+  :: PGFloating frac
+  => Expression tables grouping params ('Required (nullity frac))
+  -- ^ fractional number
+  -> Expression tables grouping params ('Required (nullity frac))
+trunc = unsafeFunction "trunc"
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ round_ pi
+-- "round(pi())"
+round_
+  :: PGFloating frac
+  => Expression tables grouping params ('Required (nullity frac))
+  -- ^ fractional number
+  -> Expression tables grouping params ('Required (nullity frac))
+round_ = unsafeFunction "round"
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGfloat4)) $ ceiling_ pi
+-- "ceiling(pi())"
+ceiling_
+  :: PGFloating frac
+  => Expression tables grouping params ('Required (nullity frac))
+  -- ^ fractional number
+  -> Expression tables grouping params ('Required (nullity frac))
+ceiling_ = unsafeFunction "ceiling"
+
+-- | A `Condition` is a boolean valued `Expression`. While SQL allows
+-- conditions to have @NULL@, squeal instead chooses to disallow @NULL@,
+-- forcing one to handle the case of @NULL@ explicitly to produce
+-- a `Condition`.
+type Condition tables grouping params =
+  Expression tables grouping params ('Required ('NotNull 'PGbool))
+
+-- | >>> renderExpression true
+-- "TRUE"
+true :: Condition tables grouping params
+true = UnsafeExpression "TRUE"
+
+-- | >>> renderExpression false
+-- "FALSE"
+false :: Condition tables grouping params
+false = UnsafeExpression "FALSE"
+
+-- | >>> renderExpression $ not_ true
+-- "(NOT TRUE)"
+not_
+  :: Condition tables grouping params
+  -> Condition tables grouping params
+not_ = unsafeUnaryOp "NOT"
+
+-- | >>> renderExpression $ true .&& false
+-- "(TRUE AND FALSE)"
+(.&&)
+  :: Condition tables grouping params
+  -> Condition tables grouping params
+  -> Condition tables grouping params
+(.&&) = unsafeBinaryOp "AND"
+
+-- | >>> renderExpression $ true .|| false
+-- "(TRUE OR FALSE)"
+(.||)
+  :: Condition tables grouping params
+  -> Condition tables grouping params
+  -> Condition tables grouping params
+(.||) = unsafeBinaryOp "OR"
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ caseWhenThenElse [(true, 1), (false, 2)] 3
+-- "CASE WHEN TRUE THEN 1 WHEN FALSE THEN 2 ELSE 3 END"
+caseWhenThenElse
+  :: [ ( Condition tables grouping params
+       , Expression tables grouping params ('Required ty)
+     ) ]
+  -> Expression tables grouping params ('Required ty)
+  -> Expression tables grouping params ('Required ty)
+caseWhenThenElse whenThens else_ = UnsafeExpression $ mconcat
+  [ "CASE"
+  , mconcat
+    [ mconcat
+      [ " WHEN ", renderExpression when_
+      , " THEN ", renderExpression then_
+      ]
+    | (when_,then_) <- whenThens
+    ]
+  , " ELSE ", renderExpression else_
+  , " END"
+  ]
+
+-- | >>> renderExpression @_ @_ @_ @(_ (_ 'PGint2)) $ ifThenElse true 1 0
+-- "CASE WHEN TRUE THEN 1 ELSE 0 END"
+ifThenElse
+  :: Condition tables grouping params
+  -> Expression tables grouping params ('Required ty)
+  -> Expression tables grouping params ('Required ty)
+  -> Expression tables grouping params ('Required ty)
+ifThenElse if_ then_ else_ = caseWhenThenElse [(if_,then_)] else_
+
+-- | Comparison operations like `.==`, `./=`, `.>`, `.>=`, `.<` and `.<=`
+-- will produce @NULL@s if one of their arguments is @NULL@.
+--
+-- >>> renderExpression $ unNull true .== null_
+-- "(TRUE = NULL)"
+(.==)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(.==) = unsafeBinaryOp "="
+infix 4 .==
+
+-- | >>> renderExpression $ unNull true ./= null_
+-- "(TRUE <> NULL)"
+(./=)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(./=) = unsafeBinaryOp "<>"
+infix 4 ./=
+
+-- | >>> renderExpression $ unNull true .>= null_
+-- "(TRUE >= NULL)"
+(.>=)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(.>=) = unsafeBinaryOp ">="
+infix 4 .>=
+
+-- | >>> renderExpression $ unNull true .< null_
+-- "(TRUE < NULL)"
+(.<)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(.<) = unsafeBinaryOp "<"
+infix 4 .<
+
+-- | >>> renderExpression $ unNull true .<= null_
+-- "(TRUE <= NULL)"
+(.<=)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(.<=) = unsafeBinaryOp "<="
+infix 4 .<=
+
+-- | >>> renderExpression $ unNull true .> null_
+-- "(TRUE > NULL)"
+(.>)
+  :: Expression tables grouping params ('Required (nullity ty)) -- ^ lhs
+  -> Expression tables grouping params ('Required (nullity ty)) -- ^ rhs
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+(.>) = unsafeBinaryOp ">"
+infix 4 .>
+
+-- | >>> renderExpression $ currentDate
+-- "CURRENT_DATE"
+currentDate
+  :: Expression tables grouping params ('Required (nullity 'PGdate))
+currentDate = UnsafeExpression "CURRENT_DATE"
+
+-- | >>> renderExpression $ currentTime
+-- "CURRENT_TIME"
+currentTime
+  :: Expression tables grouping params ('Required (nullity 'PGtimetz))
+currentTime = UnsafeExpression "CURRENT_TIME"
+
+-- | >>> renderExpression $ currentTimestamp
+-- "CURRENT_TIMESTAMP"
+currentTimestamp
+  :: Expression tables grouping params ('Required (nullity 'PGtimestamptz))
+currentTimestamp = UnsafeExpression "CURRENT_TIMESTAMP"
+
+-- | >>> renderExpression $ localTime
+-- "LOCALTIME"
+localTime
+  :: Expression tables grouping params ('Required (nullity 'PGtime))
+localTime = UnsafeExpression "LOCALTIME"
+
+-- | >>> renderExpression $ localTimestamp
+-- "LOCALTIMESTAMP"
+localTimestamp
+  :: Expression tables grouping params ('Required (nullity 'PGtimestamp))
+localTimestamp = UnsafeExpression "LOCALTIMESTAMP"
+
+{-----------------------------------------
+text
+-----------------------------------------}
+
+instance IsString
+  (Expression tables grouping params ('Required (nullity 'PGtext))) where
+    fromString str = UnsafeExpression $
+      "E\'" <> fromString (escape =<< str) <> "\'"
+      where
+        escape = \case
+          '\NUL' -> "\\0"
+          '\'' -> "''"
+          '"' -> "\\\""
+          '\b' -> "\\b"
+          '\n' -> "\\n"
+          '\r' -> "\\r"
+          '\t' -> "\\t"
+          '\\' -> "\\\\"
+          c -> [c]
+
+instance Monoid
+  (Expression tables grouping params ('Required (nullity 'PGtext))) where
+    mempty = fromString ""
+    mappend = unsafeBinaryOp "||"
+
+-- | >>> renderExpression $ lower "ARRRGGG"
+-- "lower(E'ARRRGGG')"
+lower
+  :: Expression tables grouping params ('Required (nullity 'PGtext))
+  -- ^ string to lower case
+  -> Expression tables grouping params ('Required (nullity 'PGtext))
+lower = unsafeFunction "lower"
+
+-- | >>> renderExpression $ upper "eeee"
+-- "upper(E'eeee')"
+upper
+  :: Expression tables grouping params ('Required (nullity 'PGtext))
+  -- ^ string to upper case
+  -> Expression tables grouping params ('Required (nullity 'PGtext))
+upper = unsafeFunction "upper"
+
+-- | >>> renderExpression $ charLength "four"
+-- "char_length(E'four')"
+charLength
+  :: Expression tables grouping params ('Required (nullity 'PGtext))
+  -- ^ string to measure
+  -> Expression tables grouping params ('Required (nullity 'PGint4))
+charLength = unsafeFunction "char_length"
+
+-- | The `like` expression returns true if the @string@ matches
+-- the supplied @pattern@. If @pattern@ does not contain percent signs
+-- or underscores, then the pattern only represents the string itself;
+-- in that case `like` acts like the equals operator. An underscore (_)
+-- in pattern stands for (matches) any single character; a percent sign (%)
+-- matches any sequence of zero or more characters.
+--
+-- >>> renderExpression $ "abc" `like` "a%"
+-- "(E'abc' LIKE E'a%')"
+like
+  :: Expression tables grouping params ('Required (nullity 'PGtext))
+  -- ^ string
+  -> Expression tables grouping params ('Required (nullity 'PGtext))
+  -- ^ pattern
+  -> Expression tables grouping params ('Required (nullity 'PGbool))
+like = unsafeBinaryOp "LIKE"
+
+{-----------------------------------------
+aggregation
+-----------------------------------------}
+
+-- | escape hatch to define aggregate functions
+unsafeAggregate
+  :: ByteString -- ^ aggregate function
+  -> Expression tables 'Ungrouped params ('Required xty)
+  -> Expression tables ('Grouped bys) params ('Required yty)
+unsafeAggregate fun x = UnsafeExpression $ mconcat
+  [fun, "(", renderExpression x, ")"]
+
+-- | escape hatch to define aggregate functions over distinct values
+unsafeAggregateDistinct
+  :: ByteString -- ^ aggregate function
+  -> Expression tables 'Ungrouped params ('Required xty)
+  -> Expression tables ('Grouped bys) params ('Required yty)
+unsafeAggregateDistinct fun x = UnsafeExpression $ mconcat
+  [fun, "(DISTINCT ", renderExpression x, ")"]
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGnumeric)]] $ sum_ #col
+-- "sum(col)"
+sum_
+  :: PGNum ty
+  => Expression tables 'Ungrouped params ('Required (nullity ty))
+  -- ^ what to sum
+  -> Expression tables ('Grouped bys) params ('Required (nullity ty))
+sum_ = unsafeAggregate "sum"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGnumeric)]] $ sumDistinct #col
+-- "sum(DISTINCT col)"
+sumDistinct
+  :: PGNum ty
+  => Expression tables 'Ungrouped params ('Required (nullity ty))
+  -- ^ what to sum
+  -> Expression tables ('Grouped bys) params ('Required (nullity ty))
+sumDistinct = unsafeAggregateDistinct "sum"
+
+-- | A constraint for `PGType`s that you can take averages of and the resulting
+-- `PGType`.
+class PGAvg ty avg | ty -> avg where
+  avg, avgDistinct
+    :: Expression tables 'Ungrouped params ('Required (nullity ty))
+    -- ^ what to average
+    -> Expression tables ('Grouped bys) params ('Required (nullity avg))
+  avg = unsafeAggregate "avg"
+  avgDistinct = unsafeAggregateDistinct "avg"
+instance PGAvg 'PGint2 'PGnumeric
+instance PGAvg 'PGint4 'PGnumeric
+instance PGAvg 'PGint8 'PGnumeric
+instance PGAvg 'PGnumeric 'PGnumeric
+instance PGAvg 'PGfloat4 'PGfloat8
+instance PGAvg 'PGfloat8 'PGfloat8
+instance PGAvg 'PGinterval 'PGinterval
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitAnd #col
+-- "bit_and(col)"
+bitAnd
+  :: PGIntegral int
+  => Expression tables 'Ungrouped params ('Required (nullity int))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity int))
+bitAnd = unsafeAggregate "bit_and"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitOr #col
+-- "bit_or(col)"
+bitOr
+  :: PGIntegral int
+  => Expression tables 'Ungrouped params ('Required (nullity int))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity int))
+bitOr = unsafeAggregate "bit_or"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitAndDistinct #col
+-- "bit_and(DISTINCT col)"
+bitAndDistinct
+  :: PGIntegral int
+  => Expression tables 'Ungrouped params ('Required (nullity int))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity int))
+bitAndDistinct = unsafeAggregateDistinct "bit_and"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGint4)]] $ bitOrDistinct #col
+-- "bit_or(DISTINCT col)"
+bitOrDistinct
+  :: PGIntegral int
+  => Expression tables 'Ungrouped params ('Required (nullity int))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity int))
+bitOrDistinct = unsafeAggregateDistinct "bit_or"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolAnd #col
+-- "bool_and(col)"
+boolAnd
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+boolAnd = unsafeAggregate "bool_and"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolOr #col
+-- "bool_or(col)"
+boolOr
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+boolOr = unsafeAggregate "bool_or"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolAndDistinct #col
+-- "bool_and(DISTINCT col)"
+boolAndDistinct
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+boolAndDistinct = unsafeAggregateDistinct "bool_and"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ boolOrDistinct #col
+-- "bool_or(DISTINCT col)"
+boolOrDistinct
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+boolOrDistinct = unsafeAggregateDistinct "bool_or"
+
+-- | A special aggregation that does not require an input
+--
+-- >>> renderExpression countStar
+-- "count(*)"
+countStar
+  :: Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
+countStar = UnsafeExpression $ "count(*)"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Optional _]] $ count #col
+-- "count(col)"
+count
+  :: Expression tables 'Ungrouped params ('Required ty)
+  -- ^ what to count
+  -> Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
+count = unsafeAggregate "count"
+
+-- | >>> renderExpression @'[_ ::: '["col" ::: 'Required _]] $ countDistinct #col
+-- "count(DISTINCT col)"
+countDistinct
+  :: Expression tables 'Ungrouped params ('Required ty)
+  -- ^ what to count
+  -> Expression tables ('Grouped bys) params ('Required ('NotNull 'PGint8))
+countDistinct = unsafeAggregateDistinct "count"
+
+-- | synonym for `boolAnd`
+--
+-- >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ every #col
+-- "every(col)"
+every
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+every = unsafeAggregate "every"
+
+-- | synonym for `boolAndDistinct`
+--
+-- >>> renderExpression @'[_ ::: '["col" ::: 'Required (_ 'PGbool)]] $ everyDistinct #col
+-- "every(DISTINCT col)"
+everyDistinct
+  :: Expression tables 'Ungrouped params ('Required (nullity 'PGbool))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity 'PGbool))
+everyDistinct = unsafeAggregateDistinct "every"
+
+-- | minimum and maximum aggregation
+max_, min_, maxDistinct, minDistinct
+  :: Expression tables 'Ungrouped params ('Required (nullity ty))
+  -- ^ what to aggregate
+  -> Expression tables ('Grouped bys) params ('Required (nullity ty))
+max_ = unsafeAggregate "max"
+min_ = unsafeAggregate "min"
+maxDistinct = unsafeAggregateDistinct "max"
+minDistinct = unsafeAggregateDistinct "min"
+
+{-----------------------------------------
+tables
+-----------------------------------------}
+
+-- | A `Table` from a schema without its alias with an `IsLabel` instance
+-- to call a table reference by its alias.
+newtype Table
+  (schema :: TablesType)
+  (columns :: ColumnsType)
+    = UnsafeTable { renderTable :: ByteString }
+    deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | A `HasTable` constraint indicates a table reference.
+class KnownSymbol table => HasTable table tables columns
+  | table tables -> columns where
+    getTable :: Alias table -> Table tables columns
+    getTable table = UnsafeTable $ renderAlias table
+instance {-# OVERLAPPING #-} KnownSymbol table
+  => HasTable table ((table ::: columns) ': tables) columns
+instance {-# OVERLAPPABLE #-}
+  (KnownSymbol table, HasTable table schema columns)
+    => HasTable table (table' ': schema) columns
+
+instance HasTable table schema columns
+  => IsLabel table (Table schema columns) where
+    fromLabel = getTable (Alias @table)
+
+{-----------------------------------------
+type expressions
+-----------------------------------------}
+
+-- | `TypeExpression`s are used in `cast`s and `createTable` commands.
+newtype TypeExpression (ty :: ColumnType)
+  = UnsafeTypeExpression { renderTypeExpression :: ByteString }
+  deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | logical Boolean (true/false)
+bool :: TypeExpression ('Required ('Null 'PGbool))
+bool = UnsafeTypeExpression "bool"
+-- | signed two-byte integer
+int2, smallint :: TypeExpression ('Required ('Null 'PGint2))
+int2 = UnsafeTypeExpression "int2"
+smallint = UnsafeTypeExpression "smallint"
+-- | signed four-byte integer
+int4, int, integer :: TypeExpression ('Required ('Null 'PGint4))
+int4 = UnsafeTypeExpression "int4"
+int = UnsafeTypeExpression "int"
+integer = UnsafeTypeExpression "integer"
+-- | signed eight-byte integer
+int8, bigint :: TypeExpression ('Required ('Null 'PGint8))
+int8 = UnsafeTypeExpression "int8"
+bigint = UnsafeTypeExpression "bigint"
+-- | arbitrary precision numeric type
+numeric :: TypeExpression ('Required ('Null 'PGnumeric))
+numeric = UnsafeTypeExpression "numeric"
+-- | single precision floating-point number (4 bytes)
+float4, real :: TypeExpression ('Required ('Null 'PGfloat4))
+float4 = UnsafeTypeExpression "float4"
+real = UnsafeTypeExpression "real"
+-- | double precision floating-point number (8 bytes)
+float8, doublePrecision :: TypeExpression ('Required ('Null 'PGfloat8))
+float8 = UnsafeTypeExpression "float8"
+doublePrecision = UnsafeTypeExpression "double precision"
+-- | not a true type, but merely a notational convenience for creating
+-- unique identifier columns with type `'PGint2`
+serial2, smallserial :: TypeExpression ('Optional ('NotNull 'PGint2))
+serial2 = UnsafeTypeExpression "serial2"
+smallserial = UnsafeTypeExpression "smallserial"
+-- | not a true type, but merely a notational convenience for creating
+-- unique identifier columns with type `'PGint4`
+serial4, serial :: TypeExpression ('Optional ('NotNull 'PGint4))
+serial4 = UnsafeTypeExpression "serial4"
+serial = UnsafeTypeExpression "serial"
+-- | not a true type, but merely a notational convenience for creating
+-- unique identifier columns with type `'PGint8`
+serial8, bigserial :: TypeExpression ('Optional ('NotNull 'PGint8))
+serial8 = UnsafeTypeExpression "serial8"
+bigserial = UnsafeTypeExpression "bigserial"
+-- | variable-length character string
+text :: TypeExpression ('Required ('Null 'PGtext))
+text = UnsafeTypeExpression "text"
+-- | fixed-length character string
+char, character
+  :: (KnownNat n, 1 <= n)
+  => proxy n
+  -> TypeExpression ('Required ('Null ('PGchar n)))
+char p = UnsafeTypeExpression $ "char(" <> renderNat p <> ")"
+character p = UnsafeTypeExpression $  "character(" <> renderNat p <> ")"
+-- | variable-length character string
+varchar, characterVarying
+  :: (KnownNat n, 1 <= n)
+  => proxy n
+  -> TypeExpression ('Required ('Null ('PGvarchar n)))
+varchar p = UnsafeTypeExpression $ "varchar(" <> renderNat p <> ")"
+characterVarying p = UnsafeTypeExpression $
+  "character varying(" <> renderNat p <> ")"
+-- | binary data ("byte array")
+bytea :: TypeExpression ('Required ('Null 'PGbytea))
+bytea = UnsafeTypeExpression "bytea"
+-- | date and time (no time zone)
+timestamp :: TypeExpression ('Required ('Null 'PGtimestamp))
+timestamp = UnsafeTypeExpression "timestamp"
+-- | date and time, including time zone
+timestampWithTimeZone :: TypeExpression ('Required ('Null 'PGtimestamptz))
+timestampWithTimeZone = UnsafeTypeExpression "timestamp with time zone"
+-- | calendar date (year, month, day)
+date :: TypeExpression ('Required ('Null 'PGdate))
+date = UnsafeTypeExpression "date"
+-- | time of day (no time zone)
+time :: TypeExpression ('Required ('Null 'PGtime))
+time = UnsafeTypeExpression "time"
+-- | time of day, including time zone
+timeWithTimeZone :: TypeExpression ('Required ('Null 'PGtimetz))
+timeWithTimeZone = UnsafeTypeExpression "time with time zone"
+-- | time span
+interval :: TypeExpression ('Required ('Null 'PGinterval))
+interval = UnsafeTypeExpression "interval"
+-- | universally unique identifier
+uuid :: TypeExpression ('Required ('Null 'PGuuid))
+uuid = UnsafeTypeExpression "uuid"
+-- | IPv4 or IPv6 host address
+inet :: TypeExpression ('Required ('Null 'PGinet))
+inet = UnsafeTypeExpression "inet"
+-- | textual JSON data
+json :: TypeExpression ('Required ('Null 'PGjson))
+json = UnsafeTypeExpression "json"
+-- | binary JSON data, decomposed
+jsonb :: TypeExpression ('Required ('Null 'PGjsonb))
+jsonb = UnsafeTypeExpression "jsonb"
+
+-- | used in `createTable` commands as a column constraint to ensure
+-- @NULL@ is not present
+notNull
+  :: TypeExpression (optionality ('Null ty))
+  -> TypeExpression (optionality ('NotNull ty))
+notNull ty = UnsafeTypeExpression $ renderTypeExpression ty <+> "NOT NULL"
+
+-- | used in `createTable` commands as a column constraint to give a default
+default_
+  :: Expression '[] 'Ungrouped '[] ('Required ty)
+  -> TypeExpression ('Required ty)
+  -> TypeExpression ('Optional ty)
+default_ x ty = UnsafeTypeExpression $
+  renderTypeExpression ty <+> "DEFAULT" <+> renderExpression x
+
+-- | `pgtype` is a demoted version of a `PGType`
+class PGTyped (ty :: PGType) where
+  pgtype :: TypeExpression ('Required ('Null ty))
+instance PGTyped 'PGbool where pgtype = bool
+instance PGTyped 'PGint2 where pgtype = int2
+instance PGTyped 'PGint4 where pgtype = int4
+instance PGTyped 'PGint8 where pgtype = int8
+instance PGTyped 'PGnumeric where pgtype = numeric
+instance PGTyped 'PGfloat4 where pgtype = float4
+instance PGTyped 'PGfloat8 where pgtype = float8
+instance PGTyped 'PGtext where pgtype = text
+instance (KnownNat n, 1 <= n)
+  => PGTyped ('PGchar n) where pgtype = char (Proxy @n)
+instance (KnownNat n, 1 <= n)
+  => PGTyped ('PGvarchar n) where pgtype = varchar (Proxy @n)
+instance PGTyped 'PGbytea where pgtype = bytea
+instance PGTyped 'PGtimestamp where pgtype = timestamp
+instance PGTyped 'PGtimestamptz where pgtype = timestampWithTimeZone
+instance PGTyped 'PGdate where pgtype = date
+instance PGTyped 'PGtime where pgtype = time
+instance PGTyped 'PGtimetz where pgtype = timeWithTimeZone
+instance PGTyped 'PGinterval where pgtype = interval
+instance PGTyped 'PGuuid where pgtype = uuid
+instance PGTyped 'PGjson where pgtype = json
+instance PGTyped 'PGjsonb where pgtype = jsonb
diff --git a/src/Squeal/PostgreSQL/Manipulation.hs b/src/Squeal/PostgreSQL/Manipulation.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Manipulation.hs
@@ -0,0 +1,368 @@
+{-|
+Module: Squeal.PostgreSQL.Manipulation
+Description: Squeal data manipulation language
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Squeal data manipulation language.
+-}
+
+{-# LANGUAGE
+    DataKinds
+  , DeriveDataTypeable
+  , DeriveGeneric
+  , GADTs
+  , GeneralizedNewtypeDeriving
+  , KindSignatures
+  , LambdaCase
+  , OverloadedStrings
+  , RankNTypes
+  , StandaloneDeriving
+  , TypeInType
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.Manipulation
+  ( -- * Manipulation
+    Manipulation (UnsafeManipulation, renderManipulation)
+  , queryStatement
+    -- * Insert
+  , insertInto
+  , ValuesClause (Values, ValuesQuery)
+  , renderValuesClause
+  , ReturningClause (ReturningStar, Returning)
+  , renderReturningClause
+  , ConflictClause (OnConflictDoRaise, OnConflictDoNothing, OnConflictDoUpdate)
+  , renderConflictClause
+    -- * Update
+  , update
+  , UpdateExpression (Same, Set)
+  , renderUpdateExpression
+  , deleteFrom
+  ) where
+
+import Control.DeepSeq
+import Data.ByteString
+import Data.Monoid
+
+import qualified Generics.SOP as SOP
+import qualified GHC.Generics as GHC
+
+import Squeal.PostgreSQL.Expression
+import Squeal.PostgreSQL.Prettyprint
+import Squeal.PostgreSQL.Query
+import Squeal.PostgreSQL.Schema
+
+-- | A `Manipulation` is a statement which may modify data in the database,
+-- but does not alter the schema. Examples are `insertInto`, `update` and
+-- `deleteFrom`. A `Query` is also considered a `Manipulation` even though
+-- it does not modify data.
+newtype Manipulation
+  (schema :: TablesType)
+  (params :: [ColumnType])
+  (columns :: ColumnsType)
+    = UnsafeManipulation { renderManipulation :: ByteString }
+    deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | Convert a `Query` into a `Manipulation`.
+queryStatement
+  :: Query schema params columns
+  -> Manipulation schema params columns
+queryStatement q = UnsafeManipulation $ renderQuery q <> ";"
+
+{-----------------------------------------
+INSERT statements
+-----------------------------------------}
+
+{- |
+When a table is created, it contains no data. The first thing to do
+before a database can be of much use is to insert data. Data is
+conceptually inserted one row at a time. Of course you can also insert
+more than one row, but there is no way to insert less than one row.
+Even if you know only some column values, a complete row must be created.
+
+simple insert:
+
+>>> :{
+let
+  manipulation :: Manipulation
+    '[ "tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[] '[]
+  manipulation =
+    insertInto #tab (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
+      OnConflictDoRaise (Returning Nil)
+in renderManipulation manipulation
+:}
+"INSERT INTO tab (col1, col2) VALUES (2, 4);"
+
+parameterized insert:
+
+>>> :{
+let
+  manipulation :: Manipulation
+    '[ "tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4) ]]
+    '[ 'Required ('NotNull 'PGint4)
+     , 'Required ('NotNull 'PGint4) ] '[]
+  manipulation =
+    insertInto #tab
+      (Values (param @1 `As` #col1 :* param @2 `As` #col2 :* Nil) [])
+      OnConflictDoRaise (Returning Nil)
+in renderManipulation manipulation
+:}
+"INSERT INTO tab (col1, col2) VALUES (($1 :: int4), ($2 :: int4));"
+
+returning insert:
+
+>>> :{
+let
+  manipulation :: Manipulation
+    '[ "tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[]
+    '["fromOnly" ::: 'Required ('NotNull 'PGint4)]
+  manipulation =
+    insertInto #tab (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
+      OnConflictDoRaise (Returning (#col1 `As` #fromOnly :* Nil))
+in renderManipulation manipulation
+:}
+"INSERT INTO tab (col1, col2) VALUES (2, 4) RETURNING col1 AS fromOnly;"
+
+query insert:
+
+>>> :{
+let
+  manipulation :: Manipulation
+    '[ "tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4)
+       ]
+     , "other_tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4)
+       ]
+     ] '[] '[]
+  manipulation = 
+    insertInto #tab
+      ( ValuesQuery $
+        selectStar (from (Table (#other_tab `As` #t))) )
+      OnConflictDoRaise (Returning Nil)
+in renderManipulation manipulation
+:}
+"INSERT INTO tab SELECT * FROM other_tab AS t;"
+
+upsert:
+
+>>> :{
+let
+  manipulation :: Manipulation
+    '[ "tab" :::
+      '[ "col1" ::: 'Required ('NotNull 'PGint4)
+       , "col2" ::: 'Required ('NotNull 'PGint4) ]]
+    '[] '[ "sum" ::: 'Required ('NotNull 'PGint4)]
+  manipulation =
+    insertInto #tab
+      (Values
+        (2 `As` #col1 :* 4 `As` #col2 :* Nil)
+        [6 `As` #col1 :* 8 `As` #col2 :* Nil])
+      (OnConflictDoUpdate
+        (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
+        (Just (#col1 .== #col2)))
+      (Returning $ (#col1 + #col2) `As` #sum :* Nil)
+in renderManipulation manipulation
+:}
+"INSERT INTO tab (col1, col2) VALUES (2, 4), (6, 8) ON CONFLICT DO UPDATE SET col1 = 2 WHERE (col1 = col2) RETURNING (col1 + col2) AS sum;"
+-}
+insertInto
+  :: (SOP.SListI columns, SOP.SListI results, HasTable table schema columns)
+  => Alias table -- ^ table to insert into
+  -> ValuesClause schema params columns -- ^ values to insert
+  -> ConflictClause columns params
+  -- ^ what to do in case of constraint conflict
+  -> ReturningClause columns params results -- ^ results to return
+  -> Manipulation schema params results
+insertInto table insert conflict returning = UnsafeManipulation $
+  "INSERT" <+> "INTO" <+> renderAlias table
+  <+> renderValuesClause insert
+  <> renderConflictClause conflict
+  <> renderReturningClause returning
+
+-- | A `ValuesClause` lets you insert either values, free `Expression`s,
+-- or the result of a `Query`.
+data ValuesClause
+  (schema :: TablesType)
+  (params :: [ColumnType])
+  (columns :: ColumnsType)
+    = Values
+        (NP (Aliased (Expression '[] 'Ungrouped params)) columns)
+        [NP (Aliased (Expression '[] 'Ungrouped params)) columns]
+    -- ^ at least one row of values
+    | ValuesQuery (Query schema params columns)
+
+-- | Render a `ValuesClause`.
+renderValuesClause
+  :: SOP.SListI columns
+  => ValuesClause schema params columns
+  -> ByteString
+renderValuesClause = \case
+  Values row rows ->
+    parenthesized (renderCommaSeparated renderAliasPart row)
+    <+> "VALUES"
+    <+> commaSeparated
+      (parenthesized . renderCommaSeparated renderValuePart <$> row:rows)
+    where
+      renderAliasPart, renderValuePart
+        :: Aliased (Expression '[] 'Ungrouped params) ty -> ByteString
+      renderAliasPart (_ `As` name) = renderAlias name
+      renderValuePart (value `As` _) = renderExpression value
+  ValuesQuery q -> renderQuery q
+
+-- | A `ReturningClause` computes and return value(s) based
+-- on each row actually inserted, updated or deleted. This is primarily
+-- useful for obtaining values that were supplied by defaults, such as a
+-- serial sequence number. However, any expression using the table's columns
+-- is allowed. Only rows that were successfully inserted or updated or
+-- deleted will be returned. For example, if a row was locked
+-- but not updated because an `OnConflictDoUpdate` condition was not satisfied,
+-- the row will not be returned. `ReturningStar` will return all columns
+-- in the row. Use `Returning Nil` in the common case where no return
+-- values are desired.
+data ReturningClause
+  (columns :: ColumnsType)
+  (params :: [ColumnType])
+  (results :: ColumnsType)
+  where
+    ReturningStar :: ReturningClause columns params columns
+    Returning
+      :: NP
+          (Aliased (Expression '[table ::: columns] 'Ungrouped params))
+          results
+      -> ReturningClause columns params results
+
+-- | Render a `ReturningClause`.
+renderReturningClause
+  :: SOP.SListI results
+  => ReturningClause params columns results
+  -> ByteString
+renderReturningClause = \case
+  ReturningStar -> " RETURNING *;"
+  Returning Nil -> ";"
+  Returning results -> " RETURNING"
+    <+> renderCommaSeparated (renderAliased renderExpression) results <> ";"
+
+-- | A `ConflictClause` specifies an action to perform upon a constraint
+-- violation. `OnConflictDoRaise` will raise an error.
+-- `OnConflictDoNothing` simply avoids inserting a row.
+-- `OnConflictDoUpdate` updates the existing row that conflicts with the row
+-- proposed for insertion.
+data ConflictClause columns params where
+  OnConflictDoRaise :: ConflictClause columns params
+  OnConflictDoNothing :: ConflictClause columns params
+  OnConflictDoUpdate
+    :: NP (Aliased (UpdateExpression columns params)) columns
+    -> Maybe (Condition '[table ::: columns] 'Ungrouped params)
+    -> ConflictClause columns params
+
+-- | Render a `ConflictClause`.
+renderConflictClause
+  :: SOP.SListI columns
+  => ConflictClause columns params
+  -> ByteString
+renderConflictClause = \case
+  OnConflictDoRaise -> ""
+  OnConflictDoNothing -> " ON CONFLICT DO NOTHING"
+  OnConflictDoUpdate updates whMaybe
+    -> " ON CONFLICT DO UPDATE SET"
+      <+> renderCommaSeparatedMaybe renderUpdateExpression updates
+      <> case whMaybe of
+        Nothing -> ""
+        Just wh -> " WHERE" <+> renderExpression wh
+
+{-----------------------------------------
+UPDATE statements
+-----------------------------------------}
+
+-- | An `update` command changes the values of the specified columns
+-- in all rows that satisfy the condition.
+--
+-- >>> :{
+-- let
+--   manipulation :: Manipulation
+--     '[ "tab" :::
+--       '[ "col1" ::: 'Required ('NotNull 'PGint4)
+--        , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[] '[]
+--   manipulation =
+--     update #tab (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
+--       (#col1 ./= #col2) (Returning Nil)
+-- in renderManipulation manipulation
+-- :}
+-- "UPDATE tab SET col1 = 2 WHERE (col1 <> col2);"
+update
+  :: (HasTable table schema columns, SOP.SListI columns, SOP.SListI results)
+  => Alias table -- ^ table to update
+  -> NP (Aliased (UpdateExpression columns params)) columns
+  -- ^ modified values to replace old values
+  -> Condition '[tab ::: columns] 'Ungrouped params
+  -- ^ condition under which to perform update on a row
+  -> ReturningClause columns params results -- ^ results to return
+  -> Manipulation schema params results
+update table columns wh returning = UnsafeManipulation $
+  "UPDATE"
+  <+> renderAlias table
+  <+> "SET"
+  <+> renderCommaSeparatedMaybe renderUpdateExpression columns
+  <+> "WHERE" <+> renderExpression wh
+  <> renderReturningClause returning
+
+-- | Columns to be updated are mentioned with `Set`; columns which are to
+-- remain the same are mentioned with `Same`.
+data UpdateExpression columns params ty
+  = Same
+  -- ^ column to remain the same upon update
+  | Set (forall table. Expression '[table ::: columns] 'Ungrouped params ty)
+  -- ^ column to be updated
+deriving instance Show (UpdateExpression columns params ty)
+deriving instance Eq (UpdateExpression columns params ty)
+deriving instance Ord (UpdateExpression columns params ty)
+
+-- | Render an `UpdateExpression`.
+renderUpdateExpression
+  :: Aliased (UpdateExpression params columns) column
+  -> Maybe ByteString
+renderUpdateExpression = \case
+  Same `As` _ -> Nothing
+  Set expression `As` column -> Just $
+    renderAlias column <+> "=" <+> renderExpression expression
+
+{-----------------------------------------
+DELETE statements
+-----------------------------------------}
+
+-- | Delete rows of a table.
+--
+-- >>> :{
+-- let
+--   manipulation :: Manipulation
+--     '[ "tab" :::
+--       '[ "col1" ::: 'Required ('NotNull 'PGint4)
+--        , "col2" ::: 'Required ('NotNull 'PGint4) ]] '[]
+--     '[ "col1" ::: 'Required ('NotNull 'PGint4)
+--      , "col2" ::: 'Required ('NotNull 'PGint4) ]
+--   manipulation = deleteFrom #tab (#col1 .== #col2) ReturningStar
+-- in renderManipulation manipulation
+-- :}
+-- "DELETE FROM tab WHERE (col1 = col2) RETURNING *;"
+deleteFrom
+  :: (SOP.SListI results, HasTable table schema columns)
+  => Alias table -- ^ table to delete from
+  -> Condition '[table ::: columns] 'Ungrouped params
+  -- ^ condition under which to delete a row
+  -> ReturningClause columns params results -- ^ results to return
+  -> Manipulation schema params results
+deleteFrom table wh returning = UnsafeManipulation $
+  "DELETE FROM" <+> renderAlias table
+  <+> "WHERE" <+> renderExpression wh
+  <> renderReturningClause returning
diff --git a/src/Squeal/PostgreSQL/PQ.hs b/src/Squeal/PostgreSQL/PQ.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/PQ.hs
@@ -0,0 +1,575 @@
+{-|
+Module: Squeal.PostgreSQL.PQ
+Description: PQ monad
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+`Squeal.PostgreSQL.PQ` is where Squeal statements come to actually get run by
+`LibPQ`. It contains a `PQ` indexed monad transformer to run `Definition`s and
+a `MonadPQ` constraint for running a `Manipulation` or `Query`.
+-}
+
+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
+{-# LANGUAGE
+    DataKinds
+  , DefaultSignatures
+  , FunctionalDependencies
+  , PolyKinds
+  , DeriveFunctor
+  , FlexibleContexts
+  , FlexibleInstances
+  , MagicHash
+  , MultiParamTypeClasses
+  , OverloadedStrings
+  , RankNTypes
+  , ScopedTypeVariables
+  , TypeApplications
+  , TypeFamilies
+  , TypeInType
+  , TypeOperators
+  , UndecidableInstances
+#-}
+
+module Squeal.PostgreSQL.PQ
+  ( -- * Connection
+    Connection (unConnection)
+  , connectdb
+  , finish
+  , withConnection
+    -- * PQ
+  , PQ (runPQ)
+  , execPQ
+  , pqAp
+  , pqBind
+  , pqThen
+  , define
+  , thenDefine
+    -- * MonadPQ
+  , MonadPQ (..)
+  , PQRun
+  , pqliftWith
+    -- * Result
+  , Result (Result, unResult)
+  , RowNumber (RowNumber, unRowNumber)
+  , ColumnNumber (UnsafeColumnNumber, getColumnNumber)
+  , HasColumnNumber (columnNumber)
+  , getValue
+  , getRow
+  , getRows
+  , ntuples
+  , nextRow
+  , firstRow
+  , liftResult
+  ) where
+
+import Control.Exception.Lifted
+import Control.Monad.Base
+import Control.Monad.Except
+import Control.Monad.Trans.Control
+import Data.ByteString (ByteString)
+import Data.Foldable
+import Data.Function ((&))
+import Data.Kind
+import Data.Monoid
+import Data.Traversable
+import Generics.SOP
+import GHC.Exts hiding (fromList)
+import GHC.TypeLits
+
+import qualified Database.PostgreSQL.LibPQ as LibPQ
+
+import Squeal.PostgreSQL.Binary
+import Squeal.PostgreSQL.Definition
+import Squeal.PostgreSQL.Manipulation
+import Squeal.PostgreSQL.Query
+import Squeal.PostgreSQL.Schema
+
+-- For `MonadPQ` transformer instances
+import Control.Monad.Trans.Identity
+import Control.Monad.Trans.Reader
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Cont
+import Control.Monad.Trans.List
+
+import qualified Control.Monad.Trans.State.Lazy as Lazy
+import qualified Control.Monad.Trans.State.Strict as Strict
+import qualified Control.Monad.Trans.Writer.Lazy as Lazy
+import qualified Control.Monad.Trans.Writer.Strict as Strict
+import qualified Control.Monad.Trans.RWS.Lazy as Lazy
+import qualified Control.Monad.Trans.RWS.Strict as Strict
+
+-- | A `Connection` consists of a `Database.PostgreSQL.LibPQ`
+-- `Database.PastgreSQL.LibPQ.Connection` and a phantom `TablesType`
+newtype Connection (schema :: TablesType) =
+  Connection { unConnection :: LibPQ.Connection }
+
+{- | Makes a new connection to the database server.
+
+This function opens a new database connection using the parameters taken
+from the string conninfo.
+
+The passed string can be empty to use all default parameters, or it can
+contain one or more parameter settings separated by whitespace.
+Each parameter setting is in the form keyword = value. Spaces around the equal
+sign are optional. To write an empty value or a value containing spaces,
+surround it with single quotes, e.g., keyword = 'a value'. Single quotes and
+backslashes within the value must be escaped with a backslash, i.e., ' and \.
+
+To specify the schema you wish to connect with, use type application.
+
+>>> :set -XDataKinds
+>>> :set -XTypeOperators
+>>> type Schema = '["tab" ::: '["col" ::: 'Required ('Null 'PGint2)]]
+>>> :set -XTypeApplications
+>>> :set -XOverloadedStrings
+>>> conn <- connectdb @Schema "host=localhost port=5432 dbname=exampledb"
+
+Note that, for now, squeal doesn't offer any protection from connecting
+with the wrong schema!
+-}
+connectdb
+  :: forall schema io
+   . MonadBase IO io
+  => ByteString -- ^ conninfo
+  -> io (Connection schema)
+connectdb = fmap Connection . liftBase . LibPQ.connectdb
+
+-- | Closes the connection to the server.
+finish :: MonadBase IO io => Connection schema -> io ()
+finish = liftBase . LibPQ.finish . unConnection
+
+-- | Do `connectdb` and `finish` before and after a computation.
+withConnection
+  :: forall schema io x
+   . MonadBaseControl IO io
+  => ByteString
+  -> (Connection schema -> io x)
+  -> io x
+withConnection connString = bracket (connectdb connString) finish
+
+
+-- | We keep track of the schema via an Atkey indexed state monad transformer,
+-- `PQ`.
+newtype PQ
+  (schema0 :: TablesType)
+  (schema1 :: TablesType)
+  (m :: Type -> Type)
+  (x :: Type) =
+    PQ { runPQ :: Connection schema0 -> m (x, Connection schema1) }
+    deriving Functor
+
+-- | Run a `PQ` and discard the result but keep the `Connection`. 
+execPQ
+  :: Functor m
+  => PQ schema0 schema1 m x
+  -> Connection schema0
+  -> m (Connection schema1)
+execPQ (PQ pq) = fmap snd . pq
+
+-- | indexed analog of `<*>`
+pqAp
+  :: Monad m
+  => PQ schema0 schema1 m (x -> y)
+  -> PQ schema1 schema2 m x
+  -> PQ schema0 schema2 m y
+pqAp (PQ f) (PQ x) = PQ $ \ conn -> do
+  (f', conn') <- f conn
+  (x', conn'') <- x conn'
+  return (f' x', conn'')
+
+-- | indexed analog of `=<<`
+pqBind
+  :: Monad m
+  => (x -> PQ schema1 schema2 m y)
+  -> PQ schema0 schema1 m x
+  -> PQ schema0 schema2 m y
+pqBind f (PQ x) = PQ $ \ conn -> do
+  (x', conn') <- x conn
+  runPQ (f x') conn'
+
+-- | indexed analog of flipped `>>`
+pqThen
+  :: Monad m
+  => PQ schema1 schema2 m y
+  -> PQ schema0 schema1 m x
+  -> PQ schema0 schema2 m y
+pqThen pq2 pq1 = pq1 & pqBind (\ _ -> pq2)
+
+-- | Run a `Definition` with `LibPQ.exec`, we expect that libpq obeys the law
+--
+-- @pqThen (define statement2) statement1 = define (statement2 . statement1)@
+define
+  :: MonadBase IO io
+  => Definition schema0 schema1
+  -> PQ schema0 schema1 io (Result '[])
+define (UnsafeDefinition q) = PQ $ \ (Connection conn) -> do
+  resultMaybe <- liftBase $ LibPQ.exec conn q
+  case resultMaybe of
+    Nothing -> error
+      "define: LibPQ.exec returned no results"
+    Just result -> return (Result result, Connection conn)
+
+-- | Chain together `define` actions.
+thenDefine
+  :: MonadBase IO io
+  => Definition schema1 schema2
+  -> PQ schema0 schema1 io x
+  -> PQ schema0 schema2 io (Result '[])
+thenDefine = pqThen . define
+
+{- | `MonadPQ` is an `mtl` style constraint, similar to
+`Control.Monad.State.Class.MonadState`, for using `LibPQ` to
+
+* `manipulateParams` runs a `Manipulation` with params from a type
+   with a `ToParams` constraint. It calls `LibPQ.execParams` and
+   doesn't afraid of anything.
+
+* `manipulate` is like `manipulateParams` for a parameter-free statement.
+
+* `runQueryParams` is like `manipulateParams` for query statements.
+
+* `traversePrepared` has the same type signature as a composition of
+  `traverse` and `manipulateParams` but provides an optimization by
+  preparing the statement with `LibPQ.prepare` and then traversing a
+  `Traversable` container with `LibPQ.execPrepared`. The temporary prepared
+  statement is then deallocated.
+
+* `forPrepared` is a flipped `traversePrepared`
+
+* `traversePrepared_` is like `traversePrepared` but works on `Foldable`
+  containers and returns unit.
+
+* `forPrepared_` is a flipped `traversePrepared_`.
+
+* `liftPQ` lets you lift actions from `LibPQ` that require a connection
+  into your monad.
+
+To define an instance, you can minimally define only `manipulateParams`,
+`traversePrepared`, `traversePrepared_` and `liftPQ`. Monad transformers get
+a default instance.
+
+-}
+class Monad pq => MonadPQ schema pq | pq -> schema where
+  manipulateParams
+    :: ToParams x params
+    => Manipulation schema params ys
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> x -> pq (Result ys)
+  default manipulateParams
+    :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
+    => ToParams x params
+    => Manipulation schema params ys
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> x -> pq (Result ys)
+  manipulateParams manipulation params = lift $
+    manipulateParams manipulation params
+
+  manipulate :: Manipulation schema '[] ys -> pq (Result ys)
+  manipulate statement = manipulateParams statement ()
+
+  runQueryParams
+    :: ToParams x params
+    => Query schema params ys
+    -- ^ `select` and friends
+    -> x -> pq (Result ys)
+  runQueryParams = manipulateParams . queryStatement
+
+  runQuery
+    :: Query schema '[] ys
+    -- ^ `select` and friends
+    -> pq (Result ys)
+  runQuery q = runQueryParams q ()
+
+  traversePrepared
+    :: (ToParams x params, Traversable list)
+    => Manipulation schema params ys
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> list x -> pq (list (Result ys))
+  default traversePrepared
+    :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
+    => (ToParams x params, Traversable list)
+    => Manipulation schema params ys -> list x -> pq (list (Result ys))
+  traversePrepared manipulation params = lift $
+    traversePrepared manipulation params
+
+  forPrepared
+    :: (ToParams x params, Traversable list)
+    => list x
+    -> Manipulation schema params ys
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> pq (list (Result ys))
+  forPrepared = flip traversePrepared
+
+  traversePrepared_
+    :: (ToParams x params, Foldable list)
+    => Manipulation schema params '[]
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> list x -> pq ()
+  default traversePrepared_
+    :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
+    => (ToParams x params, Foldable list)
+    => Manipulation schema params '[]
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> list x -> pq ()
+  traversePrepared_ manipulation params = lift $
+    traversePrepared_ manipulation params
+
+  forPrepared_
+    :: (ToParams x params, Traversable list)
+    => list x
+    -> Manipulation schema params '[]
+    -- ^ `insertInto`, `update` or `deleteFrom`
+    -> pq ()
+  forPrepared_ = flip traversePrepared_
+
+  liftPQ :: (LibPQ.Connection -> IO a) -> pq a
+  default liftPQ
+    :: (MonadTrans t, MonadPQ schema pq1, pq ~ t pq1)
+    => (LibPQ.Connection -> IO a) -> pq a
+  liftPQ = lift . liftPQ
+
+instance MonadBase IO io => MonadPQ schema (PQ schema schema io) where
+
+  manipulateParams
+    (UnsafeManipulation q :: Manipulation schema ps ys) (params :: x) =
+      PQ $ \ (Connection conn) -> do
+        let
+          toParam' bytes = (LibPQ.invalidOid,bytes,LibPQ.Binary)
+          params' = fmap (fmap toParam') (hcollapse (toParams @x @ps params))
+        resultMaybe <- liftBase $ LibPQ.execParams conn q params' LibPQ.Binary
+        case resultMaybe of
+          Nothing -> error
+            "manipulateParams: LibPQ.execParams returned no results"
+          Just result -> return (Result result, Connection conn)
+
+  traversePrepared
+    (UnsafeManipulation q :: Manipulation schema xs ys) (list :: list x) =
+      PQ $ \ (Connection conn) -> liftBase $ do
+        let temp = "temporary_statement"
+        prepResultMaybe <- LibPQ.prepare conn temp q Nothing
+        case prepResultMaybe of
+          Nothing -> error
+            "traversePrepared: LibPQ.prepare returned no results"
+          Just prepResult -> do
+            status <- LibPQ.resultStatus prepResult
+            unless (status == LibPQ.CommandOk) . error $
+              "traversePrepared: LibPQ.prepare status " <> show status
+        results <- for list $ \ params -> do
+          let
+            toParam' bytes = (bytes,LibPQ.Binary)
+            params' = fmap (fmap toParam') (hcollapse (toParams @x @xs params))
+          resultMaybe <- LibPQ.execPrepared conn temp params' LibPQ.Binary
+          case resultMaybe of
+            Nothing -> error
+              "traversePrepared: LibPQ.execParams returned no results"
+            Just result -> return $ Result result
+        deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";")
+        case deallocResultMaybe of
+          Nothing -> error
+            "traversePrepared: LibPQ.exec DEALLOCATE returned no results"
+          Just deallocResult -> do
+            status <- LibPQ.resultStatus deallocResult
+            unless (status == LibPQ.CommandOk) . error $
+              "traversePrepared: DEALLOCATE status " <> show status
+        return (results, Connection conn)
+
+  traversePrepared_
+    (UnsafeManipulation q :: Manipulation schema xs '[]) (list :: list x) =
+      PQ $ \ (Connection conn) -> liftBase $ do
+        let temp = "temporary_statement"
+        prepResultMaybe <- LibPQ.prepare conn temp q Nothing
+        case prepResultMaybe of
+          Nothing -> error
+            "traversePrepared_: LibPQ.prepare returned no results"
+          Just prepResult -> do
+            status <- LibPQ.resultStatus prepResult
+            unless (status == LibPQ.CommandOk) . error $
+              "traversePrepared: LibPQ.prepare status " <> show status
+        for_ list $ \ params -> do
+          let
+            toParam' bytes = (bytes,LibPQ.Binary)
+            params' = fmap (fmap toParam') (hcollapse (toParams @x @xs params))
+          resultMaybe <- LibPQ.execPrepared conn temp params' LibPQ.Binary
+          case resultMaybe of
+            Nothing -> error
+              "traversePrepared_: LibPQ.execParams returned no results"
+            Just _result -> return ()
+        deallocResultMaybe <- LibPQ.exec conn ("DEALLOCATE " <> temp <> ";")
+        case deallocResultMaybe of
+          Nothing -> error
+            "traversePrepared: LibPQ.exec DEALLOCATE returned no results"
+          Just deallocResult -> do
+            status <- LibPQ.resultStatus deallocResult
+            unless (status == LibPQ.CommandOk) . error $
+              "traversePrepared: DEALLOCATE status " <> show status
+        return ((), Connection conn)
+
+  liftPQ pq = PQ $ \ (Connection conn) -> do
+    y <- liftBase $ pq conn
+    return (y, Connection conn)
+
+instance MonadPQ schema m => MonadPQ schema (IdentityT m)
+instance MonadPQ schema m => MonadPQ schema (ReaderT r m)
+instance MonadPQ schema m => MonadPQ schema (Strict.StateT s m)
+instance MonadPQ schema m => MonadPQ schema (Lazy.StateT s m)
+instance (Monoid w, MonadPQ schema m) => MonadPQ schema (Strict.WriterT w m)
+instance (Monoid w, MonadPQ schema m) => MonadPQ schema (Lazy.WriterT w m)
+instance MonadPQ schema m => MonadPQ schema (MaybeT m)
+instance MonadPQ schema m => MonadPQ schema (ExceptT e m)
+instance (Monoid w, MonadPQ schema m) => MonadPQ schema (Strict.RWST r w s m)
+instance (Monoid w, MonadPQ schema m) => MonadPQ schema (Lazy.RWST r w s m)
+instance MonadPQ schema m => MonadPQ schema (ContT r m)
+instance MonadPQ schema m => MonadPQ schema (ListT m)
+
+instance Monad m => Applicative (PQ schema schema m) where
+  pure x = PQ $ \ conn -> pure (x, conn)
+  (<*>) = pqAp
+
+instance Monad m => Monad (PQ schema schema m) where
+  return = pure
+  (>>=) = flip pqBind
+
+instance MonadTrans (PQ schema schema) where
+  lift m = PQ $ \ conn -> do
+    x <- m
+    return (x, conn)
+
+instance MonadBase b m => MonadBase b (PQ schema schema m) where
+  liftBase = lift . liftBase
+
+-- | A snapshot of the state of a `PQ` computation.
+type PQRun schema =
+  forall m x. Monad m => PQ schema schema m x -> m (x, Connection schema)
+
+-- | Helper function in defining `MonadBaseControl` instance for `PQ`.
+pqliftWith :: Functor m => (PQRun schema -> m a) -> PQ schema schema m a
+pqliftWith f = PQ $ \ conn ->
+  fmap (\ x -> (x, conn)) (f $ \ pq -> runPQ pq conn)
+
+instance MonadBaseControl b m => MonadBaseControl b (PQ schema schema m) where
+  type StM (PQ schema schema m) x = StM m (x, Connection schema)
+  liftBaseWith f =
+    pqliftWith $ \ run -> liftBaseWith $ \ runInBase -> f $ runInBase . run
+  restoreM = PQ . const . restoreM
+
+-- | Encapsulates the result of a squeal command run by @LibPQ@.
+-- `Result`s are parameterized by a `ColumnsType` describing the column names
+-- and their types.
+newtype Result (columns :: ColumnsType)
+  = Result { unResult :: LibPQ.Result }
+
+-- | Just newtypes around a `CInt`
+newtype RowNumber = RowNumber { unRowNumber :: LibPQ.Row }
+
+-- | In addition to being newtypes around a `CInt`, a `ColumnNumber` is
+-- parameterized by a `Nat`ural number and acts as an index into a row.
+newtype ColumnNumber (n :: Nat) (cs :: [k]) (c :: k) =
+  UnsafeColumnNumber { getColumnNumber :: LibPQ.Column }
+
+-- | >>> getColumnNumber (columnNumber @5 @'[_,_,_,_,_,_])
+-- Col 5
+class KnownNat n => HasColumnNumber n columns column
+  | n columns -> column where
+  columnNumber :: ColumnNumber n columns column
+  columnNumber =
+    UnsafeColumnNumber . fromIntegral $ natVal' (proxy# :: Proxy# n)
+instance {-# OVERLAPPING #-} HasColumnNumber 0 (column1:columns) column1
+instance {-# OVERLAPPABLE #-}
+  (KnownNat n, HasColumnNumber (n-1) columns column)
+    => HasColumnNumber n (column' : columns) column
+
+-- | Get a single value corresponding to a given row and column number
+-- from a `Result`.
+getValue
+  :: (FromColumnValue colty y, MonadBase IO io)
+  => RowNumber -- ^ row
+  -> ColumnNumber n columns colty -- ^ col
+  -> Result columns -- ^ result
+  -> io y
+getValue
+  (RowNumber r)
+  (UnsafeColumnNumber c :: ColumnNumber n columns colty)
+  (Result result)
+   = fmap (fromColumnValue @colty . K) $ liftBase $ do
+      numRows <- LibPQ.ntuples result
+      when (numRows < r) $ error $
+        "getValue: expected at least " <> show r <> "rows but only saw "
+        <> show numRows
+      LibPQ.getvalue result r c
+
+-- | Get a row corresponding to a given row number from a `Result`.
+getRow
+  :: (FromRow columns y, MonadBase IO io)
+  => RowNumber
+  -- ^ row
+  -> Result columns
+  -- ^ result
+  -> io y
+getRow (RowNumber r) (Result result :: Result columns) = liftBase $ do
+  numRows <- LibPQ.ntuples result
+  when (numRows < r) $ error $
+    "getRow: expected at least " <> show r <> "rows but only saw "
+    <> show numRows
+  let len = fromIntegral (lengthSList (Proxy @columns))
+  row' <- traverse (LibPQ.getvalue result r) [0 .. len - 1]
+  case fromList row' of
+    Nothing -> error "getRow: found unexpected length"
+    Just row -> return $ fromRow @columns row
+
+-- | Returns the number of rows (tuples) in the query result.
+ntuples :: MonadBase IO io => Result columns -> io RowNumber
+ntuples (Result result) = liftBase $ RowNumber <$> LibPQ.ntuples result
+
+-- | Intended to be used for unfolding in streaming libraries, `nextRow`
+-- takes a total number of rows (which can be found with `ntuples`)
+-- and a `Result` and given a row number if it's too large returns `Nothing`,
+-- otherwise returning the row along with the next row number.
+nextRow
+  :: (FromRow columns y, MonadBase IO io)
+  => RowNumber -- ^ total number of rows
+  -> Result columns -- ^ result
+  -> RowNumber -- ^ row number
+  -> io (Maybe (RowNumber,y))
+nextRow (RowNumber total) (Result result :: Result columns) (RowNumber r)
+  = liftBase $ if r >= total then return Nothing else do
+    let len = fromIntegral (lengthSList (Proxy @columns))
+    row' <- traverse (LibPQ.getvalue result r) [0 .. len - 1]
+    case fromList row' of
+      Nothing -> error "nextRow: found unexpected length"
+      Just row -> return $ Just (RowNumber (r+1), fromRow @columns row)
+
+-- | Get all rows from a `Result`.
+getRows
+  :: (FromRow columns y, MonadBase IO io)
+  => Result columns -- ^ result
+  -> io [y]
+getRows (Result result :: Result columns) = liftBase $ do
+  let len = fromIntegral (lengthSList (Proxy @columns))
+  numRows <- LibPQ.ntuples result
+  for [0 .. numRows - 1] $ \ r -> do
+    row' <- traverse (LibPQ.getvalue result r) [0 .. len - 1]
+    case fromList row' of
+      Nothing -> error "getRows: found unexpected length"
+      Just row -> return $ fromRow @columns row
+
+-- | Get the first row if possible from a `Result`.
+firstRow
+  :: (FromRow columns y, MonadBase IO io)
+  => Result columns -- ^ result
+  -> io (Maybe y)
+firstRow (Result result :: Result columns) = liftBase $ do
+  numRows <- LibPQ.ntuples result
+  if numRows <= 0 then return Nothing else do
+    let len = fromIntegral (lengthSList (Proxy @columns))
+    row' <- traverse (LibPQ.getvalue result 0) [0 .. len - 1]
+    case fromList row' of
+      Nothing -> error "firstRow: found unexpected length"
+      Just row -> return . Just $ fromRow @columns row
+
+-- | Lifts actions on results from `LibPQ`.
+liftResult
+  :: MonadBase IO io
+  => (LibPQ.Result -> IO x)
+  -> Result results -> io x
+liftResult f (Result result) = liftBase $ f result
diff --git a/src/Squeal/PostgreSQL/Prettyprint.hs b/src/Squeal/PostgreSQL/Prettyprint.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Prettyprint.hs
@@ -0,0 +1,67 @@
+{-|
+Module: Squeal.PostgreSQL.PrettyPrint
+Description: Pretty print helper functions
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Pretty print helper functions.
+-}
+
+{-# LANGUAGE
+    MagicHash
+  , OverloadedStrings
+  , PolyKinds
+  , RankNTypes
+  , ScopedTypeVariables
+  , TypeApplications
+#-}
+
+module Squeal.PostgreSQL.Prettyprint where
+
+import Data.ByteString (ByteString)
+import Data.Maybe
+import Data.Monoid ((<>))
+import Generics.SOP
+import GHC.Exts
+import GHC.TypeLits
+
+import qualified Data.ByteString as ByteString
+
+-- | Parenthesize a `ByteString`.
+parenthesized :: ByteString -> ByteString
+parenthesized str = "(" <> str <> ")"
+
+-- | Concatenate two `ByteString`s with a space between.
+(<+>) :: ByteString -> ByteString -> ByteString
+str1 <+> str2 = str1 <> " " <> str2
+
+-- | Comma separate a list of `ByteString`s.
+commaSeparated :: [ByteString] -> ByteString
+commaSeparated = ByteString.intercalate ", "
+
+-- | Comma separate the renderings of a heterogeneous list.
+renderCommaSeparated
+  :: SListI xs
+  => (forall x. expression x -> ByteString)
+  -> NP expression xs -> ByteString
+renderCommaSeparated render
+  = commaSeparated
+  . hcollapse
+  . hmap (K . render)
+
+-- | Comma separate the `Maybe` renderings of a heterogeneous list, dropping
+-- `Nothing`s.
+renderCommaSeparatedMaybe
+  :: SListI xs
+  => (forall x. expression x -> Maybe ByteString)
+  -> NP expression xs -> ByteString
+renderCommaSeparatedMaybe render
+  = commaSeparated
+  . catMaybes
+  . hcollapse
+  . hmap (K . render)
+
+-- | Render a promoted `Nat`.
+renderNat :: KnownNat n => proxy n -> ByteString
+renderNat (_ :: proxy n) = fromString (show (natVal' (proxy# :: Proxy# n)))
diff --git a/src/Squeal/PostgreSQL/Query.hs b/src/Squeal/PostgreSQL/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Query.hs
@@ -0,0 +1,775 @@
+{-|
+Module: Squeal.PostgreSQL.Query
+Description: Squeal queries
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Squeal queries.
+-}
+
+{-# LANGUAGE
+    DataKinds
+  , DeriveDataTypeable
+  , DeriveGeneric
+  , FlexibleInstances
+  , GADTs
+  , GeneralizedNewtypeDeriving
+  , KindSignatures
+  , LambdaCase
+  , MultiParamTypeClasses
+  , OverloadedStrings
+  , ScopedTypeVariables
+  , StandaloneDeriving
+  , TypeApplications
+  , TypeInType
+  , TypeOperators
+  , UndecidableInstances
+#-}
+
+module Squeal.PostgreSQL.Query
+  ( -- * Queries
+    Query (UnsafeQuery, renderQuery)
+  , union
+  , unionAll
+  , intersect
+  , intersectAll
+  , except
+  , exceptAll
+    -- * Select
+  , select
+  , selectDistinct
+  , selectStar
+  , selectDistinctStar
+  , selectDotStar
+  , selectDistinctDotStar
+    -- * Table Expressions
+  , TableExpression (..)
+  , renderTableExpression
+  , from
+  , where_
+  , group
+  , having
+  , orderBy
+  , limit
+  , offset
+    -- * From
+  , FromClause (..)
+  , renderFromClause
+    -- * Grouping
+  , By (By, By2)
+  , renderBy
+  , GroupByClause (NoGroups, Group)
+  , renderGroupByClause
+  , HavingClause (NoHaving, Having)
+  , renderHavingClause
+    -- * Sorting
+  , SortExpression (..)
+  , renderSortExpression
+  ) where
+
+import Control.DeepSeq
+import Data.ByteString (ByteString)
+import Data.Monoid
+import Data.String
+import Data.Word
+import Generics.SOP hiding (from)
+import GHC.TypeLits
+
+import qualified GHC.Generics as GHC
+
+import Squeal.PostgreSQL.Expression
+import Squeal.PostgreSQL.Prettyprint
+import Squeal.PostgreSQL.Schema
+
+{- |
+The process of retrieving or the command to retrieve data from a database
+is called a `Query`. The `select`, `selectStar`, `selectDotStar`,
+`selectDistinct`, `selectDistinctStar` and `selectDistinctDotStar` commands
+are used to specify queries.
+
+simple query:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query = selectStar (from (Table (#tab `As` #t)))
+in renderQuery query
+:}
+"SELECT * FROM tab AS t"
+
+restricted query:
+
+>>> :{
+let
+  query :: Query
+    '[ "tab" :::
+       '[ "col1" ::: 'Required ('NotNull 'PGint4)
+        , "col2" ::: 'Required ('NotNull 'PGint4) ]]
+    '[]
+    '[ "sum" ::: 'Required ('NotNull 'PGint4)
+     , "col1" ::: 'Required ('NotNull 'PGint4) ]
+  query = 
+    select
+      ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+      ( from (Table (#tab `As` #t))
+        & where_ (#col1 .> #col2)
+        & where_ (#col2 .> 0) )
+in renderQuery query
+:}
+"SELECT (col1 + col2) AS sum, col1 AS col1 FROM tab AS t WHERE ((col1 > col2) AND (col2 > 0))"
+
+subquery:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query =
+    selectStar
+      (from (Subquery (selectStar (from (Table (#tab `As` #t))) `As` #sub)))
+in renderQuery query
+:}
+"SELECT * FROM (SELECT * FROM tab AS t) AS sub"
+
+limits and offsets:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query = selectStar
+    (from (Table (#tab `As` #t)) & limit 100 & offset 2 & limit 50 & offset 2)
+in renderQuery query
+:}
+"SELECT * FROM tab AS t LIMIT 50 OFFSET 4"
+
+parameterized query:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('NotNull 'PGfloat8)]]
+    '[ 'Required ('NotNull 'PGfloat8)]
+    '["col" ::: 'Required ('NotNull 'PGfloat8)]
+  query = selectStar
+    (from (Table (#tab `As` #t)) & where_ (#col .> param @1))
+in renderQuery query
+:}
+"SELECT * FROM tab AS t WHERE (col > ($1 :: float8))"
+
+aggregation query:
+
+>>> :{
+let
+  query :: Query
+    '[ "tab" :::
+       '[ "col1" ::: 'Required ('NotNull 'PGint4)
+        , "col2" ::: 'Required ('NotNull 'PGint4) ]]
+    '[]
+    '[ "sum" ::: 'Required ('NotNull 'PGint4)
+     , "col1" ::: 'Required ('NotNull 'PGint4) ]
+  query =
+    select (sum_ #col2 `As` #sum :* #col1 `As` #col1 :* Nil)
+    ( from (Table (#tab `As` #table1))
+      & group (By #col1 :* Nil) 
+      & having (#col1 + sum_ #col2 .> 1) )
+in renderQuery query
+:}
+"SELECT sum(col2) AS sum, col1 AS col1 FROM tab AS table1 GROUP BY col1 HAVING ((col1 + sum(col2)) > 1)"
+
+sorted query:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query = selectStar
+    (from (Table (#tab `As` #t)) & orderBy [#col & AscNullsFirst])
+in renderQuery query
+:}
+"SELECT * FROM tab AS t ORDER BY col ASC NULLS FIRST"
+
+joins:
+
+>>> :set -XFlexibleContexts
+>>> :{
+let
+  query :: Query
+    '[ "orders" :::
+         '[ "id"    ::: 'Required ('NotNull 'PGint4)
+          , "price"   ::: 'Required ('NotNull 'PGfloat4)
+          , "customer_id" ::: 'Required ('NotNull 'PGint4)
+          , "shipper_id"  ::: 'Required ('NotNull 'PGint4)
+          ]
+     , "customers" :::
+         '[ "id" ::: 'Required ('NotNull 'PGint4)
+          , "name" ::: 'Required ('NotNull 'PGtext)
+          ]
+     , "shippers" :::
+         '[ "id" ::: 'Required ('NotNull 'PGint4)
+          , "name" ::: 'Required ('NotNull 'PGtext)
+          ]
+     ]
+    '[]
+    '[ "order_price" ::: 'Required ('NotNull 'PGfloat4)
+     , "customer_name" ::: 'Required ('NotNull 'PGtext)
+     , "shipper_name" ::: 'Required ('NotNull 'PGtext)
+     ]
+  query = select
+    ( #o ! #price `As` #order_price :*
+      #c ! #name `As` #customer_name :*
+      #s ! #name `As` #shipper_name :* Nil )
+    ( from (Table (#orders `As` #o)
+      & InnerJoin (Table (#customers `As` #c))
+        (#o ! #customer_id .== #c ! #id)
+      & InnerJoin (Table (#shippers `As` #s))
+        (#o ! #shipper_id .== #s ! #id)) )
+in renderQuery query
+:}
+"SELECT o.price AS order_price, c.name AS customer_name, s.name AS shipper_name FROM orders AS o INNER JOIN customers AS c ON (o.customer_id = c.id) INNER JOIN shippers AS s ON (o.shipper_id = s.id)"
+
+self-join:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query = selectDotStar #t1
+    (from (Table (#tab `As` #t1) & CrossJoin (Table (#tab `As` #t2))))
+in renderQuery query
+:}
+"SELECT t1.* FROM tab AS t1 CROSS JOIN tab AS t2"
+
+set operations:
+
+>>> :{
+let
+  query :: Query '["tab" ::: '["col" ::: 'Required ('Null 'PGint4)]] '[]
+    '["col" ::: 'Required ('Null 'PGint4)]
+  query =
+    selectStar (from (Table (#tab `As` #t)))
+    `unionAll`
+    selectStar (from (Table (#tab `As` #t)))
+in renderQuery query
+:}
+"(SELECT * FROM tab AS t) UNION ALL (SELECT * FROM tab AS t)"
+-}
+
+newtype Query
+  (schema :: TablesType)
+  (params :: [ColumnType])
+  (columns :: ColumnsType)
+    = UnsafeQuery { renderQuery :: ByteString }
+    deriving (GHC.Generic,Show,Eq,Ord,NFData)
+
+-- | The results of two queries can be combined using the set operation
+-- `union`. Duplicate rows are eliminated. 
+union
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `union` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "UNION"
+  <+> parenthesized (renderQuery q2)
+
+-- | The results of two queries can be combined using the set operation
+-- `unionAll`, the disjoint union. Duplicate rows are retained.
+unionAll
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `unionAll` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "UNION" <+> "ALL"
+  <+> parenthesized (renderQuery q2)
+
+-- | The results of two queries can be combined using the set operation
+-- `intersect`, the intersection. Duplicate rows are eliminated.
+intersect
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `intersect` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "INTERSECT"
+  <+> parenthesized (renderQuery q2)
+
+-- | The results of two queries can be combined using the set operation
+-- `intersectAll`, the intersection. Duplicate rows are retained.
+intersectAll
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `intersectAll` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "INTERSECT" <+> "ALL"
+  <+> parenthesized (renderQuery q2)
+
+-- | The results of two queries can be combined using the set operation
+-- `except`, the set difference. Duplicate rows are eliminated.
+except
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `except` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "EXCEPT"
+  <+> parenthesized (renderQuery q2)
+
+-- | The results of two queries can be combined using the set operation
+-- `exceptAll`, the set difference. Duplicate rows are retained.
+exceptAll
+  :: Query schema params columns
+  -> Query schema params columns
+  -> Query schema params columns
+q1 `exceptAll` q2 = UnsafeQuery $
+  parenthesized (renderQuery q1)
+  <+> "EXCEPT" <+> "ALL"
+  <+> parenthesized (renderQuery q2)
+
+{-----------------------------------------
+SELECT queries
+-----------------------------------------}
+
+-- | the `TableExpression` in the `select` command constructs an intermediate
+-- virtual table by possibly combining tables, views, eliminating rows,
+-- grouping, etc. This table is finally passed on to processing by
+-- the select list. The select list determines which columns of
+-- the intermediate table are actually output.
+select
+  :: SListI columns
+  => NP (Aliased (Expression tables grouping params)) (column ': columns)
+  -- ^ select list
+  -> TableExpression schema params tables grouping
+  -- ^ intermediate virtual table
+  -> Query schema params (column ': columns)
+select list tabs = UnsafeQuery $
+  "SELECT"
+  <+> renderCommaSeparated (renderAliased renderExpression) list
+  <+> renderTableExpression tabs
+
+-- | After the select list has been processed, the result table can
+-- be subject to the elimination of duplicate rows using `selectDistinct`.
+selectDistinct
+  :: SListI columns
+  => NP (Aliased (Expression tables 'Ungrouped params)) (column ': columns)
+  -- ^ select list
+  -> TableExpression schema params tables 'Ungrouped
+  -- ^ intermediate virtual table
+  -> Query schema params (column ': columns)
+selectDistinct list tabs = UnsafeQuery $
+  "SELECT DISTINCT"
+  <+> renderCommaSeparated (renderAliased renderExpression) list
+  <+> renderTableExpression tabs
+
+-- | The simplest kind of query is `selectStar` which emits all columns
+-- that the table expression produces.
+selectStar
+  :: HasUnique table tables columns
+  => TableExpression schema params tables 'Ungrouped
+  -- ^ intermediate virtual table
+  -> Query schema params columns
+selectStar tabs = UnsafeQuery $ "SELECT" <+> "*" <+> renderTableExpression tabs
+
+-- | A `selectDistinctStar` emits all columns that the table expression
+-- produces and eliminates duplicate rows.
+selectDistinctStar
+  :: HasUnique table tables columns
+  => TableExpression schema params tables 'Ungrouped
+  -- ^ intermediate virtual table
+  -> Query schema params columns
+selectDistinctStar tabs = UnsafeQuery $
+  "SELECT DISTINCT" <+> "*" <+> renderTableExpression tabs
+
+-- | When working with multiple tables, it can also be useful to ask
+-- for all the columns of a particular table, using `selectDotStar`.
+selectDotStar
+  :: HasTable table tables columns
+  => Alias table
+  -- ^ particular virtual subtable
+  -> TableExpression schema params tables 'Ungrouped
+  -- ^ intermediate virtual table
+  -> Query schema params columns
+selectDotStar table tables = UnsafeQuery $
+  "SELECT" <+> renderAlias table <> ".*" <+> renderTableExpression tables
+
+-- | A `selectDistinctDotStar` asks for all the columns of a particular table, 
+-- and eliminates duplicate rows.
+selectDistinctDotStar
+  :: HasTable table tables columns
+  => Alias table
+  -- ^ particular virtual subtable
+  -> TableExpression schema params tables 'Ungrouped
+  -- ^ intermediate virtual table
+  -> Query schema params columns
+selectDistinctDotStar table tables = UnsafeQuery $
+  "SELECT DISTINCT" <+> renderAlias table <> ".*"
+  <+> renderTableExpression tables
+
+{-----------------------------------------
+Table Expressions
+-----------------------------------------}
+
+-- | A `TableExpression` computes a table. The table expression contains
+-- a `fromClause` that is optionally followed by a `whereClause`,
+-- `groupByClause`, `havingClause`, `orderByClause`, `limitClause`
+-- and `offsetClause`s. Trivial table expressions simply refer
+-- to a table on disk, a so-called base table, but more complex expressions
+-- can be used to modify or combine base tables in various ways.
+data TableExpression
+  (schema :: TablesType)
+  (params :: [ColumnType])
+  (tables :: TablesType)
+  (grouping :: Grouping)
+    = TableExpression
+    { fromClause :: FromClause schema params tables
+    -- ^ A table reference that can be a table name, or a derived table such
+    -- as a subquery, a @JOIN@ construct, or complex combinations of these.
+    , whereClause :: [Condition tables 'Ungrouped params]
+    -- ^ optional search coditions, combined with `.&&`. After the processing
+    -- of the `fromClause` is done, each row of the derived virtual table
+    -- is checked against the search condition. If the result of the
+    -- condition is true, the row is kept in the output table,
+    -- otherwise it is discarded. The search condition typically references
+    -- at least one column of the table generated in the `fromClause`;
+    -- this is not required, but otherwise the WHERE clause will
+    -- be fairly useless.
+    , groupByClause :: GroupByClause tables grouping
+    -- ^ The `groupByClause` is used to group together those rows in a table
+    -- that have the same values in all the columns listed. The order in which
+    -- the columns are listed does not matter. The effect is to combine each
+    -- set of rows having common values into one group row that represents all
+    -- rows in the group. This is done to eliminate redundancy in the output
+    -- and/or compute aggregates that apply to these groups.
+    , havingClause :: HavingClause tables grouping params
+    -- ^ If a table has been grouped using `groupBy`, but only certain groups
+    -- are of interest, the `havingClause` can be used, much like a
+    -- `whereClause`, to eliminate groups from the result. Expressions in the
+    -- `havingClause` can refer both to grouped expressions and to ungrouped
+    -- expressions (which necessarily involve an aggregate function).
+    , orderByClause :: [SortExpression tables grouping params]
+    -- ^ The `orderByClause` is for optional sorting. When more than one
+    -- `SortExpression` is specified, the later (right) values are used to sort
+    -- rows that are equal according to the earlier (left) values.
+    , limitClause :: [Word64]
+    -- ^ The `limitClause` is combined with `min` to give a limit count
+    -- if nonempty. If a limit count is given, no more than that many rows
+    -- will be returned (but possibly fewer, if the query itself yields
+    -- fewer rows).
+    , offsetClause :: [Word64]
+    -- ^ The `offsetClause` is combined with `+` to give an offset count
+    -- if nonempty. The offset count says to skip that many rows before
+    -- beginning to return rows. The rows are skipped before the limit count
+    -- is applied.
+    }
+
+-- | Render a `TableExpression`
+renderTableExpression
+  :: TableExpression schema params tables grouping
+  -> ByteString
+renderTableExpression
+  (TableExpression tables whs' grps' hvs' srts' lims' offs') = mconcat
+    [ "FROM ", renderFromClause tables
+    , renderWheres whs'
+    , renderGroupByClause grps'
+    , renderHavingClause hvs'
+    , renderOrderByClause srts'
+    , renderLimits lims'
+    , renderOffsets offs'
+    ]
+    where
+      renderWheres = \case
+        [] -> ""
+        wh:[] -> " WHERE" <+> renderExpression wh
+        wh:whs -> " WHERE" <+> renderExpression (foldr (.&&) wh whs)
+      renderOrderByClause = \case
+        [] -> ""
+        srts -> " ORDER BY"
+          <+> commaSeparated (renderSortExpression <$> srts)
+      renderLimits = \case
+        [] -> ""
+        lims -> " LIMIT" <+> fromString (show (minimum lims))
+      renderOffsets = \case
+        [] -> ""
+        offs -> " OFFSET" <+> fromString (show (sum offs))
+
+-- | A `from` generates a `TableExpression` from a table reference that can be
+-- a table name, or a derived table such as a subquery, a JOIN construct,
+-- or complex combinations of these. A `from` may be transformed by `where_`,
+-- `group`, `having`, `orderBy`, `limit` and `offset`, using the `&` operator
+-- to match the left-to-right sequencing of their placement in SQL.
+from
+  :: FromClause schema params tables -- ^ table reference
+  -> TableExpression schema params tables 'Ungrouped
+from tables = TableExpression tables [] NoGroups NoHaving [] [] []
+
+-- | A `where_` is an endomorphism of `TableExpression`s which adds a
+-- search condition to the `whereClause`.
+where_
+  :: Condition tables 'Ungrouped params
+  -> TableExpression schema params tables grouping
+  -> TableExpression schema params tables grouping
+where_ wh tables = tables {whereClause = wh : whereClause tables}
+
+-- | A `group` is a transformation of `TableExpression`s which switches
+-- its `Grouping` from `Ungrouped` to `Grouped`. Use @group Nil@ to perform
+-- a "grand total" aggregation query.
+group
+  :: SListI bys
+  => NP (By tables) bys
+  -> TableExpression schema params tables 'Ungrouped 
+  -> TableExpression schema params tables ('Grouped bys)
+group bys tables = TableExpression
+  { fromClause = fromClause tables
+  , whereClause = whereClause tables
+  , groupByClause = Group bys
+  , havingClause = Having []
+  , orderByClause = []
+  , limitClause = limitClause tables
+  , offsetClause = offsetClause tables
+  }
+
+-- | A `having` is an endomorphism of `TableExpression`s which adds a
+-- search condition to the `havingClause`.
+having
+  :: Condition tables ('Grouped bys) params
+  -> TableExpression schema params tables ('Grouped bys)
+  -> TableExpression schema params tables ('Grouped bys)
+having hv tables = tables
+  { havingClause = case havingClause tables of Having hvs -> Having (hv:hvs) }
+
+-- | An `orderBy` is an endomorphism of `TableExpression`s which appends an
+-- ordering to the right of the `orderByClause`.
+orderBy
+  :: [SortExpression tables grouping params]
+  -> TableExpression schema params tables grouping
+  -> TableExpression schema params tables grouping
+orderBy srts tables = tables {orderByClause = orderByClause tables ++ srts}
+
+-- | A `limit` is an endomorphism of `TableExpression`s which adds to the
+-- `limitClause`.
+limit
+  :: Word64
+  -> TableExpression schema params tables grouping
+  -> TableExpression schema params tables grouping
+limit lim tables = tables {limitClause = lim : limitClause tables}
+
+-- | An `offset` is an endomorphism of `TableExpression`s which adds to the
+-- `offsetClause`.
+offset
+  :: Word64
+  -> TableExpression schema params tables grouping
+  -> TableExpression schema params tables grouping
+offset off tables = tables {offsetClause = off : offsetClause tables}
+
+{-----------------------------------------
+FROM clauses
+-----------------------------------------}
+
+{- |
+A `FromClause` can be a table name, or a derived table such
+as a subquery, a @JOIN@ construct, or complex combinations of these.
+
+* A real `Table` is a table from the schema.
+
+* `Subquery` derives a table from a `Query`.
+
+* A joined table is a table derived from two other (real or derived) tables
+according to the rules of the particular join type. `CrossJoin`, `InnerJoin`,
+`LeftOuterJoin`, `RightOuterJoin` and `FullOuterJoin` are available and can
+be nested using the `&` operator to match the left-to-right sequencing of
+their placement in SQL.
+
+    * @t1 & CrossJoin t2@. For every possible combination of rows from
+    @t1@ and @t2@ (i.e., a Cartesian product), the joined table will contain
+    a row consisting of all columns in @t1@ followed by all columns in @t2@.
+    If the tables have @n@ and @m@ rows respectively, the joined table will
+    have @n * m@ rows.
+
+    * @t1 & InnerJoin t2 on@. For each row @r1@ of @t1@, the joined
+    table has a row for each row in @t2@ that satisfies the @on@ condition
+    with @r1@
+
+    * @t1 & LeftOuterJoin t2 on@. First, an inner join is performed.
+    Then, for each row in @t1@ that does not satisfy the @on@ condition with
+    any row in @t2@, a joined row is added with null values in columns of @t2@.
+    Thus, the joined table always has at least one row for each row in @t1@.
+
+    * @t1 & RightOuterJoin t2 on@. First, an inner join is performed.
+    Then, for each row in @t2@ that does not satisfy the @on@ condition with
+    any row in @t1@, a joined row is added with null values in columns of @t1@.
+    This is the converse of a left join: the result table will always
+    have a row for each row in @t2@.
+
+    * @t1 & FullOuterJoin t2 on@. First, an inner join is performed.
+    Then, for each row in @t1@ that does not satisfy the @on@ condition with
+    any row in @t2@, a joined row is added with null values in columns of @t2@.
+    Also, for each row of @t2@ that does not satisfy the join condition
+    with any row in @t1@, a joined row with null values in the columns of @t1@
+    is added.
+-}
+data FromClause schema params tables where
+  Table
+    :: Aliased (Table schema) table
+    -> FromClause schema params '[table]
+  Subquery
+    :: Aliased (Query schema params) table
+    -> FromClause schema params '[table]
+  CrossJoin
+    :: FromClause schema params right
+    -> FromClause schema params left
+    -> FromClause schema params (Join left right)
+  InnerJoin
+    :: FromClause schema params right
+    -> Condition (Join left right) 'Ungrouped params
+    -> FromClause schema params left
+    -> FromClause schema params (Join left right)
+  LeftOuterJoin
+    :: FromClause schema params right
+    -> Condition (Join left right) 'Ungrouped params
+    -> FromClause schema params left
+    -> FromClause schema params (Join left (NullifyTables right))
+  RightOuterJoin
+    :: FromClause schema params right
+    -> Condition (Join left right) 'Ungrouped params
+    -> FromClause schema params left
+    -> FromClause schema params (Join (NullifyTables left) right)
+  FullOuterJoin
+    :: FromClause schema params right
+    -> Condition (Join left right) 'Ungrouped params
+    -> FromClause schema params left
+    -> FromClause schema params
+        (Join (NullifyTables left) (NullifyTables right))
+
+-- | Renders a `FromClause`.
+renderFromClause :: FromClause schema params tables -> ByteString
+renderFromClause = \case
+  Table table -> renderAliased renderTable table
+  Subquery selection -> renderAliased (parenthesized . renderQuery) selection
+  CrossJoin right left ->
+    renderFromClause left <+> "CROSS JOIN" <+> renderFromClause right
+  InnerJoin right on left -> renderJoin "INNER JOIN" right on left
+  LeftOuterJoin right on left -> renderJoin "LEFT OUTER JOIN" right on left
+  RightOuterJoin right on left -> renderJoin "RIGHT OUTER JOIN" right on left
+  FullOuterJoin right on left -> renderJoin "FULL OUTER JOIN" right on left
+  where
+    renderJoin op right on left =
+      renderFromClause left <+> op <+> renderFromClause right
+      <+> "ON" <+> renderExpression on
+
+{-----------------------------------------
+Grouping
+-----------------------------------------}
+
+-- | `By`s are used in `group` to reference a list of columns which are then
+-- used to group together those rows in a table that have the same values
+-- in all the columns listed. @By \#col@ will reference an unambiguous
+-- column @col@; otherwise @By2 (\#tab \! \#col)@ will reference a table
+-- qualified column @tab.col@.
+data By
+    (tables :: TablesType)
+    (by :: (Symbol,Symbol)) where
+    By
+      :: (HasUnique table tables columns, HasColumn column columns ty)
+      => Alias column
+      -> By tables '(table, column)
+    By2
+      :: (HasTable table tables columns, HasColumn column columns ty)
+      => (Alias table, Alias column)
+      -> By tables '(table, column)
+deriving instance Show (By tables by)
+deriving instance Eq (By tables by)
+deriving instance Ord (By tables by)
+
+-- | Renders a `By`.
+renderBy :: By tables tabcolty -> ByteString
+renderBy = \case
+  By column -> renderAlias column
+  By2 (table, column) -> renderAlias table <> "." <> renderAlias column
+
+-- | A `GroupByClause` indicates the `Grouping` of a `TableExpression`.
+-- A `NoGroups` indicates `Ungrouped` while a `Group` indicates `Grouped`.
+-- @NoGroups@ is distinguised from @Group Nil@ since no aggregation can be
+-- done on @NoGroups@ while all output `Expression`s must be aggregated
+-- in @Group Nil@.
+data GroupByClause tables grouping where
+  NoGroups :: GroupByClause tables 'Ungrouped
+  Group
+    :: SListI bys
+    => NP (By tables) bys
+    -> GroupByClause tables ('Grouped bys)
+
+-- | Renders a `GroupByClause`.
+renderGroupByClause :: GroupByClause tables grouping -> ByteString
+renderGroupByClause = \case
+  NoGroups -> ""
+  Group Nil -> ""
+  Group bys -> " GROUP BY" <+> renderCommaSeparated renderBy bys
+
+-- | A `HavingClause` is used to eliminate groups that are not of interest.
+-- An `Ungrouped` `TableExpression` may only use `NoHaving` while a `Grouped`
+-- `TableExpression` must use `Having` whose conditions are combined with
+-- `.&&`.
+data HavingClause tables grouping params where
+  NoHaving :: HavingClause tables 'Ungrouped params
+  Having
+    :: [Condition tables ('Grouped bys) params]
+    -> HavingClause tables ('Grouped bys) params
+deriving instance Show (HavingClause tables grouping params)
+deriving instance Eq (HavingClause tables grouping params)
+deriving instance Ord (HavingClause tables grouping params)
+
+-- | Render a `HavingClause`.
+renderHavingClause :: HavingClause tables grouping params -> ByteString
+renderHavingClause = \case
+  NoHaving -> ""
+  Having [] -> ""
+  Having conditions ->
+    " HAVING" <+> commaSeparated (renderExpression <$> conditions)
+
+{-----------------------------------------
+Sorting
+-----------------------------------------}
+
+-- | `SortExpression`s are used by `sortBy` to optionally sort the results
+-- of a `Query`. `Asc` or `Desc` set the sort direction of a `NotNull` result
+-- column to ascending or descending. Ascending order puts smaller values
+-- first, where "smaller" is defined in terms of the `.<` operator. Similarly,
+-- descending order is determined with the `.>` operator. `AscNullsFirst`,
+-- `AscNullsLast`, `DescNullsFirst` and `DescNullsLast` options are used to
+-- determine whether nulls appear before or after non-null values in the sort
+-- ordering of a `Null` result column.
+data SortExpression tables grouping params where
+    Asc
+      :: Expression tables grouping params ('Required ('NotNull ty))
+      -> SortExpression tables grouping params
+    Desc
+      :: Expression tables grouping params ('Required ('NotNull ty))
+      -> SortExpression tables grouping params
+    AscNullsFirst
+      :: Expression tables grouping params ('Required ('Null ty))
+      -> SortExpression tables grouping params
+    AscNullsLast
+      :: Expression tables grouping params ('Required ('Null ty))
+      -> SortExpression tables grouping params
+    DescNullsFirst
+      :: Expression tables grouping params ('Required ('Null ty))
+      -> SortExpression tables grouping params
+    DescNullsLast
+      :: Expression tables grouping params ('Required ('Null ty))
+      -> SortExpression tables grouping params
+deriving instance Show (SortExpression tables grouping params)
+
+-- | Render a `SortExpression`.
+renderSortExpression :: SortExpression tables grouping params -> ByteString
+renderSortExpression = \case
+  Asc expression -> renderExpression expression <+> "ASC"
+  Desc expression -> renderExpression expression <+> "DESC"
+  AscNullsFirst expression -> renderExpression expression
+    <+> "ASC NULLS FIRST"
+  DescNullsFirst expression -> renderExpression expression
+    <+> "DESC NULLS FIRST"
+  AscNullsLast expression -> renderExpression expression <+> "ASC NULLS LAST"
+  DescNullsLast expression -> renderExpression expression <+> "DESC NULLS LAST"
diff --git a/src/Squeal/PostgreSQL/Schema.hs b/src/Squeal/PostgreSQL/Schema.hs
new file mode 100644
--- /dev/null
+++ b/src/Squeal/PostgreSQL/Schema.hs
@@ -0,0 +1,324 @@
+{-|
+Module: Squeal.PostgreSQL.Schema
+Description: Embedding of PostgreSQL type and alias system
+Copyright: (c) Eitan Chatav, 2017
+Maintainer: eitan@morphism.tech
+Stability: experimental
+
+Embedding of PostgreSQL type and alias system
+-}
+
+{-# LANGUAGE
+    ConstraintKinds
+  , DataKinds
+  , DeriveAnyClass
+  , DeriveDataTypeable
+  , DeriveGeneric
+  , FlexibleContexts
+  , FlexibleInstances
+  , GADTs
+  , MagicHash
+  , MultiParamTypeClasses
+  , OverloadedStrings
+  , PolyKinds
+  , RankNTypes
+  , ScopedTypeVariables
+  , StandaloneDeriving
+  , TypeApplications
+  , TypeFamilies
+  , TypeInType
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.Schema
+  ( -- * Kinds
+    PGType (..)
+  , NullityType (..)
+  , ColumnType (..)
+  , ColumnsType
+  , TablesType
+  , Grouping (..)
+    -- * Constraints
+  , PGNum
+  , PGIntegral
+  , PGFloating
+    -- * Aliases
+  , (:::)
+  , Alias (Alias)
+  , renderAlias
+  , Aliased (As)
+  , renderAliased
+  , IsLabel (..)
+  , IsTableColumn (..)
+    -- * Type Families
+  , In
+  , HasUnique
+  , BaseType
+  , SameTypes
+  , AllNotNull
+  , NotAllNull
+  , NullifyType
+  , NullifyColumns
+  , NullifyTables
+  , Join
+  , Create
+  , Drop
+  , Alter
+  , Rename
+    -- * Generics
+  , SameField
+  , SameFields
+  ) where
+
+import Control.DeepSeq
+import Data.ByteString
+import Data.Monoid
+import Data.String
+import Generics.SOP (AllZip)
+import GHC.Generics (Generic)
+import GHC.Exts
+import GHC.OverloadedLabels
+import GHC.TypeLits
+
+import qualified Generics.SOP.Type.Metadata as Type
+
+-- | `PGType` is the promoted datakind of PostgreSQL types.
+data PGType
+  = PGbool -- ^ logical Boolean (true/false)
+  | PGint2 -- ^ signed two-byte integer
+  | PGint4 -- ^ signed four-byte integer
+  | PGint8 -- ^ signed eight-byte integer
+  | PGnumeric -- ^ arbitrary precision numeric type
+  | PGfloat4 -- ^ single precision floating-point number (4 bytes)
+  | PGfloat8 -- ^ double precision floating-point number (8 bytes)
+  | PGchar Nat -- ^ fixed-length character string
+  | PGvarchar Nat -- ^ variable-length character string
+  | PGtext -- ^ variable-length character string
+  | PGbytea -- ^ binary data ("byte array")
+  | PGtimestamp -- ^ date and time (no time zone)
+  | PGtimestamptz -- ^ date and time, including time zone
+  | PGdate -- ^ calendar date (year, month, day)
+  | PGtime -- ^ time of day (no time zone)
+  | PGtimetz -- ^ time of day, including time zone
+  | PGinterval -- ^ time span
+  | PGuuid -- ^ universally unique identifier
+  | PGinet -- ^ IPv4 or IPv6 host address
+  | PGjson -- ^	textual JSON data
+  | PGjsonb -- ^ binary JSON data, decomposed
+  | UnsafePGType Symbol -- ^ an escape hatch for unsupported PostgreSQL types
+
+-- | `NullityType` encodes the potential presence or definite absence of a
+-- @NULL@ allowing operations which are sensitive to such to be well typed.
+data NullityType
+  = Null PGType -- ^ @NULL@ may be present
+  | NotNull PGType -- ^ @NULL@ is absent
+
+-- | `ColumnType` encodes the allowance of @DEFAULT@ and the only way
+-- to generate an `Optional` `Squeal.PostgreSQL.Expression.Expression`
+-- is to use `Squeal.PostgreSQL.Expression.def`,
+-- `Squeal.PostgreSQL.Expression.unDef` or
+-- `Squeal.PostgreSQL.Expression.param`.
+data ColumnType
+  = Optional NullityType
+  -- ^ @DEFAULT@ is allowed
+  | Required NullityType
+  -- ^ @DEFAULT@ is not allowed
+
+-- | `ColumnsType` is a kind synonym for a row of `ColumnType`s.
+type ColumnsType = [(Symbol,ColumnType)]
+
+-- | `TablesType` is a kind synonym for a row of `ColumnsType`s.
+-- It is used as a kind for both a schema, a disjoint union of tables,
+-- and a joined table `Squeal.PostgreSQL.Query.FromClause`,
+-- a product of tables.
+type TablesType = [(Symbol,ColumnsType)]
+
+-- | `Grouping` is an auxiliary namespace, created by
+-- @GROUP BY@ clauses (`Squeal.PostgreSQL.Query.group`), and used
+-- for typesafe aggregation
+data Grouping
+  = Ungrouped
+  | Grouped [(Symbol,Symbol)]
+
+-- | `PGNum` is a constraint on `PGType` whose
+-- `Squeal.PostgreSQL.Expression.Expression`s have a `Num` constraint.
+type PGNum ty =
+  In ty '[ 'PGint2, 'PGint4, 'PGint8, 'PGnumeric, 'PGfloat4, 'PGfloat8]
+
+-- | `PGFloating` is a constraint on `PGType` whose
+-- `Squeal.PostgreSQL.Expression.Expression`s
+-- have `Fractional` and `Floating` constraints.
+type PGFloating ty = In ty '[ 'PGfloat4, 'PGfloat8, 'PGnumeric]
+
+-- | `PGIntegral` is a constraint on `PGType` whose
+-- `Squeal.PostgreSQL.Expression.Expression`s
+-- have `Squeal.PostgreSQL.Expression.div_` and
+-- `Squeal.PostgreSQL.Expression.mod_` functions.
+type PGIntegral ty = In ty '[ 'PGint2, 'PGint4, 'PGint8]
+
+-- | `:::` is like a promoted version of `As`, a type level pair between
+-- an alias and some type, usually a column alias and a `ColumnType` or
+-- a table alias and a `ColumnsType`.
+type (:::) (alias :: Symbol) (ty :: polykind) = '(alias,ty)
+
+-- | `Alias`es are proxies for a type level string or `Symbol`
+-- and have an `IsLabel` instance so that with @-XOverloadedLabels@
+--
+-- >>> :set -XOverloadedLabels
+-- >>> #foobar :: Alias "foobar"
+-- Alias
+data Alias (alias :: Symbol) = Alias
+  deriving (Eq,Generic,Ord,Show,NFData)
+instance alias1 ~ alias2 => IsLabel alias1 (Alias alias2) where
+  fromLabel = Alias
+
+-- | >>> renderAlias #alias
+-- "alias"
+renderAlias :: KnownSymbol alias => Alias alias -> ByteString
+renderAlias = fromString . symbolVal
+
+-- | The `As` operator is used to name an expression. `As` is like a demoted
+-- version of `:::`.
+--
+-- >>> Just "hello" `As` #hi :: Aliased Maybe ("hi" ::: String)
+-- As (Just "hello") Alias
+data Aliased expression aliased where
+  As
+    :: KnownSymbol alias
+    => expression ty
+    -> Alias alias
+    -> Aliased expression (alias ::: ty)
+deriving instance Show (expression ty)
+  => Show (Aliased expression (alias ::: ty))
+deriving instance Eq (expression ty)
+  => Eq (Aliased expression (alias ::: ty))
+deriving instance Ord (expression ty)
+  => Ord (Aliased expression (alias ::: ty))
+
+-- | >>> let renderMaybe = fromString . maybe "Nothing" (const "Just")
+-- >>> renderAliased renderMaybe (Just (3::Int) `As` #an_int)
+-- "Just AS an_int"
+renderAliased
+  :: (forall ty. expression ty -> ByteString)
+  -> Aliased expression aliased
+  -> ByteString
+renderAliased render (expression `As` alias) =
+  render expression <> " AS " <> renderAlias alias
+
+-- | Analagous to `IsLabel`, the constraint
+-- `IsTableColumn` defines `!` for a column alias qualified
+-- by a table alias.
+class IsTableColumn table column expression where
+  (!) :: Alias table -> Alias column -> expression
+  infixl 9 !
+instance IsTableColumn table column (Alias table, Alias column) where (!) = (,)
+
+-- | @In x xs@ is a constraint that proves that @x@ is in @xs@.
+type family In x xs :: Constraint where
+  In x (x ': xs) = ()
+  In x (y ': xs) = In x xs
+
+-- | @HasUnique alias xs x@ is a constraint that proves that @xs@ is a singleton
+-- of @alias ::: x@.
+type HasUnique alias xs x = xs ~ '[alias ::: x]
+
+-- | `BaseType` forgets about @NULL@ and @DEFAULT@
+type family BaseType (ty :: ColumnType) :: PGType where
+  BaseType (optionality (nullity pg)) = pg
+
+-- | `SameTypes` is a constraint that proves two `ColumnsType`s have the same
+-- length and the same `ColumnType`s.
+type family SameTypes
+  (columns0 :: ColumnsType) (columns1 :: ColumnsType)
+    :: Constraint where
+  SameTypes '[] '[] = ()
+  SameTypes ((column0 ::: ty0) ': columns0) ((column1 ::: ty1) ': columns1)
+    = (ty0 ~ ty1, SameTypes columns0 columns1)
+
+-- | `AllNotNull` is a constraint that proves a `ColumnsType` has no @NULL@s.
+type family AllNotNull (columns :: ColumnsType) :: Constraint where
+  AllNotNull '[] = ()
+  AllNotNull ((column ::: (optionality ('NotNull ty))) ': columns)
+    = AllNotNull columns
+
+-- | `NotAllNull` is a constraint that proves a `ColumnsType` has some
+-- @NOT NULL@.
+type family NotAllNull columns :: Constraint where
+  NotAllNull ((column ::: (optionality ('NotNull ty))) ': columns) = ()
+  NotAllNull ((column ::: (optionality ('Null ty))) ': columns)
+    = NotAllNull columns
+
+-- | `NullifyType` is an idempotent that nullifies a `ColumnType`.
+type family NullifyType (ty :: ColumnType) :: ColumnType where
+  NullifyType (optionality ('Null ty)) = optionality ('Null ty)
+  NullifyType (optionality ('NotNull ty)) = optionality ('Null ty)
+
+-- | `NullifyColumns` is an idempotent that nullifies a `ColumnsType`.
+type family NullifyColumns (columns :: ColumnsType) :: ColumnsType where
+  NullifyColumns '[] = '[]
+  NullifyColumns ((column ::: ty) ': columns) =
+    (column ::: NullifyType ty) ': NullifyColumns columns
+
+-- | `NullifyTables` is an idempotent that nullifies a `TablesType`
+-- used to nullify the left or right hand side of an outer join
+-- in a `Squeal.PostgreSQL.Query.FromClause`.
+type family NullifyTables (tables :: TablesType) :: TablesType where
+  NullifyTables '[] = '[]
+  NullifyTables ((table ::: columns) ': tables) =
+    (table ::: NullifyColumns columns) ': NullifyTables tables
+
+-- | `Join` is simply promoted `++` and is used in @JOIN@s in
+-- `Squeal.PostgreSQL.Query.FromClause`s.
+type family Join xs ys where
+  Join '[] ys = ys
+  Join (x ': xs) ys = x ': Join xs ys
+
+-- | @Create alias x xs@ adds @alias ::: x@ to the end of @xs@ and is used in
+-- `Squeal.PostgreSQL.Definition.createTable` statements and in @ALTER TABLE@
+-- `Squeal.PostgreSQL.Definition.addColumnDefault` and
+-- `Squeal.PostgreSQL.Definition.addColumnNull` statements.
+type family Create alias x xs where
+  Create alias x '[] = '[alias ::: x]
+  Create alias y (x ': xs) = x ': Create alias y xs
+
+-- | @Drop alias xs@ removes the type associated with @alias@ in @xs@
+-- and is used in `Squeal.PostgreSQL.Definition.dropTable` statements
+-- and in @ALTER TABLE@ `Squeal.PostgreSQL.Definition.dropColumn` statements.
+type family Drop alias xs where
+  Drop alias ((alias ::: x) ': xs) = xs
+  Drop alias (x ': xs) = x ': Drop alias xs
+
+-- | @Alter alias xs x@ replaces the type associated with an @alias@ in @xs@
+-- with the type `x` and is used in `Squeal.PostgreSQL.Definition.alterTable`
+-- and `Squeal.PostgreSQL.Definition.alterColumn`.
+type family Alter alias xs x where
+  Alter alias ((alias ::: x0) ': xs) x1 = (alias ::: x1) ': xs
+  Alter alias (x0 ': xs) x1 = x0 ': Alter alias xs x1
+
+-- | @Rename alias0 alias1 xs@ replaces the alias @alias0@ by @alias1@ in @xs@
+-- and is used in `Squeal.PostgreSQL.Definition.alterTableRename` and
+-- `Squeal.PostgreSQL.Definition.renameColumn`.
+type family Rename alias0 alias1 xs where
+  Rename alias0 alias1 ((alias0 ::: x0) ': xs) = (alias1 ::: x0) ': xs
+  Rename alias0 alias1 (x ': xs) = x ': Rename alias0 alias1 xs
+
+-- | A `SameField` constraint is an equality constraint on a
+-- `Generics.SOP.Type.Metadata.FieldInfo` and the column alias in a `:::` pair.
+class SameField
+  (fieldInfo :: Type.FieldInfo) (fieldty :: (Symbol,ColumnType)) where
+instance field ~ column => SameField ('Type.FieldInfo field) (column ::: ty)
+
+-- | A `SameFields` constraint proves that a
+-- `Generics.SOP.Type.Metadata.DatatypeInfo` of a record type has the same
+-- field names as the column aliases of a `ColumnsType`.
+type family SameFields
+  (datatypeInfo :: Type.DatatypeInfo) (columns :: ColumnsType)
+    :: Constraint where
+  SameFields
+    ('Type.ADT _module _datatype '[ 'Type.Record _constructor fields])
+    columns
+      = AllZip SameField fields columns
+  SameFields
+    ('Type.Newtype _module _datatype ('Type.Record _constructor fields))
+    columns
+      = AllZip SameField fields columns
diff --git a/test/DocTest.hs b/test/DocTest.hs
new file mode 100644
--- /dev/null
+++ b/test/DocTest.hs
@@ -0,0 +1,14 @@
+module Main (main) where
+
+import Test.DocTest
+
+main :: IO ()
+main = doctest
+  [ "-isrc"
+  , "src/Squeal/PostgreSQL/Binary.hs"
+  , "src/Squeal/PostgreSQL/Definition.hs"
+  , "src/Squeal/PostgreSQL/Manipulation.hs"
+  , "src/Squeal/PostgreSQL/Query.hs"
+  , "src/Squeal/PostgreSQL/Expression.hs"
+  , "src/Squeal/PostgreSQL/PQ.hs"
+  ]
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Squeal/PostgreSQL/DefinitionSpec.hs b/test/Squeal/PostgreSQL/DefinitionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Squeal/PostgreSQL/DefinitionSpec.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE
+    DataKinds
+  , MagicHash
+  , OverloadedLabels
+  , OverloadedStrings
+  , TypeApplications
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.DefinitionSpec where
+
+import Control.Category ((>>>))
+import Data.Function
+import Generics.SOP hiding (from)
+import Test.Hspec
+
+import Squeal.PostgreSQL
+
+spec :: Spec
+spec = do
+  let
+    definition `definitionRenders` str =
+      renderDefinition definition `shouldBe` str
+  it "should render CREATE TABLE statements" $ do
+    createTable #table1
+      ((int4 & notNull) `As` #col1 :* (int4 & notNull) `As` #col2 :* Nil)
+      [primaryKey (Column #col1 :* Column #col2 :* Nil)]
+      `definitionRenders`
+      "CREATE TABLE table1\
+      \ (col1 int4 NOT NULL, col2 int4 NOT NULL,\
+      \ PRIMARY KEY (col1, col2));"
+    createTable #table2
+      ( serial `As` #col1 :*
+        text `As` #col2 :*
+        (int8 & notNull & default_ 8) `As` #col3 :* Nil )
+        [check (#col3 .> 0)]
+      `definitionRenders`
+      "CREATE TABLE table2\
+      \ (col1 serial,\
+      \ col2 text,\
+      \ col3 int8 NOT NULL DEFAULT 8,\
+      \ CHECK ((col3 > 0)));"
+    let
+      statement :: Definition '[]
+        '[ "users" :::
+           '[ "id" ::: 'Optional ('NotNull 'PGint4)
+            , "username" ::: 'Required ('NotNull 'PGtext)
+            ]
+         , "emails" :::
+           '[ "id" ::: 'Optional ('NotNull 'PGint4)
+            , "userid" ::: 'Required ('NotNull 'PGint4)
+            , "email" ::: 'Required ('NotNull 'PGtext)
+            ]
+         ]
+      statement =
+        createTable #users
+          (serial `As` #id :* (text & notNull) `As` #username :* Nil)
+          [primaryKey (Column #id :* Nil)]
+        >>>
+        createTable #emails
+          ( serial `As` #id :*
+            (integer & notNull) `As` #userid :*
+            (text & notNull) `As` #email :* Nil )
+          [ primaryKey (Column #id :* Nil)
+          , foreignKey (Column #userid :* Nil) #users (Column #id :* Nil)
+            OnDeleteCascade OnUpdateRestrict
+          , unique (Column #email :* Nil)
+          , check (#email ./= "")
+          ]
+    statement `definitionRenders`
+      "CREATE TABLE users\
+      \ (id serial, username text NOT NULL,\
+      \ PRIMARY KEY (id));\
+      \ \
+      \CREATE TABLE emails\
+      \ (id serial,\
+      \ userid integer NOT NULL,\
+      \ email text NOT NULL,\
+      \ PRIMARY KEY (id),\
+      \ FOREIGN KEY (userid) REFERENCES users (id)\
+      \ ON DELETE CASCADE ON UPDATE RESTRICT,\
+      \ UNIQUE (email),\
+      \ CHECK ((email <> E'')));"
+  it "should render DROP TABLE statements" $ do
+    let
+      statement :: Definition Tables '[]
+      statement = dropTable #table1
+    statement `definitionRenders` "DROP TABLE table1;"
+
+type Columns =
+  '[ "col1" ::: 'Required ('NotNull 'PGint4)
+   , "col2" ::: 'Required ('NotNull 'PGint4)
+   ]
+type Tables = '[ "table1" ::: Columns ]
+type SumAndCol1 =
+  '[ "sum" ::: 'Required ('NotNull 'PGint4)
+   , "col1" ::: 'Required ('NotNull 'PGint4)
+   ]
+type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
+type StudentsTable = '["students" ::: StudentsColumns]
+type OrderColumns =
+  '[ "orderID"    ::: 'Required ('NotNull 'PGint4)
+   , "orderVal"   ::: 'Required ('NotNull 'PGtext)
+   , "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperID"  ::: 'Required ('NotNull 'PGint4)
+   ]
+type CustomerColumns =
+  '[ "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   ]
+type ShipperColumns =
+  '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperVal" ::: 'Required ('NotNull 'PGbool)
+   ]
+type JoinTables =
+  '[ "orders"    ::: OrderColumns
+   , "customers" ::: CustomerColumns
+   , "shippers"  ::: ShipperColumns
+   ]
+type ValueColumns =
+  '[ "orderVal"    ::: 'Required ('NotNull 'PGtext)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   , "shipperVal"  ::: 'Required ('NotNull 'PGbool)
+   ]
diff --git a/test/Squeal/PostgreSQL/ManipulationSpec.hs b/test/Squeal/PostgreSQL/ManipulationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Squeal/PostgreSQL/ManipulationSpec.hs
@@ -0,0 +1,128 @@
+{-# LANGUAGE
+    DataKinds
+  , MagicHash
+  , OverloadedLabels
+  , OverloadedStrings
+  , TypeApplications
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.ManipulationSpec where
+
+import Generics.SOP hiding (from)
+import Test.Hspec
+
+import Squeal.PostgreSQL
+
+spec :: Spec
+spec = do
+  let
+    manipulation `manipulationRenders` str =
+      renderManipulation manipulation `shouldBe` str
+  it "correctly renders returning INSERTs" $ do
+    let
+      statement :: Manipulation Tables '[] SumAndCol1
+      statement =
+        insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
+          OnConflictDoRaise
+          (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+    statement `manipulationRenders`
+      "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
+      \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
+  it "correctly renders simple UPDATEs" $ do
+    let
+      statement :: Manipulation Tables '[] '[]
+      statement =
+        update #table1 (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
+          (#col1 ./= #col2) (Returning Nil)
+    statement `manipulationRenders`
+      "UPDATE table1 SET col1 = 2\
+      \ WHERE (col1 <> col2);"
+  it "correctly renders returning UPDATEs" $ do
+    let
+      statement :: Manipulation Tables '[] SumAndCol1
+      statement =
+        update #table1 (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
+          (#col1 ./= #col2)
+          (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+    statement `manipulationRenders`
+      "UPDATE table1 SET col1 = 2\
+      \ WHERE (col1 <> col2)\
+      \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
+  it "correctly renders upsert INSERTs" $ do
+    let
+      statement :: Manipulation Tables '[] '[]
+      statement =
+        insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
+          (OnConflictDoUpdate
+            (Set 2 `As` #col1 :* Same `As` #col2 :* Nil) Nothing)
+          (Returning Nil)
+    statement `manipulationRenders`
+      "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
+      \ ON CONFLICT DO UPDATE\
+      \ SET col1 = 2;"
+  it "correctly renders returning upsert INSERTs" $ do
+    let
+      statement :: Manipulation Tables '[] SumAndCol1
+      statement =
+        insertInto #table1 (Values (2 `As` #col1 :* 4 `As` #col2 :* Nil) [])
+          (OnConflictDoUpdate
+            (Set 2 `As` #col1 :* Same `As` #col2 :* Nil)
+            (Just (#col1 ./= #col2)))
+          (Returning $ (#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+    statement `manipulationRenders`
+      "INSERT INTO table1 (col1, col2) VALUES (2, 4)\
+      \ ON CONFLICT DO UPDATE\
+      \ SET col1 = 2\
+      \ WHERE (col1 <> col2)\
+      \ RETURNING (col1 + col2) AS sum, col1 AS col1;"
+  it "correctly renders DELETEs" $ do
+    let
+      statement :: Manipulation Tables '[] '[]
+      statement = deleteFrom #table1 (#col1 .== #col2) (Returning Nil)
+    statement `manipulationRenders`
+      "DELETE FROM table1 WHERE (col1 = col2);"
+  it "should be safe against SQL injection in literal text" $ do
+    let
+      statement :: Manipulation StudentsTable '[] '[]
+      statement = insertInto #students
+        (Values ("Robert'); DROP TABLE students;" `As` #name :* Nil) [])
+        OnConflictDoRaise (Returning Nil)
+    statement `manipulationRenders`
+      "INSERT INTO students (name) VALUES (E'Robert''); DROP TABLE students;');"
+
+type Columns =
+  '[ "col1" ::: 'Required ('NotNull 'PGint4)
+   , "col2" ::: 'Required ('NotNull 'PGint4)
+   ]
+type Tables = '[ "table1" ::: Columns ]
+type SumAndCol1 =
+  '[ "sum" ::: 'Required ('NotNull 'PGint4)
+   , "col1" ::: 'Required ('NotNull 'PGint4)
+   ]
+type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
+type StudentsTable = '["students" ::: StudentsColumns]
+type OrderColumns =
+  '[ "orderID"    ::: 'Required ('NotNull 'PGint4)
+   , "orderVal"   ::: 'Required ('NotNull 'PGtext)
+   , "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperID"  ::: 'Required ('NotNull 'PGint4)
+   ]
+type CustomerColumns =
+  '[ "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   ]
+type ShipperColumns =
+  '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperVal" ::: 'Required ('NotNull 'PGbool)
+   ]
+type JoinTables =
+  '[ "orders"    ::: OrderColumns
+   , "customers" ::: CustomerColumns
+   , "shippers"  ::: ShipperColumns
+   ]
+type ValueColumns =
+  '[ "orderVal"    ::: 'Required ('NotNull 'PGtext)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   , "shipperVal"  ::: 'Required ('NotNull 'PGbool)
+   ]
diff --git a/test/Squeal/PostgreSQL/QuerySpec.hs b/test/Squeal/PostgreSQL/QuerySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Squeal/PostgreSQL/QuerySpec.hs
@@ -0,0 +1,181 @@
+{-# LANGUAGE
+    DataKinds
+  , MagicHash
+  , OverloadedLabels
+  , OverloadedStrings
+  , TypeApplications
+  , TypeOperators
+#-}
+
+module Squeal.PostgreSQL.QuerySpec where
+
+import Data.Function
+import Generics.SOP hiding (from)
+import Test.Hspec
+
+import Squeal.PostgreSQL
+
+spec :: Spec
+spec = do
+  let
+    qry `queryRenders` str =
+      renderManipulation (queryStatement qry) `shouldBe` str
+  it "correctly renders a simple SELECT query" $ do
+    let
+      statement :: Query Tables '[] SumAndCol1
+      statement =
+        select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+        (from (Table (#table1 `As` #table1)) & where_ (#col1 .> #col2))
+    statement `queryRenders`
+      "SELECT (col1 + col2) AS sum, col1 AS col1\
+      \ FROM table1 AS table1 WHERE (col1 > col2);"
+  it "combines WHEREs using AND" $ do
+    let
+      statement :: Query Tables '[] SumAndCol1
+      statement =
+        select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+        (from (Table (#table1 `As` #table1)) & where_ true & where_ false)
+    statement `queryRenders`
+      "SELECT (col1 + col2) AS sum, col1 AS col1\
+      \ FROM table1 AS table1 WHERE (TRUE AND FALSE);"
+  it "performs sub SELECTs" $ do
+    let
+      statement :: Query Tables '[] SumAndCol1
+      statement =
+        selectStar (from (Subquery
+          (select ((#col1 + #col2) `As` #sum :* #col1 `As` #col1 :* Nil)
+            (from (Table (#table1 `As` #table1))) `As` #sub)))
+    statement `queryRenders`
+      "SELECT * FROM\
+      \ (SELECT (col1 + col2) AS sum, col1 AS col1\
+      \ FROM table1 AS table1) AS sub;"
+  it "does LIMIT clauses" $ do
+    let
+      statement :: Query Tables '[] Columns
+      statement = selectStar (from (Table (#table1 `As` #table1)) & limit 1)
+    statement `queryRenders` "SELECT * FROM table1 AS table1 LIMIT 1;"
+  it "should use the minimum of given LIMITs" $ do
+    let
+      statement :: Query Tables '[] Columns
+      statement =
+        selectStar (from (Table (#table1 `As` #table1)) & limit 1 & limit 2)
+    statement `queryRenders` "SELECT * FROM table1 AS table1 LIMIT 1;"
+  it "should render parameters using $ signs" $ do
+    let
+      statement :: Query Tables '[ 'Required ('NotNull 'PGbool)] Columns
+      statement = selectStar
+        (from (Table (#table1 `As` #table1)) & where_ (param @1))
+    statement `queryRenders`
+      "SELECT * FROM table1 AS table1 WHERE ($1 :: bool);"
+  it "does OFFSET clauses" $ do
+    let
+      statement :: Query Tables '[] Columns
+      statement =
+        selectStar (from (Table (#table1 `As` #table1)) & offset 1)
+    statement `queryRenders` "SELECT * FROM table1 AS table1 OFFSET 1;"
+  it "should use the sum of given OFFSETs" $ do
+    let
+      statement :: Query Tables '[] Columns
+      statement =  selectStar
+        (from (Table (#table1 `As` #table1)) & offset 1 & offset 2)
+    statement `queryRenders` "SELECT * FROM table1 AS table1 OFFSET 3;"
+  it "should render GROUP BY and HAVING clauses" $ do
+    let
+      statement :: Query Tables '[] SumAndCol1
+      statement =
+        select (sum_ #col2 `As` #sum :* #col1 `As` #col1 :* Nil)
+        ( from (Table (#table1 `As` #table1))
+          & group (By #col1 :* Nil) 
+          & having (#col1 + sum_ #col2 .> 1) )
+    statement `queryRenders`
+      "SELECT sum(col2) AS sum, col1 AS col1\
+      \ FROM table1 AS table1\
+      \ GROUP BY col1\
+      \ HAVING ((col1 + sum(col2)) > 1);"
+  describe "JOINs" $ do
+    it "should render CROSS JOINs" $ do
+      let
+        statement :: Query JoinTables '[] ValueColumns
+        statement = select
+          ( #orders ! #orderVal `As` #orderVal :*
+            #customers ! #customerVal `As` #customerVal :*
+            #shippers ! #shipperVal `As` #shipperVal :* Nil )
+          ( from (Table (#orders `As` #orders)
+            & CrossJoin (Table (#customers `As` #customers))
+            & CrossJoin (Table (#shippers `As` #shippers))) )
+      statement `queryRenders`
+        "SELECT\
+        \ orders.orderVal AS orderVal,\
+        \ customers.customerVal AS customerVal,\
+        \ shippers.shipperVal AS shipperVal\
+        \ FROM orders AS orders\
+        \ CROSS JOIN customers AS customers\
+        \ CROSS JOIN shippers AS shippers;"
+    it "should render INNER JOINs" $ do
+      let
+        statement :: Query JoinTables '[] ValueColumns
+        statement = select
+          ( (#orders ! #orderVal) `As` #orderVal
+            :* (#customers ! #customerVal) `As` #customerVal
+            :* (#shippers ! #shipperVal) `As` #shipperVal :* Nil)
+          ( from (Table (#orders `As` #orders)
+            & InnerJoin (Table (#customers `As` #customers))
+              ((#orders ! #customerID) .== (#customers ! #customerID))
+            & InnerJoin (Table (#shippers `As` #shippers))
+              ((#orders ! #shipperID) .== (#shippers ! #shipperID))))
+      statement `queryRenders`
+        "SELECT\
+        \ orders.orderVal AS orderVal,\
+        \ customers.customerVal AS customerVal,\
+        \ shippers.shipperVal AS shipperVal\
+        \ FROM orders AS orders\
+        \ INNER JOIN customers AS customers\
+        \ ON (orders.customerID = customers.customerID)\
+        \ INNER JOIN shippers AS shippers\
+        \ ON (orders.shipperID = shippers.shipperID);"
+    it "should render self JOINs" $ do
+      let
+        statement :: Query JoinTables '[] OrderColumns
+        statement = selectDotStar #orders1
+          (from (Table (#orders `As` #orders1)
+            & CrossJoin (Table (#orders `As` #orders2))))
+      statement `queryRenders`
+        "SELECT orders1.*\
+        \ FROM orders AS orders1\
+        \ CROSS JOIN orders AS orders2;"
+
+type Columns =
+  '[ "col1" ::: 'Required ('NotNull 'PGint4)
+   , "col2" ::: 'Required ('NotNull 'PGint4)
+   ]
+type Tables = '[ "table1" ::: Columns ]
+type SumAndCol1 =
+  '[ "sum" ::: 'Required ('NotNull 'PGint4)
+   , "col1" ::: 'Required ('NotNull 'PGint4)
+   ]
+type StudentsColumns = '["name" ::: 'Required ('NotNull 'PGtext)]
+type StudentsTable = '["students" ::: StudentsColumns]
+type OrderColumns =
+  '[ "orderID"    ::: 'Required ('NotNull 'PGint4)
+   , "orderVal"   ::: 'Required ('NotNull 'PGtext)
+   , "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperID"  ::: 'Required ('NotNull 'PGint4)
+   ]
+type CustomerColumns =
+  '[ "customerID" ::: 'Required ('NotNull 'PGint4)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   ]
+type ShipperColumns =
+  '[ "shipperID" ::: 'Required ('NotNull 'PGint4)
+   , "shipperVal" ::: 'Required ('NotNull 'PGbool)
+   ]
+type JoinTables =
+  '[ "orders"    ::: OrderColumns
+   , "customers" ::: CustomerColumns
+   , "shippers"  ::: ShipperColumns
+   ]
+type ValueColumns =
+  '[ "orderVal"    ::: 'Required ('NotNull 'PGtext)
+   , "customerVal" ::: 'Required ('NotNull 'PGfloat4)
+   , "shipperVal"  ::: 'Required ('NotNull 'PGbool)
+   ]
