MY.L5.FLOAT_MONEY — Money in a floating-point column
- Category: safety
- Level: 5
- Confidence: deterministic — the trigger is the column NAME, and the finding carries that caveat in its own text rather than in this field
- Downtime class: none
- Stability: stable
- Suites: audit
- Applies to: MySQL 8.4
- Not transferable to MariaDB: this page describes MySQL 8.4 behavior. MariaDB answers the same driver and does not share these semantics, so SQLens refuses it outright rather than reasoning about it — see drivers/unsupported.
What goes wrong, and why nobody notices at the time
A binary float cannot represent 0.10 exactly. Add it ten times and the total is not 1.00 — it is
near enough that every screen shows 1.00 and every comparison against 1.00 fails.
The error arrives as one cent on a statement, months after the schema decision, and whoever finds it has no route back to the column that caused it. That is the whole argument: the failure is small, late, and untraceable.
DECIMAL(19, 4) stores the digits, so the arithmetic is the arithmetic of the invoice.
Flagged
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->double('invoice_total');
});
Preferred
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->decimal('invoice_total', 19, 4);
});
Storing minor units in an integer — price_cents bigint — is the other correct answer, and is never
reported.
This is a heuristic, and the finding says so
Nothing in a catalog says a column holds money. The only available signal is the name, which is a guess about intent — so the finding admits to being a guess rather than presenting itself as a fact.
The dictionary that backs it splits terms into two confidence levels, and the split is the point:
- strong —
price,invoice_total,unit_price,rechnungsbetrag,nettopreis. Money on its own. - weak —
rate,value,total,balance,wert,summe. Ordinary language that is sometimes money.rateis an interest rate and a frame rate;valuenames a hundred things. A rule that fired on those would be noise, and noise is how a team learns to skip a whole level band.
Matching is on whole underscore-separated segments, never substrings: rating is not rate, and
pricing is not price.
What a weak term needs before anything is reported
Exactly two second signals count, and both are structural facts about the table rather than further guesses about the domain:
- A sibling currency column (
currency,currency_code,waehrung). A table that stores a currency stores money in it — the strongest evidence a catalog can offer, because it is somebody else's explicit statement. - A
centssegment in the name.balance_centsis a minor-unit amount whateverbalancemeans elsewhere.
A suffix of _amount is deliberately not a second signal, though it looks like one: amount is
itself a weak term, so accepting it would let a weak term vouch for itself and the confidence split
would stop meaning anything.
Tuning it for your schema
Both directions, without forking the package:
'audit' => [
'money_columns' => [
// Added terms count as STRONG — you have stated the intent the dictionary can only guess at.
'extra' => ['settlement', 'payout'],
// Removed from BOTH confidence levels, so an ignore keeps working if a term is reclassified.
'ignore' => ['rate'],
],
],
Changing the type rewrites the table
Converting to DECIMAL(19, 4) is not a metadata change — every row is rewritten. The lint suite classifies that
operation and carries its downtime class; this audit finding carries none, because an observation
about a state has no duration.
Sources
- MySQL 8.4: fixed-point types —
DECIMALis documented as the type for values where exactness is required, with monetary data named outright. - MySQL 8.4: problems with floating-point values — the manual's own worked example of a comparison failing on values that display identically.