Skip to content

feat(isthmus): introduce a ConverterProvider builder - #1036

Merged
nielspardon merged 4 commits into
mainfrom
feat/converterprovider-builder-api
Jul 31, 2026
Merged

feat(isthmus): introduce a ConverterProvider builder#1036
nielspardon merged 4 commits into
mainfrom
feat/converterprovider-builder-api

Conversation

@nielspardon

@nielspardon nielspardon commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Add ConverterProvider.builder() and a nested Builder — a readable, forward-compatible way to
configure a ConverterProvider, most importantly to supply a full Calcite SqlParser.Config now
that the parsers consume the provider's config (#1035). ConverterProvider.DEFAULT_SQL_PARSER_CONFIG
(SqlParser.Config.DEFAULT + TO_UPPER + SqlDdlParserImpl.FACTORY + LENIENT) is exposed as the
base for customized configs, e.g. DEFAULT_SQL_PARSER_CONFIG.withUnquotedCasing(Casing.UNCHANGED).

Sole construction path + subclassing seam

All public ConverterProvider constructors are @Deprecated in favour of builder(), and DEFAULT
is builder().build(). Subclassing routes through a new protected ConverterProvider(Builder) seam
— the single place instances are created — so adding a component later touches only the builder and
this constructor, never a subclass super(...) call. DynamicConverterProvider and
AutomaticDynamicFunctionMappingConverterProvider construct via super(builder).

Now that the provider is fully configurable, call sites that still used global defaults are routed
through it: the builder's typeConverter reaches the scalar/aggregate/window converters, and the
CREATE-statement catalog reader uses the provider's type factory and connection config.

Compatibility

Source-compatible — no public API removed, deprecated constructors still delegate to the builder,
and behavior is unchanged for the default provider.

