This is an automated email from the ASF dual-hosted git repository.
zstan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 9995443666 IGNITE-19353: Sql. Incorrect type conversion for dynamic
parameters - CAST operation ignores type precision. (#2220)
9995443666 is described below
commit 99954436663de5f3b2fe9d86b7c27487c0463a18
Author: Max Zhuravkov <[email protected]>
AuthorDate: Wed Jun 28 17:44:55 2023 +0300
IGNITE-19353: Sql. Incorrect type conversion for dynamic parameters - CAST
operation ignores type precision. (#2220)
---
.../internal/sql/engine/ItDataTypesTest.java | 268 +++++++++++++++++++++
.../sql/engine/ItDynamicParameterTest.java | 57 +++--
.../sql/engine/exec/exp/IgniteSqlFunctions.java | 77 ++++--
.../sql/engine/sql/IgniteSqlDecimalLiteral.java | 20 +-
.../engine/exec/exp/IgniteSqlFunctionsTest.java | 73 ++++++
.../engine/sql/IgniteSqlDecimalLiteralTest.java | 30 +++
6 files changed, 488 insertions(+), 37 deletions(-)
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
index 765628b178..adb6be9f34 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
@@ -17,8 +17,12 @@
package org.apache.ignite.internal.sql.engine;
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -26,14 +30,30 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.runtime.CalciteContextException;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.ignite.internal.sql.engine.util.Commons;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.apache.ignite.lang.IgniteException;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Test SQL data types.
*/
public class ItDataTypesTest extends ClusterPerClassIntegrationTest {
+
+ private static final String NUMERIC_OVERFLOW_ERROR = "Numeric field
overflow";
+
+ private static final String NUMERIC_FORMAT_ERROR = "neither a decimal
digit number";
+
/**
* Drops all created tables.
*/
@@ -315,6 +335,254 @@ public class ItDataTypesTest extends
ClusterPerClassIntegrationTest {
assertQuery("SELECT id FROM tbl WHERE val = DECIMAL
'10.20'").returns(1).check();
}
+
+ /** decimal casts - cast literal to decimal. */
+ @ParameterizedTest(name = "{2}:{1} AS {3} = {4}")
+ @MethodSource("decimalCastFromLiterals")
+ public void testDecimalCastsNumericLiterals(CaseStatus status, RelDataType
inputType, Object input,
+ RelDataType targetType, Result<BigDecimal> result) {
+
+ Assumptions.assumeTrue(status == CaseStatus.RUN);
+
+ String literal = asLiteral(input, inputType);
+ String query = format("SELECT CAST({} AS {})", literal, targetType);
+
+ QueryChecker checker = assertQuery(query);
+ expectResult(checker, result);
+ }
+
+ private static Stream<Arguments> decimalCastFromLiterals() {
+ RelDataType varcharType = varcharType();
+ // ignored
+ RelDataType numeric = decimalType(4);
+
+ return Stream.of(
+ // String
+ arguments(CaseStatus.RUN, varcharType, "100", decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, varcharType, "100.12",
decimalType(5, 1), bigDecimalVal("100.1")),
+ arguments(CaseStatus.RUN, varcharType, "lame", decimalType(5,
1), error(NUMERIC_FORMAT_ERROR)),
+ arguments(CaseStatus.RUN, varcharType, "12345", decimalType(5,
1), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.RUN, varcharType, "1234", decimalType(5,
1), bigDecimalVal("1234.0")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, varcharType, "100.12",
decimalType(1, 0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Numeric
+ arguments(CaseStatus.RUN, numeric, "100", decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, numeric, "100", decimalType(3, 0),
bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, numeric, "100.12", decimalType(5,
1), bigDecimalVal("100.1")),
+ arguments(CaseStatus.SKIP, numeric, "100.12", decimalType(5,
0), bigDecimalVal("100")),
+ arguments(CaseStatus.SKIP, numeric, "100", decimalType(2, 0),
error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, numeric, "100.12", decimalType(5,
2), bigDecimalVal("100.12"))
+ );
+ }
+
+ /** decimal casts - cast dynamic param to decimal. */
+ @ParameterizedTest(name = "{2}:?{1} AS {3} = {4}")
+ @MethodSource("decimalCasts")
+ public void testDecimalCastsDynamicParams(CaseStatus ignore, RelDataType
inputType, Object input,
+ RelDataType targetType, Result<BigDecimal> result) {
+ // We ignore status because every case should work for dynamic
parameter.
+
+ String query = format("SELECT CAST(? AS {})", targetType);
+
+ QueryChecker checker = assertQuery(query).withParams(input);
+ expectResult(checker, result);
+ }
+
+ /** decimals casts - cast numeric literal to specific type then cast the
result to decimal. */
+ @ParameterizedTest(name = "{1}: {2}::{1} AS {3} = {4}")
+ @MethodSource("decimalCasts")
+ public void testDecimalCastsFromNumeric(CaseStatus status, RelDataType
inputType, Object input,
+ RelDataType targetType, Result<BigDecimal> result) {
+
+ Assumptions.assumeTrue(status == CaseStatus.RUN);
+
+ String literal = asLiteral(input, inputType);
+ String query = format("SELECT CAST({}::{} AS {})", literal, inputType,
targetType);
+
+ QueryChecker checker = assertQuery(query);
+ expectResult(checker, result);
+ }
+
+ static String asLiteral(Object value, RelDataType type) {
+ if (SqlTypeUtil.isCharacter(type)) {
+ String str = (String) value;
+ return format("'{}'", str);
+ } else {
+ return String.valueOf(value);
+ }
+ }
+
+ /**
+ * Indicates whether a test case should run or should be skipped.
+ * We need this because the set of test cases is the same for both dynamic
params
+ * and numeric values.
+ *
+ * <p>TODO Should be removed after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ */
+ enum CaseStatus {
+ /** Case should run. */
+ RUN,
+ /** Case should be skipped. */
+ SKIP
+ }
+
+ private static Stream<Arguments> decimalCasts() {
+ RelDataType varcharType = varcharType();
+ RelDataType tinyIntType = sqlType(SqlTypeName.TINYINT);
+ RelDataType smallIntType = sqlType(SqlTypeName.SMALLINT);
+ RelDataType integerType = sqlType(SqlTypeName.INTEGER);
+ RelDataType bigintType = sqlType(SqlTypeName.BIGINT);
+ RelDataType realType = sqlType(SqlTypeName.REAL);
+ RelDataType doubleType = sqlType(SqlTypeName.DOUBLE);
+
+ return Stream.of(
+ // String
+ arguments(CaseStatus.RUN, varcharType, "100", decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, varcharType, "100", decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, varcharType, "100", decimalType(3,
0), bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, varcharType, "100", decimalType(4,
1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, varcharType, "100", decimalType(2,
0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Tinyint
+ arguments(CaseStatus.SKIP, tinyIntType, (byte) 100,
decimalType(3), bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, tinyIntType, (byte) 100,
decimalType(3, 0), bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, tinyIntType, (byte) 100,
decimalType(4, 1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, tinyIntType, (byte) 100,
decimalType(2, 0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Smallint
+ arguments(CaseStatus.RUN, smallIntType, (short) 100,
decimalType(3), bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, smallIntType, (short) 100,
decimalType(3, 0), bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, smallIntType, (short) 100,
decimalType(4, 1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, smallIntType, (short) 100,
decimalType(2, 0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Integer
+ arguments(CaseStatus.RUN, integerType, 100, decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, integerType, 100, decimalType(3, 0),
bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, integerType, 100, decimalType(4,
1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, integerType, 100, decimalType(2,
0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Bigint
+ arguments(CaseStatus.RUN, bigintType, 100L, decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, bigintType, 100L, decimalType(3, 0),
bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, bigintType, 100L, decimalType(4,
1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, bigintType, 100L, decimalType(2,
0), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Real
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, realType, 100.0f, decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.SKIP, realType, 100.0f, decimalType(3,
0), bigDecimalVal("100")),
+ arguments(CaseStatus.SKIP, realType, 100.0f, decimalType(4,
1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, realType, 100.0f, decimalType(2,
0), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, realType, 0.1f, decimalType(1, 1),
bigDecimalVal("0.1")),
+ arguments(CaseStatus.SKIP, realType, 0.1f, decimalType(2, 2),
bigDecimalVal("0.10")),
+ arguments(CaseStatus.SKIP, realType, 10.12f, decimalType(2,
1), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, realType, 0.12f, decimalType(1, 2),
error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Double
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, doubleType, 100.0d, decimalType(3),
bigDecimalVal("100")),
+ arguments(CaseStatus.SKIP, doubleType, 100.0d, decimalType(3,
0), bigDecimalVal("100")),
+ arguments(CaseStatus.SKIP, doubleType, 100.0d, decimalType(4,
1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, doubleType, 100.0d, decimalType(2,
0), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, doubleType, 0.1d, decimalType(1,
1), bigDecimalVal("0.1")),
+ arguments(CaseStatus.SKIP, doubleType, 0.1d, decimalType(2,
2), bigDecimalVal("0.10")),
+ arguments(CaseStatus.SKIP, doubleType, 10.12d, decimalType(2,
1), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, doubleType, 0.12d, decimalType(1,
2), error(NUMERIC_OVERFLOW_ERROR)),
+
+ // Decimal
+ arguments(CaseStatus.RUN, decimalType(1, 1), new
BigDecimal("0.1"), decimalType(1, 1), bigDecimalVal("0.1")),
+ arguments(CaseStatus.RUN, decimalType(3), new
BigDecimal("100"), decimalType(3), bigDecimalVal("100")),
+ arguments(CaseStatus.RUN, decimalType(3), new
BigDecimal("100"), decimalType(3, 0), bigDecimalVal("100")),
+ // TODO Uncomment these test cases after
https://issues.apache.org/jira/browse/IGNITE-19822 is fixed.
+ arguments(CaseStatus.SKIP, decimalType(3), new
BigDecimal("100"), decimalType(4, 1), bigDecimalVal("100.0")),
+ arguments(CaseStatus.SKIP, decimalType(3), new
BigDecimal("100"), decimalType(2, 0), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, decimalType(1, 1), new
BigDecimal("0.1"), decimalType(2, 2), bigDecimalVal("0.10")),
+ arguments(CaseStatus.SKIP, decimalType(4, 2), new
BigDecimal("10.12"), decimalType(2, 1), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, decimalType(2, 2), new
BigDecimal("0.12"), decimalType(1, 2), error(NUMERIC_OVERFLOW_ERROR)),
+ arguments(CaseStatus.SKIP, decimalType(1, 1), new
BigDecimal("0.1"), decimalType(1, 1), bigDecimalVal("0.1"))
+ );
+ }
+
+
+ private static RelDataType sqlType(SqlTypeName typeName) {
+ return Commons.typeFactory().createSqlType(typeName);
+ }
+
+ private static RelDataType decimalType(int precision, int scale) {
+ return Commons.typeFactory().createSqlType(SqlTypeName.DECIMAL,
precision, scale);
+ }
+
+ private static RelDataType decimalType(int precision) {
+ return Commons.typeFactory().createSqlType(SqlTypeName.DECIMAL,
precision, RelDataType.SCALE_NOT_SPECIFIED);
+ }
+
+ private static RelDataType varcharType() {
+ return Commons.typeFactory().createSqlType(SqlTypeName.VARCHAR);
+ }
+
+ /**
+ * Result contains a {@code BigDecimal} value represented by the given
string.
+ */
+ private static Result<BigDecimal> bigDecimalVal(String value) {
+ return new Result<>(new BigDecimal(value), null);
+ }
+
+ /** Result contains an error which message contains the following
substring. */
+ private static <T> Result<T> error(String error) {
+ return new Result<>(null, error);
+ }
+
+ /**
+ * Contains result of a test case. It can either be a value or an error.
+ *
+ * @param <T> Value type.
+ */
+ private static class Result<T> {
+ final T value;
+ final String error;
+
+ Result(T value, String error) {
+ if (error != null && value != null) {
+ throw new IllegalArgumentException("Both error and value have
been specified");
+ }
+ if (error == null && value == null) {
+ throw new IllegalArgumentException("Neither error nor value
have been specified");
+ }
+ this.value = value;
+ this.error = error;
+ }
+
+ @Override
+ public String toString() {
+ if (value != null) {
+ return "VAL:" + value;
+ } else {
+ return "ERR:" + error;
+ }
+ }
+ }
+
+ @Override
+ protected int nodes() {
+ return 1;
+ }
+
+ private void expectResult(QueryChecker checker, Result<?> result) {
+ if (result.error == null) {
+ checker.returns(result.value).check();
+ } else {
+ IgniteException err = assertThrows(IgniteException.class,
checker::check);
+ assertThat(err.getMessage(), containsString(result.error));
+ }
+ }
+
private LocalDate sqlDate(String str) {
return LocalDate.parse(str);
}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
index caf4e0f470..3058d7f581 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
@@ -22,11 +22,17 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.sql.Date;
import java.time.LocalDate;
import java.util.List;
+import java.util.stream.Stream;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.runtime.CalciteContextException;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.MetadataMatcher;
import org.apache.ignite.internal.sql.util.SqlTestUtils;
import org.apache.ignite.internal.testframework.IgniteTestUtils;
@@ -36,9 +42,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.EnumSource.Mode;
+import org.junit.jupiter.params.provider.MethodSource;
/** Dynamic parameters checks. */
public class ItDynamicParameterTest extends ClusterPerClassIntegrationTest {
@@ -235,21 +242,41 @@ public class ItDynamicParameterTest extends
ClusterPerClassIntegrationTest {
assertUnexpectedNumberOfParameters("SELECT * FROM t1 OFFSET ?", 1, 2);
}
- /** var char casts. */
+ /** varchar casts - literals. */
@ParameterizedTest
- @CsvSource({
- //input, type, result
- "abcde, VARCHAR, abcde",
- "abcde, VARCHAR(3), abc",
- "abcde, CHAR(3), abc",
- "abcde, CHAR, a",
- })
- public void testVarcharCasts(String param, String type, String expected) {
- String q1 = format("SELECT CAST('{}' AS {})", param, type);
- assertQuery(q1).returns(expected).check();
-
- String q2 = format("SELECT CAST(? AS {})", type);
- assertQuery(q2).withParams(param).returns(expected).check();
+ @MethodSource("varcharCasts")
+ public void testVarcharCastsLiterals(String value, RelDataType type,
String result) {
+ String query = format("SELECT CAST('{}' AS {})", value, type);
+ assertQuery(query).returns(result).check();
+ }
+
+ /** varchar casts - dynamic params. */
+ @ParameterizedTest
+ @MethodSource("varcharCasts")
+ public void testVarcharCastsDynamicParams(String value, RelDataType type,
String result) {
+ String query = format("SELECT CAST(? AS {})", type);
+ assertQuery(query).withParams(value).returns(result).check();
+ }
+
+ private static Stream<Arguments> varcharCasts() {
+ IgniteTypeFactory typeFactory = Commons.typeFactory();
+
+ return Stream.of(
+ // varchar
+ arguments("abcde",
typeFactory.createSqlType(SqlTypeName.VARCHAR, 3), "abc"),
+ arguments("abcde",
typeFactory.createSqlType(SqlTypeName.VARCHAR, 5), "abcde"),
+ arguments("abcde",
typeFactory.createSqlType(SqlTypeName.VARCHAR, 6), "abcde"),
+ arguments("abcde",
typeFactory.createSqlType(SqlTypeName.VARCHAR), "abcde"),
+
+ // char
+ arguments("abcde",
typeFactory.createSqlType(SqlTypeName.CHAR), "a"),
+ arguments("abcde", typeFactory.createSqlType(SqlTypeName.CHAR,
3), "abc")
+ );
+ }
+
+ @Override
+ protected int nodes() {
+ return 1;
}
private static void assertUnexpectedNumberOfParameters(String query,
Object... params) {
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
index a00007f172..6cb3ef60ba 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctions.java
@@ -54,6 +54,7 @@ import
org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.TypeUtils;
import org.apache.ignite.lang.IgniteInternalException;
+import org.apache.ignite.sql.SqlException;
import org.jetbrains.annotations.Nullable;
/**
@@ -61,6 +62,7 @@ import org.jetbrains.annotations.Nullable;
*/
public class IgniteSqlFunctions {
private static final DateTimeFormatter ISO_LOCAL_DATE_TIME_EX;
+ private static final String NUMERIC_FIELD_OVERFLOW_ERROR = "Numeric field
overflow";
static {
ISO_LOCAL_DATE_TIME_EX = new DateTimeFormatterBuilder()
@@ -133,38 +135,36 @@ public class IgniteSqlFunctions {
/** CAST(DOUBLE AS DECIMAL). */
public static BigDecimal toBigDecimal(double val, int precision, int
scale) {
- BigDecimal decimal = BigDecimal.valueOf(val);
- return setScale(precision, scale, decimal);
+ return toBigDecimal((Double) val, precision, scale);
}
/** CAST(FLOAT AS DECIMAL). */
public static BigDecimal toBigDecimal(float val, int precision, int scale)
{
- BigDecimal decimal = new BigDecimal(String.valueOf(val));
- return setScale(precision, scale, decimal);
+ return toBigDecimal((Float) val, precision, scale);
}
/** CAST(java long AS DECIMAL). */
public static BigDecimal toBigDecimal(long val, int precision, int scale) {
BigDecimal decimal = BigDecimal.valueOf(val);
- return setScale(precision, scale, decimal);
+ return convertDecimal(decimal, precision, scale);
}
/** CAST(INT AS DECIMAL). */
public static BigDecimal toBigDecimal(int val, int precision, int scale) {
BigDecimal decimal = new BigDecimal(val);
- return setScale(precision, scale, decimal);
+ return convertDecimal(decimal, precision, scale);
}
/** CAST(java short AS DECIMAL). */
public static BigDecimal toBigDecimal(short val, int precision, int scale)
{
- BigDecimal decimal = new BigDecimal(String.valueOf(val));
- return setScale(precision, scale, decimal);
+ BigDecimal decimal = new BigDecimal(val);
+ return convertDecimal(decimal, precision, scale);
}
/** CAST(java byte AS DECIMAL). */
public static BigDecimal toBigDecimal(byte val, int precision, int scale) {
- BigDecimal decimal = new BigDecimal(String.valueOf(val));
- return setScale(precision, scale, decimal);
+ BigDecimal decimal = new BigDecimal(val);
+ return convertDecimal(decimal, precision, scale);
}
/** CAST(BOOL AS DECIMAL). */
@@ -178,7 +178,7 @@ public class IgniteSqlFunctions {
return null;
}
BigDecimal decimal = new BigDecimal(s.trim());
- return setScale(precision, scale, decimal);
+ return convertDecimal(decimal, precision, scale);
}
/** CAST(REAL AS DECIMAL). */
@@ -186,13 +186,21 @@ public class IgniteSqlFunctions {
if (num == null) {
return null;
}
- // There are some values of "long" that cannot be represented as
"double".
- // Not so "int". If it isn't a long, go straight to double.
- BigDecimal decimal = num instanceof BigDecimal ? ((BigDecimal) num)
- : num instanceof BigInteger ? new BigDecimal((BigInteger) num)
- : num instanceof Long ? new BigDecimal(num.longValue())
- : BigDecimal.valueOf(num.doubleValue());
- return setScale(precision, scale, decimal);
+
+ BigDecimal dec;
+ if (num instanceof Float) {
+ dec = new BigDecimal(num.floatValue());
+ } else if (num instanceof Double) {
+ dec = new BigDecimal(num.doubleValue());
+ } else if (num instanceof BigDecimal) {
+ dec = (BigDecimal) num;
+ } else if (num instanceof BigInteger) {
+ dec = new BigDecimal((BigInteger) num);
+ } else {
+ dec = new BigDecimal(num.longValue());
+ }
+
+ return convertDecimal(dec, precision, scale);
}
/** Cast object depending on type to DECIMAL. */
@@ -209,6 +217,39 @@ public class IgniteSqlFunctions {
: toBigDecimal(o.toString(), precision, scale);
}
+ /**
+ * Converts the given {@code BigDecimal} to a decimal with the given
{@code precision} and {@code scale}
+ * according to SQL spec for CAST specification: General Rules, 8.
+ */
+ public static BigDecimal convertDecimal(BigDecimal value, int precision,
int scale) {
+ assert precision > 0 : "Invalid precision: " + precision;
+
+ int defaultPrecision =
IgniteTypeSystem.INSTANCE.getDefaultPrecision(SqlTypeName.DECIMAL);
+ if (precision == defaultPrecision) {
+ // This branch covers at least one known case: access to dynamic
parameter from context.
+ // In this scenario precision = DefaultTypePrecision, because
types for dynamic params
+ // are created by toSql(createType(param.class)).
+ return value;
+ }
+
+ boolean nonZero = !value.unscaledValue().equals(BigInteger.ZERO);
+
+ if (nonZero) {
+ if (scale > precision) {
+ throw new SqlException(QUERY_INVALID_ERR,
NUMERIC_FIELD_OVERFLOW_ERROR);
+ } else {
+ int currentSignificantDigits = value.precision() -
value.scale();
+ int expectedSignificantDigits = precision - scale;
+
+ if (currentSignificantDigits > expectedSignificantDigits) {
+ throw new SqlException(QUERY_INVALID_ERR,
NUMERIC_FIELD_OVERFLOW_ERROR);
+ }
+ }
+ }
+
+ return value.setScale(scale, RoundingMode.HALF_UP);
+ }
+
/** CAST(VARCHAR AS VARBINARY). */
public static ByteString toByteString(String s) {
return s == null ? null : new
ByteString(s.getBytes(Commons.typeFactory().getDefaultCharset()));
diff --git
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteral.java
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteral.java
index fd6981513a..d6569f17dd 100644
---
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteral.java
+++
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteral.java
@@ -40,9 +40,7 @@ public final class IgniteSqlDecimalLiteral extends
SqlNumericLiteral {
* Constructor.
*/
private IgniteSqlDecimalLiteral(BigDecimal value, SqlParserPos pos) {
- // We are using precision/scale from BigDecimal because calcite's
values
- // for those are not incorrect as they include an additional digit in
precision for negative numbers.
- super(value, value.precision(), value.scale(), true, pos);
+ super(value, getPrecision(value), value.scale(), true, pos);
}
/** Creates a decimal literal. */
@@ -65,8 +63,9 @@ public final class IgniteSqlDecimalLiteral extends
SqlNumericLiteral {
@Override
public RelDataType createSqlType(RelDataTypeFactory typeFactory) {
var value = getDecimalValue();
+ var precision = getPrecision(value);
- return typeFactory.createSqlType(SqlTypeName.DECIMAL,
value.precision(), value.scale());
+ return typeFactory.createSqlType(SqlTypeName.DECIMAL, precision,
value.scale());
}
/** {@inheritDoc} **/
@@ -98,4 +97,17 @@ public final class IgniteSqlDecimalLiteral extends
SqlNumericLiteral {
assert value != null : "bigDecimalValue returned null for a subclass
exact numeric literal: " + this;
return value;
}
+
+ private static int getPrecision(BigDecimal value) {
+ int scale = value.scale();
+
+ if (value.precision() == 1 && value.compareTo(BigDecimal.ONE) < 0) {
+ // For numbers less than 1 we have different precision between
Java's BigDecimal and Calcite:
+ // 0.01 - BigDecimal precision=1, scale=2, Calcite: precision=3,
scale=2
+
+ return 1 + scale;
+ } else {
+ return value.precision();
+ }
+ }
}
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
index 0b8fb2a8dd..be033ab911 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteSqlFunctionsTest.java
@@ -19,10 +19,17 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
+import java.util.function.Supplier;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeSystem;
+import org.apache.ignite.sql.SqlException;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
/**
* Sql functions test.
@@ -138,4 +145,70 @@ public class IgniteSqlFunctionsTest {
IgniteSqlFunctions.toBigDecimal(Double.valueOf(10.101d), 10, 3)
);
}
+
+ /** Access of dynamic parameter value - parameter is not transformed. */
+ @Test
+ public void testToBigDecimalFromObject() {
+ Object value = new BigDecimal("100.1");
+ int defaultPrecision =
IgniteTypeSystem.INSTANCE.getDefaultPrecision(SqlTypeName.DECIMAL);
+
+ assertSame(value, IgniteSqlFunctions.toBigDecimal(value,
defaultPrecision, 0));
+ }
+
+ /** Tests for decimal conversion function. */
+ @ParameterizedTest
+ @CsvSource({
+ // input, precision, scale, result (number or error)
+ "0, 1, 0, 0",
+ "0, 1, 2, 0.00",
+ "0, 2, 2, 0.00",
+ "0, 2, 4, 0.0000",
+
+ "1, 1, 0, 1",
+ "1, 3, 2, 1.00",
+ "1, 2, 2, overflow",
+
+ "0.1, 1, 1, 0.1",
+
+ "0.12, 2, 1, 0.1",
+ "0.12, 2, 2, 0.12",
+ "0.123, 2, 2, 0.12",
+ "0.123, 2, 1, 0.1",
+ "0.123, 5, 5, 0.12300",
+
+ "1.23, 2, 1, 1.2",
+
+ "10, 2, 0, 10",
+ "10.0, 2, 0, 10",
+ "10, 3, 0, 10",
+
+ "10.01, 2, 0, 10",
+ "10.1, 3, 0, 10",
+ "10.11, 2, 1, overflow",
+ "10.11, 3, 1, 10.1",
+ "10.00, 3, 1, 10.0",
+
+ "100.0, 3, 1, overflow",
+
+ "100.01, 4, 1, 100.0",
+ "100.01, 4, 0, 100",
+ "100.111, 3, 1, overflow",
+
+ "11.1, 5, 3, 11.100",
+ "11.100, 5, 3, 11.100",
+
+ "-10.1, 3, 1, -10.1",
+ "-10.1, 2, 0, -10",
+ "-10.1, 2, 1, overflow",
+ })
+ public void testConvertDecimal(String input, int precision, int scale,
String result) {
+ Supplier<BigDecimal> convert = () ->
IgniteSqlFunctions.convertDecimal(new BigDecimal(input), precision, scale);
+
+ if (!"overflow".equalsIgnoreCase(result)) {
+ BigDecimal expected = convert.get();
+ assertEquals(new BigDecimal(result), expected);
+ } else {
+ assertThrows(SqlException.class, convert::get);
+ }
+ }
}
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteralTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteralTest.java
index aa3ab707ed..926e6c1105 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteralTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlDecimalLiteralTest.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlWriter;
@@ -34,10 +35,13 @@ import org.apache.calcite.sql.pretty.SqlPrettyWriter;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.Litmus;
import org.apache.ignite.internal.sql.engine.planner.AbstractPlannerTest;
+import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
+import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.sql.SqlException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
/**
@@ -66,6 +70,32 @@ public class IgniteSqlDecimalLiteralTest extends
AbstractPlannerTest {
assertEquals(expectedType, actualType, "type");
}
+ /**
+ * Type of numeric decimal literal and type of decimal literal should
match.
+ */
+ @ParameterizedTest
+ @CsvSource({
+ "-0.01",
+ "-0.1",
+ "-10.0",
+ "-10.122",
+ "0.0",
+ "0.1",
+ "0.01",
+ "10.0",
+ "10.122",
+ })
+ public void testLiteralTypeMatch(String val) throws Exception {
+ String query = format("SELECT {}, DECIMAL '{}'", val, val);
+
+ IgniteRel rel = physicalPlan(query, new IgniteSchema("PUBLIC"));
+
+ RelDataType numericLitType =
rel.getRowType().getFieldList().get(0).getType();
+ RelDataType decimalLitType =
rel.getRowType().getFieldList().get(1).getType();
+
+ assertEquals(numericLitType, decimalLitType);
+ }
+
/**
* Tests {@link IgniteSqlDecimalLiteral#unparse(SqlWriter, int, int)}.
*/