Repository navigation
fix(model): bind decimals beyond 15 significant digits exactly - #4190
Conversation
Lucee and BoxLang bind cf_sql_decimal / cf_sql_numeric through a double, so a value with more than 15 significant digits was stored, or matched in a WHERE, as a different number on PostgreSQL, SQL Server, MySQL and CockroachDB. Such a value now binds as cf_sql_varchar (SQL Server, MySQL) or cf_sql_other (PostgreSQL, CockroachDB); values with up to 15 significant digits, exponent forms and nulls keep their existing bind. Fixes #4172 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r1 review @ e54bced (draft, 4.3): APPROVE on code; merge gated on compat-matrix run 37114648853 and on 4.2.0 shipping.
For the matrix read I'll check L128 on every MySQL leg. MySQL documents that a string compared with a |
Wheels Test Results216 999 tests 212 827 ✅ 1h 5m 9s ⏱️ Results for commit 8a0617e. ♻️ This comment has been updated with latest results. |
The text bind for a decimal beyond 15 significant digits was still inexact on two databases. MySQL compares a multi-element IN list of text values with a DECIMAL column as doubles, so a neighbouring value matched. SQL Server converts the text to the column type, rounding a value with more fraction digits than the column onto the stored value, with = and IN. Both adapters now bind such a value inside CAST(? AS DECIMAL(p, s)), s being the value own fraction digits (MySQL 65/30, SQL Server 38/38), via Base.$castWideDecimalParams() from a $performQuery() override. An IN value the database cannot store is dropped while another remains; a lone unstorable value keeps the text bind. Specs pin =, IN, NOT IN and <> on scale-0 and scale-10 columns on all four databases. Refs #4172 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r1 delta review @ ed98313 (from e54bced; draft, 4.3): APPROVE on code; merge gated on compat-matrix run 37118814632 and on 4.2.0 shipping.
|
|
Compat matrix 37118814632 on ed98313: fully green.
Approved by rev1-r1 at ed98313. Stays a draft until after the 4.2.0 cut. |
Final: APPROVE @ 8a0617e (lifted into 4.2; matrix 37262664711, all 32 legs read)Delta since my approval at ed98313
Matrix
No open findings. |
Fixes #4172
Problem
Lucee and BoxLang bind
cf_sql_decimal/cf_sql_numericparams through a double. ADECIMAL/NUMERICvalue with more than 15 significant digits was therefore stored, or compared in aWHERE, as a different number on PostgreSQL, SQL Server, MySQL and CockroachDB. Adobe binds these exactly when a scale is passed, which Wheels always does.Change
1. Exact bind type (
Base.$applyWideDecimalBind(), from$queryParams())A decimal / numeric param switches to the adapter's exact form when:
^-?[0-9]+(\.[0-9]+)?$: no exponent,+or grouping), andcf_sql_varchar, inside an exact cast (step 2)cf_sql_other(PostgreSQL rejects varchar fornumeric; CockroachDB's varcharWHEREfails)2. Exact cast on MySQL and SQL Server (
Base.$castWideDecimalParams(), called from a$performQuery()override in both adapters)Text alone isn't compared exactly on these two databases:
INlist of text values with aDECIMALcolumn as doubles, so a neighbouring value matched.=, single-elementINandORof=were already exact.=andIN. SQL Server's=already behaved this way before this PR (through the double bind), so this is not a regression.So a wide param, one value or an
INlist, binds asCAST(? AS DECIMAL(p, s)), one param per value:sis the value's own fraction digits, never the column's. A cast to the column's scale would round an over-precise value onto a stored value it doesn't equal.pis 65 on MySQL (scale up to 30) and 38 on SQL Server (scale up to 38).DECIMALequals no stored value. It is left out of anINlist while another value remains, which leavesINandNOT INunchanged. A lone such value, or a list of only such values, keeps the text bind: dropping them all would changeNOT INfor NULL rows, and on SQL Server the cast would overflow.parameterize=false(decimal literals in the SQL text, already exact) is untouched.Values with up to 15 significant digits, exponent forms and nulls keep their existing bind: no behaviour change below the threshold. A probe confirmed that today's
cfqueryparamscale does not round ordinary values:amount = 3.004andamount IN (1.5, 3.004)both match nothing against aDECIMAL(10,2)holding 3.00, on all 4 databases. So per-value scale keeps=andINconsistent.Tests
vendor/wheels/tests/specs/model/wideDecimalBindSpec.cfc(with aDecAmounttest model):Unit (any database):
cf_sql_decimal/cf_sql_numericon all 4 adapters;parameterize=falseand the other 5 adapters are left uncast;Live (PostgreSQL, SQL Server, MySQL, CockroachDB): columns
amount DECIMAL(10,2),wide DECIMAL(38,10)andwhole DECIMAL(38,0).1234567890123456789.1234567890is stored exactly, and a 27-digit integer part is exact;=andIN;=andIN:NOT INand<>behave accordingly.Runs
RED:
INnegative (0 → 1). Lucee 7 × SQL Server matches the extra-fraction value with=andIN(0 → 1).GREEN on ed98313 (pass / fail+error):
Full suite, because the
$performQuery()override sees every query on these two databases:Full compat matrix: run 37118814632 on ed98313.
Related: #4162 / #4173 is the same engine behaviour for Oracle
NUMBER(20..38, 0)integers.🤖 Generated with Claude Code