fix(db): add CHECK constraint for userId XOR guestToken invariant
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 3m9s
CI / E2E browser tests (pull_request) Failing after 2m31s

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
This commit is contained in:
Hermes Agent 2026-07-18 11:35:22 +00:00
parent 48c2a50c5d
commit 928a995298

View file

@ -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));