-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DBDecimal type loses precision when transferring both into and out of the database #11477
Comments
@emteknetnz I think you're likely to have opinions on this given the |
What Guy's referring to is that DBDecimal is now validated by the new DecimalFieldValidator in CMS 6 DBDecimal currently uses float internally, and DecimalFieldValidator will validate the value is a numeric non-string, so either an int or a float. The int is allowed so that we're not failing validation if someone set the value using an int e.g. $myDBDecimal->setValue(3); which would be needlessly annoying
That seems a little harsh IMO, float seems appropriate, though only up to a point ~16 decimal places
One possible solution here, though perhaps it's not great, is to just throw an exception during dev/build if more that 16 DP are specified by the column spec |
I guess I was a little harsh in my wording, sorry. What I meant by that is that, since every number stored in a DBDecimal field is converted to a float or int before and after being in the database, a DBDecimal's sole difference currently from a DBFloat is that it automatically rounds to a certain number of decimal digits |
There are some other differences, the main one being that DBDecimal will use the mysql decimal type, while DBFloat will use the mysql float type Also, in CMS 6, at least in its current state, DBDecimal will have some actual validation on write(), so that oversized values (i.e. more precision than the DB column would allow) will no longer get "truncated" on write and will instead now throw a ValidationException. Meanwhile the validation on DBFloat is limited to simply checking that the value is numeric and not a string. Still, your original point remains that floats are a limitation to the amount of precision that DBDecimal has |
The precision of float isn't quite 16 decimal digits. It's very close to 16 decimal digits if all precision is on the left side of the decimal point, but as a result of the floating point's behaviour, the fractional part can't store the same numbers as decimal can. For example,79071992547409.85 will become 79071992547409.84 or 550719925.4740935 becomes 550719925.4740936. So to avoid rounding from floating points we'd need to throw on total precision of 16 digits or higher. |
Ideally, there should probably be two Decimal field types. One with the convenience of float representation and one with extended precision but necessarily a string. The problem I see with implementing that, though, is that it's the database connector that's responsible for type coercion when extracting from the database, and the only context it has to go on is the type the database driver reports for the column. For one type to be a string in a newly-hydrated DO while the other is a float, the connectors would need to return string uncoerced and it would have to be DataObject's responsibility to coerce into a float if needed. But DataObject's design is currently such that the record array isn't validated, just set directly as the row returned from the database connector. |
Module version(s) affected
6.*, present in previous versions but fixing would be a breaking change
Description
DBDecimal is the DBField type for fixed-precision DECIMAL SQL types. The PHP float type is not sufficient to store the values of such a type, as all database engines support precisions of DECIMAL much higher than float can store. This means we lose out on the precision of the DECIMAL SQL type.
How to reproduce
I'm using the default MySQLiConnector
Create a DataObject
try to store a precise value in the DataObject
Possible Solution
I'm pretty sure that fixing this would be a breaking change, but the only PHP-native representation suitable for SQL Decimals is string. The BCMath PHP extension is precedent for fixed-precision numbers represented as strings.
mysqli natively binds decimal type selects as strings, and MySQLiConnector goes out of its way to convert those to floats via MySQLStatement (https://github.com/silverstripe/silverstripe-framework/blob/6/src/ORM/Connect/MySQLStatement.php#L111). Just remove decimal from the types it coerces to float. I don't know how the other database connectors are written.
DBDecimal (and thus possibly DBCurrency and DBMoney) would have to be modified so that they use string representations. Possibly DBCurrency and DBMoney could still do conversion to float, but then DBCurrency would need to limit its precision argument (a
'Currency(20,2)'
field would still be rounded when converted to float) so that it's within what a float can store.Additional Context
No response
Validations
silverstripe/installer
(with any code examples you've provided)The text was updated successfully, but these errors were encountered: