Skip to content

isthmus: derive the Calcite type system's limits from a Substrait dialect instead of hardcoding them #1124

Description

@nielspardon

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 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:

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.

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

  • Behaviour on exceeding a limit. Reject (today's behaviour for datetime precision), narrow silently, or make it a configurable policy? Rejecting is honest but makes some plans unconvertible; narrowing is lossy in ways isthmus: precision_timestamp_tz literals cap at precision 3 converting to Calcite and come back as plain precision_timestamp #1113 shows are easy to miss.
  • 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.

🤖 Generated with AI

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions