Skip to main content

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:

  • strongprice, invoice_total, unit_price, rechnungsbetrag, nettopreis. Money on its own.
  • weakrate, value, total, balance, wert, summe. Ordinary language that is sometimes money. rate is an interest rate and a frame rate; value names 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:

  1. 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.
  2. A cents segment in the name. balance_cents is a minor-unit amount whatever balance means 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