{"id":"code-dev-sql-upsert-add-not-overwrite","audience":"developers","category":"coding","track":"coding","technique":"context","title":"Replace a racy read-then-write with a single upsert","free":false,"task":"Replace the select-then-insert-or-update code with one PostgreSQL statement using placeholders $1 (sku), $2 (warehouse_id) and $3 (qty change). On conflict, add the change to the existing qty rather than overwriting it and set updated_at = now(). Warehouse staff can lock a row during stock counts, and locked rows must not be updated. End with RETURNING qty. Exactly 1 code block of at most 8 lines, under 25 words of text.","materialLabel":"The table and the current code (attached to your prompt automatically)","material":"-- PostgreSQL 16, Northbay Supply warehouse service\nCREATE TABLE inventory (\n  sku           TEXT NOT NULL,\n  warehouse_id  INT  NOT NULL,\n  qty           INT  NOT NULL,\n  locked        BOOLEAN NOT NULL DEFAULT false,\n  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now(),\n  PRIMARY KEY (sku, warehouse_id)\n);\n\n# services/stock.py (current, Python + psycopg 3)\ndef adjust_stock(conn, sku, warehouse_id, change):\n    row = conn.execute(\n        \"SELECT qty FROM inventory WHERE sku = %s AND warehouse_id = %s\",\n        (sku, warehouse_id),\n    ).fetchone()\n    if row is None:\n        conn.execute(\n            \"INSERT INTO inventory (sku, warehouse_id, qty) VALUES (%s, %s, %s)\",\n            (sku, warehouse_id, change),\n        )\n    else:\n        conn.execute(\n            \"UPDATE inventory SET qty = %s WHERE sku = %s AND warehouse_id = %s\",\n            (row[0] + change, sku, warehouse_id),\n        )\n\n# Incident WH-311: two scanners adjusted the same SKU at the same moment;\n# both read qty=40, both wrote their own result, one +5 was lost.\n#\n# The new data layer uses asyncpg, so placeholders are $1, $2, $3.","rules":[{"type":"codeBlocks","count":1},{"type":"maxCodeLines","max":8},{"type":"maxProseWords","max":25}],"mistakes":[{"title":"Overwriting instead of adding","why":"A default upsert sets qty = EXCLUDED.qty, which throws away the stock already there.","fix":"Add: \"On conflict, qty = inventory.qty + EXCLUDED.qty.\"","checks":["r1"]},{"title":"Ignoring locks","why":"The AI can't know staff lock rows during counts.","fix":"Add: \"Don't update locked rows: DO UPDATE ... WHERE NOT inventory.locked.\"","checks":["r2"]},{"title":"Wrong placeholders","why":"The AI keeps %s from the old psycopg code, which asyncpg won't accept.","fix":"Add: \"Use $1, $2, $3.\"","checks":["r0"]},{"title":"A rewritten service","why":"The AI rewrites the Python function and explains race conditions.","fix":"Add: \"Only the SQL, one code block, no text.\"","checks":["codeBlocks","maxCodeLines","maxProseWords"]}],"promptHints":[{"label":"Asks for one statement","patterns":["on conflict","upsert","one statement","single statement"]},{"label":"Says to add, not overwrite","patterns":["add","\\+ ?excluded","not overwrite","increment"]},{"label":"Mentions locked rows","patterns":["lock"]},{"label":"Gives the placeholders","patterns":["\\$1","\\$3","asyncpg"]},{"label":"Asks for RETURNING","patterns":["returning"]}],"hint":"An upsert can still lose data if it overwrites. Tell the AI how to combine the values, which rows are off-limits, the placeholder style and what to return.","rubricLabels":["Is one INSERT INTO inventory (sku, warehouse_id, qty, ...) VALUES ($1, $2, $3, ...) ... ON CONFLICT (sku, warehouse_id) DO UPDATE statement","The update adds the change (qty = inventory.qty + EXCLUDED.qty) rather than overwriting it, and sets updated_at = now()","The DO UPDATE has a WHERE that skips locked rows (e.g. WHERE inventory.locked = false or NOT inventory.locked), and the statement ends with RETURNING qty"]}