Yeda AI Tips · #091

Español

Test Data Invariants at the Database Layer

Your app validation is a suggestion. Your database constraint is a law. Application-level checks only run when writes go through your application — and your application is not the only writer. Migrations, backfill scripts, admin consoles, cron jobs, and other services all touch the same tables. If an invariant matters — "emails are unique", "every order has a customer", "quantity is never negative" — it needs to be enforced where every writer must pass: the database. And then it needs to be tested there, not just declared.

Why app-layer checks leak

An ORM unique: true validation typically does a SELECT-then-INSERT. Two concurrent requests can both pass the SELECT and both insert — the only thing that actually stops the duplicate is a database unique constraint. The same pattern repeats everywhere: presence checks skipped by a raw-SQL backfill, "not negative" rules a data-fix script never heard of, orphaned rows left behind by a service that doesn't load your models. App checks are useful UX; they are not integrity.

Test on a real, disposable database

  1. Spin up a throwaway instance of the same engine and major version as production. Testcontainers does exactly this — throwaway, code-defined database containers for tests, with libraries for Java, Go, .NET, Node.js, Python, Rust, and more.
  2. Run your real migrations against it, from zero. The schema under test is the schema you ship — not a hand-maintained fixture.
  3. Seed rows that attack each constraint: insert a duplicate key, delete a parent row with children, insert a value the check constraint forbids — and assert the database rejects each one.
  4. Tear it down. Next run starts clean.

A test that expects INSERT to fail is the proof the law exists. If someone drops the constraint in a migration, this test — not a production incident — catches it.

Engine gotchas worth a test each

EngineGotcha
PostgreSQLUNIQUE treats two NULLs as distinct by default — duplicate null rows are allowed unless you use NULLS NOT DISTINCT. A CHECK is also satisfied when it evaluates to NULL, so it won't block nulls; pair it with NOT NULL.
MySQLCHECK constraints are only enforced from 8.0.16 — on older versions they parse and do nothing. Expressions can't use other tables, subqueries, or stored functions.
SQLiteForeign key enforcement is off by default and per-connection: without PRAGMA foreign_keys = ON on every connection, your FKs are decoration.

Every row in that table is a reason a mocked or in-memory "compatible" database gives you false confidence. Test against the engine you deploy.

Power-user moves

Resources

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog