Build1 distinct publisher3 min readUpdated
The defect belongs to the graph, not to any one relationship, so diagram review and migration both pass it. It surfaces the first time somebody seeds an empty database.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Make `departments.manager_id` NOT NULL referencing `employees`, and `employees.department_id` NOT NULL referencing `departments`, and you have a schema that is valid, a diagram that looks tidy, a migration that applies cleanly, and a database that will never accept a single row [1]. Every insert requires a row that does not exist yet [2], and the bill does not arrive at review: it arrives the first time somebody points a seed script at an empty database, which means a new staging environment, a contributor's laptop, a fresh tenant, or the disaster-recovery rehearsal [3].
Production is not affected, which is exactly why the defect survives. It was populated years ago by whoever fought through it once, and only the empty case exposes the shape [4]. The reason nobody catches it earlier is structural rather than careless: the cycle is a property of the whole graph, not of any one relationship, so there is nothing wrong with either foreign key when you look at it on the diagram [5].
Two problems get conflated here and they have different answers. Creating the pair is a DDL ordering question: the first `CREATE TABLE` cannot reference a table that does not exist, so the second constraint goes on afterwards with `ALTER TABLE ... ADD CONSTRAINT`, which is mechanical, works on every engine, and leaves the catalog perfectly content [6]. That confusion is old. On 22 June 2000 Eric Du asked the PostgreSQL mailing list why he could not create two mutually referencing tables; his `INITIALLY DEFERRED` attempt failed at `CREATE TABLE` with `ERROR: Relation 't2' does not exist`, because deferral postpones checking data, not the existence of a table that has not been created yet [7].
Inserting into the pair turns entirely on nullability. If one column in the cycle is nullable, there is no problem: insert the department with `manager_id` NULL, insert the employee pointing at it, update the department [8]. If all of them are NOT NULL, no ordering works, because ordering is not the difficulty; the set of rows the schema demands is self-referential, and no sequence of statements produces a set that contains itself [9].
The escape hatch is deferred checking at `COMMIT` with both inserts in one transaction, and whether you have it is decided by the engine, not the model [10]. MySQL's manual states plainly that because MySQL does not support deferred constraint checking, `NO ACTION` is treated as `RESTRICT` [11]. SQL Server has no deferrable constraints either [12]. Even where deferral exists it is narrower than it looks: the PostgreSQL `CREATE TABLE` documentation limits the clause to UNIQUE, PRIMARY KEY, EXCLUDE and REFERENCES, and says NOT NULL and CHECK are not deferrable [13]. So the NOT NULL is still checked immediately, and you must insert the department pointing at an employee id that does not exist yet, minting the key yourself from a sequence or as a client-side UUID [14].
That leaves three remedies, all modelling decisions: make one side nullable, defer on an engine that can, or move the reference into a third table [15]. On MySQL and SQL Server, only two of those three are available [18].
Worth watching: whether your pipeline ever runs a seed against a genuinely empty database, because that is the only test that fails. Schemity, a desktop ERD tool whose author wrote the source post, computes the cycle from the open diagram with a rule named `fk-cycle-all-not-null` and marks the entities in the margin [16]; the same check is expressible as a graph query over your own catalog. Twenty-six years after Du's question, the modelling version of the knot is still being written up fresh [17].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The failure arrives the first time somebody points the seed script at an empty database: a new staging environment, a contributor's local machine, a fresh tenant, the disaster-recovery rehearsal.
Production is fine because production was populated years ago by whoever fought through it once; the defect sat in the schema the whole time and only the empty case exposes it.
The cycle is a property of the whole graph rather than of any one relationship, so nobody spots it by looking at the diagram; it surfaces at seed time on a fresh environment.
If departments.manager_id is NOT NULL and references employees, and employees.department_id is NOT NULL and references departments, the schema is valid, the diagram is tidy, the migration applies cleanly, and the database will never accept a single row.
In a cycle of NOT NULL foreign keys, each insert needs a row that does not exist yet.
If all foreign key columns in the cycle are NOT NULL, no ordering works, because ordering is not the difficulty: the set of rows the schema demands is self-referential and no sequence of statements produces a set that contains itself.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanism is self-evidencing and doc-cited, but single-source
The core claim is a deterministic consequence of relational constraint semantics and is argued explicitly, and the post anchors engine behaviour in named primary documentation (MySQL foreign key constraints page, PostgreSQL CREATE TABLE page) plus a dated 2000 PostgreSQL mailing-list exchange. Against that, the cluster contains exactly one source, that source is the interested vendor, and the documentation is quoted rather than independently confirmed here, so the evidence base is credible in substance but thin in breadth.
No adoption data in the cluster
The only adoption-adjacent item is the vendor's self-reported description of its own lint rule and rule count. There are no releases with versions, deployments, users, downloads, benchmark results, or third-party usage disclosures, and the cluster gives no indication of how widely the described defect or the described detection tooling occurs in practice. Inferring an adoption level from a single capability disclosure would be a guess.
Technically sound, mildly overstated around detection and tooling
The substantive claims are conservative and mostly verifiable, so the gap is small. It is positive rather than zero because the post generalises detection difficulty ('nobody spots it by looking at the diagram', 'no amount of staring at the ERD finds it') without evidence on incidence or on existing CI and open-source linting that also catches cycles, and because the concluding remedy is the author's own commercial tool whose behaviour no independent source in the cluster confirms.
Disclosed vendor content marketing
The post opens with an explicit disclosure that the author builds Schemity, that the piece is from the company blog, and that the examples use the product; it closes by pointing to the product's lint rule as the answer. That is a clear commercial incentive to frame the problem as undetectable by eye. The disclosure is prominent and the technical argument is independently checkable against cited documentation, which moderates but does not remove the incentive.
High on the mechanics, low on the tooling and prevalence
Confidence in the constraint-semantics claims is high because they follow from stated rules and cited vendor documentation and are easy for a reader to reproduce. Confidence in the surrounding framing - how often this ships, how invisible it really is in practice, and what the described lint rule actually does - is low, because the cluster has a single interested source, no corroboration, and no adoption measurement.
build
Your ORM picked the wrong timestamp type, and a migration linter cannot find it1 distinct publisher
build
DuckDB is growing a server, and someone on your team will have to run it1 distinct publisher
build
1,254 dead mutants, a 100% score, and a payment charged twice1 distinct publisher
build
The agent asks, the gateway decides: why read-only is not a security boundary1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 18, 2026