You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Isthmus should be initializable from a Substrait dialect, and that dialect's declared type limits should drive the Calcite type system it builds — rather than the limits being hardcoded in SubstraitTypeSystem as they are today.
The dialect model already carries exactly the information needed: SupportedType exposes maxPrecision(), maxScale() and maxLength() as Optional<Integer> (added in #808). Nothing consumes them — grep -rl 'io.substrait.dialect' isthmus/src/main/java returns no files, and SubstraitTypeSystem is a hardcoded switch: 6 for the TIME/TIMESTAMP/TIMESTAMP_WITH_LOCAL_TIME_ZONE/INTERVAL_* family, 38 for DECIMAL, with getMaxScale overriding only DECIMAL.
The result is that "what precision can this target actually represent" is currently a property of Isthmus rather than of the engine being targeted, even though we publish per-engine dialects that are entitled to say so.
Why this is worth doing
Every hardcoded limit is a place where Isthmus either rejects a plan the target could execute, or accepts one it cannot. Three existing issues are all instances of the same missing seam:
Spark hardcodes the same number independently: Util.assertMicroseconds throws for any interval_day precision but 6, and ToSubstraitType only ever emits 6. Spark reaches Substrait without going through Isthmus, so it would need to consume the dialect on its own, but the hardcode is the same shape.
Known blocker: the type system is a reflectively-constructed singleton
SubstraitTypeSystem exposes TYPE_SYSTEM and TYPE_FACTORY as static singletons and keeps an explicit public no-arg constructor because Frameworks/Avatica reinstantiates the class from its class name via reflection. A dialect-parameterized type system cannot be constructed that way, so the instance has to be threaded explicitly rather than discovered by name.
The seam for that already exists: ConverterProvider.Builder.typeFactory(RelDataTypeFactory) lets a caller supply the factory, and TypeConverter.toCalcite takes the RelDataTypeFactory as a parameter throughout. What is missing is that nothing constructs a dialect-derived one, and the no-arg reflective path needs to keep working as a permissive default.
Correctness prerequisite
For intervals, Calcite splits the two dimensions: IntervalSqlType.getPrecision() is the leading-field bound (getMaxPrecision) and getScale() is the fractional-second bound (getMaxScale). SubstraitTypeSystem overrides getMaxPrecision(INTERVAL_DAY) to 6 and does not override getMaxScale for any interval, so the fractional-second ceiling is still Calcite's 9 while the digit count callers actually consult is the leading-field one. Any dialect that declares maxScale on interval_day would therefore be ignored today.
Whichever direction this goes, the interval limits need to be read through getMaxScale (of INTERVAL_DAY_SECOND, which is the SqlTypeName the produced type carries) before a dialect can meaningfully set them.
Sibling axis, not a child. #1013 is entirely about operations — function mapping, relation mapping, the dialect-boundary unparse layer, and faithful-vs-compatible mode selection. Type limits share its "dialect at the boundary" principle but none of its four work items, and this is independently landable without the function-mapping foundation in #1012.
Dialect limits vs. Calcite's own capabilities. A dialect could declare a limit Calcite cannot represent. Does the effective limit become the intersection, and does exceeding Calcite's side produce a distinguishable error?
Default with no dialect supplied. Today's hardcoded values as a permissive baseline, or the union of the shipped dialects?
Which limits are in scope.maxPrecision/maxScale/maxLength are what SupportedType carries; getMaxNumericPrecision/getMaxNumericScale and the string-length caps are the other knobs RelDataTypeSystem exposes.
Thesis
Isthmus should be initializable from a Substrait dialect, and that dialect's declared type limits should drive the Calcite type system it builds — rather than the limits being hardcoded in
SubstraitTypeSystemas they are today.The dialect model already carries exactly the information needed:
SupportedTypeexposesmaxPrecision(),maxScale()andmaxLength()asOptional<Integer>(added in #808). Nothing consumes them —grep -rl 'io.substrait.dialect' isthmus/src/main/javareturns no files, andSubstraitTypeSystemis a hardcodedswitch: 6 for theTIME/TIMESTAMP/TIMESTAMP_WITH_LOCAL_TIME_ZONE/INTERVAL_*family, 38 forDECIMAL, withgetMaxScaleoverriding onlyDECIMAL.The result is that "what precision can this target actually represent" is currently a property of Isthmus rather than of the engine being targeted, even though we publish per-engine dialects that are entitled to say so.
Why this is worth doing
Every hardcoded limit is a place where Isthmus either rejects a plan the target could execute, or accepts one it cannot. Three existing issues are all instances of the same missing seam:
precision_time/precision_timestamp/precision_timestamp_tzabove 6, though the spec allows 0–12. That issue asks whether the hardcoded ceiling can be raised; dialect-driven limits reframe it as "whose ceiling is it", which is the more answerable question.precision_timestamp_tzliteral guard readsSqlTypeName.TIMESTAMP_TZ, whichSubstraitTypeSystemdoes not override, so it caps at Calcite's default 3 while the type conversion caps at 6. A single declared source of limits removes the class of bug where two lookups disagree.Util.assertMicrosecondsthrows for anyinterval_dayprecision but 6, andToSubstraitTypeonly ever emits 6. Spark reaches Substrait without going through Isthmus, so it would need to consume the dialect on its own, but the hardcode is the same shape.Known blocker: the type system is a reflectively-constructed singleton
SubstraitTypeSystemexposesTYPE_SYSTEMandTYPE_FACTORYas static singletons and keeps an explicit public no-arg constructor because Frameworks/Avatica reinstantiates the class from its class name via reflection. A dialect-parameterized type system cannot be constructed that way, so the instance has to be threaded explicitly rather than discovered by name.The seam for that already exists:
ConverterProvider.Builder.typeFactory(RelDataTypeFactory)lets a caller supply the factory, andTypeConverter.toCalcitetakes theRelDataTypeFactoryas a parameter throughout. What is missing is that nothing constructs a dialect-derived one, and the no-arg reflective path needs to keep working as a permissive default.Correctness prerequisite
For intervals, Calcite splits the two dimensions:
IntervalSqlType.getPrecision()is the leading-field bound (getMaxPrecision) andgetScale()is the fractional-second bound (getMaxScale).SubstraitTypeSystemoverridesgetMaxPrecision(INTERVAL_DAY)to 6 and does not overridegetMaxScalefor any interval, so the fractional-second ceiling is still Calcite's 9 while the digit count callers actually consult is the leading-field one. Any dialect that declaresmaxScaleoninterval_daywould therefore be ignored today.Whichever direction this goes, the interval limits need to be read through
getMaxScale(ofINTERVAL_DAY_SECOND, which is theSqlTypeNamethe produced type carries) before a dialect can meaningfully set them.Relationship to #1013
Sibling axis, not a child. #1013 is entirely about operations — function mapping, relation mapping, the dialect-boundary unparse layer, and faithful-vs-compatible mode selection. Type limits share its "dialect at the boundary" principle but none of its four work items, and this is independently landable without the function-mapping foundation in #1012.
Open questions
maxPrecision/maxScale/maxLengthare whatSupportedTypecarries;getMaxNumericPrecision/getMaxNumericScaleand the string-length caps are the other knobsRelDataTypeSystemexposes.🤖 Generated with AI