orville-postgresql-1.0.0.0: src/Orville/PostgreSQL/Internal/IndexDefinition.hs
{- |
Copyright : Flipstone Technology Partners 2023
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.Internal.IndexDefinition
( IndexDefinition
, indexCreationStrategy
, setIndexCreationStrategy
, uniqueIndex
, uniqueNamedIndex
, nonUniqueIndex
, nonUniqueNamedIndex
, mkIndexDefinition
, mkNamedIndexDefinition
, IndexMigrationKey (AttributeBasedIndexKey, NamedIndexKey)
, AttributeBasedIndexMigrationKey (AttributeBasedIndexMigrationKey, indexKeyUniqueness, indexKeyColumns)
, NamedIndexMigrationKey
, indexMigrationKey
, indexCreateExpr
, IndexCreationStrategy (Transactional, Concurrent)
)
where
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NEL
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.Marshall.FieldDefinition as FieldDefinition
{- |
Defines an index that can be added to a 'Orville.PostgreSQL.TableDefinition'.
Use one of the constructor functions below (such as 'uniqueIndex') to
construct the index definition you wish to have and then use
'Orville.PostgreSQL.addTableIndexes' to add them to your table definition.
Orville will then add the index next time you run auto-migrations.
@since 1.0.0.0
-}
data IndexDefinition = IndexDefinition
{ i_indexCreateExpr ::
IndexCreationStrategy ->
Expr.Qualified Expr.TableName ->
Expr.CreateIndexExpr
, i_indexMigrationKey :: IndexMigrationKey
, i_indexCreationStrategy :: IndexCreationStrategy
}
{- |
Sets the 'IndexCreationStrategy' to be used when creating the index described
by the 'IndexDefinition'. By default, all indexes are created using the
'Transactional' strategy, but some tables are too large for this to be
feasible. See the 'Concurrent' creation strategy for how to work around this.
@since 1.0.0.0
-}
setIndexCreationStrategy ::
IndexCreationStrategy ->
IndexDefinition ->
IndexDefinition
setIndexCreationStrategy strategy indexDef =
indexDef
{ i_indexCreationStrategy = strategy
}
{- |
Gets the 'IndexCreationStrategy' to be used when creating the index described
by the 'IndexDefinition'. By default, all indexes are created using the
'Transactional' strategy.
@since 1.0.0.0
-}
indexCreationStrategy ::
IndexDefinition ->
IndexCreationStrategy
indexCreationStrategy =
i_indexCreationStrategy
{- |
Defines how an 'IndexDefinition' will be executed to add an index to a table.
By default, all indexes are created using the 'Transactional' strategy.
@since 1.0.0.0
-}
data IndexCreationStrategy
= -- |
-- The default strategy. The index will be added as part of a
-- database transaction along with all the other DDL being executed
-- to migrate the database schema. If any migration should fail, the
-- index creation will be rolled back as part of the transaction.
-- This is how schema migrations work in general in Orville.
Transactional
| -- |
-- Creates the index using the @CONCURRENTLY@ keyword in PostgreSQL.
-- Index creation will not lock the table during creation, allowing
-- the application to access the table normally while the index is
-- created. Concurrent index creation cannot be done in a
-- transaction, so indexes created using @CONCURRENTLY@ are created
-- outside the normal schema transaction. Index creation may fail
-- when using the 'Concurrent' strategy. Orville has no special
-- provision to detect or recover from this failure currently. You
-- should manually check that index creation has succeeded. If
-- necessary, you can manually drop the index to cause Orville to
-- recreate it the next time migrations are run. Note that while the
-- table will not be locked, index migration will still block
-- application startup by default. See the information about schema
-- migration options in "Orville.PostgreSQL.AutoMigration" for
-- details about how to work around this if it is a problem for you.
-- Also, it a good idea to read the PostgreSQL docs about creating
-- indexes concurrently before you use this strategy. See
-- https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY.
Concurrent
deriving (Eq, Show)
{- |
Orville uses 'IndexMigrationKey' values while performing auto migrations to
determine whether an index needs to be added or dropped. For most use cases
the constructor functions that build an 'IndexDefinition' will create this
automatically for you.
@since 1.0.0.0
-}
data IndexMigrationKey
= AttributeBasedIndexKey AttributeBasedIndexMigrationKey
| NamedIndexKey NamedIndexMigrationKey
deriving (Eq, Ord)
{- |
An 'IndexMigrationKey' using 'AttributeBasedIndexMigrationKey' will cause
Orville to compare the structure of the indexes found in the database to the
index structure it wants to create. If no matching index is found it will
create a new index.
@since 1.0.0.0
-}
data AttributeBasedIndexMigrationKey = AttributeBasedIndexMigrationKey
{ indexKeyUniqueness :: Expr.IndexUniqueness
, indexKeyColumns :: [FieldDefinition.FieldName]
}
deriving (Eq, Ord, Show)
{- |
An 'IndexMigrationKey' using 'NamedIndexMigrationKey' will cause Orville to
compare the only the names of indexes found in the database when determine
whether to create the index. If an index with a matching name is found no
index will be created. If no matching index name is found a new index will be
created. This is often required when you create indexes using custom SQL
where Orville is not able to do an accurate structural comparison of the
desired index structure against the existing indexes.
@since 1.0.0.0
-}
type NamedIndexMigrationKey = String
{- |
Gets the 'IndexMigrationKey' for the 'IndexDefinition'
@since 1.0.0.0
-}
indexMigrationKey :: IndexDefinition -> IndexMigrationKey
indexMigrationKey = i_indexMigrationKey
{- |
Gets the SQL expression that will be used to add the index to the specified
table.
@since 1.0.0.0
-}
indexCreateExpr :: IndexDefinition -> Expr.Qualified Expr.TableName -> Expr.CreateIndexExpr
indexCreateExpr indexDef =
i_indexCreateExpr
indexDef
(i_indexCreationStrategy indexDef)
{- |
Constructs an 'IndexDefinition' for a non-unique index on the given columns.
@since 1.0.0.0
-}
nonUniqueIndex :: NonEmpty FieldDefinition.FieldName -> IndexDefinition
nonUniqueIndex =
mkIndexDefinition Expr.NonUniqueIndex
{- |
Constructs an 'IndexDefinition' for a non-unique index with given SQL and
index name.
@since 1.0.0.0
-}
nonUniqueNamedIndex :: String -> Expr.IndexBodyExpr -> IndexDefinition
nonUniqueNamedIndex =
mkNamedIndexDefinition Expr.NonUniqueIndex
{- |
Constructs an 'IndexDefinition' for a @UNIQUE@ index on the given columns.
@since 1.0.0.0
-}
uniqueIndex :: NonEmpty FieldDefinition.FieldName -> IndexDefinition
uniqueIndex =
mkIndexDefinition Expr.UniqueIndex
{- |
Constructs an 'IndexDefinition' for a @UNIQUE@ index with given SQL and index
name.
@since 1.0.0.0
-}
uniqueNamedIndex :: String -> Expr.IndexBodyExpr -> IndexDefinition
uniqueNamedIndex =
mkNamedIndexDefinition Expr.UniqueIndex
{- |
Constructs an 'IndexDefinition' for an index on the given columns with the
given uniqueness.
@since 1.0.0.0
-}
mkIndexDefinition ::
Expr.IndexUniqueness ->
NonEmpty FieldDefinition.FieldName ->
IndexDefinition
mkIndexDefinition uniqueness fieldNames =
let
expr strategy tableName =
Expr.createIndexExpr
uniqueness
(mkMaybeConcurrently strategy)
tableName
(fmap FieldDefinition.fieldNameToColumnName fieldNames)
migrationKey =
AttributeBasedIndexMigrationKey
{ indexKeyUniqueness = uniqueness
, indexKeyColumns = NEL.toList fieldNames
}
in
IndexDefinition
{ i_indexCreateExpr = expr
, i_indexMigrationKey = AttributeBasedIndexKey migrationKey
, i_indexCreationStrategy = Transactional
}
{- |
Constructs an 'IndexDefinition' for an index with the given uniqueness, given
name, and given SQL.
@since 1.0.0.0
-}
mkNamedIndexDefinition ::
Expr.IndexUniqueness ->
String ->
Expr.IndexBodyExpr ->
IndexDefinition
mkNamedIndexDefinition uniqueness indexName bodyExpr =
let
expr strategy tableName =
Expr.createNamedIndexExpr
uniqueness
(mkMaybeConcurrently strategy)
tableName
(Expr.indexName indexName)
bodyExpr
in
IndexDefinition
{ i_indexCreateExpr = expr
, i_indexMigrationKey = NamedIndexKey indexName
, i_indexCreationStrategy = Transactional
}
{- |
Internal helper to determine whether @CONCURRENTLY@ should be included in
the SQL to create the index.
@since 1.0.0.0
-}
mkMaybeConcurrently :: IndexCreationStrategy -> Maybe Expr.ConcurrentlyExpr
mkMaybeConcurrently strategy =
case strategy of
Transactional -> Nothing
Concurrent -> Just Expr.concurrently