Skip to content
About
Main content

Idempotent sink with a conditional insert

Break it on purposeNeeds Python

Problem

delivery means the sink will see some records twice, so the sink has to make a repeat harmless. The database can do that if it enforces uniqueness: a key on the record id plus "insert, and do nothing on conflict". Doing the same check in application code (select to see whether the row exists, then insert if it does not) looks equivalent and is not: two consumers can both check, both see nothing, and both insert.

Take the output of the previous lab (Auto-commit versus commit-after-process) and write the records into SQLite or PostgreSQL instead of a file. First use a plain INSERT into a table without a key and repeat the crash test: count rows per record number. Then add PRIMARY KEY (record_id) and use INSERT ... ON CONFLICT DO NOTHING. Then break it on purpose: implement dedup as "SELECT, then INSERT if absent" in application code and run two consumer processes on the same partition set.

The script redelivers a 100-record batch after each of 10 simulated crashes, and runs the check-then-insert version as two concurrent consumers with their own connections.

The question. How many duplicates does each sink end with: a plain insert, a keyed conditional insert, and check-then-insert run by two consumers?


Predict

Check-then-insert (SELECT, then INSERT), run by two consumers over the same ids: how many duplicates?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    idempotent-sink-with-a-conditional-insert.py71 linesDownload
    View the code
    idempotent-sink-with-a-conditional-insert.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Idempotent sink with a conditional insert.
    
    Records written to SQLite by a consumer that crashes and redelivers its last
    batch: a plain INSERT into a table without a key, a PRIMARY KEY with
    INSERT ... ON CONFLICT DO NOTHING, and dedup done in application code as
    "SELECT, then INSERT if absent" by two consumers at once.
    """
    import argparse, json, os, random, sqlite3, tempfile, threading, time
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--records", type=int, default=10_000, help="Records.")
    p.add_argument("--batch", type=int, default=100, help="Records per commit (the duplicate blast radius on a crash).")
    p.add_argument("--crashes", type=int, default=10, help="Crashes during the run.")
    p.add_argument("--seed", type=int, default=0, help="Random seed.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    random.seed(args.seed)
    
    
    def consume(db, insert_sql):
        """Apply records in batches; each crash happens after a batch is written but before its offset is committed,
        so the batch is delivered again on restart."""
        crash_at = set(random.sample(range(args.batch, args.records, args.batch), args.crashes))
        pos = 0
        while pos < args.records:
            batch = range(pos, min(pos + args.batch, args.records))
            db.executemany(insert_sql, [(r, f"v{r}") for r in batch]); db.commit()
            if pos in crash_at:
                crash_at.discard(pos); continue                      # crash: offset not committed, redeliver
            pos += args.batch
    
    
    def check_then_insert(path, ids, barrier):
        db = sqlite3.connect(path, timeout=30, isolation_level=None)
        barrier.wait()
        for r in ids:
            if db.execute("SELECT 1 FROM t WHERE id = ?", (r,)).fetchone() is None:   # check ...
                time.sleep(0.0002)
                db.execute("INSERT INTO t VALUES (?, ?)", (r, f"v{r}"))              # ... then act
        db.close()
    
    
    rows = []
    for name, ddl, sql in (("plain INSERT, no key", "CREATE TABLE t (id INTEGER, v TEXT)", "INSERT INTO t VALUES (?, ?)"),
                           ("PRIMARY KEY + ON CONFLICT DO NOTHING", "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)",
                            "INSERT INTO t VALUES (?, ?) ON CONFLICT DO NOTHING")):
        db = sqlite3.connect(":memory:"); db.execute(ddl)
        consume(db, sql)
        total, distinct = db.execute("SELECT COUNT(*), COUNT(DISTINCT id) FROM t").fetchone()
        rows.append({"sink": name, "rows": total, "duplicates": total - distinct})
    
    path = os.path.join(tempfile.mkdtemp(), "race.db")
    db = sqlite3.connect(path); db.execute("CREATE TABLE t (id INTEGER, v TEXT)"); db.execute("PRAGMA journal_mode=WAL"); db.commit(); db.close()
    barrier = threading.Barrier(2)
    ids = list(range(2000))
    ps = [threading.Thread(target=check_then_insert, args=(path, ids, barrier)) for _ in range(2)]   # two consumers, own connections
    [x.start() for x in ps]; [x.join() for x in ps]
    db = sqlite3.connect(path)
    total, distinct = db.execute("SELECT COUNT(*), COUNT(DISTINCT id) FROM t").fetchone()
    rows.append({"sink": "SELECT then INSERT, two consumers", "rows": total, "duplicates": total - distinct})
    for r in rows: print(f"{r['sink']:<38} {r['rows']:>6} rows, {r['duplicates']:>5} duplicates")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"sinks": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run idempotent-sink-with-a-conditional-insert.py
    uv run idempotent-sink-with-a-conditional-insert.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--records10,000 Records.
--batch100 Records per commit (the duplicate blast radius on a crash).
--crashes10 Crashes during the run.
--seed0 Random seed.

Our run

The same redeliveries, three sinks

The same redeliveries, three sinks
SinkRowsDuplicates
plain INSERT, no key11K1,000
PRIMARY KEY + ON CONFLICT DO NOTHING10K0
SELECT then INSERT, two consumers2,960964
Reference runApple M2 · 8 cores · 8 GB
Output · uv run idempotent-sink-with-a-conditional-insert.py
plain INSERT, no key                    11000 rows,  1000 duplicates
PRIMARY KEY + ON CONFLICT DO NOTHING    10000 rows,     0 duplicates
SELECT then INSERT, two consumers        2964 rows,   964 duplicates
macOS 26.6.2 · Python 3.13.3 · run 2026-10-25

What we found

The plain insert ended with 1,000 duplicates, one per redelivered record. The keyed conditional insert ended with 0. Check-then-insert by two consumers ended with 964 duplicates: far more than "occasional", because both consumers worked through the same ids at the same moment, so most of their checks raced.


Read it

This shows that check-then-act is a race and that the database must enforce uniqueness.

For every effect, ask: what is the key and what is the store? If nobody can answer in one sentence, that effect is at-least-once with no dedup, which means it produces duplicates. The unique constraint is the dedup, and it is the only version that holds under concurrency.