Skip to content

Commit 0836c6e

Browse files
committed
Added auto-setup for updated_at trigger
1 parent 4f8c4f0 commit 0836c6e

File tree

2 files changed

+139
-58
lines changed

2 files changed

+139
-58
lines changed

IHP/IDE/SchemaDesigner/Controller/Columns.hs

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,14 @@ instance Controller ColumnsController where
110110
let tableName = param "tableName"
111111
let columnId = param "columnId"
112112
let columnName = param "columnName"
113-
case findForeignKey statements tableName columnName of
114-
Just AddConstraint { constraint = constraint@(ForeignKeyConstraint { name = Just constraintName }) } -> updateSchema (deleteForeignKeyConstraint constraintName)
115-
otherwise -> pure ()
116113

117-
let indicesToDelete = findIndicesReferencingColumn statements (tableName, columnName)
118-
forEach indicesToDelete \CreateIndex { indexName } -> updateSchema (deleteTableIndex indexName)
119-
updateSchema (map (deleteColumnInTable tableName columnId))
114+
let options = SchemaOperations.DeleteColumnOptions
115+
{ tableName
116+
, columnName
117+
, columnId
118+
}
119+
120+
updateSchema $ SchemaOperations.deleteColumn options
120121

121122
redirectTo ShowTableAction { .. }
122123

@@ -249,55 +250,4 @@ validateColumn = validateNameInSchema "column name" [] Nothing
249250
referencingColumnForeignKeyConstraints tableName columnName =
250251
find \case
251252
AddConstraint { tableName = constraintTable, constraint = ForeignKeyConstraint { columnName = fkColumnName } } -> constraintTable == tableName && fkColumnName == columnName
252-
otherwise -> False
253-
254-
255-
-- | Returns the list of CreateIndex statements that reference a specific column
256-
--
257-
-- E.g. given a schema like this:
258-
-- > CREATE TABLE users (
259-
-- > email TEXT NOT NULL
260-
-- > );
261-
-- >
262-
-- > CREATE UNIQUE INDEX users_email_index ON users (LOWER(email));
263-
-- >
264-
--
265-
-- You can find all indices to the email column of the users table like this:
266-
--
267-
-- >>> findIndicesReferencingColumn database ("users", "email")
268-
-- [CreateIndex { indexName = "users_email", unique = True, tableName = "users", expressions = [CallExpression "LOWER" [VarEpression "email"]] }]
269-
--
270-
findIndicesReferencingColumn :: [Statement] -> (Text, Text) -> [Statement]
271-
findIndicesReferencingColumn database (tableName, columnName) = database |> filter isReferenced
272-
where
273-
-- | Returns True if a statement is an CreateIndex statement that references our specific column
274-
--
275-
-- An index references a table if it references the target table and one of the index expressions contains a reference to our column
276-
isReferenced :: Statement -> Bool
277-
isReferenced CreateIndex { tableName = indexTableName, columns } = indexTableName == tableName && expressionsReferencesColumn (map (get #column) columns)
278-
isReferenced otherwise = False
279-
280-
-- | Returns True if a list of expressions references the columnName
281-
expressionsReferencesColumn :: [Expression] -> Bool
282-
expressionsReferencesColumn expressions = expressions
283-
|> map expressionReferencesColumn
284-
|> List.or
285-
286-
-- | Walks the expression tree and returns True if there's a VarExpression with the column name
287-
expressionReferencesColumn :: Expression -> Bool
288-
expressionReferencesColumn = \case
289-
TextExpression _ -> False
290-
VarExpression varName -> varName == columnName
291-
CallExpression _ expressions -> expressions
292-
|> map expressionReferencesColumn
293-
|> List.or
294-
NotEqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
295-
EqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
296-
AndExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
297-
IsExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
298-
NotExpression a -> expressionReferencesColumn a
299-
OrExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
300-
LessThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
301-
LessThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
302-
GreaterThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
303-
GreaterThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
253+
otherwise -> False

IHP/IDE/SchemaDesigner/SchemaOperations.hs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ addColumn options@(AddColumnOptions { .. }) =
7373
. (if withIndex
7474
then appendStatement index
7575
else \schema -> schema)
76+
. (if columnName == "updated_at"
77+
then addUpdatedAtTrigger tableName
78+
else \schema -> schema)
7679

7780
newColumn :: AddColumnOptions -> Column
7881
newColumn AddColumnOptions { .. } = Column
@@ -333,3 +336,131 @@ deleteTable tableName statements =
333336
CreatePolicy { tableName = policyTable } | policyTable == tableName -> False
334337
CreateTrigger { tableName = triggerTable } | triggerTable == tableName -> False
335338
otherwise -> True
339+
340+
updatedAtTriggerName :: Text -> Text
341+
updatedAtTriggerName tableName = "update_" <> tableName <> "_updated_at"
342+
343+
addUpdatedAtTrigger :: Text -> [Statement] -> [Statement]
344+
addUpdatedAtTrigger tableName schema =
345+
addFunctionOperator <> schema <> [trigger]
346+
where
347+
trigger :: Statement
348+
trigger = CreateTrigger
349+
{ name = updatedAtTriggerName tableName
350+
, eventWhen = Before
351+
, event = TriggerOnUpdate
352+
, tableName
353+
, for = ForEachRow
354+
, whenCondition = Nothing
355+
, functionName = get #functionName setUpdatedAtToNowTrigger
356+
, arguments = []
357+
}
358+
359+
addFunctionOperator :: [Statement]
360+
addFunctionOperator =
361+
if hasFunction (get #functionName setUpdatedAtToNowTrigger)
362+
then []
363+
else [setUpdatedAtToNowTrigger]
364+
365+
hasFunction :: Text -> Bool
366+
hasFunction name = schema
367+
|> find \case
368+
CreateFunction { functionName = fnName } -> name == fnName
369+
otherwise -> False
370+
|> isJust
371+
372+
setUpdatedAtToNowTrigger :: Statement
373+
setUpdatedAtToNowTrigger =
374+
CreateFunction
375+
{ functionName = "set_updated_at_to_now"
376+
, functionBody = "\n" <> [trimming|
377+
BEGIN
378+
NEW.updated_at = NOW();
379+
RETURN NEW;
380+
END;
381+
|] <> "\n"
382+
, functionArguments = []
383+
, orReplace = False
384+
, returns = PTrigger
385+
, language = "plpgsql"
386+
}
387+
388+
deleteTriggerIfExists :: Text -> [Statement] -> [Statement]
389+
deleteTriggerIfExists triggerName statements = filter (not . isTheTriggerToBeDeleted) statements
390+
where
391+
isTheTriggerToBeDeleted CreateTrigger { name } = triggerName == name
392+
isTheTriggerToBeDeleted _ = False
393+
394+
data DeleteColumnOptions
395+
= DeleteColumnOptions
396+
{ tableName :: !Text
397+
, columnName :: !Text
398+
, columnId :: !Int
399+
}
400+
401+
deleteColumn :: DeleteColumnOptions -> Schema -> Schema
402+
deleteColumn DeleteColumnOptions { .. } schema =
403+
schema
404+
|> map deleteColumnInTable
405+
|> (filter \case
406+
AddConstraint { tableName = fkTable, constraint = ForeignKeyConstraint { columnName = fkColumn } } | fkTable == tableName && fkColumn == columnName -> False
407+
index@(CreateIndex {}) | isIndexStatementReferencingTableColumn index tableName columnName -> False
408+
)
409+
|> (if columnName == "updated_at"
410+
then deleteTriggerIfExists (updatedAtTriggerName tableName)
411+
else \schema -> schema
412+
)
413+
where
414+
deleteColumnInTable :: Statement -> Statement
415+
deleteColumnInTable (StatementCreateTable table@CreateTable { name, columns }) | name == tableName = StatementCreateTable $ table { columns = delete (columns !! columnId) columns}
416+
deleteColumnInTable statement = statement
417+
418+
-- | Returns True if a CreateIndex statement references a specific column
419+
--
420+
-- E.g. given a schema like this:
421+
-- > CREATE TABLE users (
422+
-- > email TEXT NOT NULL
423+
-- > );
424+
-- >
425+
-- > CREATE UNIQUE INDEX users_email_index ON users (LOWER(email));
426+
-- >
427+
--
428+
-- You can find all indices to the email column of the users table like this:
429+
--
430+
-- >>> filter (isIndexStatementReferencingTableColumn "users" "email") database
431+
-- [CreateIndex { indexName = "users_email", unique = True, tableName = "users", expressions = [CallExpression "LOWER" [VarEpression "email"]] }]
432+
--
433+
isIndexStatementReferencingTableColumn :: Statement -> Text -> Text -> Bool
434+
isIndexStatementReferencingTableColumn statement tableName columnName = isReferenced statement
435+
where
436+
-- | Returns True if a statement is an CreateIndex statement that references our specific column
437+
--
438+
-- An index references a table if it references the target table and one of the index expressions contains a reference to our column
439+
isReferenced :: Statement -> Bool
440+
isReferenced CreateIndex { tableName = indexTableName, columns } = indexTableName == tableName && expressionsReferencesColumn (map (get #column) columns)
441+
isReferenced otherwise = False
442+
443+
-- | Returns True if a list of expressions references the columnName
444+
expressionsReferencesColumn :: [Expression] -> Bool
445+
expressionsReferencesColumn expressions = expressions
446+
|> map expressionReferencesColumn
447+
|> List.or
448+
449+
-- | Walks the expression tree and returns True if there's a VarExpression with the column name
450+
expressionReferencesColumn :: Expression -> Bool
451+
expressionReferencesColumn = \case
452+
TextExpression _ -> False
453+
VarExpression varName -> varName == columnName
454+
CallExpression _ expressions -> expressions
455+
|> map expressionReferencesColumn
456+
|> List.or
457+
NotEqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
458+
EqExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
459+
AndExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
460+
IsExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
461+
NotExpression a -> expressionReferencesColumn a
462+
OrExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
463+
LessThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
464+
LessThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
465+
GreaterThanExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b
466+
GreaterThanOrEqualToExpression a b -> expressionReferencesColumn a || expressionReferencesColumn b

0 commit comments

Comments
 (0)