content: {-|
  Module: IHP.IDE.SchemaDesigner.SchemaOperations
  Description: Apply high level operations to the Schema.sql
  Copyright: (c) digitally induced GmbH, 2021
  -}
  module IHP.IDE.SchemaDesigner.SchemaOperations where
  
  import IHP.Prelude
  import IHP.IDE.SchemaDesigner.Types
  import Data.Maybe (fromJust)
  import qualified Data.List as List
  import qualified Data.Text as Text
  
  -- | A Schema.sql basically is just a list of sql DDL statements
  type Schema = [Statement]
  
  -- | Creates a new tables with a 'id' columns as the primary key
  addTable :: Text -> Schema -> Schema
  addTable tableName list = list <> [StatementCreateTable CreateTable
      { name = tableName
      , columns =
          [Column
              { name = "id"
              , columnType = PUUID
              , defaultValue = Just (CallExpression "uuid_generate_v4" [])
              , notNull = True
              , isUnique = False
              , generator = Nothing
              }]
      , primaryKeyConstraint = PrimaryKeyConstraint ["id"]
      , constraints = []
      , unlogged = False
      }]
  
  
  data AddColumnOptions = AddColumnOptions
      { tableName :: !Text
      , columnName :: !Text
      , columnType :: !PostgresType
      , defaultValue :: !(Maybe Expression)
      , isArray :: !Bool
      , allowNull :: !Bool
      , isUnique :: !Bool
      , isReference :: !Bool
      , referenceTable :: !(Maybe Text)
      , primaryKey :: !Bool
      , withIndex :: !Bool
      , autoPolicy :: !Bool
      }
  addColumn :: AddColumnOptions -> Schema -> Schema
  addColumn options@(AddColumnOptions { .. }) =
      let
          column = newColumn options
          addColumnToTable :: Text -> Column -> Bool -> Statement -> Statement
          addColumnToTable tableName (column@Column { name = columnName }) isPrimaryKey (StatementCreateTable table@CreateTable { name, columns, primaryKeyConstraint = PrimaryKeyConstraint pks})
              | name == tableName =
                  let primaryKeyConstraint =
                        if isPrimaryKey
                        then PrimaryKeyConstraint (pks <> [columnName])
                        else PrimaryKeyConstraint pks
                  in StatementCreateTable (table { columns = columns <> [column] , primaryKeyConstraint })
          addColumnToTable tableName column isPrimaryKey statement = statement
  
          addTableOp :: Schema -> Schema = map (addColumnToTable tableName column primaryKey)
  
          foreignKeyConstraint = newForeignKeyConstraint tableName columnName (fromJust referenceTable)
          index = newColumnIndex tableName columnName
  
          handleAutoPolicy statements =
              if autoPolicy
                  then
                      let
                          isTable (StatementCreateTable CreateTable { name }) = name == tableName
                          isTable otherwise = False
                          (Just table) = find isTable statements
                          suggestedPolicy = suggestPolicy statements table
                      in if (suggestedPolicy.name /= "" && not (doesHaveExistingPolicies statements tableName))
                              then
                                  statements
                                  |> enableRowLevelSecurity tableName
                                  |> addPolicy AddPolicyOptions
                                          { tableName = tableName
                                          , name = suggestedPolicy.name
                                          , using = suggestedPolicy.using
                                          , check = suggestedPolicy.check
                                          }
                              else statements
                  else statements
      in
          if isReference then
              \statements -> statements
              |> addTableOp
              |> appendStatement index
              |> appendStatement foreignKeyConstraint
              |> handleAutoPolicy
          else
              addTableOp
              . (if withIndex
                      then appendStatement index
                      else \schema -> schema)
              . (if columnName == "updated_at"
                  then addUpdatedAtTrigger tableName
                  else \schema -> schema)
  
  data UpdateColumnOptions = UpdateColumnOptions
      { tableName :: !Text
      , columnName :: !Text
      , columnType :: !PostgresType
      , defaultValue :: !(Maybe Expression)
      , isArray :: !Bool
      , allowNull :: !Bool
      , isUnique :: !Bool
      , primaryKey :: !Bool
      , columnId :: !Int
      }
  updateColumn :: UpdateColumnOptions -> Schema -> Schema
  updateColumn options@(UpdateColumnOptions { .. }) schema =
      let
          updateColumnAtIndex :: [Column] -> [Column]
          updateColumnAtIndex columns = mapWithIndex updateColumnAtIndex' columns
  
          mapWithIndex :: (a -> Int -> b) -> [a] -> [b]
          mapWithIndex mapFn items = mapWithIndex' mapFn items 0
              where
                  mapWithIndex' :: (a -> Int -> b) -> [a] -> Int -> [b]
                  mapWithIndex' mapFn [] _ = []
                  mapWithIndex' mapFn (item:rest) i = (mapFn item i):(mapWithIndex' mapFn rest (i + 1))
  
          updateColumnAtIndex' :: Column -> Int -> Column
          updateColumnAtIndex' column index | index == columnId = column
                  { name = columnName
                  , columnType = arrayifytype isArray columnType
                  , defaultValue = defaultValue
                  , notNull = not allowNull
                  , isUnique
                  }
          updateColumnAtIndex' column index = column
  
          updateTableOp :: [Statement] -> [Statement]
          updateTableOp = map \case
                  (StatementCreateTable table@(CreateTable { name, columns, primaryKeyConstraint })) | name == tableName ->
                      let
                          oldColumn :: Column
                          oldColumn = columns
                                  |> (\c -> zip c [0..])
                                  |> find ((\(c, index) -> index == columnId))
                                  |> fromMaybe (error "could not find column with id")
                                  |> fst
                      in StatementCreateTable $ (table :: CreateTable)
                              { columns = updateColumnAtIndex columns
                              , primaryKeyConstraint = updatePrimaryKeyConstraint oldColumn primaryKey primaryKeyConstraint
                              }
                  otherwise -> otherwise
  
          -- | Add or remove a column from the primary key constraint
          updatePrimaryKeyConstraint :: Column -> Bool -> PrimaryKeyConstraint -> PrimaryKeyConstraint
          updatePrimaryKeyConstraint Column { name } isPrimaryKey primaryKeyConstraint@PrimaryKeyConstraint { primaryKeyColumnNames } =
            case (isPrimaryKey, name `elem` primaryKeyColumnNames) of
                (False, False) -> primaryKeyConstraint
                (False, True) -> PrimaryKeyConstraint (filter (/= name) primaryKeyColumnNames)
                (True, False) -> PrimaryKeyConstraint (primaryKeyColumnNames <> [name])
                (True, True) -> primaryKeyConstraint
  
          updateForeignKeyConstraints = map \case
                  statement@(AddConstraint { tableName = constraintTable, constraint = constraint@(ForeignKeyConstraint { name = fkName, columnName = fkColumnName  })  }) | constraintTable == tableName && fkColumnName == (oldColumn.name) ->
                      let newName = Text.replace (oldColumn.name) columnName <$> fkName
                      in statement { constraint = constraint { columnName, name = newName } }
                  index@(CreateIndex { indexName, tableName = indexTable, columns = indexColumns }) | indexTable == tableName ->
                      let
                          updateIndexColumn :: IndexColumn -> IndexColumn
                          updateIndexColumn indexColumn@(IndexColumn { column = VarExpression varName }) | varName == (oldColumn.name) = indexColumn { column = VarExpression columnName }
                          updateIndexColumn otherwise = otherwise
                      in
                          (index :: Statement) { columns = map updateIndexColumn indexColumns, indexName = Text.replace (oldColumn.name) columnName indexName }
                  otherwise -> otherwise
          findOldColumn statements = mapMaybe findOldColumn' statements
                  |> head
                  |> fromMaybe (error "Could not find old column")
          findOldColumn' (StatementCreateTable table@(CreateTable { name, columns, primaryKeyConstraint })) | name == tableName =
                      let
                          oldColumn :: Column
                          oldColumn = columns
                                  |> (\c -> zip c [0..])
                                  |> find ((\(c, index) -> index == columnId))
                                  |> fromMaybe (error "could not find column with id")
                                  |> fst
                      in
                          Just oldColumn
          findOldColumn' _ = Nothing
  
          oldColumn :: Column
          oldColumn = findOldColumn schema
      in
          schema
          |> updateTableOp
          |> updateForeignKeyConstraints
  
  newColumn :: AddColumnOptions -> Column
  newColumn AddColumnOptions { .. } = Column
      { name = columnName
      , columnType = arrayifytype isArray columnType
      , defaultValue = defaultValue
      , notNull = (not allowNull)
      , isUnique = isUnique
      , generator = Nothing
      }
  
  newForeignKeyConstraint :: Text -> Text -> Text -> Statement
  newForeignKeyConstraint tableName columnName referenceTable =
      AddConstraint
      { tableName
      , constraint = ForeignKeyConstraint
          { name = Just $ tableName <> "_ref_" <> columnName
          , columnName = columnName
          , referenceTable = referenceTable
          , referenceColumn = "id"
          , onDelete = (Just NoAction)
          }
      , deferrable = Nothing
      , deferrableType = Nothing
      }
  
  newColumnIndex :: Text -> Text -> Statement
  newColumnIndex tableName columnName =
      CreateIndex
      { indexName = tableName <> "_" <> columnName <> "_index"
      , unique = False
      , tableName
      , columns = [IndexColumn { column = VarExpression columnName, columnOrder = [] }]
      , whereClause = Nothing
      , indexType = Nothing
      }
  
  data AddIndexOptions = AddIndexOptions
      { tableName :: !Text
      , columnName :: !Text
      }
  addIndex :: AddIndexOptions -> Schema -> Schema
  addIndex AddIndexOptions { tableName, columnName } =
      appendStatement (newColumnIndex tableName columnName)
  
  appendStatement :: Statement -> [Statement] -> [Statement]
  appendStatement statement statements = statements <> [statement]
  
  arrayifytype :: Bool -> PostgresType -> PostgresType
  arrayifytype False   (PArray coltype) = coltype
  arrayifytype True  a@(PArray coltype) = a
  arrayifytype False coltype = coltype
  arrayifytype True  coltype = PArray coltype
  
  addForeignKeyConstraint :: Text -> Text -> Text -> Text -> OnDelete -> [Statement] -> [Statement]
  addForeignKeyConstraint tableName columnName constraintName referenceTable onDelete list = list <> [AddConstraint { tableName = tableName, constraint = ForeignKeyConstraint { name = Just constraintName, columnName = columnName, referenceTable = referenceTable, referenceColumn = "id", onDelete = (Just onDelete) }, deferrable = Nothing, deferrableType = Nothing }]
  
  addTableIndex :: Text -> Bool -> Text -> [Text] -> [Statement] -> [Statement]
  addTableIndex indexName unique tableName columnNames list = list <> [CreateIndex { indexName, unique, tableName, columns = columnNames |> map (\columnName -> IndexColumn { column = VarExpression columnName, columnOrder = [] }), whereClause = Nothing, indexType = Nothing }]
  
  -- | An enum is added after all existing enum statements, but right before @CREATE TABLE@ statements
  addEnum :: Text -> Schema -> Schema
  addEnum enumName statements = a <> enum <> b
      where
          enum = [CreateEnumType { name = enumName, values = []}]
          (a, b) = List.splitAt insertionIndex statements
  
          insertionIndex = findInsertionIndex statements 0
  
          -- Finds the index after comments and existing enum types, just before the CREATE TABLE statements
          findInsertionIndex ((Comment{}):xs) !i = findInsertionIndex xs (i + 1)
          findInsertionIndex ((CreateEnumType{}):xs) !i = findInsertionIndex xs (i + 1)
          findInsertionIndex (x:xs) !i = i
          findInsertionIndex [] !i = i
  
  addValueToEnum :: Text -> Text -> Schema -> Schema
  addValueToEnum enumName enumValueName statements = map addValueToEnum' statements
      where
          addValueToEnum' (table@CreateEnumType { name, values }) | name == enumName =
              table { values = values <> [enumValueName] }
          addValueToEnum' statement = statement
  
  data UpdatePolicyOptions = UpdatePolicyOptions
      { currentName :: !Text -- ^ Current name of the policy
      , tableName :: !Text -- ^ Table of the policy
      , name :: !Text -- ^ New name of the policy
      , using :: !(Maybe Expression)
      , check :: !(Maybe Expression)
      }
  
  updatePolicy :: UpdatePolicyOptions -> Schema -> Schema
  updatePolicy UpdatePolicyOptions { .. } statements =
          statements
          |> map updatePolicy'
      where
          updatePolicy' policy@CreatePolicy { name = pName, action, tableName = pTable } | pName == currentName && pTable == tableName = CreatePolicy { tableName, action, name, using, check }
          updatePolicy' otherwise                                                                                              = otherwise
  
  data AddPolicyOptions = AddPolicyOptions
      { tableName :: !Text
      , name :: !Text
      , using :: !(Maybe Expression)
      , check :: !(Maybe Expression)
      }
  
  addPolicy :: AddPolicyOptions -> Schema -> Schema
  addPolicy AddPolicyOptions { .. } statements = statements <> createPolicyStatement
      where
          createPolicyStatement = [ CreatePolicy { tableName, action = Nothing, name, using, check } ]
  
  data DeletePolicyOptions = DeletePolicyOptions
      { tableName :: !Text
      , policyName :: !Text
      }
  
  deletePolicy :: DeletePolicyOptions -> Schema -> Schema
  deletePolicy DeletePolicyOptions { .. } statements =
          statements
          |> filter (not . isSelectedPolicy)
      where
          isSelectedPolicy :: Statement -> Bool
          isSelectedPolicy policy@CreatePolicy { name = pName, tableName = pTable } = pName == policyName && pTable == tableName
          isSelectedPolicy otherwise                                                = False
  
  enableRowLevelSecurity :: Text -> Schema -> Schema
  enableRowLevelSecurity tableName schema =
      let
          rlsEnabled = schema
                  |> find \case
                      EnableRowLevelSecurity { tableName = rlsTable } -> rlsTable == tableName
                      otherwise                                       -> False
                  |> isJust
      in if rlsEnabled
          then schema
          else schema <> [ EnableRowLevelSecurity { tableName } ]
  
  disableRowLevelSecurity :: Text -> Schema -> Schema
  disableRowLevelSecurity tableName schema = schema
          |> filter \case
              EnableRowLevelSecurity { tableName = rlsTable } -> rlsTable /= tableName
              otherwise                                       -> True
  
  disableRowLevelSecurityIfNoPolicies :: Text -> Schema -> Schema
  disableRowLevelSecurityIfNoPolicies tableName schema =
      let
          tableHasPolicies = schema
                  |> find \case
                      CreatePolicy { tableName = policyTable } -> policyTable == tableName
                      otherwise                                -> False
                  |> isJust
      in if tableHasPolicies
          then schema
          else disableRowLevelSecurity tableName schema
  
  
  -- | Checks if there exists a @user_id@ column, and returns a policy based on that.
  -- If there's no @user_id@ field on the table it will return an empty policy
  --
  -- This function also follows foreign keys to find the shortest path to a user_id.
  -- E.g. when having a schema post_meta_tags (no user_id column) <-> posts (has a user_id) <-> users:
  --
  -- >                                                 post_id
  -- >                            posts_meta_infos ────────────────►  posts
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │ user_id
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │
  -- >                                                                 │
  -- >                                          users  ◄───────────────┘
  --
  suggestPolicy :: Schema -> Statement -> Statement
  suggestPolicy schema (StatementCreateTable CreateTable { name = tableName, columns })
      | isJust (find isUserIdColumn columns)  = CreatePolicy
          { name = "Users can manage their " <> tableName
          , action = Nothing
          , tableName
          , using = Just compareUserId
          , check = Just compareUserId
          }
      where
          compareUserId = EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" [])
  suggestPolicy schema (StatementCreateTable CreateTable { name = tableName, columns }) =
              columnsWithFKAndRefTable
                  |> mapMaybe columnWithFKAndRefTableToPolicy
                  |> head
                  |> fromMaybe (emptyPolicy)
          where
              referenced = columns
  
              columnWithFKAndRefTableToPolicy :: (Column, Constraint, CreateTable) -> Maybe Statement
              columnWithFKAndRefTableToPolicy (column, ForeignKeyConstraint { referenceColumn }, CreateTable { name = refTableName, columns = refTableColumns }) | isJust (find isUserIdColumn refTableColumns) = Just CreatePolicy
                      { name = "Users can manage the " <> tableName <> " if they can see the " <> tableNameToModelName refTableName
                      , action = Nothing
                      , tableName
                      , using = Just delegateCheck
                      , check = Just delegateCheck
                      }
                  where
                      delegateCheck = ExistsExpression (
                              SelectExpression (
                                  Select
                                  { columns = [IntExpression 1]
                                  , from = DotExpression (VarExpression "public") refTableName
                                  , alias = Nothing
                                  , whereClause = EqExpression (DotExpression (VarExpression refTableName) refColumnName) (DotExpression (VarExpression tableName) (column.name))
                                  }
                              )
                          )
                      refColumnName = referenceColumn |> fromMaybe "id"
  
              columnWithFKAndRefTableToPolicy otherwise = Nothing
  
  
              columnsWithFKAndRefTable :: [(Column, Constraint, CreateTable)]
              columnsWithFKAndRefTable =
                   columns
                   |> map findFK
                   |> zip columns
                   |> mapMaybe \case
                          (col, Just fk) -> Just (col, fk)
                          (col, Nothing) -> Nothing
                   |> map (\(column, fk) -> (column, fk, resolveFK fk))
                   |> mapMaybe  \case
                          (column, fk, Just refTable) -> Just (column, fk, refTable)
                          (column, fk, Nothing)       -> Nothing
  
              findFK :: Column -> Maybe Constraint
              findFK column =
                  schema
                      |> mapMaybe (\case
                          AddConstraint { tableName = fkTable, constraint = fk@(ForeignKeyConstraint { columnName = fkCol }) } ->
                              if fkTable == tableName && fkCol == column.name
                                  then Just fk
                                  else Nothing
                          otherwise -> Nothing)
                      |> head
  
              resolveFK :: Constraint -> Maybe CreateTable
              resolveFK ForeignKeyConstraint { referenceTable } = schema
                      |> find \case
                          StatementCreateTable CreateTable {  name }  -> name == referenceTable
                          otheriwse                                   -> False
                      |> fmap \case
                          StatementCreateTable table -> table
  
              emptyPolicy = CreatePolicy { name = "", action = Nothing, tableName, using = Nothing, check = Nothing }
  
  isUserIdColumn :: Column -> Bool
  isUserIdColumn Column { name = "user_id" } = True
  isUserIdColumn otherwise                   = False
  
  
  deleteTable :: Text -> Schema -> Schema
  deleteTable tableName statements =
      statements
      |> filter \case
          StatementCreateTable CreateTable { name }       | name == tableName            -> False
          AddConstraint { tableName = constraintTable }   | constraintTable == tableName -> False
          CreateIndex { tableName = indexTable }          | indexTable == tableName      -> False
          EnableRowLevelSecurity { tableName = rlsTable } | rlsTable == tableName        -> False
          CreatePolicy { tableName = policyTable }        | policyTable == tableName     -> False
          CreateTrigger { tableName = triggerTable }      | triggerTable == tableName    -> False
          otherwise -> True
  
  updateTable :: Int -> Text -> Schema -> Schema
  updateTable tableId tableName statements =
      let
          oldTableName = ((.name) . unsafeGetCreateTable) (statements !! tableId)
      in
          statements
          |> map \case
              (StatementCreateTable table@(CreateTable { name })) | name == oldTableName -> StatementCreateTable (table { name = tableName })
              constraint@(AddConstraint { tableName = constraintTable, constraint = c }) | constraintTable == oldTableName -> (constraint :: Statement) { tableName, constraint = c { name = Text.replace oldTableName tableName <$> (c.name) } }
              index@(CreateIndex { tableName = indexTable, indexName }) | indexTable == oldTableName -> (index :: Statement) { tableName, indexName = Text.replace oldTableName tableName indexName } 
              rls@(EnableRowLevelSecurity { tableName = rlsTable }) | rlsTable == oldTableName -> (rls :: Statement) { tableName }
              policy@(CreatePolicy { tableName = policyTable, name }) | policyTable == oldTableName -> (policy :: Statement) { tableName, name = Text.replace oldTableName tableName name }
              trigger@(CreateTrigger { tableName = triggerTable, name }) | triggerTable == oldTableName -> (trigger :: Statement) { tableName, name = Text.replace oldTableName tableName name }
              otherwise -> otherwise  
  
  
  updatedAtTriggerName :: Text -> Text
  updatedAtTriggerName tableName = "update_" <> tableName <> "_updated_at"
  
  addUpdatedAtTrigger :: Text -> [Statement] -> [Statement]
  addUpdatedAtTrigger tableName schema =
          addFunctionOperator <> schema <> [trigger]
      where
          trigger :: Statement
          trigger = CreateTrigger
              { name = updatedAtTriggerName tableName
              , eventWhen = Before
              , event = TriggerOnUpdate
              , tableName
              , for = ForEachRow
              , whenCondition = Nothing
              , functionName = setUpdatedAtToNowTrigger.functionName
              , arguments = []
              }
  
          addFunctionOperator :: [Statement]
          addFunctionOperator =
              if hasFunction (setUpdatedAtToNowTrigger.functionName)
                  then []
                  else [setUpdatedAtToNowTrigger]
  
          hasFunction :: Text -> Bool
          hasFunction name = schema
                  |> find \case
                      CreateFunction { functionName = fnName } -> name == fnName
                      otherwise -> False
                  |> isJust
  
          setUpdatedAtToNowTrigger :: Statement
          setUpdatedAtToNowTrigger = 
              CreateFunction
                  { functionName = "set_updated_at_to_now"
                  , functionBody = "\n" <> [trimming|
                      BEGIN
                          NEW.updated_at = NOW();
                          RETURN NEW;
                      END;
                  |] <> "\n"
                  , functionArguments = []
                  , orReplace = False
                  , returns = PTrigger
                  , language = "plpgsql"
                  }
  
  deleteTriggerIfExists :: Text -> [Statement] -> [Statement]
  deleteTriggerIfExists triggerName statements = filter (not . isTheTriggerToBeDeleted) statements
      where
          isTheTriggerToBeDeleted CreateTrigger { name } = triggerName == name
          isTheTriggerToBeDeleted _                      = False
  
  data DeleteColumnOptions
      = DeleteColumnOptions
      { tableName :: !Text
      , columnName :: !Text
      , columnId :: !Int
      }
  
  deleteColumn :: DeleteColumnOptions -> Schema -> Schema
  deleteColumn DeleteColumnOptions { .. } schema =
          schema
          |> map deleteColumnInTable
          |> (filter \case
                  AddConstraint { tableName = fkTable, constraint = ForeignKeyConstraint { columnName = fkColumn } } | fkTable == tableName && fkColumn == columnName -> False
                  index@(CreateIndex {}) | isIndexStatementReferencingTableColumn index tableName columnName -> False
                  otherwise -> True
              )
          |> (if columnName == "updated_at"
                  then deleteTriggerIfExists (updatedAtTriggerName tableName)
                  else \schema -> schema
              )
          |> filter deletePolicyReferencingPolicy
      where
          deleteColumnInTable :: Statement -> Statement
          deleteColumnInTable (StatementCreateTable table@CreateTable { name, columns }) | name == tableName = StatementCreateTable $ table { columns = delete (columns !! columnId) columns}
          deleteColumnInTable statement = statement
  
          deletePolicyReferencingPolicy :: Statement -> Bool
          deletePolicyReferencingPolicy CreatePolicy { tableName = policyTable, using, check } | policyTable == tableName = 
              case (using, check) of
                  (Just using, Nothing) -> not (isRef using)
                  (Nothing, Just check) -> not (isRef check)
                  (Just using, Just check) -> not (isRef using && isRef check)
              where
                  isRef :: Expression -> Bool
                  isRef (TextExpression {}) = False
                  isRef (VarExpression var) = var == columnName
                  isRef (CallExpression _ args) = foldl' (||) False (map isRef args)
                  isRef (NotEqExpression a b) = isRef a || isRef b
                  isRef (EqExpression a b) = isRef a || isRef b
                  isRef (AndExpression a b) = isRef a || isRef b
                  isRef (IsExpression a b) = isRef a || isRef b
                  isRef (InExpression a b) = isRef a || isRef b
                  isRef (NotExpression a) = isRef a
                  isRef (ExistsExpression a) = isRef a
                  isRef (OrExpression a b) = isRef a || isRef b
                  isRef (LessThanExpression a b) = isRef a || isRef b
                  isRef (LessThanOrEqualToExpression a b) = isRef a || isRef b
                  isRef (GreaterThanExpression a b) = isRef a || isRef b
                  isRef (GreaterThanOrEqualToExpression a b) = isRef a || isRef b
                  isRef (DoubleExpression _) = False
                  isRef (IntExpression _) = False
                  isRef (TypeCastExpression a _) = isRef a
                  isRef (SelectExpression _) = False
                  isRef (DotExpression a _) = isRef a
                  isRef (ConcatenationExpression a b) = isRef a || isRef b
          deletePolicyReferencingPolicy otherwise = True
  
  -- | Returns True if a CreateIndex statement references a specific column
  --
  -- E.g. given a schema like this:
  -- > CREATE TABLE users (
  -- >     email TEXT NOT NULL
  -- > );
  -- >
  -- > CREATE UNIQUE INDEX users_email_index ON users (LOWER(email));
  -- >
  --
  -- You can find all indices to the email column of the users table like this:
  --
  -- >>> filter (isIndexStatementReferencingTableColumn "users" "email") database
  -- [CreateIndex { indexName = "users_email", unique = True, tableName = "users", expressions = [CallExpression "LOWER" [VarEpression "email"]] }]
  --
  isIndexStatementReferencingTableColumn :: Statement -> Text -> Text -> Bool
  isIndexStatementReferencingTableColumn statement tableName columnName = isReferenced statement
      where
          -- | Returns True if a statement is an CreateIndex statement that references our specific column
          --
          -- An index references a table if it references the target table and one of the index expressions contains a reference to our column
          isReferenced :: Statement -> Bool
          isReferenced CreateIndex { tableName = indexTableName, columns } = indexTableName == tableName && expressionsReferencesColumn (map (.column) columns)
          isReferenced otherwise = False
  
          -- | Returns True if a list of expressions references the columnName
          expressionsReferencesColumn :: [Expression] -> Bool
          expressionsReferencesColumn expressions = expressions
                  |> map expressionReferencesColumn
                  |> List.or
  
          -- | Walks the expression tree and returns True if there's a VarExpression with the column name
          expressionReferencesColumn :: Expression -> Bool
          expressionReferencesColumn = \case
              TextExpression _ -> False
              VarExpression varName -> varName == columnName
              CallExpression _ expressions -> expressions
                      |> map expressionReferencesColumn
                      |> List.or
              NotEqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              EqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              AndExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              IsExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              NotExpression a -> expressionReferencesColumn a
              OrExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              LessThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              LessThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              GreaterThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
              GreaterThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
  
  doesHaveExistingPolicies :: [Statement] -> Text -> Bool
  doesHaveExistingPolicies statements tableName = statements
                  |> find \case
                      CreatePolicy { tableName = tableName' } -> tableName' == tableName
                      otherwise                               -> False
                  |> isJust
  
  
  deleteIndex :: Text -> Schema -> Schema
  deleteIndex indexName statements =
      statements
      |> filter \case
          CreateIndex { indexName = name } | name == indexName -> False 
          otherwise                                            -> True
  
  
  data UpdateIndexOptions = UpdateIndexOptions
      { indexName :: !Text
      , newIndexName :: !Text
      , indexColumns :: ![IndexColumn]
      }
  updateIndex :: UpdateIndexOptions -> Schema -> Schema
  updateIndex UpdateIndexOptions { indexName, newIndexName, indexColumns } schema =
          map updateFn schema
       where
          updateFn :: Statement -> Statement
          updateFn index@(CreateIndex { indexName = n }) | n == indexName = index { indexName = newIndexName, columns = indexColumns }
          updateFn statement = statement