Skip to content

Commit

Permalink
BUG#35707417: Return decimal value losing precision as string
Browse files Browse the repository at this point in the history
Some values stored in DECIMAL and NUMERIC columns are losing precision
when returned back to the client after executing a table query. Although
some unsafe values are already returned as a raw string, others are not.
This is due to an issue in the decision-making process for when a
decimal value should be converted to JavaScript number or not.

This patch introduces the changes to address the shortcomings of that
decision-making process and ensures all unsafe values (integers or
decimals) are not converted to a JavaScript number.

Change-Id: Ica4016014a0f437b87e1cc797965eeba82ddfac1
  • Loading branch information
ruiquelhas committed Aug 23, 2023
1 parent 12daae7 commit 7b531ff
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ v8.0.35

- Rows can now be inserted in a table using CRUD with an X DevAPI expression
- Date and time operators are now allowed in X DevAPI expressions
- All unsafe decimal values are now being returned as a raw string

v8.0.33
=======
Expand Down
22 changes: 18 additions & 4 deletions lib/Protocol/Wrappers/Messages/Resultset/Row.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates.
* Copyright (c) 2020, 2023, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0, as
Expand Down Expand Up @@ -362,9 +362,23 @@ function formatSafeNumber (value) {
const int = matcher[1];
const decimal = matcher[2];

const isUnsafe = Number.parseFloat(int) < Number.MIN_SAFE_INTEGER ||
Number.parseFloat(int) > Number.MAX_SAFE_INTEGER ||
Number.parseFloat(decimal) > Number.MAX_SAFE_INTEGER;
// JavaScript number is represented in 64-bit format IEEE-754, so there
// are exactly 64 bits to store a number: 52 of them are used to store the
// digits, 11 of them store the position of the decimal point, and 1 bit
// is for the sign.
let isUnsafe = false;

if (!decimal) {
// If the decimal part does not exist, we only need to worry about
// integer precision.
isUnsafe = BigInt(int) > Number.MAX_SAFE_INTEGER || BigInt(int) < Number.MIN_SAFE_INTEGER;
} else {
// If the decimal part exists, we can check if the number is safe is by
// comparing the original raw string with the string resulting from
// calling "toFixed()" using the number of decimal digits.
isUnsafe = `${int}.${decimal}` !== Number.parseFloat(`${int}.${decimal}`)
.toFixed(decimal.length);
}

// If the value is unsafe, we should not convert it to a JavaScript number
// because we might be loosing precision.
Expand Down
26 changes: 25 additions & 1 deletion test/functional/default/relational-tables/table-select.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates.
* Copyright (c) 2020, 2023, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0, as
Expand Down Expand Up @@ -669,6 +669,30 @@ describe('selecting rows from a table using CRUD', () => {
});
});

context('BUG#35707417', () => {
beforeEach('add relevant columns to the existing table', () => {
return session.sql(`ALTER TABLE \`${schema.getName()}\`.\`${table.getName()}\`
ADD COLUMN de1 DECIMAL(18, 17),
ADD COLUMN de2 DECIMAL(18, 13)`)
.execute();
});

beforeEach('populate the table', async () => {
return table.insert('de1', 'de2')
.values('1.23456789012345678', '12345.6789012345678')
.execute();
});

it('does not loose precision when retrieving values from fixed-point decimal columns', async () => {
const want = ['1.23456789012345678', '12345.6789012345678'];
const res = await table.select('de1', 'de2')
.execute();

const got = res.fetchOne();
expect(got).to.deep.equal(want);
});
});

context('when debug mode is enabled', () => {
beforeEach('populate table', () => {
return table.insert(['id', 'name', 'age'])
Expand Down

0 comments on commit 7b531ff

Please sign in to comment.