This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new fdabee35b0 [CALCITE-7844] Decimal division scale reduction uses total
precision instead of integral digits
fdabee35b0 is described below
commit fdabee35b0ab2003c7c1352b83ef28667fcd51c7
Author: Aleksandr Efimov <[email protected]>
AuthorDate: Wed Oct 28 13:03:24 2026 +0300
[CALCITE-7844] Decimal division scale reduction uses total precision
instead of integral digits
---
.../rel/type/DecimalDivisionTypeSystem.java | 65 ++++++++++
.../sql/type/DecimalDivisionTypeSystemTest.java | 139 +++++++++++++++++++++
2 files changed, 204 insertions(+)
diff --git
a/core/src/main/java/org/apache/calcite/rel/type/DecimalDivisionTypeSystem.java
b/core/src/main/java/org/apache/calcite/rel/type/DecimalDivisionTypeSystem.java
new file mode 100644
index 0000000000..0ab0b7a3af
--- /dev/null
+++
b/core/src/main/java/org/apache/calcite/rel/type/DecimalDivisionTypeSystem.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.rel.type;
+
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Type system that reduces decimal division scale based on the number of
+ * integral digits, rather than the total precision.
+ *
+ * <p>Other type rules and limits are the same as {@link
RelDataTypeSystem#DEFAULT}.
+ * To enable it, set the {@code typeSystem} connection property to
+ * {@code org.apache.calcite.rel.type.DecimalDivisionTypeSystem}.
+ * For example, dividing two {@code DECIMAL(6, 2)} values yields
+ * {@code DECIMAL(15, 9)}, rather than the default {@code DECIMAL(15, 6)}.
+ */
+public class DecimalDivisionTypeSystem extends RelDataTypeSystemImpl {
+ @Override public @Nullable RelDataType deriveDecimalDivideType(
+ RelDataTypeFactory typeFactory, RelDataType type1, RelDataType type2) {
+ if (!SqlTypeUtil.isExactNumeric(type1)
+ || !SqlTypeUtil.isExactNumeric(type2)
+ || !(SqlTypeUtil.isDecimal(type1) || SqlTypeUtil.isDecimal(type2))) {
+ return null;
+ }
+
+ // Java numeric types need their default decimal precision and scale.
+ type1 = RelDataTypeFactoryImpl.isJavaType(type1)
+ ? typeFactory.decimalOf(type1) : type1;
+ type2 = RelDataTypeFactoryImpl.isJavaType(type2)
+ ? typeFactory.decimalOf(type2) : type2;
+ final int maxPrecision = getMaxPrecision(SqlTypeName.DECIMAL);
+ final int maxScale = getMaxScale(SqlTypeName.DECIMAL);
+ final int minScale = Math.min(6, maxScale);
+ final int integral = type1.getPrecision() - type1.getScale() +
type2.getScale();
+ int scale =
+ Math.min(Math.max(minScale, type1.getScale() + type2.getPrecision() +
1), maxScale);
+ final int precision = Math.min(integral + scale, maxPrecision);
+
+ // Preserve fractional digits when the integral part fits. Otherwise keep
+ // at most six fractional digits, as in the default division rule.
+ if (integral <= maxPrecision - minScale) {
+ scale = Math.min(scale, maxPrecision - integral);
+ } else {
+ scale = Math.min(minScale, scale);
+ }
+ return typeFactory.createSqlType(SqlTypeName.DECIMAL, precision, scale);
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/sql/type/DecimalDivisionTypeSystemTest.java
b/core/src/test/java/org/apache/calcite/sql/type/DecimalDivisionTypeSystemTest.java
new file mode 100644
index 0000000000..6eb0637924
--- /dev/null
+++
b/core/src/test/java/org/apache/calcite/sql/type/DecimalDivisionTypeSystemTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql.type;
+
+import org.apache.calcite.config.CalciteConnectionProperty;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.rel.type.DecimalDivisionTypeSystem;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Properties;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasToString;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/** Tests for {@link DecimalDivisionTypeSystem}. */
+class DecimalDivisionTypeSystemTest {
+ @BeforeAll static void initializeDefaultTypeSystem() {
+ // Initialize the default before constructing custom type systems in
parallel.
+ assertThat(RelDataTypeSystem.DEFAULT.getMaxPrecision(SqlTypeName.DECIMAL),
is(19));
+ }
+
+ @Test void testDefaultLimits() {
+ checkDivide(RelDataTypeSystem.DEFAULT, 6, 2, 6, 2, 15, 6);
+ checkDivide(new DecimalDivisionTypeSystem(), 6, 2, 6, 2, 15, 9);
+ checkDivide(new DecimalDivisionTypeSystem(), 5, 2, 10, 2, 18, 13);
+ checkDivide(new DecimalDivisionTypeSystem(), 19, 2, 19, 2, 19, 6);
+ }
+
+ @Test void testPrecision38() {
+ final RelDataTypeSystem typeSystem = new DecimalDivisionTypeSystem() {
+ @Override public int getMaxPrecision(SqlTypeName typeName) {
+ return typeName == SqlTypeName.DECIMAL ? 38 :
super.getMaxPrecision(typeName);
+ }
+
+ @Override public int getMaxScale(SqlTypeName typeName) {
+ return typeName == SqlTypeName.DECIMAL ? 38 :
super.getMaxScale(typeName);
+ }
+ };
+ checkDivide(typeSystem, 18, 6, 18, 6, 38, 20);
+ checkDivide(typeSystem, 18, 0, 18, 0, 37, 19);
+ checkDivide(typeSystem, 31, 0, 18, 0, 38, 7);
+ checkDivide(typeSystem, 32, 0, 18, 0, 38, 6);
+ checkDivide(typeSystem, 32, 0, 1, 0, 38, 6);
+ checkDivide(typeSystem, 33, 0, 1, 0, 38, 6);
+ checkDivide(typeSystem, 38, 20, 38, 20, 38, 6);
+ }
+
+ @Test void testNonDecimalOperands() {
+ final RelDataTypeSystem typeSystem = new DecimalDivisionTypeSystem();
+ final SqlTypeFactoryImpl factory = new SqlTypeFactoryImpl(typeSystem);
+ final RelDataType decimal = factory.createSqlType(SqlTypeName.DECIMAL, 6,
2);
+ final RelDataType integer = factory.createSqlType(SqlTypeName.INTEGER);
+ final RelDataType approximate = factory.createSqlType(SqlTypeName.DOUBLE);
+ assertNull(typeSystem.deriveDecimalDivideType(factory, integer, integer));
+ assertNull(typeSystem.deriveDecimalDivideType(factory, decimal,
approximate));
+ assertNull(typeSystem.deriveDecimalDivideType(factory, approximate,
decimal));
+ final RelDataType result =
+ SqlStdOperatorTable.DIVIDE.inferReturnType(factory,
ImmutableList.of(decimal, integer));
+ assertThat(result, hasToString("DECIMAL(17, 13)"));
+ }
+
+ @Test void testJavaNumericOperand() {
+ final JavaTypeFactoryImpl factory =
+ new JavaTypeFactoryImpl(new DecimalDivisionTypeSystem());
+ final RelDataType decimal = factory.createSqlType(SqlTypeName.DECIMAL, 6,
2);
+ final RelDataType javaInteger = factory.createJavaType(int.class);
+ final RelDataType sqlInteger = factory.createSqlType(SqlTypeName.INTEGER);
+ for (boolean javaLeft : ImmutableList.of(false, true)) {
+ final List<RelDataType> javaOperands = javaLeft
+ ? ImmutableList.of(javaInteger, decimal) : ImmutableList.of(decimal,
javaInteger);
+ final List<RelDataType> sqlOperands = javaLeft
+ ? ImmutableList.of(sqlInteger, decimal) : ImmutableList.of(decimal,
sqlInteger);
+ final RelDataType actual =
+ SqlStdOperatorTable.DIVIDE.inferReturnType(factory, javaOperands);
+ final RelDataType expected =
+ SqlStdOperatorTable.DIVIDE.inferReturnType(factory, sqlOperands);
+ assertThat(actual, is(expected));
+ }
+ }
+
+ @Test void testConnectionProperty() throws SQLException {
+ final Properties properties = new Properties();
+ checkConnection(properties, 6);
+ properties.setProperty(CalciteConnectionProperty.TYPE_SYSTEM.camelName(),
+ DecimalDivisionTypeSystem.class.getName());
+ checkConnection(properties, 9);
+ }
+
+ private static void checkConnection(Properties properties, int scale) throws
SQLException {
+ final String sql = "values (cast(1 as decimal(6, 2)) / cast(3 as
decimal(6, 2)))";
+ try (Connection connection = DriverManager.getConnection("jdbc:calcite:",
properties);
+ Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ assertThat(resultSet.getMetaData().getPrecision(1), is(15));
+ assertThat(resultSet.getMetaData().getScale(1), is(scale));
+ assertThat(resultSet.next(), is(true));
+ }
+ }
+
+ private static void checkDivide(RelDataTypeSystem typeSystem,
+ int p1, int s1, int p2, int s2, int precision, int scale) {
+ final SqlTypeFactoryImpl factory = new SqlTypeFactoryImpl(typeSystem);
+ final RelDataType left = factory.createSqlType(SqlTypeName.DECIMAL, p1,
s1);
+ final RelDataType right = factory.createSqlType(SqlTypeName.DECIMAL, p2,
s2);
+ final RelDataType result =
+ SqlStdOperatorTable.DIVIDE.inferReturnType(factory,
ImmutableList.of(left, right));
+ assertThat(result.getPrecision(), is(precision));
+ assertThat(result.getScale(), is(scale));
+ }
+}