Skip to content

Commit 817ebed

Browse files
authored
Extend the Trigger interface (#119)
Add support for some regular triggers, ie: `AFTER` triggers without constraints and `BEFORE` triggers.
1 parent eee38fb commit 817ebed

3 files changed

Lines changed: 117 additions & 46 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
nested `AND` and `OR` conditions.
88
* Add `SqlWhereAll` and `SqlWhereAny` so they can be used in signatures.
99
* Add rudimentary support for enum types.
10+
* Add support for some regular triggers, ie: `AFTER` triggers without constraints
11+
and `BEFORE` triggers.
1012

1113
# hpqtypes-extras-1.16.4.4 (2023-08-23)
1214
* Switch from `cryptonite` to `crypton`.

‎src/Database/PostgreSQL/PQTypes/Model/Trigger.hs‎

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
-- |
22
-- Module: Database.PostgreSQL.PQTypes.Model.Trigger
33
--
4-
-- Trigger name must be unique among triggers of same table. Only @CONSTRAINT@ triggers are
5-
-- supported. They can only be run @AFTER@ an event. The associated functions are always
6-
-- created with no arguments and always @RETURN TRIGGER@.
4+
-- Trigger name must be unique among triggers of the same table.
75
--
8-
-- For details, see <https://www.postgresql.org/docs/11/sql-createtrigger.html>.
6+
-- For details, see <https://www.postgresql.org/docs/current/sql-createtrigger.html>.
97
module Database.PostgreSQL.PQTypes.Model.Trigger
108
( -- * Triggers
11-
TriggerEvent (..)
9+
TriggerKind (..)
10+
, TriggerActionTime (..)
11+
, TriggerEvent (..)
1212
, Trigger (..)
1313
, triggerMakeName
1414
, triggerBaseName
@@ -22,6 +22,9 @@ module Database.PostgreSQL.PQTypes.Model.Trigger
2222
, sqlCreateTriggerFunction
2323
, sqlDropTriggerFunction
2424
, triggerFunctionMakeName
25+
26+
-- * Constraints
27+
, ConstraintAttributes (..)
2528
) where
2629

2730
import Data.Bits (testBit)
@@ -35,6 +38,40 @@ import Data.Text qualified as Text
3538
import Database.PostgreSQL.PQTypes
3639
import Database.PostgreSQL.PQTypes.SQL.Builder
3740

41+
-- | Timing for a regular trigger.
42+
--
43+
-- @since 1.17.0.0
44+
data TriggerActionTime
45+
= -- | An @AFTER@ trigger.
46+
After
47+
| -- | A @BEFORE@ trigger.
48+
Before
49+
deriving (Eq, Show)
50+
51+
-- | Possible combinations of constraint attributes.
52+
--
53+
-- @since 1.17.0.0
54+
data ConstraintAttributes
55+
= -- | The @NOT DEFERRABLE [INITIALLY IMMEDIATE]@ constraint.
56+
-- A @NOT DEFERRABLE@ constraint is @INITIALLY IMMEDIATE@ by default.
57+
NotDeferrable
58+
| -- | The @DEFERRABLE [INITIALLY IMMEDIATE]@ constraint.
59+
-- A @DEFERRABLE@ constraint is @INITIALLY IMMEDIATE@ by default.
60+
Deferrable
61+
| -- | The @DEFERRABLE INITIALLY DEFERRED@ constraint.
62+
DeferrableInitiallyDeferred
63+
deriving (Eq, Show)
64+
65+
-- | Trigger kind.
66+
--
67+
-- @since 1.17.0.0
68+
data TriggerKind
69+
= -- | Create a regular trigger: @CREATE TRIGGER@
70+
TriggerRegular TriggerActionTime
71+
| -- | Create a constraint trigger: @CREATE CONSTRAINT TRIGGER@
72+
TriggerConstraint ConstraintAttributes
73+
deriving (Eq, Show)
74+
3875
-- | Trigger event name.
3976
--
4077
-- @since 1.15.0.0
@@ -58,29 +95,28 @@ data Trigger = Trigger
5895
, triggerName :: RawSQL ()
5996
-- ^ The internal name without any prefixes. Trigger name must be unique among
6097
-- triggers of same table. See 'triggerMakeName'.
98+
, triggerKind :: TriggerKind
99+
-- ^ The kind of trigger we want to create.
100+
-- @since 1.17.0.0
61101
, triggerEvents :: Set TriggerEvent
62102
-- ^ The set of events. Corresponds to the @{ __event__ [ OR ... ] }@ in the trigger
63103
-- definition. The order in which they are defined doesn't matter and there can
64104
-- only be one of each.
65-
, triggerDeferrable :: Bool
66-
-- ^ Is the trigger @DEFERRABLE@ or @NOT DEFERRABLE@ ?
67-
, triggerInitiallyDeferred :: Bool
68-
-- ^ Is the trigger @INITIALLY DEFERRED@ or @INITIALLY IMMEDIATE@ ?
69105
, triggerWhen :: Maybe (RawSQL ())
70106
-- ^ The condition that specifies whether the trigger should fire. Corresponds to the
71107
-- @WHEN ( __condition__ )@ in the trigger definition.
72108
, triggerFunction :: RawSQL ()
73109
-- ^ The function to execute when the trigger fires.
110+
-- The function is declared as taking no arguments and returning type @trigger@.
74111
}
75112
deriving (Show)
76113

