content: {-|
  Module: Squeal.PostgreSQL.Type.Schema
  Description: Postgres type system
  Copyright: (c) Eitan Chatav, 2019
  Maintainer: eitan@morphism.tech
  Stability: experimental
  
  Provides a type-level DSL for kinds of Postgres types,
  tables, schema, constraints, and more.
  It also defines useful type families to operate on these.
  -}
  
  {-# LANGUAGE
      AllowAmbiguousTypes
    , ConstraintKinds
    , DeriveAnyClass
    , DeriveGeneric
    , FlexibleContexts
    , FlexibleInstances
    , FunctionalDependencies
    , GADTs
    , LambdaCase
    , OverloadedStrings
    , QuantifiedConstraints
    , RankNTypes
    , ScopedTypeVariables
    , StandaloneDeriving
    , TypeApplications
    , TypeFamilyDependencies
    , DataKinds
    , PolyKinds
    , TypeOperators
    , UndecidableInstances
    , UndecidableSuperClasses
  #-}
  
  module Squeal.PostgreSQL.Type.Schema
    ( -- * Postgres Type
      PGType (..)
    , NullType (..)
    , RowType
    , FromType
      -- * Schema Type
    , ColumnType
    , ColumnsType
    , TableType
    , SchemumType (..)
    , IndexType (..)
    , FunctionType
    , ReturnsType (..)
    , SchemaType
    , SchemasType
    , Public
      -- * Database Subsets
    , SubDB
    , SubsetDB
    , ElemDB
      -- * Constraint
    , (:=>)
    , Optionality (..)
    , TableConstraint (..)
    , TableConstraints
    , Uniquely
      -- * Enumerated Label
    , IsPGlabel (..)
    , PGlabel (..)
      -- * Data Definition
    , Create
    , CreateIfNotExists
    , CreateOrReplace
    , Drop
    , DropSchemum
    , DropIfExists
    , DropSchemumIfExists
    , Alter
    , AlterIfExists
    , Rename
    , RenameIfExists
    , SetSchema
    , ConstraintInvolves
    , DropIfConstraintsInvolve
      -- * Type Classification
    , PGNum
    , PGIntegral
    , PGFloating
    , PGJsonType
    , PGJsonKey
    , SamePGType
    , AllNotNull
    , NotAllNull
      -- * Nullification
    , NullifyType
    , NullifyRow
    , NullifyFrom
      -- * Table Conversion
    , TableToColumns
    , ColumnsToRow
    , TableToRow
      -- * Updatable
    , Updatable
    , AllUnique
    , IsNotElem
      -- * User Type Lookup
    , DbEnums
    , SchemaEnums
    , DbRelations
    , SchemaRelations
    , FindQualified
    , FindName
    , FindNamespace
      -- * Schema Error Printing
    , PrettyPrintPartitionedSchema
    , PartitionedSchema(..)
    , PartitionSchema
    , SchemaFunctions
    , SchemaIndexes
    , SchemaProcedures
    , SchemaTables
    , SchemaTypes
    , SchemaUnsafes
    , SchemaViews
    , IntersperseNewlines
    , FilterNonEmpty
    , FieldIfNonEmpty
    , PartitionSchema'
    ) where
  
  import Control.Category
  import Data.Kind
  import Data.Monoid hiding (All)
  import Data.Type.Bool
  import Generics.SOP
  import GHC.TypeLits
  import Prelude hiding (id, (.))
  
  import Squeal.PostgreSQL.Type.Alias
  import Squeal.PostgreSQL.Type.List
  import Squeal.PostgreSQL.Render
  
  -- $setup
  -- >>> import Squeal.PostgreSQL
  
  -- | `PGType` is the promoted datakind of PostgreSQL types.
  --
  -- >>> :kind 'PGbool
  -- 'PGbool :: PGType
  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)
    | PGmoney -- ^ currency amount
    | 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
    | PGvararray NullType -- ^ variable length array
    | PGfixarray [Nat] NullType -- ^ fixed length array
    | PGenum [Symbol] -- ^ enumerated (enum) types are data types that comprise a static, ordered set of values.
    | PGcomposite RowType -- ^ a composite type represents the structure of a row or record; it is essentially just a list of field names and their data types.
    | PGtsvector -- ^ A tsvector value is a sorted list of distinct lexemes, which are words that have been normalized to merge different variants of the same word.
    | PGtsquery -- ^ A tsquery value stores lexemes that are to be searched for.
    | PGoid -- ^ Object identifiers (OIDs) are used internally by PostgreSQL as primary keys for various system tables.
    | PGrange PGType -- ^ Range types are data types representing a range of values of some element type (called the range's subtype).
    | UnsafePGType Symbol -- ^ an escape hatch for unsupported PostgreSQL types
  
  -- | `NullType` encodes the potential presence or definite absence of a
  -- @NULL@ allowing operations which are sensitive to such to be well typed.
  --
  -- >>> :kind 'Null 'PGint4
  -- 'Null 'PGint4 :: NullType
  -- >>> :kind 'NotNull ('PGvarchar 50)
  -- 'NotNull ('PGvarchar 50) :: NullType
  data NullType
    = Null PGType -- ^ @NULL@ may be present
    | NotNull PGType -- ^ @NULL@ is absent
  
  -- | The constraint  operator, `:=>` is a type level pair
  -- between a "constraint" and some type, for use in pairing
  -- an `Optionality` with a `NullType` to produce a `ColumnType`
  -- or a `TableConstraints` and a `ColumnsType` to produce a `TableType`.
  type (:=>) constraint ty = '(constraint,ty)
  infixr 7 :=>
  
  -- | `Optionality` encodes the availability of @DEFAULT@ for inserts and updates.
  -- A column can be assigned a default value.
  -- A data `Squeal.PostgreSQL.Manipulations.Manipulation` command can also
  -- request explicitly that a column be set to its default value,
  -- without having to know what that value is.
  data Optionality
    = Def -- ^ @DEFAULT@ is available for inserts and updates
    | NoDef -- ^ @DEFAULT@ is unavailable for inserts and updates
  
  -- | `ColumnType` encodes the allowance of @DEFAULT@ and @NULL@ and the
  -- base `PGType` for a column.
  --
  -- >>> :set -XTypeFamilies -XDataKinds -XPolyKinds
  -- >>> import GHC.TypeLits
  -- >>> type family IdColumn :: ColumnType where IdColumn = 'Def :=> 'NotNull 'PGint4
  -- >>> type family EmailColumn :: ColumnType where EmailColumn = 'NoDef :=> 'Null 'PGtext
  type ColumnType = (Optionality,NullType)
  
  -- | `ColumnsType` is a row of `ColumnType`s.
  --
  -- >>> :{
  -- type family UsersColumns :: ColumnsType where
  --   UsersColumns =
  --     '[ "name" ::: 'NoDef :=> 'NotNull 'PGtext
  --      , "id"   :::   'Def :=> 'NotNull 'PGint4
  --      ]
  -- :}
  type ColumnsType = [(Symbol,ColumnType)]
  
  type instance PrettyPrintHaystack (haystack :: ColumnsType) =
    'PrettyPrintInfo ('Text "column definition (ColumnType)") ('Text "table (ColumnsType)") ('ShowType (Sort (MapFst haystack)))
  
  -- | `TableConstraint` encodes various forms of data constraints
  -- of columns in a table.
  -- `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.
  data TableConstraint
    = Check [Symbol]
    | Unique [Symbol]
    | PrimaryKey [Symbol]
    | ForeignKey [Symbol] Symbol Symbol [Symbol]
  
  {- | A `TableConstraints` is a row of `TableConstraint`s.
  
  >>> :{
  type family UsersConstraints :: TableConstraints where
    UsersConstraints = '[ "pk_users" ::: 'PrimaryKey '["id"] ]
  :}
  -}
  type TableConstraints = [(Symbol,TableConstraint)]
  
  type instance PrettyPrintHaystack (haystack :: TableConstraints) =
    'PrettyPrintInfo ('Text "constraint (TableConstraint)") ('Text "table (TableConstraints)") ('ShowType (Sort (MapFst haystack)))
  
  -- | A `ForeignKey` must reference columns that either are
  -- a `PrimaryKey` or form a `Unique` constraint.
  type family Uniquely
    (key :: [Symbol])
    (constraints :: TableConstraints) :: Constraint where
      Uniquely key (uq ::: 'Unique key ': constraints) = ()
      Uniquely key (pk ::: 'PrimaryKey key ': constraints) = ()
      Uniquely key (_ ': constraints) = Uniquely key constraints
  
  -- | `TableType` encodes a row of constraints on a table as well as the types
  -- of its columns.
  --
  -- >>> :{
  -- type family UsersTable :: TableType where
  --   UsersTable =
  --     '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
  --     '[ "id"       :::   'Def :=> 'NotNull 'PGint4
  --      , "name"     ::: 'NoDef :=> 'NotNull 'PGtext
  --      ]
  -- :}
  type TableType = (TableConstraints,ColumnsType)
  
  {- | A `RowType` is a row of `NullType`s. They correspond to Haskell
  record types by means of `Squeal.PostgreSQL.Binary.RowPG` and are used in many places.
  
  >>> :{
  type family PersonRow :: RowType where
    PersonRow =
      '[ "name"        ::: 'NotNull 'PGtext
       , "age"         ::: 'NotNull 'PGint4
       , "dateOfBirth" :::    'Null 'PGdate
       ]
  :}
  -}
  type RowType = [(Symbol,NullType)]
  
  type instance PrettyPrintHaystack (haystack :: RowType) =
    'PrettyPrintInfo ('Text "column (NullType)") ('Text "row (RowType)") ('ShowType (Sort (MapFst haystack)))
  
  {- | `FromType` is a row of `RowType`s. It can be thought of as
  a product, or horizontal gluing and is used in `Squeal.PostgreSQL.Query.From.FromClause`s
  and `Squeal.PostgreSQL.Query.Table.TableExpression`s.
  -}
  type FromType = [(Symbol,RowType)]
  
  type instance PrettyPrintHaystack (haystack :: FromType) =
    'PrettyPrintInfo ('Text "row (RowType)") ('Text "from clause (FromType)") ('ShowType (Sort (MapFst haystack)))
  
  -- | `ColumnsToRow` removes column constraints.
  type family ColumnsToRow (columns :: ColumnsType) :: RowType where
    ColumnsToRow (column ::: _ :=> ty ': columns) =
      column ::: ty ': ColumnsToRow columns
    ColumnsToRow '[] = '[]
  
  -- | `TableToColumns` removes table constraints.
  type family TableToColumns (table :: TableType) :: ColumnsType where
    TableToColumns (constraints :=> columns) = columns
  
  -- | Convert a table to a row type.
  type family TableToRow (table :: TableType) :: RowType where
    TableToRow tab = ColumnsToRow (TableToColumns tab)
  
  -- | Numeric Postgres types.
  type PGNum =
    '[ 'PGint2, 'PGint4, 'PGint8, 'PGnumeric, 'PGfloat4, 'PGfloat8]
  
  -- | Floating Postgres types.
  type PGFloating = '[ 'PGfloat4, 'PGfloat8, 'PGnumeric]
  
  -- | Integral Postgres types.
  type PGIntegral = '[ 'PGint2, 'PGint4, 'PGint8]
  
  -- | Equality constraint on the underlying `PGType` of two columns.
  class SamePGType
    (ty0 :: (Symbol,ColumnType)) (ty1 :: (Symbol,ColumnType)) where
  instance ty0 ~ ty1 => SamePGType
    (alias0 ::: def0 :=> null0 ty0)
    (alias1 ::: def1 :=> null1 ty1)
  
  -- | `AllNotNull` is a constraint that proves a `ColumnsType` has no @NULL@s.
  type family AllNotNull (columns :: ColumnsType) :: Constraint where
    AllNotNull (_ ::: _ :=> 'NotNull _ ': columns) = AllNotNull columns
    AllNotNull '[] = ()
  
  -- | `NotAllNull` is a constraint that proves a `ColumnsType` has some
  -- @NOT NULL@.
  type family NotAllNull (columns :: ColumnsType) :: Constraint where
    NotAllNull (_ ::: _ :=> 'NotNull _ ': _) = ()
    NotAllNull (_ ::: _ :=> 'Null _ ': columns) = NotAllNull columns
  
  -- | `NullifyType` is an idempotent that nullifies a `NullType`.
  type family NullifyType (ty :: NullType) :: NullType where
    NullifyType (null ty) = 'Null ty
  
  -- | `NullifyRow` is an idempotent that nullifies a `RowType`.
  type family NullifyRow (columns :: RowType) :: RowType where
    NullifyRow (column ::: ty ': columns) =
      column ::: NullifyType ty ': NullifyRow columns
    NullifyRow '[] = '[]
  
  -- | `NullifyFrom` is an idempotent that nullifies a `FromType`
  -- used to nullify the left or right hand side of an outer join
  -- in a `Squeal.PostgreSQL.Query.From.FromClause`.
  type family NullifyFrom (tables :: FromType) :: FromType where
    NullifyFrom (table ::: columns ': tables) =
      table ::: NullifyRow columns ': NullifyFrom tables
    NullifyFrom '[] = '[]
  
  -- | @Create alias x xs@ adds @alias ::: x@ to the end of @xs@ and is used in
  -- `Squeal.PostgreSQL.Definition.Table.createTable` statements and in @ALTER TABLE@
  -- `Squeal.PostgreSQL.Definition.Table.addColumn`.
  type family Create alias x xs where
    Create alias x '[] = '[alias ::: x]
    Create alias x (alias ::: y ': xs) = TypeError
      ('Text "Create: alias "
      ':<>: 'ShowType alias
      ':<>: 'Text "already exists")
    Create alias y (x ': xs) = x ': Create alias y xs
  
  {-| Similar to `Create` but no error on pre-existence-}
  type family CreateIfNotExists alias x xs where
    CreateIfNotExists alias x '[] = '[alias ::: x]
    CreateIfNotExists alias x (alias ::: y ': xs) = alias ::: y ': xs
    CreateIfNotExists alias y (x ': xs) = x ': CreateIfNotExists alias y xs
  
  {-| Similar to `Create` but used to replace values
  with the same type.-}
  type family CreateOrReplace alias x xs where
    CreateOrReplace alias x '[] = '[alias ::: x]
    CreateOrReplace alias x (alias ::: x ': xs) = alias ::: x ': xs
    CreateOrReplace alias x (alias ::: y ': xs) = TypeError
      ('Text "CreateOrReplace: expected type "
      ':<>: 'ShowType x
      ':<>: 'Text " but alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " has type "
      ':<>: 'ShowType y)
    CreateOrReplace alias y (x ': xs) = x ': CreateOrReplace 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 '[] = TypeError
      ('Text "Drop: alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " does not exist" )
    Drop alias (alias ::: x ': xs) = xs
    Drop alias (x ': xs) = x ': Drop alias xs
  
  -- | Drop a particular flavor of schemum type
  type family DropSchemum alias sch xs where
    DropSchemum alias sch '[] = TypeError
      ('Text "DropSchemum: alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " does not exist" )
    DropSchemum alias sch (alias ::: sch x ': xs) = xs
    DropSchemum alias sch0 (alias ::: sch1 x ': xs) = TypeError
      ('Text "DropSchemum: expected schemum "
      ':<>: 'ShowType sch0
      ':<>: 'Text " but alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " has schemum "
      ':<>: 'ShowType sch1)
    DropSchemum alias sch (x ': xs) = x ': DropSchemum alias sch xs
  
  -- | Similar to `Drop` but no error on non-existence
  type family DropIfExists alias xs where
    DropIfExists alias '[] = '[]
    DropIfExists alias (alias ::: x ': xs) = xs
    DropIfExists alias (x ': xs) = x ': DropIfExists alias xs
  
  -- | Similar to `DropSchemum` but no error on non-existence
  type family DropSchemumIfExists alias sch xs where
    DropSchemumIfExists alias sch '[] = '[]
    DropSchemumIfExists alias sch (alias ::: sch x ': xs) = xs
    DropSchemumIfExists alias sch0 (alias ::: sch1 x ': xs) = TypeError
      ('Text "DropSchemumIfExists: expected schemum "
      ':<>: 'ShowType sch1
      ':<>: 'Text " but alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " has schemum "
      ':<>: 'ShowType sch0)
    DropSchemumIfExists alias sch (x ': xs) = x ': DropSchemumIfExists alias sch xs
  
  -- | @Alter alias x xs@ 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 x xs where
    Alter alias x '[] = TypeError
      ('Text "Alter: alias "
      ':<>: 'ShowType alias
      ':<>: 'Text " does not exist" )
    Alter alias x1 (alias ::: x0 ': xs) = alias ::: x1 ': xs
    Alter alias x1 (x0 ': xs) = x0 ': Alter alias x1 xs
  
  -- | Similar to `Alter` but no error on non-existence
  type family AlterIfExists alias x xs where
    AlterIfExists alias x '[] = '[]
    AlterIfExists alias x1 (alias ::: x0 ': xs) = alias ::: x1 ': xs
    AlterIfExists alias x1 (x0 ': xs) = x0 ': AlterIfExists alias x1 xs
  
  -- | @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 '[] = TypeError
      ('Text "Rename: alias "
      ':<>: 'ShowType alias0
      ':<>: 'Text " does not exist" )
    Rename alias0 alias1 ((alias0 ::: x0) ': xs) = (alias1 ::: x0) ': xs
    Rename alias0 alias1 (x ': xs) = x ': Rename alias0 alias1 xs
  
  -- | Similar to `Rename` but no error on non-existence
  type family RenameIfExists alias0 alias1 xs where
    RenameIfExists alias x '[] = '[]
    RenameIfExists alias0 alias1 ((alias0 ::: x0) ': xs) = (alias1 ::: x0) ': xs
    RenameIfExists alias0 alias1 (x ': xs) = x ': RenameIfExists alias0 alias1 xs
  
  -- | Move an object from one schema to another
  type family SetSchema sch0 sch1 schema0 schema1 obj srt ty db where
    SetSchema sch0 sch1 schema0 schema1 obj srt ty db = Alter sch1
      (Create obj (srt ty) schema1)
      (Alter sch0 (DropSchemum obj srt schema0) db)
  
  {- | `SubDB` checks that one `SchemasType` is a sublist of another,
  with the same ordering.
  
  >>> :kind! SubDB '["a" ::: '["b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4]]
  SubDB '["a" ::: '["b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4]] :: Bool
  = 'True
  -}
  type family SubDB (db0 :: SchemasType) (db1 :: SchemasType) :: Bool where
    SubDB '[] db1 = 'True
    SubDB (sch ': db0) '[] = 'False
    SubDB (sch ::: schema0 ': db0) (sch ::: schema1 ': db1) =
      If (SubList schema0 schema1)
        (SubDB db0 db1)
        (SubDB (sch ::: schema0 ': db0) db1)
    SubDB db0 (sch1 ': db1) = SubDB db0 db1
  
  {- | `SubsetDB` checks that one `SchemasType` is a subset of another,
  regardless of ordering.
  
  >>> :kind! SubsetDB '["a" ::: '["d" ::: 'Typedef 'PGint2, "b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4, "d" ::: 'Typedef 'PGint2]]
  SubsetDB '["a" ::: '["d" ::: 'Typedef 'PGint2, "b" ::: 'View '[]]] '["a" ::: '["b" ::: 'View '[], "c" ::: 'Typedef 'PGint4, "d" ::: 'Typedef 'PGint2]] :: Bool
  = 'True
  -}
  type family SubsetDB (db0 :: SchemasType) (db1 :: SchemasType) :: Bool where
    SubsetDB '[] db1 = 'True
    SubsetDB (sch ': db0) db1 = ElemDB sch db1 && SubsetDB db0 db1
  
  {- | `ElemDB` checks that a schema may be found as a subset of another in a database,
  regardless of ordering.
  -}
  type family ElemDB (sch :: (Symbol, SchemaType)) (db :: SchemasType) :: Bool where
    ElemDB sch '[] = 'False
    ElemDB (sch ::: schema0) (sch ::: schema1 ': _) = SubsetList schema0 schema1
    ElemDB sch (_ ': schs) = ElemDB sch schs
  
  -- | Check if a `TableConstraint` involves a column
  type family ConstraintInvolves column constraint where
    ConstraintInvolves column ('Check columns) = column `Elem` columns
    ConstraintInvolves column ('Unique columns) = column `Elem` columns
    ConstraintInvolves column ('PrimaryKey columns) = column `Elem` columns
    ConstraintInvolves column ('ForeignKey columns sch tab refcolumns)
      = column `Elem` columns
  
  -- | Drop all `TableConstraint`s that involve a column
  type family DropIfConstraintsInvolve column constraints where
    DropIfConstraintsInvolve column '[] = '[]
    DropIfConstraintsInvolve column (alias ::: constraint ': constraints)
      = If (ConstraintInvolves column constraint)
          (DropIfConstraintsInvolve column constraints)
          (alias ::: constraint ': DropIfConstraintsInvolve column constraints)
  
  -- | A `SchemumType` is a user-created type, like a `Table`,
  -- `View` or `Typedef`.
  data SchemumType
    = Table TableType
    | View RowType
    | Typedef PGType
    | Index IndexType
    | Function FunctionType
    | Procedure [NullType]
    | UnsafeSchemum Symbol
  
  {- | Use `:=>` to pair the parameter types with the return
  type of a function.
  
  >>> :{
  type family Fn :: FunctionType where
    Fn = '[ 'NotNull 'PGint4] :=> 'Returns ('NotNull 'PGint4)
  :}
  -}
  type FunctionType = ([NullType], ReturnsType)
  
  {- |
  PostgreSQL provides several index types:
  B-tree, Hash, GiST, SP-GiST, GIN and BRIN.
  Each index type uses a different algorithm
  that is best suited to different types of queries.
  -}
  data IndexType
    = Btree
    -- ^ B-trees can handle equality and range queries on data
    -- that can be sorted into some ordering.
    | Hash
    -- ^ Hash indexes can only handle simple equality comparisons.
    | Gist
    -- ^ GiST indexes are not a single kind of index,
    -- but rather an infrastructure within which many different
    -- indexing strategies can be implemented.
    | Spgist
    -- ^ SP-GiST indexes, like GiST indexes,
    -- offer an infrastructure that supports various kinds of searches.
    | Gin
    -- ^ GIN indexes are “inverted indexes” which are appropriate for
    -- data values that contain multiple component values, such as arrays.
    | Brin
    -- ^ BRIN indexes (a shorthand for Block Range INdexes) store summaries
    -- about the values stored in consecutive physical block ranges of a table.
  
  {- | Return type of a function-}
  data ReturnsType
    = Returns NullType -- ^ function
    | ReturnsTable RowType -- ^ set returning function
  
  {- | A schema of a database consists of a list of aliased,
  user-defined `SchemumType`s.
  
  >>> :{
  type family Schema :: SchemaType where
    Schema =
      '[ "users" ::: 'Table (
          '[ "pk_users" ::: 'PrimaryKey '["id"] ] :=>
          '[ "id"   :::   'Def :=> 'NotNull 'PGint4
          , "name" ::: 'NoDef :=> 'NotNull 'PGtext
          ])
      , "emails" ::: 'Table (
          '[ "pk_emails"  ::: 'PrimaryKey '["id"]
          , "fk_user_id" ::: 'ForeignKey '["user_id"] "public" "users" '["id"]
          ] :=>
          '[ "id"      :::   'Def :=> 'NotNull 'PGint4
          , "user_id" ::: 'NoDef :=> 'NotNull 'PGint4
          , "email"   ::: 'NoDef :=>    'Null 'PGtext
          ])
      ]
  :}
  -}
  type SchemaType = [(Symbol,SchemumType)]
  
  -- | A @PartitionedSchema@ is a @SchemaType@ where each constructor of @SchemumType@ has
  -- been separated into its own list
  data PartitionedSchema = PartitionedSchema
    { _tables     :: [(Symbol, TableType)]
    , _views      :: [(Symbol, RowType)]
    , _types      :: [(Symbol, PGType)]
    , _indexes    :: [(Symbol, IndexType)]
    , _functions  :: [(Symbol, FunctionType)]
    , _procedures :: [(Symbol, [NullType])]
    , _unsafes    :: [(Symbol, Symbol)]
    }
  
  -- | @PartitionSchema@ partitions a @SchemaType@ into a @PartitionedSchema@
  type PartitionSchema schema = PartitionSchema' schema ('PartitionedSchema '[] '[] '[] '[] '[] '[] '[])
  
  -- | Utility type family for `PartitionSchema`.
  type family PartitionSchema' (remaining :: SchemaType) (acc :: PartitionedSchema) :: PartitionedSchema where
    PartitionSchema' '[] ps = ps
    PartitionSchema' ('(s, 'Table table) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema ('(s, table) ': tables) views types indexes functions procedures unsafe)
    PartitionSchema' ('(s, 'View view) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables ('(s, view) ': views) types indexes functions procedures unsafe)
    PartitionSchema' ('(s, 'Typedef typ) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables views ('(s, typ) ': types) indexes functions procedures unsafe)
    PartitionSchema' ('(s, 'Index ix) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables views types ('(s, ix) ': indexes) functions procedures unsafe)
    PartitionSchema' ('(s, 'Function f) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables views types indexes ('(s, f) ': functions) procedures unsafe)
    PartitionSchema' ('(s, 'Procedure p) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables views types indexes functions ('(s, p) ': procedures) unsafe)
    PartitionSchema' ('(s, 'UnsafeSchemum u) ': rest) ('PartitionedSchema tables views types indexes functions procedures unsafe)
      = PartitionSchema' rest ('PartitionedSchema tables views types indexes functions procedures ('(s, u) ': unsafe))
  
  -- | Get the tables from a @PartitionedSchema@
  type family SchemaTables (schema :: PartitionedSchema) :: [(Symbol, TableType)] where
    SchemaTables ('PartitionedSchema tables _ _ _ _ _ _) = tables
  -- | Get the views from a @PartitionedSchema@
  type family SchemaViews (schema :: PartitionedSchema) :: [(Symbol, RowType)] where
    SchemaViews ('PartitionedSchema _ views _ _ _ _ _) = views
  -- | Get the typedefs from a @PartitionedSchema@
  type family SchemaTypes (schema :: PartitionedSchema) :: [(Symbol, PGType)] where
    SchemaTypes ('PartitionedSchema _ _ types _ _ _ _) = types
  -- | Get the indexes from a @PartitionedSchema@
  type family SchemaIndexes (schema :: PartitionedSchema) :: [(Symbol, IndexType)] where
    SchemaIndexes ('PartitionedSchema _ _ _ indexes _ _ _) = indexes
  -- | Get the functions from a @PartitionedSchema@
  type family SchemaFunctions (schema :: PartitionedSchema) :: [(Symbol, FunctionType)] where
    SchemaFunctions ('PartitionedSchema _ _ _ _ functions _ _) = functions
  -- | Get the procedured from a @PartitionedSchema@
  type family SchemaProcedures (schema :: PartitionedSchema) :: [(Symbol, [NullType])] where
    SchemaProcedures ('PartitionedSchema _ _ _ _ _ procedures _) = procedures
  -- | Get the unsafe schema types from a @PartitionedSchema@
  type family SchemaUnsafes (schema :: PartitionedSchema) :: [(Symbol, Symbol)] where
    SchemaUnsafes ('PartitionedSchema _ _ _ _ _ _ unsafes) = unsafes
  
  -- | @PrettyPrintPartitionedSchema@ makes a nice @ErrorMessage@ showing a @PartitionedSchema@,
  -- only including the names of the things in it and not the values. Additionally, empty
  -- fields are omitted
  type family PrettyPrintPartitionedSchema (schema :: PartitionedSchema) :: ErrorMessage where
    PrettyPrintPartitionedSchema schema = IntersperseNewlines (FilterNonEmpty
      [ FieldIfNonEmpty "Tables"              (SchemaTables schema)
      , FieldIfNonEmpty "Views"               (SchemaViews schema)
      , FieldIfNonEmpty "Types"               (SchemaTypes schema)
      , FieldIfNonEmpty "Indexes"             (SchemaIndexes schema)
      , FieldIfNonEmpty "Functions"           (SchemaFunctions schema)
      , FieldIfNonEmpty "Procedures"          (SchemaProcedures schema)
      , FieldIfNonEmpty "Unsafe schema items" (SchemaUnsafes schema)
      ])
  
  -- | Print field name (if corresponding values are non-empty).
  type family FieldIfNonEmpty (fieldName :: Symbol) (value :: [(Symbol, k)]) :: ErrorMessage where
    FieldIfNonEmpty _ '[] = 'Text ""
    FieldIfNonEmpty n xs = 'Text "  " ':<>: 'Text n ':<>: 'Text ":" ':$$: 'Text "    " ':<>: 'ShowType (Sort (MapFst xs))
  
  -- | Filter out empty error messages.
  type family FilterNonEmpty (ls :: [ErrorMessage]) :: [ErrorMessage] where
    FilterNonEmpty ('Text "" ': rest) = FilterNonEmpty rest
    FilterNonEmpty (x ': rest) = x ': FilterNonEmpty rest
    FilterNonEmpty '[] = '[]
  
  -- | Vertically concatenate error messages.
  type family IntersperseNewlines (ls :: [ErrorMessage]) :: ErrorMessage where
    IntersperseNewlines (x ': y ': '[]) = x ':$$: y
    IntersperseNewlines (x ': xs) = x ':$$: IntersperseNewlines xs
    IntersperseNewlines '[] = 'Text ""
  
  type instance PrettyPrintHaystack (haystack :: SchemaType) =
    'PrettyPrintInfo ('Text "table, view, typedef, index, function, or procedure (SchemumType)") ('Text "schema (SchemaType)")
    ( PrettyPrintPartitionedSchema (PartitionSchema haystack)
    )
  
  {- |
  A database contains one or more named schemas, which in turn contain tables.
  The same object name can be used in different schemas without conflict;
  for example, both schema1 and myschema can contain tables named mytable.
  Unlike databases, schemas are not rigidly separated:
  a user can access objects in any of the schemas in the database they are connected to,
  if they have privileges to do so.
  
  There are several reasons why one might want to use schemas:
  
    * To allow many users to use one database without interfering with each other.
    * To organize database objects into logical groups to make them more manageable.
    * Third-party applications can be put into separate schemas
    so they do not collide with the names of other objects.
  -}
  type SchemasType = [(Symbol,SchemaType)]
  
  type instance PrettyPrintHaystack (haystack :: SchemasType) =
    'PrettyPrintInfo ('Text "schema (SchemaType)") ('Text "database (SchemasType)") ('Text "  " ':<>: 'ShowType (Sort (MapFst haystack)))
  
  -- | A type family to use for a single schema database.
  type family Public (schema :: SchemaType) :: SchemasType
    where Public schema = '["public" ::: schema]
  
  -- | `IsPGlabel` looks very much like the `IsLabel` class. Whereas
  -- the overloaded label, `fromLabel` is used for column references,
  -- `label`s are used for enum terms. A `label` is called with
  -- type application like `label` @"beef".
  class IsPGlabel (label :: Symbol) expr where label :: expr
  instance label ~ label1
    => IsPGlabel label (PGlabel label1) where label = PGlabel
  instance labels ~ '[label]
    => IsPGlabel label (NP PGlabel labels) where label = PGlabel :* Nil
  instance IsPGlabel label (y -> K y label) where label = K
  instance IsPGlabel label (y -> NP (K y) '[label]) where label y = K y :* Nil
  instance {-# OVERLAPPING #-}
    IsPGlabel label0 (NS PGlabel (label0 ': labels)) where
      label = Z PGlabel
  instance {-# OVERLAPPABLE #-} IsPGlabel label0 (NS PGlabel labels)
    => IsPGlabel label0 (NS PGlabel (label1 ': labels)) where
      label = S (label @label0)
  -- | A `PGlabel` unit type with an `IsPGlabel` instance
  data PGlabel (label :: Symbol) = PGlabel
  instance KnownSymbol label => RenderSQL (PGlabel label) where
    renderSQL _ = "\'" <> renderSymbol @label <> "\'"
  instance All KnownSymbol labels => RenderSQL (NP PGlabel labels) where
    renderSQL
      = commaSeparated
      . hcollapse
      . hcmap (Proxy @KnownSymbol) (K . renderSQL)
  
  -- | Is a type a valid JSON key?
  type PGJsonKey = '[ 'PGint2, 'PGint4, 'PGtext ]
  
  -- | Is a type a valid JSON type?
  type PGJsonType = '[ 'PGjson, 'PGjsonb ]
  
  -- | Utility class for `AllUnique` to provide nicer error messages.
  class IsNotElem x isElem where
  instance IsNotElem x 'False where
  instance (TypeError (      'Text "Cannot assign to "
                        ':<>: 'ShowType alias
                        ':<>: 'Text " more than once"))
      => IsNotElem '(alias, a) 'True where
  
  -- | No elem of @xs@ appears more than once, in the context of assignment.
  class AllUnique (xs :: [(Symbol, a)]) where
  instance AllUnique '[] where
  instance (IsNotElem x (Elem x xs), AllUnique xs) => AllUnique (x ': xs) where
  
  -- | Updatable lists of columns
  type Updatable table columns =
    ( All (HasIn (TableToColumns table)) columns
    , AllUnique columns
    , SListI (TableToColumns table) )
  
  {- | Filters a schema down to labels of all enum typedefs.
  -}
  type family SchemaEnums schema where
    SchemaEnums '[] = '[]
    SchemaEnums (enum ::: 'Typedef ('PGenum labels) ': schema) =
      enum ::: labels ': SchemaEnums schema
    SchemaEnums (_ ': schema) = SchemaEnums schema
  
  {- | Filters schemas down to labels of all enum typedefs.
  -}
  type family DbEnums db where
    DbEnums '[] = '[]
    DbEnums (sch ::: schema ': schemas) =
      sch ::: SchemaEnums schema ': DbEnums schemas
  
  {- | Filters a schema down to rows of relations;
  all composites, tables and views.
  -}
  type family SchemaRelations schema where
    SchemaRelations '[] = '[]
    SchemaRelations (ty ::: 'Typedef ('PGcomposite row) ': schema) =
      ty ::: row ': SchemaRelations schema
    SchemaRelations (tab ::: 'Table table ': schema) =
      tab ::: TableToRow table ': SchemaRelations schema
    SchemaRelations (vw ::: 'View row ': schema) =
      vw ::: row ': SchemaRelations schema
    SchemaRelations (_ ': schema) = SchemaRelations schema
  
  {- | Filters schemas down to rows of relations;
  all composites, tables and views.
  -}
  type family DbRelations db where
    DbRelations '[] = '[]
    DbRelations (sch ::: schema ': schemas) =
      sch ::: SchemaRelations schema ': DbRelations schemas
  
  -- | Used in `FindQualified`
  type family FindName xs x where
    FindName '[] xs = 'Nothing
    FindName ( '(name, x) ': _) x = 'Just name
    FindName (_ ': xs) x = FindName xs x
  
  -- | Used in `FindQualified`
  type family FindNamespace err nsp name xss x where
    FindNamespace err _ 'Nothing xss x = FindQualified err xss x
    FindNamespace _ nsp ('Just name) _ _ = '(nsp, name)
  
  {- | Find fully qualified name with a type error if lookup fails.
  This is used to find the qualified name of a user defined type.
  
  >>> :kind! FindQualified "my error message:"
  FindQualified "my error message:" :: [(k1, [(k2, k3)])]
                                       -> k3 -> (k1, k2)
  = FindQualified "my error message:"
  
  >>> :kind! FindQualified "couldn't find type:" '[ "foo" ::: '["bar" ::: Double]] Double
  FindQualified "couldn't find type:" '[ "foo" ::: '["bar" ::: Double]] Double :: (Symbol,
                                                                                   Symbol)
  = '("foo", "bar")
  
  >>> :kind! FindQualified "couldn't find type:" '[ "foo" ::: '["bar" ::: Double]] Bool
  FindQualified "couldn't find type:" '[ "foo" ::: '["bar" ::: Double]] Bool :: (Symbol,
                                                                                 Symbol)
  = (TypeError ...)
  -}
  type family FindQualified err xss x where
    FindQualified err '[] x = TypeError
      ('Text err ':$$: 'ShowType x)
    FindQualified err ( '(nsp, xs) ': xss) x =
      FindNamespace err nsp (FindName xs x) xss x
  