MY.L9.TYPE_IMPLICIT_CAST — A foreign key whose two ends are different types
- Category: idiom
- Level: 9
- Confidence: deterministic
- Downtime class: none
- Stability: stable
- Suites: audit
- Applies to: MySQL 8.4 and newer
What it checks
A foreign key whose referencing column and referenced column are not the same base type.
Reported:
Schema::create('moments', function (Blueprint $table) {
$table->dateTime('at')->primary(); // datetime
});
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->timestamp('happened_at')->nullable(); // timestamp -- a different type
$table->foreign('happened_at')->references('at')->on('moments');
});
Not reported:
Schema::create('moments', function (Blueprint $table) {
$table->dateTime('at')->primary(); // datetime
});
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->dateTime('happened_at')->nullable(); // the same type on both ends
$table->foreign('happened_at')->references('at')->on('moments');
});
Half of what its sibling reports cannot exist here
MySQL 8.4.10 refuses an integer-width mismatch outright. Measured:
CREATE TABLE invoices (account_id int, FOREIGN KEY (account_id) REFERENCES accounts (id));
ERROR 3780 (HY000): Referencing column 'account_id' and referenced column 'id'
in foreign key constraint 'invoices_ibfk_1' are incompatible.
Signed or unsigned makes no difference — both are refused. So the case that motivates
PG.L9.TYPE_IMPLICIT_CAST never reaches a MySQL catalog, and a rule that went looking for it here
would be a check that cannot fail.
What DOES survive is the interesting half
Also measured on 8.4.10, created without complaint:
timestamp referencing datetime -- DIFFERENT types, converted on every comparison
varchar(50) referencing varchar(100) -- same type, different length: no conversion
The first is what this rule is for: a pair MySQL accepted and converts at runtime, on a join the application runs constantly, with nothing in the schema recording that it was ever intended.
A declared length is not a mismatch
varchar(50) referencing varchar(100) is created by both engines, costs nothing, and is the most
common shape in a real schema. The comparison is the base type, so it is silent — a rule
comparing declared types would fire on almost every text key it saw.
MySQL has no free pair
PostgreSQL has one — text and varchar share storage. MySQL does not: varchar and char differ
in padding, and text differs from both in storage and in how an index over it works. The
equivalence list for this engine is therefore empty, and it stays empty until a pair is shown to
cost nothing. Every group added to it silences a real finding.
Audit only, and that is structural
A migration adding a foreign key carries the type of neither column. The types live in the
CREATE TABLE statements, and the referenced table was almost always created by a migration that is
no longer pending when this one runs. The catalog is the only place both ends exist at once.
A lint half would therefore answer undetermined for nearly every foreign key — noise rather than
honesty. What the rule does refuse to do is pass silently: a key whose far end could not be read is
reported as undetermined, never as agreement.
Suppressing it
Level 9 is the strictest band and is not reached unless a project asks for it. Within it, the
ordinary routes apply: a baseline entry, an ignore rule in config/sqlens.php, or lowering the
level below 9.
Sources
- MySQL 8.4 — FOREIGN KEY constraints — corresponding columns must have similar types, integer size and sign must match, string lengths need not
- MySQL 8.4 — Data types — what
timestampanddatetimeare, and why they are not the same type
Not transferable to MariaDB. Its foreign-key compatibility rules and its information_schema shapes
are its own, and this package targets MySQL 8.4 semantics — advice derived from them and applied
elsewhere would be confident, specific and about another product.