Skip to content

refactor(isthmus): CallConverters fuse their SqlKind predicate into the conversion, so a dialect cannot reuse them #1207

Description

@nielspardon

Context

#1166 gave ConverterProvider.Builder a callConverters(UnaryOperator<List<CallConverter>>) transform, so a caller can place its own CallConverter anywhere in the list. That solves placement. It does not let a caller reuse a built-in converter, because every built-in except one fuses its operator predicate into the body of a public static final value rather than exposing the conversion itself.

The problem

Each built-in opens by testing call.getKind() and returning null otherwise, and the body is unreachable any other way — these are constants, not methods:

  • CASTswitch (call.getKind()) { case CAST: … case SAFE_CAST: … default: return null; } (CallConverters.java:54)
  • CASEif (call.getKind() != SqlKind.CASE) { return null; } (:215)
  • REINTERPRET (:101), ROW (:162), CREATE_SEARCH_CONV (:257) — same shape

A dialect that spells the same operation with a different SqlKind therefore cannot delegate. CallConverters.CASE.apply(call, visitor) returns null for an operator carrying SqlKind.OTHER, and there is no convertCase(...) to call instead. The caller's only options are to reconstruct the RexCall against SqlStdOperatorTable.CASE before delegating, or to copy the body.

Copying is the part that will bite. CAST's body carries an invariant documented at CallConverters.java:41-45: a nullability-only cast over a non-null literal is how ExpressionRexConverter encodes a nullable Substrait literal, so it must fold back to a literal rather than an Expression.Cast. A downstream copy of that body is a fork of an internal encoding — when the encoding changes, every copy silently produces plans that no longer round-trip, and nothing in this repository can detect it. CASE's body likewise carries the odd-operand validation and the query-order requirement of the IfClause loop.

The precedent is already in the same file

EXECUTION_CONTEXT_VARIABLE handles exactly this situation and its javadoc says so (CallConverters.java:273):

Matching is done on operator identity (these are niladic SqlKind.OTHER_FUNCTION functions with no dedicated SqlKind).

"An operator with no dedicated SqlKind" is precisely the dialect case. One converter in the set already matches on operator identity instead of kind; the rest cannot be asked to.

Proposal

Separate the predicate from the conversion, so a dialect supplies only what differs:

  • factories that take the predicate — CallConverters.cast(TypeConverter, Predicate<RexCall>), CallConverters.caseWhen(Predicate<RexCall>) — with the existing constants becoming the kind-based instances of those, or
  • the bodies as callable helpers a caller's own CallConverter can invoke after its own matching.

Either keeps one copy of the CAST literal-folding and CASE validation logic in this repository and reduces a dialect converter to its predicate.

Notes

Independent of #1166 — the transform decides where a converter sits, not whether the built-in conversion can be reached. Adjacent to but not covered by #1012, which is about the function-mapping layer (Sig tables, the matcher, reverse lookup) rather than these non-function converters.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions