@@ -73,6 +73,9 @@ addColumn options@(AddColumnOptions { .. }) =
73
73
. (if withIndex
74
74
then appendStatement index
75
75
else \ schema -> schema)
76
+ . (if columnName == " updated_at"
77
+ then addUpdatedAtTrigger tableName
78
+ else \ schema -> schema)
76
79
77
80
newColumn :: AddColumnOptions -> Column
78
81
newColumn AddColumnOptions { .. } = Column
@@ -333,3 +336,131 @@ deleteTable tableName statements =
333
336
CreatePolicy { tableName = policyTable } | policyTable == tableName -> False
334
337
CreateTrigger { tableName = triggerTable } | triggerTable == tableName -> False
335
338
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