Triggers
Statements the database runs by itself before, after or instead of a change.
A trigger is a list of statements the database runs by itself whenever a table changes. This one logs every fish that leaves the bowl:
CREATE TRIGGER fish_gone AFTER DELETE ON fish
BEGIN
INSERT INTO gone VALUES (old.id, old.name);
END;DELETE FROM fish WHERE weight < 4 now also writes one gone row for each
fish it deletes. The code is in crates/engine/src/trigger.rs.
When a trigger fires
SQLite has only row triggers: the body runs once for each row the statement changes, never once per statement. For each row, Fishbowl:
- runs the
BEFOREtriggers, - writes the row,
- runs the
AFTERtriggers.
Several triggers on the same event fire newest first. WHEN new.x > 0
skips the trigger for rows where the condition is not true. UPDATE OF price
fires only when the SET list names price, whether or not the value
changes.
Inside the body, NEW.column is the row being written and OLD.column the
row being replaced or deleted. An INSERT has no OLD and a DELETE has no
NEW. Naming one is "no such column", and SQLite only reports it when the
trigger fires, not when it is created.
How NEW and OLD get their values
A trigger keeps its body as SQL text. Each time it fires, Fishbowl replaces
each NEW.x and OLD.x with the row's value written as a literal, then
parses and plans the statement like any other. INSERT INTO gone VALUES (old.id, old.name) runs as INSERT INTO gone VALUES (7, 'nemo').
Planning once per row is slow, but it keeps triggers out of the planner and
the executor. It is also exact. A column reference normally brings its
column's affinity to a comparison, and SQLite's
documentation says NEW.x does too. SQLite 3.54 does not do that: with an
INTEGER column x holding 5, NEW.x = '5' is false. A bare literal gives
the same result.
All or nothing, including the triggers
A statement and every trigger it fires share one undo log. If any of them fails, such as a body INSERT that breaks a UNIQUE rule, Fishbowl reverses all of it, newest change first. See how constraints are checked.
RAISE() lets a trigger stop the statement on purpose:
CREATE TRIGGER no_sharks BEFORE INSERT ON fish
WHEN new.species = 'shark'
BEGIN
SELECT RAISE(ABORT, 'no sharks');
END;RAISE(ABORT, msg)fails the statement and undoes it.RAISE(FAIL, msg)fails it but keeps the rows written before.RAISE(ROLLBACK, msg)ends the whole transaction.RAISE(IGNORE)skips this row and the triggers still to fire for it, and the statement carries on.
The RAISE's own rule wins over the statement's OR clause, so RAISE(FAIL)
inside INSERT OR ROLLBACK keeps the earlier rows.
Details that show
- No recursion. A trigger that is running does not fire again, even if
its body writes to its own table. Other triggers still fire. This is
SQLite's default,
PRAGMA recursive_triggers = OFF. - Rowid order. When an UPDATE or DELETE fires triggers, SQLite collects the matching rowids and visits them in order, whatever order its scan found them in. The order shows in what the triggers write, so Fishbowl does the same.
- Rows can change under the statement. A
BEFORE UPDATEtrigger may change the row about to be updated. The UPDATE then takes the columns it does not set from the row as the trigger left it. A row that an earlier trigger deleted is skipped. - An unassigned rowid is -1. In a
BEFORE INSERTtrigger,NEW.rowidand anINTEGER PRIMARY KEYleft NULL read as -1, because the row has no rowid yet.
Views
A view holds no rows, so INSERT INTO v is normally an error. An INSTEAD OF trigger makes it work: the statement writes nothing itself, and the
trigger fires once for each row it would have written, with NEW and OLD
holding the view's columns. The trigger decides what really changes.
Storage
Like a view, a trigger is only its CREATE TRIGGER statement, kept in the
schema table with type trigger. Opening the database replays it, so
triggers survive a restart and crash recovery. Dropping a table or view drops
its triggers. A TEMP trigger lives in memory and is gone on close.
Not supported yet
RAISE()anywhere but as a result column of a trigger's SELECT, or as aCASEresult inside one.- A trigger body that writes to a view through its
INSTEAD OFtriggers. - Qualified names, as in
CREATE TRIGGER main.t.