77114
instance Eq Trigger where
78115
t1 == t2 =
79116
triggerTable t1 == triggerTable t2
80117
&& triggerName t1 == triggerName t2
118+
&& triggerKind t1 == triggerKind t2
81119
&& triggerEvents t1 == triggerEvents t2
82-
&& triggerDeferrable t1 == triggerDeferrable t2
83-
&& triggerInitiallyDeferred t1 == triggerInitiallyDeferred t2
84120
&& triggerWhen t1 == triggerWhen t2
85121

86122
-- Function source code is not guaranteed to be equal, so we ignore it.
@@ -117,37 +153,41 @@ triggerEventName = \case
117153

118154
-- | Build an SQL statement that creates a trigger.
119155
--
120-
-- Only supports @CONSTRAINT@ triggers which can only run @AFTER@.
121-
--
122156
-- @since 1.15.0
123157
sqlCreateTrigger :: Trigger -> RawSQL ()
124158
sqlCreateTrigger Trigger {..} =
125-
"CREATE CONSTRAINT TRIGGER"
159+
"CREATE"
160+
<+> trgKind
126161
<+> trgName
127-
<+> "AFTER"
162+
<+> tgrActionTime
128163
<+> trgEvents
129164
<+> "ON"
130165
<+> triggerTable
131-
<+> trgTiming
166+
<+> trgConstraintAttributes
132167
<+> "FOR EACH ROW"
133168
<+> trgWhen
134169
<+> "EXECUTE FUNCTION"
135170
<+> trgFunction
136171
<+> "()"
137172
where
173+
trgKind = case triggerKind of
174+
TriggerRegular _ -> "TRIGGER"
175+
TriggerConstraint _ -> "CONSTRAINT TRIGGER"
138176
trgName
139177
| triggerName == "" = error "Trigger must have a name."
140178
| otherwise = triggerMakeName triggerName triggerTable
179+
tgrActionTime = case triggerKind of
180+
TriggerRegular After -> "AFTER"
181+
TriggerRegular Before -> "BEFORE"
182+
TriggerConstraint _ -> "AFTER"
141183
trgEvents
142184
| triggerEvents == Set.empty = error "Trigger must have at least one event."
143185
| otherwise = mintercalate " OR " . map triggerEventName $ Set.toList triggerEvents
144-
trgTiming =
145-
let deferrable = (if triggerDeferrable then "" else "NOT") <+> "DEFERRABLE"
146-
deferred =
147-
if triggerInitiallyDeferred
148-
then "INITIALLY DEFERRED"
149-
else "INITIALLY IMMEDIATE"
150-
in deferrable <+> deferred
186+
trgConstraintAttributes = case triggerKind of
187+
TriggerRegular _ -> ""
188+
TriggerConstraint NotDeferrable -> "NOT DEFERRABLE"
189+
TriggerConstraint Deferrable -> "DEFERRABLE"
190+
TriggerConstraint DeferrableInitiallyDeferred -> "DEFERRABLE INITIALLY DEFERRED"
151191
trgWhen = maybe "" (\w -> "WHEN (" <+> w <+> ")") triggerWhen
152192
trgFunction = triggerFunctionMakeName triggerName
153193

