From 928a9952986572b94f400a2784dbe681601b6c08 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 18 Jul 2026 11:35:22 +0000 Subject: [PATCH] fix(db): add CHECK constraint for userId XOR guestToken invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Order entity Javadoc states "Either userId or guestToken is set; never both, never neither" but only the @PrePersist lifecycle callback enforced this in Java. A stray INSERT (admin tooling, manual SQL) could violate it silently — creating an order with neither set (invisible to both JWT and guest lookup paths) or both set (ambiguous ownership). Changes: - V13 Flyway migration adds a CHECK constraint on the orders table: CHECK ((user_id IS NULL) <> (guest_token IS NULL)) This evaluates TRUE when exactly one column is NULL (the other is set), and FALSE when both are NULL or both are set. - Standard SQL CHECK constraint, supported by both H2 (tests/dev) and PostgreSQL (prod). Verified: - Flyway migration check passes (V13 is next available version) - ./gradlew :backend:test — BUILD SUCCESSFUL (all tests pass) - H2 accepts the constraint at Flyway migration time Closes #23 --- .../V13__add_user_guest_check_constraint.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V13__add_user_guest_check_constraint.sql diff --git a/backend/src/main/resources/db/migration/V13__add_user_guest_check_constraint.sql b/backend/src/main/resources/db/migration/V13__add_user_guest_check_constraint.sql new file mode 100644 index 0000000..ad7e8bb --- /dev/null +++ b/backend/src/main/resources/db/migration/V13__add_user_guest_check_constraint.sql @@ -0,0 +1,17 @@ +-- Enforce the Order entity invariant at the database level: exactly one of +-- (user_id, guest_token) must be set — never both, never neither. +-- +-- The Order Javadoc states "Either userId or guestToken is set; never both, +-- never neither", but previously only the @PrePersist lifecycle callback +-- enforced this in Java. A stray INSERT (admin tooling, manual SQL) could +-- violate it silently. +-- +-- The CHECK expression (user_id IS NULL) <> (guest_token IS NULL) evaluates: +-- TRUE when exactly one column is NULL (the other is set) — allowed +-- FALSE when both are NULL or both are set — rejected +-- +-- Standard SQL CHECK constraint, supported by both H2 (tests/dev) and +-- PostgreSQL (prod). +ALTER TABLE orders + ADD CONSTRAINT chk_user_or_guest + CHECK ((user_id IS NULL) <> (guest_token IS NULL)); -- 2.45.2