Skip to content
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

fix: check for precision overflow when parsing float as decimal #7627

Merged
merged 2 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions datafusion/sql/src/expr/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use crate::planner::{ContextProvider, PlannerContext, SqlToRel};
use arrow::compute::kernels::cast_utils::parse_interval_month_day_nano;
use arrow::datatypes::DECIMAL128_MAX_PRECISION;
use arrow_schema::DataType;
use datafusion_common::{
not_impl_err, plan_err, DFSchema, DataFusionError, Result, ScalarValue,
Expand Down Expand Up @@ -66,23 +67,19 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> {
let p = str.len() - 1;
let s = str.len() - i - 1;
let str = str.replace('.', "");
let n = str.parse::<i128>().map_err(|_| {
DataFusionError::from(ParserError(format!(
"Cannot parse {str} as i128 when building decimal"
)))
})?;
let n = parse_decimal_128_without_scale(&str)?;
Ok(Expr::Literal(ScalarValue::Decimal128(
Some(n),
p as u8,
s as i8,
)))
} else {
let number = n.parse::<i128>().map_err(|_| {
DataFusionError::from(ParserError(format!(
"Cannot parse {n} as i128 when building decimal"
)))
})?;
Ok(Expr::Literal(ScalarValue::Decimal128(Some(number), 38, 0)))
let number = parse_decimal_128_without_scale(str)?;
Ok(Expr::Literal(ScalarValue::Decimal128(
Some(number),
str.len() as u8,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to use the minimum precision instead of the maximum precision.

0,
)))
}
} else {
n.parse::<f64>().map(lit).map_err(|_| {
Expand Down Expand Up @@ -394,6 +391,23 @@ const fn try_decode_hex_char(c: u8) -> Option<u8> {
}
}

fn parse_decimal_128_without_scale(s: &str) -> Result<i128> {
let number = s.parse::<i128>().map_err(|e| {
DataFusionError::from(ParserError(format!(
"Cannot parse {s} as i128 when building decimal: {e}"
)))
})?;

const DECIMAL128_MAX_VALUE: i128 = 10_i128.pow(DECIMAL128_MAX_PRECISION as u32) - 1;
if number > DECIMAL128_MAX_VALUE {
return Err(DataFusionError::from(ParserError(format!(
"Cannot parse {s} as i128 when building decimal: precision overflow"
))));
}

Ok(number)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion datafusion/sql/tests/sql_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn parse_decimals() {
("18446744073709551615", "UInt64(18446744073709551615)"),
(
"18446744073709551616",
"Decimal128(Some(18446744073709551616),38,0)",
"Decimal128(Some(18446744073709551616),20,0)",
),
];
for (a, b) in test_data {
Expand Down
27 changes: 0 additions & 27 deletions datafusion/sqllogictest/test_files/ddl.slt
Original file line number Diff line number Diff line change
Expand Up @@ -670,33 +670,6 @@ describe TABLE_WITHOUT_NORMALIZATION
FIELD1 Int64 YES
FIELD2 Int64 YES

query R
select 10000000000000000000.01
----
10000000000000000000

query T
select arrow_typeof(10000000000000000000.01)
----
Float64

statement ok
set datafusion.sql_parser.parse_float_as_decimal = true;

query R
select 10000000000000000000.01
----
10000000000000000000.01

query T
select arrow_typeof(10000000000000000000.01)
----
Decimal128(22, 2)

# Restore those to default value again
statement ok
set datafusion.sql_parser.parse_float_as_decimal = false;

statement ok
set datafusion.sql_parser.enable_ident_normalization = true;

Expand Down
69 changes: 69 additions & 0 deletions datafusion/sqllogictest/test_files/options.slt
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,72 @@ set datafusion.execution.batch_size = 8192;

statement ok
drop table a

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the tests to options.slt. I think those tests are not related to DDL.

# test datafusion.sql_parser.parse_float_as_decimal
# default option value is false
query R
select 10000000000000000000.01
----
10000000000000000000

query T
select arrow_typeof(10000000000000000000.01)
----
Float64

statement ok
set datafusion.sql_parser.parse_float_as_decimal = true;

query R
select 10000000000000000000.01
----
10000000000000000000.01

query T
select arrow_typeof(10000000000000000000.01)
----
Decimal128(22, 2)

query R
select 999999999999999999999999999999999999
----
999999999999999999999999999999999999

query T
select arrow_typeof(999999999999999999999999999999999999)
----
Decimal128(36, 0)

query R
select 99999999999999999999999999999999999999
----
99999999999999999999999999999999999999

query T
select arrow_typeof(99999999999999999999999999999999999999)
----
Decimal128(38, 0)

query R
select 9999999999999999999999999999999999.9999
----
9999999999999999999999999999999999.9999

query T
select arrow_typeof(9999999999999999999999999999999999.9999)
----
Decimal128(38, 4)

# precision overflow
statement error SQL error: ParserError\("Cannot parse 123456789012345678901234567890123456789 as i128 when building decimal: precision overflow"\)
select 123456789.012345678901234567890123456789

# can not fit in i128
statement error SQL error: ParserError\("Cannot parse 1234567890123456789012345678901234567890 as i128 when building decimal: number too large to fit in target type"\)
select 123456789.0123456789012345678901234567890

# Restore those to default value again
statement ok
set datafusion.sql_parser.parse_float_as_decimal = false;