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));
+  }
+}

Reply via email to