diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml new file mode 100644 index 00000000..fdb026c8 --- /dev/null +++ b/.stylish-haskell.yaml @@ -0,0 +1,254 @@ +# stylish-haskell configuration file +# ================================== + +# The stylish-haskell tool is mainly configured by specifying steps. These steps +# are a list, so they have an order, and one specific step may appear more than +# once (if needed). Each file is processed by these steps in the given order. +steps: + # Convert some ASCII sequences to their Unicode equivalents. This is disabled + # by default. + # - unicode_syntax: + # # In order to make this work, we also need to insert the UnicodeSyntax + # # language pragma. If this flag is set to true, we insert it when it's + # # not already present. You may want to disable it if you configure + # # language extensions using some other method than pragmas. Default: + # # true. + # add_language_pragma: true + + # Align the right hand side of some elements. This is quite conservative + # and only applies to statements where each element occupies a single + # line. + # - simple_align: + # cases: true + # top_level_patterns: true + # records: true + + # Import cleanup + - imports: + # There are different ways we can align names and lists. + # + # - global: Align the import names and import list throughout the entire + # file. + # + # - file: Like global, but don't add padding when there are no qualified + # imports in the file. + # + # - group: Only align the imports per group (a group is formed by adjacent + # import lines). + # + # - none: Do not perform any alignment. + # + # Default: global. + align: file + + # The following options affect only import list alignment. + # + # List align has following options: + # + # - after_alias: Import list is aligned with end of import including + # 'as' and 'hiding' keywords. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_alias: Import list is aligned with start of alias or hiding. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - new_line: Import list starts always on new line. + # + # > import qualified Data.List as List + # > (concat, foldl, foldr, head, init, last, length) + # + # Default: after_alias + list_align: after_alias + + # Right-pad the module names to align imports in a group: + # + # - true: a little more readable + # + # > import qualified Data.List as List (concat, foldl, foldr, + # > init, last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # - false: diff-safe + # + # > import qualified Data.List as List (concat, foldl, foldr, init, + # > last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # Default: true + pad_module_names: false + + # Long list align style takes effect when import is too long. This is + # determined by 'columns' setting. + # + # - inline: This option will put as much specs on same line as possible. + # + # - new_line: Import list will start on new line. + # + # - new_line_multiline: Import list will start on new line when it's + # short enough to fit to single line. Otherwise it'll be multiline. + # + # - multiline: One line per import list entry. + # Type with constructor list acts like single import. + # + # > import qualified Data.Map as M + # > ( empty + # > , singleton + # > , ... + # > , delete + # > ) + # + # Default: inline + long_list_align: inline + + # Align empty list (importing instances) + # + # Empty list align has following options + # + # - inherit: inherit list_align setting + # + # - right_after: () is right after the module name: + # + # > import Vector.Instances () + # + # Default: inherit + empty_list_align: inherit + + # List padding determines indentation of import list on lines after import. + # This option affects 'long_list_align'. + # + # - : constant value + # + # - module_name: align under start of module name. + # Useful for 'file' and 'group' align settings. + list_padding: 4 + + # Separate lists option affects formatting of import list for type + # or class. The only difference is single space between type and list + # of constructors, selectors and class functions. + # + # - true: There is single space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable (fold, foldl, foldMap)) + # + # - false: There is no space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable(fold, foldl, foldMap)) + # + # Default: true + separate_lists: true + + # Space surround option affects formatting of import lists on a single + # line. The only difference is single space after the initial + # parenthesis and a single space before the terminal parenthesis. + # + # - true: There is single space associated with the enclosing + # parenthesis. + # + # > import Data.Foo ( foo ) + # + # - false: There is no space associated with the enclosing parenthesis + # + # > import Data.Foo (foo) + # + # Default: false + space_surround: false + + # Language pragmas + - language_pragmas: + # We can generate different styles of language pragma lists. + # + # - vertical: Vertical-spaced language pragmas, one per line. + # + # - compact: A more compact style. + # + # - compact_line: Similar to compact, but wrap each line with + # `{-#LANGUAGE #-}'. + # + # Default: vertical. + style: vertical + + # Align affects alignment of closing pragma brackets. + # + # - true: Brackets are aligned in same column. + # + # - false: Brackets are not aligned together. There is only one space + # between actual import and closing bracket. + # + # Default: true + align: false + + # stylish-haskell can detect redundancy of some language pragmas. If this + # is set to true, it will remove those redundant pragmas. Default: true. + remove_redundant: true + + # Replace tabs by spaces. This is disabled by default. + # - tabs: + # # Number of spaces to use for each tab. Default: 8, as specified by the + # # Haskell report. + # spaces: 8 + + # Remove trailing whitespace + - trailing_whitespace: {} + + # Squash multiple spaces between the left and right hand sides of some + # elements into single spaces. Basically, this undoes the effect of + # simple_align but is a bit less conservative. + # - squash: {} + +# A common setting is the number of columns (parts of) code will be wrapped +# to. Different steps take this into account. Default: 80. +columns: 80 + +# By default, line endings are converted according to the OS. You can override +# preferred format here. +# +# - native: Native newline format. CRLF on Windows, LF on other OSes. +# +# - lf: Convert to LF ("\n"). +# +# - crlf: Convert to CRLF ("\r\n"). +# +# Default: native. +newline: native + +# Sometimes, language extensions are specified in a cabal file or from the +# command line instead of using language pragmas in the file. stylish-haskell +# needs to be aware of these, so it can parse the file correctly. +language_extensions: + - BangPatterns + - ConstraintKinds + - CPP + - DataKinds + - DefaultSignatures + - DeriveDataTypeable + - DeriveGeneric + - ExplicitNamespaces + - FlexibleContexts + - FlexibleInstances + - FunctionalDependencies + - GADTs + - GeneralizedNewtypeDeriving + - InstanceSigs + - LambdaCase + - MultiParamTypeClasses + - MultiWayIf + - NamedFieldPuns + - NoImplicitPrelude + - OverloadedStrings + - PolyKinds + - RecordWildCards + - ScopedTypeVariables + - StandaloneDeriving + - TemplateHaskell + - TupleSections + - TypeApplications + - TypeFamilies + - ViewPatterns diff --git a/beam-core/Database/Beam/Query/Combinators.hs b/beam-core/Database/Beam/Query/Combinators.hs index c4be1e84..8cf93266 100644 --- a/beam-core/Database/Beam/Query/Combinators.hs +++ b/beam-core/Database/Beam/Query/Combinators.hs @@ -1,6 +1,5 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE CPP #-} module Database.Beam.Query.Combinators ( -- * Various SQL functions and constructs @@ -65,22 +64,21 @@ module Database.Beam.Query.Combinators , orderBy_, asc_, desc_, nullsFirst_, nullsLast_ ) where -import Database.Beam.Backend.Types import Database.Beam.Backend.SQL +import Database.Beam.Backend.Types import Database.Beam.Query.Internal -import Database.Beam.Query.Ord import Database.Beam.Query.Operator +import Database.Beam.Query.Ord import Database.Beam.Query.Types import Database.Beam.Schema.Tables -import Control.Monad.Identity -import Control.Monad.Free import Control.Applicative +import Control.Monad.Free +import Control.Monad.Identity #if !MIN_VERSION_base(4, 11, 0) -import Control.Monad.Writer hiding ((<>)) import Data.Semigroup #endif diff --git a/beam-core/Database/Beam/Query/SQL92.hs b/beam-core/Database/Beam/Query/SQL92.hs index 52f85398..203d854e 100644 --- a/beam-core/Database/Beam/Query/SQL92.hs +++ b/beam-core/Database/Beam/Query/SQL92.hs @@ -1,24 +1,23 @@ {-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-unused-binds #-} -{-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE UndecidableInstances #-} module Database.Beam.Query.SQL92 ( buildSql92Query' ) where -import Database.Beam.Query.Internal import Database.Beam.Backend.SQL +import Database.Beam.Query.Internal -import Control.Monad.Free.Church import Control.Monad.Free +import Control.Monad.Free.Church #if !MIN_VERSION_base(4, 11, 0) -import Control.Monad.Writer hiding ((<>)) import Data.Semigroup #endif import Data.Maybe -import Data.Proxy (Proxy(Proxy)) +import Data.Proxy (Proxy (Proxy)) import Data.String import qualified Data.Text as T diff --git a/beam-core/Database/Beam/Schema.hs b/beam-core/Database/Beam/Schema.hs index 155187b2..8ddeda64 100644 --- a/beam-core/Database/Beam/Schema.hs +++ b/beam-core/Database/Beam/Schema.hs @@ -38,10 +38,11 @@ module Database.Beam.Schema -- * Types for lens generation , Lenses, LensFor(..) + -- TODO: does it worth reexporting index stuff here? , module Database.Beam.Schema.Lenses ) where -import Database.Beam.Schema.Tables import Database.Beam.Schema.Lenses +import Database.Beam.Schema.Tables -- $db-construction -- Types and functions to express database types and auto-generate name mappings diff --git a/beam-core/Database/Beam/Schema/Indices.hs b/beam-core/Database/Beam/Schema/Indices.hs new file mode 100644 index 00000000..373a163f --- /dev/null +++ b/beam-core/Database/Beam/Schema/Indices.hs @@ -0,0 +1,360 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | This module provides support for table indices definition, both +-- manual and automatic (see 'dbIndices' and 'addDefaultDbIndices' functions). +module Database.Beam.Schema.Indices + ( TableIndex (..) + , Index (..) + , IndexOptions (..) + , indexOptions + + , FieldIndexBuilder + , IndexBuilder (..) + , EntityIndices (..) + , DatabaseIndices + , (:->) + , IndexFromReference (..) + + , indexOptionsEnglishDescription + , mkIndexName + , buildDbIndices + , tableIndex + , dbIndices + , mergeDbIndices + , defaultTableIndices + , defaultDbIndices + , addDefaultDbIndices + ) where + +import Control.Monad.Writer.Strict (runWriter, tell) + +import Data.Aeson +import Data.DList (DList) +import qualified Data.DList as DL +import Data.Functor.Identity +import Data.Hashable (Hashable (..)) +import Data.List.NonEmpty (NonEmpty (..)) +import Data.Proxy +import Data.Text (Text) +import qualified Data.Text as T +#if ! MIN_VERSION_base(4,11,0) +import Data.Semigroup +#endif + +import GHC.Exts (Constraint, fromList) +import GHC.Generics hiding (C, R) +import GHC.TypeLits + +import Database.Beam.Schema.Tables + +-- Some day it should have more options and allow to modify them depending on the backend. +-- | Index options. +data IndexOptions = IndexOptions + { indexUnique :: Bool + } deriving (Show, Eq, Ord, Generic) + +instance Hashable IndexOptions + +instance ToJSON IndexOptions where + toJSON (IndexOptions unique) = + object [ "unique" .= unique ] +instance FromJSON IndexOptions where + parseJSON = withObject "IndexOptions" $ \o -> + IndexOptions <$> o .: "unique" + +-- | Default options. +-- For now, they just describe a non-@UNIQUE@ index. +indexOptions :: IndexOptions +indexOptions = IndexOptions + { indexUnique = False + } + +indexOptionsEnglishDescription :: IndexOptions -> String +indexOptionsEnglishDescription (IndexOptions uniq) = + (if uniq then "unique " else " ") + +-- | Single index settings for some table. +data TableIndex = TableIndex (NonEmpty Text) IndexOptions + deriving (Show, Eq, Ord) + +-- | Single index settings. +data Index = Index !Text !TableIndex + deriving (Show, Eq, Ord) + +-- | Make a name for an index. +mkIndexName :: Text -> [Text] -> Text +mkIndexName tblNm fields = + "idx_" <> tblNm <> "_" <> T.intercalate "_" fields + +-- | Indices for an entity (table). +-- +-- Usually, you use the 'dbIndices' function to generate an empty set of indices +-- and modify its fields with 'mconcat' of several 'tableIndex'es, later wrapping +-- it with 'addDefaultDbIndices'. +newtype EntityIndices be db entity = EntityIndices + { _entityIndices :: DList (DatabaseEntity be db entity -> Index) + -- ^ Multiple indices info assuming the database settings is given. + -- We have to accept database settings here rather than take them at index building + -- stage because migrations require that. + } deriving (Semigroup, Monoid) + +-- | When parameterized by this entity tag, a database type will hold +-- a schema of indices, i.e. a way to build material indices when corresponding database +-- settings are provided. Under the hood, each entity type is transformed into its +-- 'EntityIndices' type. For tables, this includes, accordingly, indices for this table; all other types of entities are assumed to be empty. +type DatabaseIndices be db = db (EntityIndices be db) + +-- | Construct material indices from the given database schema and indices schema. +buildDbIndices + :: forall be db. + Database be db + => DatabaseSettings be db -> DatabaseIndices be db -> [Index] +buildDbIndices dbSettings dbIdxs = + let (_ :: DatabaseSettings be db, indices) = + runWriter $ zipTables (Proxy @be) + (\dbEntity (EntityIndices mkIndices) -> do + tell [ mkIndex dbEntity | mkIndex <- DL.toList mkIndices ] + return dbEntity) + dbSettings dbIdxs + in indices + +-- | Return empty 'DatabaseIndices'. You can use it like +-- +-- > dbIndices{ tbl1 = tableIndex field1 indexOptions{ uniqueIndex = True } <> +-- > tableIndex (field2, field3) indexOptions } +dbIndices :: forall be db. Database be db => DatabaseIndices be db +dbIndices = runIdentity $ zipTables (Proxy @be) (\_ _ -> pure mempty) undefined undefined + +-- This function can't be used as 'Semigroup.<>', because 'DatabaseIndices' is not +-- a normal type we can infer an instance for. +-- | Combine two indices settings. +mergeDbIndices + :: forall be db. + Database be db + => DatabaseIndices be db -> DatabaseIndices be db -> DatabaseIndices be db +mergeDbIndices i1 i2 = + runIdentity $ zipTables (Proxy @be) (\ei1 ei2 -> pure (ei1 <> ei2)) i1 i2 + + +-- * Manual indices definition + +-- | Helper for 'IsNotEmptyData'. +type family GIsNotEmptyData (item :: Symbol) (rep :: * -> *) :: Constraint where + GIsNotEmptyData item (D1 _d (C1 _c U1)) = + TypeError ('Text item ':<>: 'Text " without fields is not allowed here") + GIsNotEmptyData _ _ = () + +-- | Ensures a datatype has at least one field. +type IsNotEmptyData item x = (Generic x, GIsNotEmptyData item (Rep x)) + +-- | Gathers index fields from the given field of a table. +class FieldIndexBuilder field where + buildFieldIndex :: field -> NonEmpty Text + +instance FieldIndexBuilder (TableField table a) where + buildFieldIndex field = (:| []) $ _fieldName field + +instance (Beamable (PrimaryKey table), + IsNotEmptyData "Primary key" (PrimaryKey table Identity)) => + FieldIndexBuilder (PrimaryKey table (TableField table')) where + buildFieldIndex = + fromList . allBeamValues (\(Columnar' (TableField _ fieldNm)) -> fieldNm) + +instance (Beamable (PrimaryKey table), + IsNotEmptyData "Primary key" (PrimaryKey table Identity)) => + FieldIndexBuilder (PrimaryKey table (Nullable (TableField table'))) where + buildFieldIndex = + fromList . allBeamValues (\(Columnar' (TableField _ fieldNm)) -> fieldNm) + +-- | Gathers index fields from a user-supplied pack of table fields. +class IndexBuilder table a where + buildIndex :: TableSettings table -> a -> NonEmpty Text + +-- | Field accessors are building blocks for indices. +instance (f ~ TableField table, table ~ table', FieldIndexBuilder field) => + IndexBuilder table' (table f -> field) where + buildIndex settings getter = + buildFieldIndex $ getter settings + +instance (IndexBuilder table a, IndexBuilder table b) => + IndexBuilder table (a, b) where + buildIndex settings (a, b) = buildIndex settings a <> buildIndex settings b + +instance (IndexBuilder table a, IndexBuilder table b, IndexBuilder table c) => + IndexBuilder table (a, b, c) where + buildIndex settings (a, b, c) = + buildIndex settings a <> buildIndex settings b <> buildIndex settings c + +-- | Make a table index builder covering the specified fields. +-- Basic usage is to pass a table field accessor or a tuple of them to this function. +-- Currently, no more than 3 elements in a tuple are supported, but feel free to nest +-- tuples. +-- Order of fields is preserved: tuples expand straightforwardly, while primary keys +-- expand to a list of fields in the same order as they are mentioned in the +-- corresponding constructor. +tableIndex + :: IndexBuilder table a + => a + -> IndexOptions + -> EntityIndices be db (TableEntity table) +tableIndex builder idxOpts = + EntityIndices . DL.singleton $ + \(DatabaseEntity (DatabaseTable _ _ tblNm tblSettings)) -> + Index tblNm $ TableIndex (buildIndex tblSettings builder) idxOpts + +-- | For the given part of table @tblp@, indices derived from it. +type TableIndicesBuilder tblp = DList (tblp -> TableIndex) + +-- | Helper for GAutoTableIndices +contramapTableIndicesBuilder :: (b -> a) -> TableIndicesBuilder a -> TableIndicesBuilder b +contramapTableIndicesBuilder f = fmap (. f) + +-- * Automatic indices definition + +-- | Indicates a reference from table @tbl@ to table @tbl'@. +data (tbl :: (* -> *) -> *) :-> (tbl' :: (* -> *) -> *) + +-- | Provide options for an automatically created index, which is caused by a primary key +-- of one table being embedded into another. +-- This typeclass is only needed to be defined for inter-table references (see ':->'). +class IndexFromReference reference where + referenceIndexOptions :: Proxy reference -> IndexOptions + referenceIndexOptions _ = indexOptions + +{- @martoon TODO: I see several minor (or not) problems with making user define instances of + this typeclass: + + 1. Logic locality - instances can occur to be far away from user's database settings + declaration. + 2. Garbage - user have to care itself about removing redundant instances which no more + in use. + 3. Extensibility - each time we want to add a type-level restriction on a created index + (e.g. between these two tables only "hash" indices are allowed - nonsence, but we might + want something like this one day), then user's code will break on each such addition. + + I'm thinking of an alternative approach when user has to supply some 'HList' stuff, + element in 'HList' would be an 'IndexOption' tagged with @tbl :-> tbl'@. + If the user annotates his 'IndexOption' with the @tbl :-> tbl'@ reference then + he pretty well controlls correspondence between options and indices they relate to. + This would look like the following: + + > defaultDbIndices ( + > (autoOption @(Table1 :-> Table1) indexOptions) :& + > (autoOption @(Table2 :-> Table1) indexOptions .*. + > autoOption @(Table2 :-> Table2) indexOptions { ... }) + > ) + + Although this all is pretty cumbersome and scaring at a first glance, and dependencies on + 'hlist' and 'vinyl' which may be required here are quite heavyweight. +-} + +-- | Generic helper for 'AutoTableIndices'. +class GAutoTableIndices (x :: * -> *) where + -- | Returns list of deferred indices. + -- Exactly this type is required, later in migrations knowing that list size does + -- not depend on names (only on structure of the database) is important. + autoTableIndices' :: DList (x p -> TableIndex) + +instance GAutoTableIndices x => GAutoTableIndices (M1 i f x) where + autoTableIndices' = contramapTableIndicesBuilder unM1 $ autoTableIndices' @x + +instance (GAutoTableIndices x, GAutoTableIndices y) => + GAutoTableIndices (x :*: y) where + autoTableIndices' = + contramapTableIndicesBuilder (\(x :*: _) -> x) (autoTableIndices' @x) <> + contramapTableIndicesBuilder (\(_ :*: y) -> y) (autoTableIndices' @y) + +instance GAutoTableIndices (Rec0 x) where + autoTableIndices' = mempty + +instance {-# OVERLAPPING #-} + (Beamable (PrimaryKey tbl'), IndexFromReference (tbl :-> tbl')) => + GAutoTableIndices (Rec0 (PrimaryKey tbl' (TableField tbl))) where + autoTableIndices' = + if tableValuesNeeded (Proxy @(PrimaryKey tbl')) == 0 + then DL.empty + else DL.singleton $ \(K1 referringField) -> + let pkFields = allBeamValues + (\(Columnar' (TableField _ fieldNm)) -> fieldNm) + referringField + opts = referenceIndexOptions (Proxy @(tbl :-> tbl')) + -- unsafe call, but at this point we know the list is not empty + in TableIndex (fromList pkFields) opts + +instance {-# OVERLAPPING #-} + (Beamable (PrimaryKey tbl'), IndexFromReference (tbl :-> tbl')) => + GAutoTableIndices (Rec0 (PrimaryKey tbl' (Nullable (TableField tbl)))) where + autoTableIndices' = + if tableValuesNeeded (Proxy @(PrimaryKey tbl')) == 0 + then DL.empty + else DL.singleton $ \(K1 referringField) -> + let pkFields = allBeamValues + (\(Columnar' (TableField _ fieldNm)) -> fieldNm) + referringField + opts = referenceIndexOptions (Proxy @(tbl :-> tbl')) + -- unsafe call, but at this point we know the list is not empty + in TableIndex (fromList pkFields) opts + +-- | Traverses fields of the given table and builds indices for all encountered 'PrimaryKey's. +class AutoEntityIndex be db tbl where + autoEntityIndices :: EntityIndices be db tbl + +-- Other types of entities are approaching, and we probably don't want to define +-- instances for all of them. +-- | Traverses the given table and for every field which is some 'PrimaryKey' +-- makes corresponding SQL index, this allows "JOIN"s on this table perform nicely. +instance {-# OVERLAPPABLE #-} + AutoEntityIndex be db entity where + autoEntityIndices = mempty + +instance (Generic (TableSettings tbl), + GAutoTableIndices (Rep (TableSettings tbl))) => + AutoEntityIndex be db (TableEntity tbl) where + autoEntityIndices = + EntityIndices $ flip fmap autoTableIndices' $ + \mkIndex (DatabaseEntity (DatabaseTable _ _ tblNm tblSettings)) -> + Index tblNm (mkIndex (from tblSettings)) + +-- | Automatically creates indices for every 'PrimaryKey' embedded into the given table. +defaultTableIndices + :: (Generic (TableSettings table), + GAutoTableIndices (Rep (TableSettings table))) + => EntityIndices be db (TableEntity table) +defaultTableIndices = autoEntityIndices + +-- | Traverses all tables in database and builds indices for all encountered 'PrimaryKey's. +class GAutoDbIndices (x :: * -> *) where + autoDbIndices' :: x p + +instance GAutoDbIndices x => GAutoDbIndices (M1 i f x) where + autoDbIndices' = M1 autoDbIndices' + +instance (GAutoDbIndices x, GAutoDbIndices y) => + GAutoDbIndices (x :*: y) where + autoDbIndices' = autoDbIndices' :*: autoDbIndices' + +instance AutoEntityIndex be db tbl => + GAutoDbIndices (Rec0 (EntityIndices be db tbl)) where + autoDbIndices' = K1 $ autoEntityIndices @be @db + +-- | Automatically creates indices for every 'PrimaryKey' embedded into a table, for +-- @JOIN@s sake. +-- Tables' primary keys themselves are not included as soon as for each primary key +-- a corresponding index is provided by a database engine. +defaultDbIndices + :: forall be db. + (Database be db, + Generic (DatabaseIndices be db), GAutoDbIndices (Rep (DatabaseIndices be db))) + => DatabaseIndices be db +defaultDbIndices = to autoDbIndices' + +-- | Attaches default indices to the given ones. Usually more convenient than plain +-- 'defaultDbIndices': +-- +-- > ... `withDbIndices` addDefaultDbIndices dbIndices{ table1 = ..., ... } +addDefaultDbIndices + :: (Database be db, + Generic (DatabaseIndices be db), GAutoDbIndices (Rep (DatabaseIndices be db))) + => DatabaseIndices be db -> DatabaseIndices be db +addDefaultDbIndices = mergeDbIndices defaultDbIndices diff --git a/beam-core/beam-core.cabal b/beam-core/beam-core.cabal index 54de152d..d83f8684 100644 --- a/beam-core/beam-core.cabal +++ b/beam-core/beam-core.cabal @@ -34,6 +34,7 @@ library Database.Beam.Schema Database.Beam.Schema.Tables + Database.Beam.Schema.Indices Database.Beam.Backend.Types Database.Beam.Backend.URI @@ -69,7 +70,8 @@ library scientific >=0.3 && <0.4, vector >=0.11 && <0.13, vector-sized >=0.5 && <1.1, - tagged >=0.8 && <0.9 + tagged >=0.8 && <0.9, + vector-sized >=0.5 && <1.2 Default-language: Haskell2010 default-extensions: ScopedTypeVariables, OverloadedStrings, GADTs, RecursiveDo, FlexibleInstances, FlexibleContexts, TypeFamilies, GeneralizedNewtypeDeriving, RankNTypes, TupleSections, ConstraintKinds, StandaloneDeriving, TypeOperators, @@ -89,7 +91,7 @@ test-suite beam-core-tests hs-source-dirs: test main-is: Main.hs other-modules: Database.Beam.Test.Schema Database.Beam.Test.SQL - build-depends: base, beam-core, text, bytestring, time, tasty, tasty-hunit + build-depends: base, beam-core, text, bytestring, microlens, time, tasty, tasty-hunit default-language: Haskell2010 default-extensions: OverloadedStrings, FlexibleInstances, FlexibleContexts, GADTs, TypeFamilies, DeriveGeneric, DefaultSignatures, RankNTypes, StandaloneDeriving, KindSignatures, diff --git a/beam-core/test/Database/Beam/Test/Schema.hs b/beam-core/test/Database/Beam/Test/Schema.hs index 6a5dd786..8491e5be 100644 --- a/beam-core/test/Database/Beam/Test/Schema.hs +++ b/beam-core/test/Database/Beam/Test/Schema.hs @@ -1,5 +1,6 @@ -{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} module Database.Beam.Test.Schema ( EmployeeT(..), DepartmentT(..) @@ -14,17 +15,23 @@ module Database.Beam.Test.Schema , tests ) where import Database.Beam -import Database.Beam.Schema.Tables import Database.Beam.Backend import Database.Beam.Backend.SQL.AST +import Database.Beam.Schema.Indices +import Database.Beam.Schema.Tables -import Data.List.NonEmpty ( NonEmpty((:|)) ) +import Data.List (sort) +import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Monoid import Data.Proxy import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime) +import GHC.Exts (fromList) + +import Lens.Micro + import Test.Tasty import Test.Tasty.HUnit @@ -36,9 +43,11 @@ tests = testGroup "Schema Tests" , parametricAndFixedNestedBeamsAreEquivalent -- , automaticNestedFieldsAreUnset -- , nullableForeignKeysGivenMaybeType - , underscoresAreHandledGracefully ] + , underscoresAreHandledGracefully -- , dbSchemaGeneration ] --- , dbSchemaModification ] +-- , dbSchemaModification + , indicesAreBuiltCorrectly + ] data DummyBackend @@ -241,7 +250,7 @@ data BDepartmentVehiculeT f = BDepartmentVehicule , _bRelatesTo :: VehiculeT f , _bMetaInfo :: VehiculeInformationT (Nullable f) } deriving Generic --- +-- -- ["departament__name","relates_to__id","relates_to__type","relates_to__of_wheels","meta_info__price"] @@ -250,8 +259,8 @@ instance (Beamable metaInfo, Beamable prop) => Beamable (DepartamentRelatedT me instance (Table metaInfo, Table prop) => Table (DepartamentRelatedT metaInfo prop) where - data PrimaryKey (DepartamentRelatedT metaInfo prop) f = DepReKeyA (PrimaryKey DepartmentT f) - (PrimaryKey prop f) + data PrimaryKey (DepartamentRelatedT metaInfo prop) f = DepReKeyA (PrimaryKey DepartmentT f) + (PrimaryKey prop f) deriving(Generic) primaryKey = DepReKeyA <$> _aDepartament <*> (primaryKey._aRelatesTo) @@ -349,3 +358,108 @@ employeeDbSettingsRuleMods = defaultDbSettings `withDbModification` -- (EmployeeId (TableField "head__first_name" (DummyField True False (DummyFieldMaybe DummyFieldText))) -- (TableField "head__last_name" (DummyField True False (DummyFieldMaybe DummyFieldText))) -- (TableField "head__created" (DummyField True False (DummyFieldMaybe DummyFieldUTCTime)))) + +-- * Indices are built correctly + +data ColonistT f = Colonist + { _cSpaceId :: C f Int + , _cFullName :: C f Text + , _cFather :: PrimaryKey ColonistT (Nullable f) + , _cOrigin :: PrimaryKey PlanetT f + } deriving (Generic) + +data PlanetT f = Planet + { _pSpaceId :: C f Int + , _pIntergalacticName :: C f Text + , _pDiameter :: C f Int + , _pFounder :: PrimaryKey ColonistT (Nullable f) + } deriving (Generic) + +-- just to check automatic derivation supports various kinds of entity. +data DummyViewT f = DummyView + { _dvSomething :: C f Int + } deriving (Generic) + +data ColonistDb f = ColonistDb + { _colonists :: f (TableEntity ColonistT) + , _planets :: f (TableEntity PlanetT) + , _dummy :: f (ViewEntity DummyViewT) + } deriving (Generic) + +instance Table ColonistT where + newtype PrimaryKey ColonistT f = ColonistId (C f Int) + deriving (Generic) + primaryKey = ColonistId . _cSpaceId + +instance Table PlanetT where + data PrimaryKey PlanetT f = PlanetId (C f Int) (C f Text) + deriving (Generic) + primaryKey = PlanetId <$> _pSpaceId <*> _pIntergalacticName + +instance Beamable ColonistT +instance Beamable (PrimaryKey ColonistT) +instance Beamable PlanetT +instance Beamable (PrimaryKey PlanetT) +instance Beamable DummyViewT + +instance Database be ColonistDb + +instance IndexFromReference (ColonistT :-> ColonistT) +instance IndexFromReference (ColonistT :-> PlanetT) +instance IndexFromReference (PlanetT :-> ColonistT) where + referenceIndexOptions _ = indexOptions{ indexUnique = True } + +colonistsDbSettings :: DatabaseSettings be ColonistDb +colonistsDbSettings = defaultDbSettings + +colonistsTableSchema :: TableSettings ColonistT +colonistsTableName :: Text +(colonistsTableSchema, colonistsTableName) = + let DatabaseEntity (DatabaseTable _ _ tblName tableSettings) = _colonists colonistsDbSettings + in (tableSettings, tblName) + +planetsTableSchema :: TableSettings PlanetT +planetsTableName :: Text +(planetsTableSchema, planetsTableName) = + let DatabaseEntity (DatabaseTable _ _ tblName tableSettings) = _planets colonistsDbSettings + in (tableSettings, tblName) + +indicesAreBuiltCorrectly :: TestTree +indicesAreBuiltCorrectly = + testCase "Indices are built correctly" $ + do let colonistsFieldNames = allBeamValues (\(Columnar' f) -> _fieldName f) colonistsTableSchema + planetsFieldNames = allBeamValues (\(Columnar' f) -> _fieldName f) planetsTableSchema + + extraIndices = buildDbIndices colonistsDbSettings dbIndices + { _colonists = mconcat + [ tableIndex _cOrigin indexOptions + , tableIndex (_cFullName, _cFather) indexOptions + ] + , _planets = + tableIndex _pSpaceId indexOptions{ indexUnique = True } + } + + autoIndices = buildDbIndices colonistsDbSettings defaultDbIndices + + extraIndices @?= [ Index colonistsTableName $ + TableIndex (fromList $ colonistsFieldNames ^.. (ix 3 <> ix 4)) + indexOptions + , Index colonistsTableName $ + TableIndex (fromList $ colonistsFieldNames ^.. (ix 1 <> ix 2)) + indexOptions + , Index planetsTableName $ + TableIndex (fromList $ planetsFieldNames ^.. ix 0) + indexOptions{ indexUnique = True } + ] + + sort autoIndices @?= sort + [ Index colonistsTableName $ + TableIndex (fromList $ colonistsFieldNames ^.. ix 2) + indexOptions + , Index colonistsTableName $ + TableIndex (fromList $ colonistsFieldNames ^.. (ix 3 <> ix 4)) + indexOptions + , Index planetsTableName $ + TableIndex (fromList $ planetsFieldNames ^.. ix 3) + indexOptions{ indexUnique = True } + ] diff --git a/beam-migrate/Database/Beam/Haskell/Syntax.hs b/beam-migrate/Database/Beam/Haskell/Syntax.hs index 7324d4c9..c77149fc 100644 --- a/beam-migrate/Database/Beam/Haskell/Syntax.hs +++ b/beam-migrate/Database/Beam/Haskell/Syntax.hs @@ -1,6 +1,6 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Instances that allow us to use Haskell as a backend syntax. This allows us -- to use migrations defined a la 'Database.Beam.Migrate.SQL' to generate a beam @@ -16,10 +16,10 @@ import Database.Beam import Database.Beam.Backend.SQL import Database.Beam.Backend.SQL.AST import Database.Beam.Backend.SQL.Builder -import Database.Beam.Migrate.Checks (HasDataTypeCreatedCheck(..)) +import Database.Beam.Migrate.Checks (HasDataTypeCreatedCheck (..)) +import Database.Beam.Migrate.Serialization import Database.Beam.Migrate.SQL.SQL92 import Database.Beam.Migrate.SQL.Types -import Database.Beam.Migrate.Serialization import Data.Char (toLower, toUpper) import Data.Hashable @@ -465,6 +465,11 @@ instance IsSql92AlterTableActionSyntax HsNone where renameTableToSyntax _ = HsNone renameColumnToSyntax _ _ = HsNone +instance IsSql92IndexSyntax HsNone where + type Sql92IndexTableNameSyntax HsNone = TableName + addIndexSyntax _ _ _ _ = HsNone + dropIndexSyntax _ = HsNone + instance IsSql92AlterColumnActionSyntax HsNone where setNullSyntax = HsNone setNotNullSyntax = HsNone diff --git a/beam-migrate/Database/Beam/Migrate/Actions.hs b/beam-migrate/Database/Beam/Migrate/Actions.hs index 1ad02395..a246a232 100644 --- a/beam-migrate/Database/Beam/Migrate/Actions.hs +++ b/beam-migrate/Database/Beam/Migrate/Actions.hs @@ -1,10 +1,8 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -{-# LANGUAGE TupleSections #-} {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE CPP #-} {-# LANGUAGE UndecidableInstances #-} -- | Data types and functions to discover sequences of DDL commands to go from @@ -82,7 +80,10 @@ module Database.Beam.Migrate.Actions , addColumnProvider , addColumnNullProvider , dropColumnNullProvider + , addIndexProvider + , dropIndexProvider , defaultActionProvider + , indexActionProvider -- * Solver , Solver(..), FinalSolution(..) @@ -94,7 +95,9 @@ import Database.Beam.Backend.SQL import Database.Beam.Migrate.Checks import Database.Beam.Migrate.SQL import Database.Beam.Migrate.Types -import Database.Beam.Migrate.Types.Predicates (qnameAsText, qnameAsTableName) +import Database.Beam.Migrate.Types.Predicates (qnameAsTableName, + qnameAsText) +import Database.Beam.Schema.Indices import Control.Applicative import Control.DeepSeq @@ -267,11 +270,14 @@ instance Monoid (ActionProvider be) where withStrategy (rparWith (parList rseq)) bRes `seq` aRes ++ bRes -createTableWeight, dropTableWeight, addColumnWeight, dropColumnWeight :: Int +createTableWeight, dropTableWeight, addColumnWeight, dropColumnWeight, + addIndexWeight, dropIndexWeight :: Int createTableWeight = 500 dropTableWeight = 100 addColumnWeight = 1 dropColumnWeight = 1 +addIndexWeight = 1 +dropIndexWeight = 1 -- | Proceeds only if no predicate matches the given pattern. See the -- implementation of 'dropTableActionProvider' for an example of usage. @@ -412,6 +418,9 @@ dropColumnProvider = ActionProvider provider -- TableHasColumn tblNm' colNm' colType' :: TableHasColumn (Sql92DdlCommandColumnSchemaSyntax cmd) <- -- findPostConditions -- guard (tblNm' == tblNm && colNm == colNm' && colType == colType') -- This column exists as a different type + ensuringNot_ $ do + SomeDatabasePredicate pred' <- findPreConditions + guard (pred' `predicateRestrictsDropOf` colP) relatedPreds <- --pure [] pure $ do p'@(SomeDatabasePredicate pred') <- findPreConditions @@ -472,6 +481,50 @@ dropColumnNullProvider = ActionProvider provider (Seq.singleton (MigrationCommand cmd MigrationKeepsData)) ("Drop not null constraint for " <> colNm <> " on " <> qnameAsText tblNm) 100) +-- | Action provider for SQL92 @ADD INDEX ...@ actions. +-- Note that this is not included into 'defaultActionProvider', consider adding +-- 'indexActionProvider' to your migration backend if your engine supports that. +addIndexProvider :: forall be + . ( BeamMigrateOnlySqlBackend be, BeamMigrateOnlyIndexBackend be ) + => ActionProvider be +addIndexProvider = + ActionProvider provider + where + provider :: ActionProviderFn be + provider findPreConditions findPostConditions = + do idxP@(TableHasIndex tblNm colNms opts) <- findPostConditions + TableExistsPredicate tblNm' <- findPreConditions + guard (tblNm' == tblNm) + ensuringNot_ $ do + TableHasIndex tblNm'' colNms' _ :: TableHasIndex <- findPreConditions + guard (tblNm'' == tblNm && colNms == colNms') -- An index on these columns already exists + + let cmd = addIndexSyntax (qnameAsTableName tblNm) idxNm colNms opts + QualifiedName _ bareTblNm = tblNm + idxNm = mkIndexName bareTblNm colNms + pure (PotentialAction mempty (HS.fromList [SomeDatabasePredicate idxP]) + (Seq.singleton (MigrationCommand cmd MigrationKeepsData)) + ("Add index " <> T.intercalate "," colNms <> " to " <> qnameAsText tblNm) + (addIndexWeight + fromIntegral (sum $ map T.length (qnameAsText tblNm : colNms)))) + +-- | Action provider for SQL92 @DROP INDEX ...@ actions. +dropIndexProvider :: forall be + . ( BeamMigrateOnlySqlBackend be, BeamMigrateOnlyIndexBackend be ) + => ActionProvider be +dropIndexProvider = ActionProvider provider + where + provider :: ActionProviderFn be + provider findPreConditions _ = + do idxP@(TableHasIndex tblNm colNms _) <- findPreConditions + + let cmd = alterTableCmd (dropIndexSyntax idxNm) + QualifiedName _ bareTblNm = tblNm + idxNm = mkIndexName bareTblNm colNms + pure (PotentialAction (HS.fromList [SomeDatabasePredicate idxP]) mempty + (Seq.singleton (MigrationCommand cmd MigrationKeepsData)) + ("Drop index " <> T.intercalate "," colNms <> " from " <> qnameAsText tblNm) + (dropIndexWeight + fromIntegral (sum $ map T.length (qnameAsText tblNm : colNms)))) + -- | Default action providers for any SQL92 compliant syntax. -- -- In particular, this provides edges consisting of the following statements: @@ -495,6 +548,20 @@ defaultActionProvider = , addColumnNullProvider , dropColumnNullProvider ] +-- | Action providers for indices management syntax. +-- +-- In particular, this provides edges consisting of the following statements: +-- +-- * ADD INDEX ... +-- * DROP INDEX ... +indexActionProvider :: ( BeamMigrateOnlySqlBackend be, BeamMigrateOnlyIndexBackend be ) + => ActionProvider be +indexActionProvider = + mconcat + [ addIndexProvider + , dropIndexProvider + ] + -- | Represents current state of a database graph search. -- -- If 'ProvideSolution', the destination database has been reached, and the diff --git a/beam-migrate/Database/Beam/Migrate/Checks.hs b/beam-migrate/Database/Beam/Migrate/Checks.hs index a84fd6d1..bbf78594 100644 --- a/beam-migrate/Database/Beam/Migrate/Checks.hs +++ b/beam-migrate/Database/Beam/Migrate/Checks.hs @@ -1,27 +1,35 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE CPP #-} -- | Defines common 'DatabasePredicate's that are shared among backends module Database.Beam.Migrate.Checks where +import Control.Monad (guard) import Database.Beam.Backend.SQL.SQL92 + +import Database.Beam.Migrate.Serialization import Database.Beam.Migrate.SQL.SQL92 import Database.Beam.Migrate.SQL.Types -import Database.Beam.Migrate.Serialization import Database.Beam.Migrate.Types.Predicates +import Database.Beam.Schema.Indices +import Database.Beam.Schema.Tables -import Data.Aeson ((.:), (.=), withObject, object) +import Data.Aeson (object, withObject, (.:), (.=)) import Data.Aeson.Types (Parser, Value) -import Data.Hashable (Hashable(..)) +import Data.Hashable (Hashable (..)) +import Data.Proxy (Proxy (..)) import Data.Text (Text) -import Data.Typeable (Typeable, cast) +import Data.Typeable (Typeable, cast, splitTyConApp, typeOf, typeRep) #if !MIN_VERSION_base(4, 11, 0) import Data.Semigroup #endif +import GHC.Exts (fromList, toList) import GHC.Generics (Generic) +import Unsafe.Coerce (unsafeCoerce) + -- * Table checks -- | Asserts that a table with the given name exists in a database @@ -122,6 +130,67 @@ instance DatabasePredicate TableHasPrimaryKey where | Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm | otherwise = False +-- | Asserts that the given table has a primary key made of the given columns. +-- The order of the columns is significant. +data TableHasIndex + = TableHasIndex + { hasIndex_table :: QualifiedName {-^ Table name -} + , hasIndex_cols :: [Text] {-^ Column names -} + , hasIndex_opts :: IndexOptions + } deriving (Show, Eq, Generic) +instance Hashable TableHasIndex where + hashWithSalt salt (TableHasIndex tbl cols opts) = hashWithSalt salt (tbl, toList cols, opts) +instance DatabasePredicate TableHasIndex where + englishDescription (TableHasIndex tblName colNames opts) = + "Table " <> show tblName <> " has " <> indexOptionsEnglishDescription opts <> + "index " <> show colNames + + predicateSpecificity _ = PredicateSpecificityOnlyBackend "" + + serializePredicate (TableHasIndex tbl cols opts) = + object [ "has-index" .= object [ "table" .= tbl + , "columns" .= cols + , "options" .= opts ] ] + + -- we do not provide cascading delete of 'TableHasColumn' check, + -- index should be removed explicitely first + predicateCascadesDropOn (TableHasIndex tblNm _ _) p' + | Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm + | otherwise = False + + predicateRestrictsDropOf (TableHasIndex tblNm colNms _) p' + | Just (tblNm', colNm') <- + withTyCon (\(TableHasColumn tblNm' colNm' _) -> (tblNm', colNm')) p' = + tblNm == tblNm' && (colNm' `elem` colNms) + | otherwise = False + +-- | Match a given item's type against a type-level application with the given +-- type constructor. Applies the given function and returns 'Just' its result on match, +-- 'Nothing' otherwise. +-- Unlike 'cast', this function does not require @a@ type to be instance of 'Typeable'. +withTyCon + :: forall (con :: * -> *) (item :: *) r. + (Typeable con, Typeable item) + => (forall a. con a -> r) -> item -> Maybe r +withTyCon f x = do + (itemTyCon, itemTyArgs@(_ : _)) <- pure $ splitTyConApp (typeOf x) + (conTyCon, conTyArgs) <- pure $ splitTyConApp (typeRep (Proxy @con)) + guard (itemTyCon == conTyCon && init itemTyArgs == conTyArgs) + return (f $ unsafeCoerce x) + +-- | Convert gathered indices into checks. +entityIndicesToChecks + :: Table table + => DatabaseEntityDescriptor be (TableEntity table) + -> EntityIndices be db (TableEntity table) + -> [TableCheck table] +entityIndicesToChecks (DatabaseTable _ origNm _ _) (EntityIndices mkTableIndices) = + flip map (toList mkTableIndices) $ \mkTableIndex -> + TableCheck $ \qTblNm@(QualifiedName tblSchema tblNm) tblSettings -> + let dbEntity = DatabaseEntity (DatabaseTable tblSchema origNm tblNm tblSettings) + Index _ (TableIndex index opts) = mkTableIndex dbEntity + in SomeDatabasePredicate $ TableHasIndex qTblNm (fromList $ toList index) opts + -- * Deserialization -- | 'BeamDeserializers' for all the predicates defined in this module @@ -135,6 +204,7 @@ beamCheckDeserializers = mconcat , beamDeserializer (const deserializeTableHasPrimaryKeyPredicate) , beamDeserializer deserializeTableHasColumnPredicate , beamDeserializer deserializeTableColumnHasConstraintPredicate + , beamDeserializer (const deserializeTableHasIndexPredicate) ] where deserializeTableExistsPredicate :: Value -> Parser SomeDatabasePredicate @@ -170,3 +240,11 @@ beamCheckDeserializers = mconcat fmap (id @(TableColumnHasConstraint be)) (TableColumnHasConstraint <$> v' .: "table" <*> v' .: "column" <*> (beamDeserialize d =<< v' .: "constraint"))) + + deserializeTableHasIndexPredicate :: Value -> Parser SomeDatabasePredicate + deserializeTableHasIndexPredicate = + withObject "TableHasIndex" $ \v -> + v .: "has-index" >>= + (withObject "TableHasIndex" $ \v' -> + SomeDatabasePredicate <$> + (TableHasIndex <$> v' .: "table" <*> v' .: "columns" <*> v' .: "options")) diff --git a/beam-migrate/Database/Beam/Migrate/Generics.hs b/beam-migrate/Database/Beam/Migrate/Generics.hs index 230df962..49f0e3dc 100644 --- a/beam-migrate/Database/Beam/Migrate/Generics.hs +++ b/beam-migrate/Database/Beam/Migrate/Generics.hs @@ -8,16 +8,20 @@ module Database.Beam.Migrate.Generics ( -- * Default checked database settings defaultMigratableDbSettings + , withDbIndices -- * Extending the defaulting sytem , HasDefaultSqlDataType(..) , HasNullableConstraint, NullableStatus ) where -import Database.Beam.Migrate.Types import Database.Beam.Migrate.Generics.Tables import Database.Beam.Migrate.Generics.Types +import Database.Beam.Migrate.Types +import Database.Beam.Schema.Indices +import Database.Beam.Schema.Tables +import Data.Functor.Identity import Data.Proxy import GHC.Generics @@ -34,3 +38,27 @@ defaultMigratableDbSettings defaultMigratableDbSettings = to (defaultMigratableDbSettings' (Proxy @be) :: Rep (CheckedDatabaseSettings be db) ()) +-- | Attach checks which require the checked database to contain the given +-- indices. +-- +-- This function nicely composes with 'withDbModification' when used +-- in its infix form: +-- +-- @ +-- defaultMigratableDbSettings +-- `withDbModification` dbModification{ ... } +-- `withDbIndices` dbIndices{ ... } +-- @ +withDbIndices + :: forall be db. + Database be db + => CheckedDatabaseSettings be db + -> DatabaseIndices be db + -> CheckedDatabaseSettings be db +withDbIndices checkedDbSettings indices = + runIdentity $ + zipTables (Proxy @be) + (\(CheckedDatabaseEntity dbSettings dbPredicates) indexEntity -> + pure $ CheckedDatabaseEntity (addIndexChecks dbSettings indexEntity) dbPredicates + ) + checkedDbSettings indices diff --git a/beam-migrate/Database/Beam/Migrate/SQL/SQL92.hs b/beam-migrate/Database/Beam/Migrate/SQL/SQL92.hs index 0d57f030..41041ebe 100644 --- a/beam-migrate/Database/Beam/Migrate/SQL/SQL92.hs +++ b/beam-migrate/Database/Beam/Migrate/SQL/SQL92.hs @@ -1,5 +1,5 @@ -{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE ConstraintKinds #-} -- | Finally-tagless encoding of SQL92 DDL commands. -- @@ -9,6 +9,7 @@ module Database.Beam.Migrate.SQL.SQL92 where import Database.Beam.Backend.SQL.SQL92 +import Database.Beam.Schema.Indices import Data.Aeson (Value) import Data.Hashable @@ -29,6 +30,10 @@ type Sql92SaneDdlCommandSyntax cmd = , Sql92ColumnSchemaExpressionSyntax (Sql92DdlCommandColumnSchemaSyntax cmd) ~ Sql92ExpressionSyntax cmd ) +-- | Syntax constraints required for indices manipulation. +type Sql92IndexCommandSyntax cmd = + IsSql92IndexSyntax (Sql92DdlCommandAlterTableSyntax cmd) + -- | Syntax equalities for any reasonable DDL syntax, only including -- types defined here. type Sql92SaneDdlCommandSyntaxMigrateOnly cmd = @@ -118,6 +123,14 @@ class ( IsSql92ColumnSchemaSyntax (Sql92AlterTableColumnSchemaSyntax syntax) class IsSql92AlterColumnActionSyntax syntax where setNotNullSyntax, setNullSyntax :: syntax +class IsSql92TableNameSyntax (Sql92IndexTableNameSyntax syntax) => + IsSql92IndexSyntax syntax where + + type Sql92IndexTableNameSyntax syntax :: * + + addIndexSyntax :: Sql92IndexTableNameSyntax syntax -> Text -> [Text] -> IndexOptions -> syntax + dropIndexSyntax :: Text -> syntax + class ( IsSql92ColumnConstraintDefinitionSyntax (Sql92ColumnSchemaColumnConstraintDefinitionSyntax columnSchema) , IsSql92DataTypeSyntax (Sql92ColumnSchemaColumnTypeSyntax columnSchema) , Typeable (Sql92ColumnSchemaColumnTypeSyntax columnSchema) diff --git a/beam-migrate/Database/Beam/Migrate/SQL/Tables.hs b/beam-migrate/Database/Beam/Migrate/SQL/Tables.hs index f5a9a581..a02171f6 100644 --- a/beam-migrate/Database/Beam/Migrate/SQL/Tables.hs +++ b/beam-migrate/Database/Beam/Migrate/SQL/Tables.hs @@ -1,8 +1,8 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Database.Beam.Migrate.SQL.Tables ( -- * Table manipulation @@ -18,6 +18,7 @@ module Database.Beam.Migrate.SQL.Tables , renameTableTo, renameColumnTo , addColumn, dropColumn + , addIndex, dropIndex -- * Field specification , DefaultValue, Constraint(..), NotNullConstraint @@ -31,29 +32,30 @@ module Database.Beam.Migrate.SQL.Tables , FieldReturnType(..) ) where -import Database.Beam -import Database.Beam.Schema.Tables -import Database.Beam.Backend.SQL -import Database.Beam.Backend.SQL.AST (TableName(..)) -import Database.Beam.Query.Internal (tableNameFromEntity) +import Database.Beam +import Database.Beam.Backend.SQL +import Database.Beam.Backend.SQL.AST (TableName (..)) +import Database.Beam.Query.Internal (tableNameFromEntity) +import Database.Beam.Schema.Indices +import Database.Beam.Schema.Tables -import Database.Beam.Migrate.Types -import Database.Beam.Migrate.Checks -import Database.Beam.Migrate.SQL.Types -import Database.Beam.Migrate.SQL.SQL92 +import Database.Beam.Migrate.Checks +import Database.Beam.Migrate.SQL.SQL92 +import Database.Beam.Migrate.SQL.Types +import Database.Beam.Migrate.Types -import Control.Applicative -import Control.Monad.Identity -import Control.Monad.Writer.Strict -import Control.Monad.State +import Control.Applicative +import Control.Monad.Identity +import Control.Monad.State +import Control.Monad.Writer.Strict -import Data.Text (Text) -import Data.Typeable import qualified Data.Kind as Kind (Constraint) +import Data.Text (Text) +import Data.Typeable -import GHC.TypeLits +import GHC.TypeLits -import Lens.Micro ((^.)) +import Lens.Micro ((^.)) -- * Table manipulation @@ -113,7 +115,7 @@ data ColumnMigration a -- | Monad representing a series of @ALTER TABLE@ statements newtype TableMigration be a - = TableMigration (WriterT [BeamSqlBackendAlterTableSyntax be] (State (TableName, [TableCheck])) a) + = TableMigration (WriterT [BeamSqlBackendAlterTableSyntax be] (State (TableName, [SomeTableCheck])) a) deriving (Monad, Applicative, Functor) -- | @ALTER TABLE ... RENAME TO@ command @@ -195,7 +197,7 @@ alterTable (CheckedDatabaseEntity (CheckedDatabaseTable dt tblChecks tblFieldChe ((newTbl, cmds), (TableName tblSchema' tblNm', tblChecks')) = runState (runWriterT alterColumns') ( TableName (dbTableSchema dt) (dbTableCurrentName dt) - , tblChecks ) + , SomeTableCheck <$> tblChecks ) fieldChecks' = changeBeamRep (\(Columnar' (ColumnMigration _ checks) :: Columnar' ColumnMigration a) -> Columnar' (Const checks) :: Columnar' (Const [FieldCheck]) a) @@ -209,7 +211,27 @@ alterTable (CheckedDatabaseEntity (CheckedDatabaseTable dt tblChecks tblFieldChe pure (CheckedDatabaseEntity (CheckedDatabaseTable (DatabaseTable tblSchema' (dbTableOrigName dt) tblNm' tbl') - tblChecks' fieldChecks') entityChecks) + (givenTableChecks tblChecks') fieldChecks') entityChecks) + +-- | @ALTER TABLE ... ADD INDEX ...@ command +addIndex :: (BeamMigrateSqlBackend be, BeamMigrateOnlyIndexBackend be) + => [ColumnMigration a] -> IndexOptions -> TableMigration be () +addIndex columns opts = TableMigration $ do + (TableName curSchema curNm, _) <- get + let columnNms = map columnMigrationFieldName columns + idxName = mkIndexName curNm columnNms + tell [ addIndexSyntax (tableName curSchema curNm) idxName columnNms opts ] + -- TODO: should I add index checks here? + +-- | @ALTER TABLE ... DROP INDEX ...@ command +dropIndex :: (BeamMigrateSqlBackend be, BeamMigrateOnlyIndexBackend be) + => [ColumnMigration a] -> TableMigration be () +dropIndex columns = TableMigration $ do + (TableName _ curTblNm, _) <- get + let columnNms = map columnMigrationFieldName columns + idxName = mkIndexName curTblNm columnNms + tell [ dropIndexSyntax idxName ] + -- TODO: should I remove index checks here? -- * Fields diff --git a/beam-migrate/Database/Beam/Migrate/SQL/Types.hs b/beam-migrate/Database/Beam/Migrate/SQL/Types.hs index e3465345..4579029b 100644 --- a/beam-migrate/Database/Beam/Migrate/SQL/Types.hs +++ b/beam-migrate/Database/Beam/Migrate/SQL/Types.hs @@ -7,6 +7,7 @@ module Database.Beam.Migrate.SQL.Types , BeamMigrateOnlySqlBackend , BeamMigrateSqlBackend + , BeamMigrateOnlyIndexBackend , BeamMigrateSql99Backend , BeamSqlBackendConstraintSyntax , BeamSqlBackendColumnConstraintDefinitionSyntax @@ -18,9 +19,9 @@ module Database.Beam.Migrate.SQL.Types , BeamSqlBackendConstraintAttributesSyntax ) where -import Database.Beam.Migrate.Types.Predicates -import Database.Beam.Migrate.SQL.SQL92 import Database.Beam.Backend.SQL +import Database.Beam.Migrate.SQL.SQL92 +import Database.Beam.Migrate.Types.Predicates import Data.Text (Text) import Data.Typeable (Typeable) @@ -58,6 +59,11 @@ type BeamMigrateSqlBackend be = , Sql92SaneDdlCommandSyntax (BeamSqlBackendSyntax be) , BeamSqlBackend be ) +type BeamMigrateOnlyIndexBackend be = + ( Sql92IndexCommandSyntax (BeamSqlBackendSyntax be) + , IsSql92IndexSyntax (BeamSqlBackendSyntax be) + ) + type BeamMigrateSql99Backend be = ( BeamMigrateSqlBackend be , IsSql99DataTypeSyntax (BeamSqlBackendDataTypeSyntax be)) diff --git a/beam-migrate/Database/Beam/Migrate/Types.hs b/beam-migrate/Database/Beam/Migrate/Types.hs index c68c4ac9..3b0df65c 100644 --- a/beam-migrate/Database/Beam/Migrate/Types.hs +++ b/beam-migrate/Database/Beam/Migrate/Types.hs @@ -1,8 +1,8 @@ -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE CPP #-} module Database.Beam.Migrate.Types ( -- * Checked database entities @@ -33,8 +33,9 @@ module Database.Beam.Migrate.Types , p -- * Entity checks - , TableCheck(..), DomainCheck(..) - , FieldCheck(..) + , TableCheck(..), SomeTableCheck(..) + , DomainCheck(..), FieldCheck(..) + , givenTableChecks -- * Migrations , MigrationStep(..), MigrationSteps(..) @@ -48,12 +49,12 @@ module Database.Beam.Migrate.Types , migrateScript, evaluateDatabase, stepNames ) where +import Control.Arrow +import Control.Category (Category) +import Control.Monad.Free.Church import Database.Beam.Backend.SQL import Database.Beam.Migrate.Types.CheckedEntities import Database.Beam.Migrate.Types.Predicates -import Control.Monad.Free.Church -import Control.Arrow -import Control.Category (Category) #if !MIN_VERSION_base(4, 11, 0) import Data.Semigroup @@ -205,4 +206,3 @@ stepNames (MigrationSteps f) = runF (runKleisli f ()) (\_ x -> x) (\(MigrationSt where runMigration :: forall a'. Migration be a' -> a' runMigration migration = runF migration id (\(MigrationRunCommand _ _ next) -> next) - diff --git a/beam-migrate/Database/Beam/Migrate/Types/CheckedEntities.hs b/beam-migrate/Database/Beam/Migrate/Types/CheckedEntities.hs index 6274f78e..183eb465 100644 --- a/beam-migrate/Database/Beam/Migrate/Types/CheckedEntities.hs +++ b/beam-migrate/Database/Beam/Migrate/Types/CheckedEntities.hs @@ -5,6 +5,7 @@ module Database.Beam.Migrate.Types.CheckedEntities where import Database.Beam import Database.Beam.Backend.SQL +import Database.Beam.Schema.Indices import Database.Beam.Schema.Tables import Database.Beam.Migrate.Checks @@ -12,17 +13,17 @@ import Database.Beam.Migrate.Generics.Tables import Database.Beam.Migrate.Types.Predicates import Control.Applicative -import Control.Monad.Writer import Control.Monad.Identity +import Control.Monad.Writer import Data.Proxy -import Data.Text (Text) import Data.String +import Data.Text (Text) -import GHC.Types import GHC.Generics +import GHC.Types -import Lens.Micro (Lens', (&), (^.), (.~), (%~)) +import Lens.Micro (Lens', (%~), (&), (.~), (^.)) -- * Checked Database Entities @@ -54,6 +55,11 @@ class IsDatabaseEntity be entity => IsCheckedDatabaseEntity be entity where checkedDbEntityAuto :: CheckedDatabaseEntityDefaultRequirements be entity => Text -> CheckedDatabaseEntityDescriptor be entity + addIndexChecks :: CheckedDatabaseEntityDescriptor be entity + -> EntityIndices be db entity + -> CheckedDatabaseEntityDescriptor be entity + addIndexChecks = const + -- | Like 'DatabaseEntity' but for checked databases data CheckedDatabaseEntity be (db :: (* -> *) -> *) entityType where CheckedDatabaseEntity :: IsCheckedDatabaseEntity be entityType @@ -105,7 +111,7 @@ instance Beamable tbl => IsCheckedDatabaseEntity be (TableEntity tbl) where data CheckedDatabaseEntityDescriptor be (TableEntity tbl) where CheckedDatabaseTable :: Table tbl => DatabaseEntityDescriptor be (TableEntity tbl) - -> [ TableCheck ] + -> [ TableCheck tbl ] -> tbl (Const [FieldCheck]) -> CheckedDatabaseEntityDescriptor be (TableEntity tbl) @@ -133,6 +139,10 @@ instance Beamable tbl => IsCheckedDatabaseEntity be (TableEntity tbl) where fieldChecks = to (gDefaultTblSettingsChecks (Proxy @be) (Proxy @(Rep (tbl Identity))) False) in CheckedDatabaseTable (dbEntityAuto tblTypeName) tblChecks fieldChecks + addIndexChecks (CheckedDatabaseTable dbTable@(DatabaseTable _ _ _ _) tblChecks tblFieldChecks) + entityIndices = + CheckedDatabaseTable dbTable (entityIndicesToChecks dbTable entityIndices ++ tblChecks) tblFieldChecks + -- | Purposefully opaque type describing how to modify a table field. Used to -- parameterize the second argument to 'modifyCheckedTable'. For now, the only -- way to construct a value is the 'IsString' instance, which allows you to diff --git a/beam-migrate/Database/Beam/Migrate/Types/Predicates.hs b/beam-migrate/Database/Beam/Migrate/Types/Predicates.hs index 5d81c8d4..798023ce 100644 --- a/beam-migrate/Database/Beam/Migrate/Types/Predicates.hs +++ b/beam-migrate/Database/Beam/Migrate/Types/Predicates.hs @@ -9,9 +9,10 @@ import Database.Beam.Schema.Tables import Control.DeepSeq +import Data.Maybe import Data.Aeson -import Data.Text (Text) import Data.Hashable +import Data.Text (Text) import Data.Typeable #if !MIN_VERSION_base(4, 11, 0) @@ -44,13 +45,21 @@ class (Typeable p, Hashable p, Eq p) => DatabasePredicate p where -- order for a table to have a column, that table must exist. This function -- takes in the current predicate and another arbitrary database predicate. It -- should return 'True' if this predicate needs the other predicate to be true - -- in order to exist. + -- in order to exist. Once the predicate on which the given one depends does + -- not hold, the given predicate is removed without any additional actions. -- -- By default, this simply returns 'False', which makes sense for many -- predicates. predicateCascadesDropOn :: DatabasePredicate p' => p -> p' -> Bool predicateCascadesDropOn _ _ = False + -- | Similarly to 'predicateCascadesDropOn', should return 'True' if this predicate + -- needs another one to be true in order to exist. The difference is, the other + -- predicate cannot be removed until this one exists, this predicate has to be + -- explicitely removed with a corresponding action first. + predicateRestrictsDropOf :: DatabasePredicate p' => p -> p' -> Bool + predicateRestrictsDropOf _ _ = False + -- | A Database predicate is a value of any type which satisfies -- 'DatabasePredicate'. We often want to store these in lists and sets, so we -- need a monomorphic container that can store these polymorphic values. @@ -82,7 +91,7 @@ instance Hashable PredicateSpecificity instance ToJSON PredicateSpecificity where toJSON PredicateSpecificityAllBackends = "all" - toJSON (PredicateSpecificityOnlyBackend s) = object [ "backend" .= toJSON s ] + toJSON (PredicateSpecificityOnlyBackend s) = object [ "backend" .= toJSON s ] instance FromJSON PredicateSpecificity where parseJSON "all" = pure PredicateSpecificityAllBackends parseJSON (Object o) = PredicateSpecificityOnlyBackend <$> o .: "backend" @@ -128,7 +137,14 @@ qnameAsTableName :: IsSql92TableNameSyntax syntax => QualifiedName -> syntax qnameAsTableName (QualifiedName sch t) = tableName sch t -- | A predicate that depends on the name of a table as well as its fields -newtype TableCheck = TableCheck (forall tbl. Table tbl => QualifiedName -> tbl (TableField tbl) -> SomeDatabasePredicate) +newtype TableCheck tbl = TableCheck (QualifiedName -> tbl (TableField tbl) -> SomeDatabasePredicate) + +-- | Allows to drop all checks into one pack, used in migration state +data SomeTableCheck = forall tbl. (Typeable tbl, Table tbl) => SomeTableCheck (TableCheck tbl) + +-- | Leave only checks for the given table. +givenTableChecks :: Typeable tbl => [SomeTableCheck] -> [TableCheck tbl] +givenTableChecks = catMaybes . map (\(SomeTableCheck check) -> cast check) -- | A predicate that depends on the name of a domain type newtype DomainCheck = DomainCheck (QualifiedName -> SomeDatabasePredicate) diff --git a/beam-postgres/Database/Beam/Postgres/CustomTypes.hs b/beam-postgres/Database/Beam/Postgres/CustomTypes.hs index 613c8e2e..395278ba 100644 --- a/beam-postgres/Database/Beam/Postgres/CustomTypes.hs +++ b/beam-postgres/Database/Beam/Postgres/CustomTypes.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE EmptyCase #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE UndecidableInstances #-} module Database.Beam.Postgres.CustomTypes ( PgType, PgTypeCheck(..) , PgDataTypeSchema @@ -27,12 +27,12 @@ module Database.Beam.Postgres.CustomTypes ) where import Database.Beam -import Database.Beam.Schema.Tables import Database.Beam.Backend.SQL import Database.Beam.Backend.Types import Database.Beam.Migrate -import Database.Beam.Postgres.Types import Database.Beam.Postgres.Syntax +import Database.Beam.Postgres.Types +import Database.Beam.Schema.Tables import Control.Monad import Control.Monad.Free.Church @@ -40,7 +40,7 @@ import Data.Aeson (object, (.=)) import qualified Data.ByteString.Char8 as BC import Data.Functor.Const import qualified Data.HashSet as HS -import Data.Proxy (Proxy(..)) +import Data.Proxy (Proxy (..)) #if !MIN_VERSION_base(4,11,0) import Data.Semigroup #endif diff --git a/beam-postgres/Database/Beam/Postgres/Extensions.hs b/beam-postgres/Database/Beam/Postgres/Extensions.hs index e20cdeb3..428772ad 100644 --- a/beam-postgres/Database/Beam/Postgres/Extensions.hs +++ b/beam-postgres/Database/Beam/Postgres/Extensions.hs @@ -1,6 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE CPP #-} -- | Postgres extensions are run-time loadable plugins that can extend Postgres -- functionality. Extensions are part of the database schema. @@ -15,16 +14,16 @@ module Database.Beam.Postgres.Extensions where import Database.Beam import Database.Beam.Schema.Tables -import Database.Beam.Postgres.Types import Database.Beam.Postgres.Syntax +import Database.Beam.Postgres.Types import Database.Beam.Migrate import Control.Monad import Data.Aeson -import qualified Data.HashSet as HS import Data.Hashable (Hashable) +import qualified Data.HashSet as HS import Data.Proxy import Data.Text (Text) #if !MIN_VERSION_base(4, 11, 0) diff --git a/beam-postgres/Database/Beam/Postgres/Migrate.hs b/beam-postgres/Database/Beam/Postgres/Migrate.hs index 01b97deb..f1643a62 100644 --- a/beam-postgres/Database/Beam/Postgres/Migrate.hs +++ b/beam-postgres/Database/Beam/Postgres/Migrate.hs @@ -1,10 +1,10 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE DataKinds #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE CPP #-} {-# OPTIONS_GHC -fno-warn-orphans -fno-warn-type-defaults #-} -- | Migrations support for beam-postgres. See "Database.Beam.Migrate" for more @@ -31,9 +31,9 @@ import Database.Beam.Backend.SQL import Database.Beam.Migrate.Actions (defaultActionProvider) import qualified Database.Beam.Migrate.Backend as Tool import qualified Database.Beam.Migrate.Checks as Db +import qualified Database.Beam.Migrate.Serialization as Db import qualified Database.Beam.Migrate.SQL as Db import Database.Beam.Migrate.SQL.BeamExtensions -import qualified Database.Beam.Migrate.Serialization as Db import qualified Database.Beam.Migrate.Types as Db import qualified Database.Beam.Query.DataTypes as Db @@ -47,8 +47,8 @@ import Database.Beam.Postgres.Types import Database.Beam.Haskell.Syntax import qualified Database.PostgreSQL.Simple as Pg -import qualified Database.PostgreSQL.Simple.Types as Pg import qualified Database.PostgreSQL.Simple.TypeInfo.Static as Pg +import qualified Database.PostgreSQL.Simple.Types as Pg import Control.Applicative ((<|>)) import Control.Arrow @@ -73,7 +73,7 @@ import qualified Data.Vector as V #if !MIN_VERSION_base(4, 11, 0) import Data.Semigroup #else -import Data.Monoid (Endo(..)) +import Data.Monoid (Endo (..)) #endif import Data.Word (Word64) @@ -366,6 +366,7 @@ getDbConstraints conn = map (\(enumNm, _, options) -> Db.SomeDatabasePredicate (PgHasEnum enumNm (V.toList options))) enumerationData pure (tblsExist ++ columnChecks ++ primaryKeys ++ enumerations) + -- TODO: do not forget to update this -- * Postgres-specific data types diff --git a/beam-postgres/Database/Beam/Postgres/Syntax.hs b/beam-postgres/Database/Beam/Postgres/Syntax.hs index 258a3c55..5c9ce279 100644 --- a/beam-postgres/Database/Beam/Postgres/Syntax.hs +++ b/beam-postgres/Database/Beam/Postgres/Syntax.hs @@ -1,16 +1,16 @@ {-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-name-shadowing #-} -{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE CPP #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeFamilies #-} -- | Data types for Postgres syntax. Access is given mainly for extension -- modules. The types and definitions here are likely to change. @@ -87,9 +87,11 @@ module Database.Beam.Postgres.Syntax import Database.Beam hiding (insert) import Database.Beam.Backend.SQL import Database.Beam.Migrate -import Database.Beam.Migrate.Checks (HasDataTypeCreatedCheck(..)) -import Database.Beam.Migrate.SQL.Builder hiding (fromSqlConstraintAttributes) +import Database.Beam.Migrate.Checks (HasDataTypeCreatedCheck (..)) import Database.Beam.Migrate.Serialization +import Database.Beam.Migrate.SQL.Builder hiding + (fromSqlConstraintAttributes) +import Database.Beam.Schema.Indices import Control.Monad (guard) import Control.Monad.Free @@ -98,7 +100,8 @@ import Control.Monad.Free.Church import Data.Aeson (Value, object, (.=)) import Data.Bits import Data.ByteString (ByteString) -import Data.ByteString.Builder (Builder, byteString, char8, toLazyByteString) +import Data.ByteString.Builder (Builder, byteString, char8, + toLazyByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Lazy.Char8 (toStrict) import qualified Data.ByteString.Lazy.Char8 as BL @@ -108,24 +111,30 @@ import Data.Coerce import Data.Functor.Classes import Data.Hashable import Data.Int +import qualified Data.List as L import Data.Maybe import Data.Scientific (Scientific) -import Data.String (IsString(..), fromString) +import Data.String (IsString (..), fromString) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.Lazy as TL -import Data.Time (LocalTime, UTCTime, ZonedTime, TimeOfDay, NominalDiffTime, Day) +import Data.Time (Day, LocalTime, NominalDiffTime, TimeOfDay, UTCTime, + ZonedTime) import Data.UUID.Types (UUID) import Data.Word #if !MIN_VERSION_base(4, 11, 0) import Data.Semigroup #endif +import qualified Database.PostgreSQL.Simple.HStore as Pg (HStoreBuilder, + HStoreList, HStoreMap) +import qualified Database.PostgreSQL.Simple.Time as Pg (Date, LocalTimestamp, + UTCTimestamp, + ZonedTimestamp) import qualified Database.PostgreSQL.Simple.ToField as Pg import qualified Database.PostgreSQL.Simple.TypeInfo.Static as Pg -import qualified Database.PostgreSQL.Simple.Types as Pg (Oid(..), Binary(..), Null(..)) -import qualified Database.PostgreSQL.Simple.Time as Pg (Date, ZonedTimestamp, LocalTimestamp, UTCTimestamp) -import qualified Database.PostgreSQL.Simple.HStore as Pg (HStoreList, HStoreMap, HStoreBuilder) +import qualified Database.PostgreSQL.Simple.Types as Pg (Binary (..), Null (..), + Oid (..)) data PostgresInaccessible @@ -374,6 +383,7 @@ newtype PgDropTableSyntax = PgDropTableSyntax { fromPgDropTable :: PgSyntax } newtype PgAlterTableSyntax = PgAlterTableSyntax { fromPgAlterTable :: PgSyntax } newtype PgAlterTableActionSyntax = PgAlterTableActionSyntax { fromPgAlterTableAction :: PgSyntax } newtype PgAlterColumnActionSyntax = PgAlterColumnActionSyntax { fromPgAlterColumnAction :: PgSyntax } +newtype PgIndexSyntax = PgIndexSyntax { fromPgIndex :: PgSyntax } newtype PgWindowFrameSyntax = PgWindowFrameSyntax { fromPgWindowFrame :: PgSyntax } newtype PgWindowFrameBoundsSyntax = PgWindowFrameBoundsSyntax { fromPgWindowFrameBounds :: PgSyntax } newtype PgWindowFrameBoundSyntax = PgWindowFrameBoundSyntax { fromPgWindowFrameBound :: ByteString -> PgSyntax } @@ -492,7 +502,7 @@ instance IsSql92FromSyntax PgFromSyntax where coerce tableSrc <> emit " AS " <> pgQuotedIdentifier nm <> maybe mempty (\colNms' -> pgParens (pgSepBy (emit ",") (map pgQuotedIdentifier colNms'))) colNms - innerJoin a b Nothing = PgFromSyntax (fromPgFrom a <> emit " CROSS JOIN " <> fromPgFrom b) + innerJoin a b Nothing = PgFromSyntax (fromPgFrom a <> emit " CROSS JOIN " <> fromPgFrom b) innerJoin a b (Just e) = pgJoin "INNER JOIN" a b (Just e) leftJoin = pgJoin "LEFT JOIN" @@ -1025,6 +1035,20 @@ instance IsSql92AlterTableActionSyntax PgAlterTableActionSyntax where PgAlterTableActionSyntax $ emit "RENAME COLUMN " <> pgQuotedIdentifier oldNm <> emit " TO " <> pgQuotedIdentifier newNm +instance IsSql92IndexSyntax PgIndexSyntax where + type Sql92IndexTableNameSyntax PgIndexSyntax = PgTableNameSyntax + addIndexSyntax tblNm idxNm colNms (IndexOptions uniq) = + PgIndexSyntax $ + emit "CREATE " <> emit (if uniq then "UNIQUE " else " ") <> + emit "INDEX " <> pgQuotedIdentifier idxNm <> + emit " ON " <> fromPgTableName tblNm <> + emit "(" <> + mconcat (L.intersperse (emit ", ") (map pgQuotedIdentifier colNms)) <> + emit ")" + dropIndexSyntax idxNm = + PgIndexSyntax $ + emit "DROP INDEX " <> pgQuotedIdentifier idxNm + instance IsSql92AlterColumnActionSyntax PgAlterColumnActionSyntax where setNullSyntax = PgAlterColumnActionSyntax (emit "DROP NOT NULL") setNotNullSyntax = PgAlterColumnActionSyntax (emit "SET NOT NULL") @@ -1397,4 +1421,3 @@ pgRenderSyntaxScript (PgSyntax mkQuery) = where quoteIdentifierChar '"' = char8 '"' <> char8 '"' quoteIdentifierChar c = char8 c - diff --git a/stack.yaml b/stack.yaml index 7ab35410..b14b8d1f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,7 +1,7 @@ #resolver: lts-9.0 #resolver: lts-10.3 #resolver: lts-10.5 -resolver: lts-12.17 +resolver: lts-12.10 # resolver: nightly-2018-11-05 packages: - beam-sqlite