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

推荐订阅源

有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
罗磊的独立博客
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Docker
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
J
Java Code Geeks
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
美团技术团队
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
C
Check Point 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.