squeal-postgresql-0.1.0.0: src/Squeal/PostgreSQL/Schema.hs
{-|
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