@@ -205,6 +245,7 @@ getDBTriggers tableName = do
205245
runQuery_ . sqlSelect "pg_trigger t" $ do
206246
sqlResult "t.tgname::text" -- name
207247
sqlResult "t.tgtype" -- smallint == int2 => (2 bytes)
248+
sqlResult "t.tgconstraint > 0" -- we only check if CONSTRAINT has been specified
208249
sqlResult "t.tgdeferrable" -- boolean
209250
sqlResult "t.tginitdeferred" -- boolean
210251
-- This gets the entire query that created this trigger. Note that it's decompiled and
@@ -221,14 +262,13 @@ getDBTriggers tableName = do
221262
sqlWhereEq "c.relname" $ unRawSQL tableName
222263
fetchMany getTrigger
223264
where
224-
getTrigger :: (String, Int16, Bool, Bool, String, String, String, String) -> (Trigger, RawSQL ())
225-
getTrigger (tgname, tgtype, tgdeferrable, tginitdeferrable, triggerdef, proname, prosrc, tblName) =
265+
getTrigger :: (String, Int16, Bool, Bool, Bool, String, String, String, String) -> (Trigger, RawSQL ())
266+
getTrigger (tgname, tgtype, tgconstraint, tgdeferrable, tginitdeferrable, triggerdef, proname, prosrc, tblName) =
226267
( Trigger
227268
{ triggerTable = tableName'
228269
, triggerName = triggerBaseName (unsafeSQL tgname) tableName'
270+
, triggerKind = tgrKind
229271
, triggerEvents = trgEvents
230-
, triggerDeferrable = tgdeferrable
231-
, triggerInitiallyDeferred = tginitdeferrable
232272
, triggerWhen = tgrWhen
233273
, triggerFunction = unsafeSQL prosrc
234274
}
@@ -251,6 +291,30 @@ getDBTriggers tableName = do
251291
tgrWhen :: Maybe (RawSQL ())
252292
tgrWhen = parseBetween "WHEN (" ") EXECUTE"
253293

294+
tgrKind :: TriggerKind
295+
tgrKind =
296+
if tgconstraint
297+
then TriggerConstraint trgConstraintAttrs
298+
else TriggerRegular tgrActionTime
299+
300+
trgConstraintAttrs :: ConstraintAttributes
301+
trgConstraintAttrs = case (tgdeferrable, tginitdeferrable) of
302+
(False, False) -> NotDeferrable
303+
(True, False) -> Deferrable
304+
(True, True) -> DeferrableInitiallyDeferred
305+
(False, True) -> error "A constraint declared INITIALLY DEFERRED must be DEFERRABLE."
306+
307+
tgrActionTime :: TriggerActionTime
308+
tgrActionTime = case (tgtypeInsteadBit, tgtypeBeforeBit) of
309+
(False, False) -> After
310+
(False, True) -> Before
311+
(True, False) -> error "INSTEAD OF triggers are not available on tables."
312+
(True, True) -> error "The tgtype can't match more than one timing."
313+
where
314+
-- Taken from PostgreSQL sources: src/include/catalog/pg_trigger.h:
315+
tgtypeBeforeBit = testBit tgtype 1 -- #define TRIGGER_TYPE_BEFORE (1 << 1)
316+
tgtypeInsteadBit = testBit tgtype 6 -- #define TRIGGER_TYPE_INSTEAD (1 << 6)
317+
254318
-- Similarly, in case of UPDATE OF, the columns can be simply parsed from the
255319
-- original query. Note that UPDATE and UPDATE OF are mutually exclusive and have
256320
-- the same bit set in the underlying tgtype bit field.

‎test/Main.hs‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,9 +1124,8 @@ bankTrigger1 =
11241124
Trigger
11251125
{ triggerTable = "bank"
11261126
, triggerName = "trigger_1"
1127+
, triggerKind = TriggerConstraint NotDeferrable
11271128
, triggerEvents = Set.fromList [TriggerInsert]
1128-
, triggerDeferrable = False
1129-
, triggerInitiallyDeferred = False
11301129
, triggerWhen = Nothing
11311130
, triggerFunction =
11321131
"begin"
@@ -1149,9 +1148,8 @@ bankTrigger3 =
11491148
Trigger
11501149
{ triggerTable = "bank"
11511150
, triggerName = "trigger_3"
1151+
, triggerKind = TriggerConstraint DeferrableInitiallyDeferred
11521152
, triggerEvents = Set.fromList [TriggerInsert, TriggerUpdateOf [unsafeSQL "location"]]
1153-
, triggerDeferrable = True
1154-
, triggerInitiallyDeferred = True
11551153
, triggerWhen = Nothing
11561154
, triggerFunction =
11571155
"begin"
@@ -1318,8 +1316,8 @@ testTriggers step = do
13181316
verify [trg] True
13191317

13201318
do
1321-
let msg = "successfully migrate trigger that is deferrable"
1322-
trg = bankTrigger1 {triggerDeferrable = True}
1319+
let msg = "successfully migrate a constraint trigger that is deferrable"
1320+
trg = bankTrigger1 {triggerKind = TriggerConstraint Deferrable}
13231321
ts =
13241322
[ tableBankSchema1
13251323
{ tblVersion = 2
@@ -1332,12 +1330,8 @@ testTriggers step = do
13321330
verify [trg] True
13331331

13341332
do
1335-
let msg = "successfully migrate trigger that is deferrable and initially deferred"
1336-
trg =
1337-
bankTrigger1
1338-
{ triggerDeferrable = True
1339-
, triggerInitiallyDeferred = True
1340-
}
1333+
let msg = "successfully migrate a constraint trigger that is deferrable and initially deferred"
1334+
trg = bankTrigger1 {triggerKind = TriggerConstraint DeferrableInitiallyDeferred}
13411335
ts =
13421336
[ tableBankSchema1
13431337
{ tblVersion = 2
@@ -1350,12 +1344,8 @@ testTriggers step = do
13501344
verify [trg] True
13511345

13521346
do
1353-
let msg = "database exception is raised if trigger is initially deferred but not deferrable"
1354-
trg =
1355-
bankTrigger1
1356-
{ triggerDeferrable = False
1357-
, triggerInitiallyDeferred = True
1358-
}
1347+
let msg = "successfully migrate a regular AFTER trigger"
1348+
trg = bankTrigger1 {triggerKind = TriggerRegular After}
13591349
ts =
13601350
[ tableBankSchema1
13611351
{ tblVersion = 2
@@ -1364,7 +1354,22 @@ testTriggers step = do
13641354
]
13651355
ms = [createTriggerMigration 1 trg]
13661356
triggerStep msg $ do
1367-
assertDBException msg $ migrate ts ms
1357+
assertNoException msg $ migrate ts ms
1358+
verify [trg] True
1359+
1360+
do
1361+
let msg = "successfully migrate a regular BEFORE trigger"
1362+
trg = bankTrigger1 {triggerKind = TriggerRegular Before}
1363+
ts =
1364+
[ tableBankSchema1
1365+
{ tblVersion = 2
1366+
, tblTriggers = [trg]
1367+
}
1368+
]
1369+
ms = [createTriggerMigration 1 trg]
1370+
triggerStep msg $ do
1371+
assertNoException msg $ migrate ts ms
1372+
verify [trg] True
13681373

13691374
do
13701375
let msg = "database exception is raised if dropping trigger that does not exist"

0 commit comments

Comments
 (0)