Skip to content

Commit b370df5

Browse files
authored
fix(isthmus): use Substrait type system when building Calcite RelBuilder (#973)
When converting Substrait to Calcite, the `RelBuilder` was created via `RelBuilder.create(Frameworks...)` without specifying a type system, so its internal `RexBuilder` used Calcite's default type system (max decimal precision 19) while the converted expressions carried `SubstraitTypeSystem` types (precision 38). `RelBuilder.project` runs `RexSimplify` by default; with the mismatched type system it re-derived decimal arithmetic at precision 19 and then wrapped the result in a preserving `CAST(... AS DECIMAL(38,0))` to keep the declared type. For TPC-H queries 8 and 14 this surfaced as the numerator `SUM(CAST(CASE ... AS DECIMAL(19,0)))` while the denominator was left uncast. In PostgreSQL, where the `DECIMAL` columns are arbitrary-precision `NUMERIC`, the cast truncated the fractional values in the numerator only, so the round-tripped SQL produced different results from the reference. This was latent until Calcite 1.42, which changed `RelDataTypeSystemImpl` so the max numeric precision/scale are derived from `getMaxPrecision(DECIMAL)`/`getMaxScale(DECIMAL)`. ## Changes - Build the Substrait-to-Calcite `RelBuilder` with the Substrait type system, exposed via a new `ConverterProvider.getTypeSystem()` accessor (derived from the type factory so the two never disagree). - `SubstraitTypeSystem` gains a public no-arg constructor because Calcite's `Frameworks`/Avatica machinery re-instantiates the type system from its class name when it is supplied to a `FrameworkConfig`. - Removed queries 8 and 14 from the integration test's expected-failures list, and dropped the now-empty expected-failures code path. ## Testing - `./gradlew :isthmus:test` — all unit tests pass. - `./gradlew :isthmus:integrationTest` — all 22 TPC-H queries (including 8 and 14) now produce results matching the reference SQL in PostgreSQL. Closes #954 Closes #955 🤖 Generated with AI
1 parent b07d8a3 commit b370df5

4 files changed

Lines changed: 36 additions & 21 deletions

File tree

isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.calcite.jdbc.CalciteSchema;
2525
import org.apache.calcite.prepare.Prepare;
2626
import org.apache.calcite.rel.type.RelDataTypeFactory;
27+
import org.apache.calcite.rel.type.RelDataTypeSystem;
2728
import org.apache.calcite.rex.RexBuilder;
2829
import org.apache.calcite.sql.SqlOperatorTable;
2930
import org.apache.calcite.sql.parser.SqlParser;
@@ -332,7 +333,11 @@ public ExpressionRexConverter getExpressionRexConverter(
332333
* @return a new RelBuilder instance
333334
*/
334335
public RelBuilder getRelBuilder(CalciteSchema schema) {
335-
return RelBuilder.create(Frameworks.newConfigBuilder().defaultSchema(schema.plus()).build());
336+
return RelBuilder.create(
337+
Frameworks.newConfigBuilder()
338+
.defaultSchema(schema.plus())
339+
.typeSystem(getTypeSystem())
340+
.build());
336341
}
337342

338343
// Utility Getters
@@ -346,6 +351,19 @@ public RelDataTypeFactory getTypeFactory() {
346351
return typeFactory;
347352
}
348353

354+
/**
355+
* Returns the Calcite {@link RelDataTypeSystem} used by this converter provider.
356+
*
357+
* <p>Derived from the {@link #getTypeFactory() type factory} so the two never disagree. This is
358+
* the type system supplied to the {@link RelBuilder} used when converting Substrait to Calcite,
359+
* ensuring its type derivation matches the types carried by converted expressions.
360+
*
361+
* @return the type system
362+
*/
363+
public RelDataTypeSystem getTypeSystem() {
364+
return typeFactory.getTypeSystem();
365+
}
366+
349367
/**
350368
* Returns the Substrait extension collection used by this converter provider.
351369
*

isthmus/src/main/java/io/substrait/isthmus/SubstraitRelNodeConverter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public static RelNode convert(
161161
.parserConfig(converterProvider.getSqlParserConfig())
162162
.defaultSchema(catalogReader.getRootSchema().plus())
163163
.traitDefs((List<RelTraitDef>) null)
164+
.typeSystem(converterProvider.getTypeSystem())
164165
.programs()
165166
.build());
166167
return relRoot.accept(

isthmus/src/main/java/io/substrait/isthmus/SubstraitTypeSystem.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,16 @@ public class SubstraitTypeSystem extends RelDataTypeSystemImpl {
3232
public static final SqlIntervalQualifier DAY_SECOND_INTERVAL =
3333
new SqlIntervalQualifier(TimeUnit.DAY, -1, TimeUnit.SECOND, 6, SqlParserPos.ZERO);
3434

35-
/** Private constructor to enforce singleton usage. */
36-
private SubstraitTypeSystem() {}
35+
/**
36+
* Public no-argument constructor.
37+
*
38+
* <p>Prefer the shared {@link #TYPE_SYSTEM} singleton. This constructor exists because Calcite's
39+
* {@link org.apache.calcite.tools.Frameworks}/Avatica machinery re-instantiates a type system
40+
* from its class name (via a default constructor) when it is supplied to a {@link
41+
* org.apache.calcite.tools.FrameworkConfig}. The type system is stateless, so additional
42+
* instances are equivalent to the singleton.
43+
*/
44+
public SubstraitTypeSystem() {}
3745

3846
/**
3947
* Returns the maximum precision for the given SQL type.

isthmus/src/test/java/io/substrait/isthmus/integration/PostgreSqlIntegrationTest.java

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertFalse;
5-
import static org.junit.jupiter.api.Assertions.assertNotEquals;
65
import static org.junit.jupiter.api.Assertions.assertTrue;
76

87
import io.substrait.isthmus.ConverterProvider;
@@ -39,9 +38,6 @@
3938
class PostgreSqlIntegrationTest extends PlanTestBase {
4039
private static final Logger LOG = LoggerFactory.getLogger(PostgreSqlIntegrationTest.class);
4140

42-
// TODO: These queries produce different results when generated from Substrait
43-
private static final List<Integer> EXPECTED_FAILURES = List.of(8, 14);
44-
4541
private static final DockerImageName UV_IMAGE =
4642
DockerImageName.parse("ghcr.io/astral-sh/uv:python3.14-trixie-slim");
4743
private static final DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:18");
@@ -126,20 +122,12 @@ void testTpcH(final int queryNo)
126122

127123
// the count should be zero if both the reference and generated SQL produce the same results
128124
int differenceCount = result.getInt(1);
129-
if (EXPECTED_FAILURES.contains(queryNo)) {
130-
assertNotEquals(
131-
0,
132-
differenceCount,
133-
String.format(
134-
"Expected query %d to fail but it matched the reference results", queryNo));
135-
} else {
136-
assertEquals(
137-
0,
138-
differenceCount,
139-
String.format(
140-
"Reference and generated SQL produce %d different results.\n\nReference SQL:\n%s\n\nGenerated SQL:\n%s",
141-
differenceCount, referenceSql, generatedSql));
142-
}
125+
assertEquals(
126+
0,
127+
differenceCount,
128+
String.format(
129+
"Reference and generated SQL produce %d different results.\n\nReference SQL:\n%s\n\nGenerated SQL:\n%s",
130+
differenceCount, referenceSql, generatedSql));
143131

144132
// we expect exactly one row
145133
assertFalse(result.next());

0 commit comments

Comments
 (0)