Skip to content

v1.11.0: a foreign key that points at the wrong table

Latest

Choose a tag to compare

@albertoarena albertoarena released this 01 Sep 18:53
· 1 commit to main since this release

Added

  • TRUSS-INT-010 finds a foreign key that points at the wrong table: a single-column *_id key that references one table while a table named after the column exists and is a different one. Found on Lunar 1.5.0, where lunar_cart_line_discount.cart_line_id is constrained against lunar_carts rather than lunar_cart_lines, straight from the 2022 migration's constrained() call. CartLine::discounts() is a belongsToMany through that table, so the application writes cart line ids into a column validated against cart ids: an insert only succeeds when that id happens to exist as a cart too, ON DELETE CASCADE fires when the wrong parent goes, so deleting a cart deletes discount rows whose cart_line_id merely equals that cart's id, and deleting a cart line cascades nothing and leaves orphans. The hard part is not detection, it is silence. Most name mismatches are deliberate and correct: six of the seven mismatches among Lunar's 83 foreign keys are ordinary aliases (merged_id, parent_transaction_id, product_parent_id, product_target_id, value_id, variant_id), so a rule that flagged every mismatch would be wrong 86% of the time, which is the same calibration trap that TRUSS-INT-007 fell into. The test is therefore not "the names differ" but "the name names a table that actually exists, and the key points somewhere else". An alias survives because there is no authors or mergeds table for its name to name. Ambiguity is silence: candidates resolve plural first then singular, and only when exactly one table matches, so more than one match reports nothing rather than guessing. A prefixed schema is matched through the prefix the referenced table already carries, so lunar_ tables are neither invisible nor confused with another application's. Morph targets and composite keys are skipped, since neither is named after one table. The rule is high confidence and an error, so it runs under the default recommended preset and a CI job running truss:doctor --fail-on=error can see a new failure on a schema that has not changed.

Full documentation: https://trussphp.com/guides/schema-doctor/