惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
D
DataBreaches.Net
月光博客
月光博客
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
U
Unit 42
腾讯CDC
爱范儿
爱范儿
J
Java Code Geeks
有赞技术团队
有赞技术团队
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
B
Blog
Stack Overflow Blog
Stack Overflow Blog
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Y
Y Combinator Blog
大猫的无限游戏
大猫的无限游戏
Microsoft Azure Blog
Microsoft Azure Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

alexwlchan’s notes

What is WS11 1DB? Blocking referrers with Caddy How to type a Spanish question mark (¿) on a Mac Non-overlapping type comparisons and Python type checkers Why does t.Setenv panic after t.Parallel? Use Path.glob() and Path.rglob() for typed versions of glob.glob() Curious clocks and colourful eyes Track which templates are used by Jinja2 Archeologists distinguish between “sherds” and “shards” A single command to test all my changed Go packages Disable the new message animations in WhatsApp Finding high-churn folders that bother Backblaze Always-on SSH agent forwarding with my Git pushes Managing the caption of a photo with AppleScript (but not PhotoKit) Goodhart’s and Campbell’s Law are different Notes from The Cornishman No. 176 (Spring 2026) Notes from The Cornishman No. 176 (Spring 2026) GitUp can’t diff text files larger than 8MB Home Testing the width of a page on a mobile device using Playwright Disable AirPods charging notifications Start a Caddy server in a subprocess during a Python session Filter a list of JSON object based on a list of tags HOME_GET_ME_HOME is a Citymapper Shortcuts action The FileExistsError exception exposes a filename attribute The red-lined bubble snail Why can’t Python connect to example.com? Useful type hints for Python How to truncate the middle of long command output AirPlay Receiver can interfere with Flask apps
Use SQL triggers to prevent overwriting a value
2026-02-20 · via alexwlchan’s notes

A trigger lets you run an action when you INSERT, UPDATE or DELETE a value.

Today I wanted to write a value to a SQLite database, and error if the database already had a conflicting value.

There are a variety of ways you could do this – I decided to read the current stored value and check it in Go – but I also discovered there’s a way you could do it in SQL alone using CREATE TRIGGER. I did this with SQLite, but it looks like this is supported by other SQL dialects, including PostgreSQL and MySQL.

Setup

Let’s create a table which we’ll use to store write-once values:

sqlite> CREATE TABLE KeyValuePairs (
   ...>     Key   TEXT NOT NULL PRIMARY KEY,
   ...>     Value TEXT NOT NULL
   ...> );

If I try to INSERT a duplicate key into this table, it fails:

sqlite> INSERT INTO KeyValuePairs (Key, Value)
   ...> VALUES ('Colour', 'Red');
sqlite> INSERT INTO KeyValuePairs (Key, Value)
   ...> VALUES ('Colour', 'Green');
Runtime error: UNIQUE constraint failed: WriteOnce.Key (19)

But I can overwrite an existing key with an INSERT OR REPLACE or UPDATE:

sqlite> INSERT OR REPLACE INTO KeyValuePairs (Key, Value)
   ...> VALUES ('Colour', 'Green');
sqlite> SELECT * FROM WriteOnce;
Parse error: no such table: WriteOnce
sqlite> SELECT * FROM KeyValuePairs;
Colour|Green

sqlite> UPDATE KeyValuePairs
   ...> SET Value = 'Blue'
   ...> WHERE Key = 'Colour';
sqlite> SELECT * FROM KeyValuePairs;
Colour|Blue

Adding triggers

Let’s suppose I want to prevent somebody from overwriting the Colour key with a different value.

I can use CREATE TRIGGER to create a trigger on my table – that is, an action that runs whenever I perform an INSERT, UPDATE or DELETE.

For the INSERT case, I look for an existing key-value pair, and check if the existing value matches the inserted value. If not, I call a special RAISE() function which aborts the transaction, and nothing is written:

sqlite> CREATE TRIGGER IF NOT EXISTS prevent_insert_overwrite_colour
   ...> BEFORE INSERT ON KeyValuePairs
   ...> FOR EACH ROW
   ...> WHEN NEW.Key = 'Colour'
   ...> AND EXISTS (SELECT 1 FROM KeyValuePairs WHERE Key = 'Colour')
   ...> BEGIN
   ...>     SELECT CASE
   ...>         WHEN (
   ...>             SELECT Value
   ...>             FROM KeyValuePairs
   ...>             WHERE Key = 'Colour'
   ...>         ) != New.Value
   ...>         THEN RAISE(ABORT, 'Error: Colour already exists with a different value.')
   ...>     END;
   ...> END;

For the UPDATE case, I can use the OLD reference to inspect the existing value in the table:

sqlite> CREATE TRIGGER IF NOT EXISTS prevent_update_overwrite_colour
   ...> BEFORE UPDATE ON KeyValuePairs
   ...> FOR EACH ROW
   ...> WHEN NEW.Key = 'Colour'
   ...> AND EXISTS (SELECT 1 FROM KeyValuePairs WHERE Key = 'Colour')
   ...> BEGIN
   ...>     SELECT CASE
   ...>         WHEN OLD.Value != New.Value
   ...>         THEN RAISE(ABORT, 'Error: Colour already exists with a different value.')
   ...>     END;
   ...> END;

With these two triggers in place, running an INSERT or UPDATE that matches the existing value is a no-op:

sqlite> INSERT OR REPLACE INTO KeyValuePairs (Key, Value)
   ...> VALUES ('Colour', 'Blue');
sqlite> UPDATE KeyValuePairs
   ...> SET Value = 'Blue'
   ...> WHERE Key = 'Colour';
sqlite> SELECT * FROM KeyValuePairs;

But trying to INSERT or UPDATE a conflicting value throws my custom error, and leaves the value as-is:

sqlite> INSERT OR REPLACE INTO KeyValuePairs (Key, Value)
   ...> VALUES ('Colour', 'Orange');
Runtime error: Error: Colour already exists with a different value. (19)
sqlite> UPDATE KeyValuePairs
   ...> SET Value = 'Purple'
   ...> WHERE Key = 'Colour';
Runtime error: Error: Colour already exists with a different value. (19)
sqlite> SELECT * FROM KeyValuePairs;
Colour|Blue

The projects I work on usually put this sort of logic in the application code, but it’s neat to see how this could be implemented in the database layer.