Stack (#983 split, merge bottom-up)

  1. feat(isthmus)!: inject ConverterProvider into the SQL statement parsers #1035 — inject ConverterProvider into the SQL statement parsers (merged)
  2. feat(isthmus): introduce a ConverterProvider builder #1036 — this PR
  3. feat(isthmus): configurable unquoted identifier casing via the ConverterProvider builder #1037 — configurable unquoted identifier casing via the builder

Add ConverterProvider.builder() and a nested Builder that carries a full Calcite
SqlParser.Config alongside extensions, typeFactory, the function converters,
typeConverter and executionBehavior. getSqlParserConfig() now returns the stored
config, and DEFAULT_SQL_PARSER_CONFIG (the isthmus default: DEFAULT + TO_UPPER +
SqlDdlParserImpl.FACTORY + LENIENT) is exposed as the recommended base for
customization.

Make the builder the sole public construction path: a protected
ConverterProvider(Builder) is the stable subclassing seam (build() and the
delegating public constructors route through it), and all public constructors are
@deprecated in favour of builder(). DEFAULT is now builder().build(). The two
subclasses, DynamicConverterProvider and
AutomaticDynamicFunctionMappingConverterProvider, gain a public (Builder)
constructor calling super(builder) and deprecate their positional constructors.
Callers, tests and examples are migrated to builder()/DEFAULT.
…nverterProvider

processCreateStatementsToCatalog now builds the CalciteCatalogReader with the
injected ConverterProvider's type factory (getTypeFactory()) and connection
config (getCalciteConnectionConfig()) rather than the hardcoded
SubstraitTypeSystem.TYPE_FACTORY / SqlConverterBase.CONNECTION_CONFIG, so a
custom provider's type factory and connection config are honored end-to-end.
The no-arg overload now delegates to the ConverterProvider overload (with
DEFAULT), removing the duplicated reader construction. Behavior is unchanged
for the default provider — same type factory and connection config.
@nielspardon
nielspardon force-pushed the feat/converterprovider-builder-api branch from b065c1c to defa554 Compare July 25, 2026 06:41
…rter

Two spots still used TypeConverter.DEFAULT instead of the provider's:

- ConverterProvider(Builder) built the scalar/aggregate/window function
  converters via their 2-arg constructors, which fall back to
  TypeConverter.DEFAULT, so a TypeConverter set through Builder.typeConverter(...)
  was ignored by those converters. Pass builder.typeConverter through.
- SqlExpressionToSubstrait.toNamedStruct converted column types with
  TypeConverter.DEFAULT; use the injected provider's getTypeConverter().

Behavior is unchanged for the default provider, whose type converter is
TypeConverter.DEFAULT.

@vbarua vbarua left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable overall. I did leave a number of suggestions, but nothing I would consider blocking.

One bigger item I did want to point out is that now we have two ways of customizing the ConverterProvider using the builder or subclassing, and they can also be mixed, which does make this a bit confusing potentially.

Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java Outdated
Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java Outdated
Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java Outdated
Comment thread isthmus/src/main/java/io/substrait/isthmus/DynamicConverterProvider.java Outdated
Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java Outdated
* @param builder the builder carrying the configured components
*/
public DynamicConverterProvider(ConverterProvider.Builder builder) {
super(builder);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially out of scope. With both a builder and subclassing available as extension mechanisms, we do start to open ourselves up a bit to questions about which to use. For example
why use subclassing here when we could inject a modified ConverterProvider like:

ConverterProvider
  .builder()
  .scalarFunctionConverter(createScalarFunctionConverter())
  .sqlOperatorTable(buildSqlOperatorTable())

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair concern, and I agree it is the real design question here. I have left it out of this PR and opened #1048 for it.

The reason the two dynamic providers subclass rather than configure is that the builder cannot currently express what they override: getSqlOperatorTable() (both chain generated operators onto the base table) and getCallConverters() (DynamicConverterProvider appends an extra ScalarFunctionConverter). Making them builder configurations means adding sqlOperatorTable and callConverters to Builder and exposing the derivation helpers, since those values depend on the resolved extensions and type factory. That is a bigger change than this PR, and #1037 is stacked on top.

In the meantime the overlap is at least no longer silent: passing a builder-configured function converter to either dynamic provider now throws instead of being discarded.

Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java
* ConverterProvider#getSqlParserConfig()}.
* <p>To use a custom parser configuration, build the {@link ConverterProvider} via {@link
* ConverterProvider#builder()} and its {@code sqlParserConfig(...)}; for fully dynamic behaviour,
* subclass {@link ConverterProvider} and override {@link ConverterProvider#getSqlParserConfig()}.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: I think we can just point users to using the builder for customization for this use case, and not full on extension.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed to point at the builder only.

Model the three deferred function converters on ConverterProvider.Builder
as Optional rather than nullable fields, and expose them through getters.
"Unset" becomes explicit at the type level, and the primary constructor
derives them with orElseGet instead of a null check.

With the setting now observable, the two providers that derive their own
function converters reject a builder that configures one instead of
silently discarding it:

- DynamicConverterProvider rejects a configured scalarFunctionConverter.
- AutomaticDynamicFunctionMappingConverterProvider rejects a configured
  scalar, aggregate or window function converter.

Both validate inside the super(...) argument, since statements before
super() are not available on JDK 17.

Also group the directly-set builder fields ahead of the derived ones, and
trim Javadoc that explained why a builder is used or pointed at
subclassing where the builder now suffices.
@nielspardon

Copy link
Copy Markdown
Member Author

Thanks for the review! Pushed ff3176d addressing the suggestions:

  • Javadoc: Isthmus casing, the "why a builder" rationale removed from the primary constructor, the "seam" language dropped, and SubstraitSqlStatementParser now points at the builder only.
  • Builder's three deferred function converters are Optional with matching getters; the primary constructor derives via orElseGet instead of a null check.
  • Builder fields, setters and constructor assignments regrouped: directly-set first, derived after.
  • Both dynamic providers now throw IllegalArgumentException when handed a builder that configures a function converter they derive themselves, rather than silently discarding it. New ConverterProviderBuilderTest covers this.

On the bigger point about two customization mechanisms — I agree, and opened #1048 rather than growing this PR (the builder would need sqlOperatorTable and callConverters before the dynamic providers could be expressed as configuration, and #1037 is stacked on top).

@nielspardon
nielspardon merged commit 8643e2f into main Jul 31, 2026
13 checks passed
@nielspardon
nielspardon deleted the feat/converterprovider-builder-api branch July 31, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants