Skip to content

feat(isthmus): configurable unquoted identifier casing via the ConverterProvider builder - #1037

Merged
nielspardon merged 3 commits into
mainfrom
feat/builder-unquoted-casing
Aug 4, 2026
Merged

feat(isthmus): configurable unquoted identifier casing via the ConverterProvider builder#1037
nielspardon merged 3 commits into
mainfrom
feat/builder-unquoted-casing

Conversation

@nielspardon

@nielspardon nielspardon commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Add ConverterProvider.Builder.unquotedCasing(Casing) — a convenience for the common case of
overriding only the unquoted-identifier casing, layered on top of the current parser configuration
(equivalent to sqlParserConfig(current.withUnquotedCasing(casing))).

Motivation

TO_UPPER is the ANSI/Calcite default, but a large class of engines fold unquoted identifiers to
lower-case or preserve them (Presto/Trino, Spark, DuckDB, Postgres), so a plan generated from
SELECT … FROM orders comes out with a NamedScan named ORDERS that those engines can't resolve.
This gives library and CLI users a one-liner to control it, while the full sqlParserConfig(...)
path (previous PR) remains for anything else.

Changes

  • ConverterProvider.Builder.unquotedCasing(Casing) convenience.
  • IsthmusEntryPoint (CLI): --unquotedcasing now maps to
    builder().unquotedCasing(...).
  • FromSql example: uses builder().unquotedCasing(Casing.UNCHANGED) (matching the lower-case
    identifiers in its CREATE TABLE statements), replacing the deprecated
    convert(sql, catalog, SqlDialect) call.

Split of #983

Per @vbarua's request, #983 was split into three stacked PRs (review/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 — introduce the ConverterProvider builder (merged)
  3. feat(isthmus): configurable unquoted identifier casing via the ConverterProvider builder #1037 — configurable unquoted identifier casing via the builder (this PR)

Both dependencies have merged, so this PR is now rebased onto main and is the last commit in the
stack — no longer blocked on anything.

🤖 Generated with AI

@nielspardon
nielspardon requested a review from vbarua July 24, 2026 06:30
Base automatically changed from feat/converterprovider-builder-api to main July 31, 2026 08:38
nielspardon added a commit that referenced this pull request Jul 31, 2026
## 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. #1035 — inject `ConverterProvider` into the SQL statement parsers
(merged)
2. **#1036 — this PR**
3. #1037 — configurable unquoted identifier casing via the builder
…builder

Add Builder.unquotedCasing(Casing) as a convenience for the common case of
overriding only the unquoted-identifier casing, layered on top of the current
parser configuration. Route the isthmus CLI's --unquotedcasing option through it,
use it in the FromSql example (Casing.UNCHANGED, matching the lower-case
identifiers in the example's CREATE TABLE statements), and add UnquotedCasingTest
covering the default casing, the convenience for each Casing, the full
sqlParserConfig path (retaining LENIENT conformance), and end-to-end NamedScan
naming (EMPLOYEES under TO_UPPER, employees under UNCHANGED).
@nielspardon
nielspardon force-pushed the feat/builder-unquoted-casing branch from 3cda7ed to 130e81f Compare July 31, 2026 08:50
@nielspardon

Copy link
Copy Markdown
Member Author

Rebased onto main now that #1035 and #1036 have merged. The branch previously carried pre-squash copies of both, which is where the conflicts came from — it is now a single commit on top of main.

The one real conflict was positional: #1036 gained review changes that regrouped the Builder members, so unquotedCasing(...) now sits directly after sqlParserConfig(...) (with the parser-config settings) rather than ahead of the derived function converters. Verified with :isthmus:check, :isthmus:javadoc, :isthmus-cli:compileJava and :examples:isthmus-api:build — all 6 UnquotedCasingTest tests pass. Ready for review.

Comment thread isthmus/src/main/java/io/substrait/isthmus/ConverterProvider.java
…erConfig

The convenience mutated the builder's parser config eagerly, so the result
depended on call order: sqlParserConfig(y).unquotedCasing(x) produced y with x
applied, while unquotedCasing(x).sqlParserConfig(y) silently discarded x. The
Javadoc documented that ordering requirement rather than removing it.

Hold the casing as an Optional<Casing> instead and apply it over the parser
config in the primary constructor. Both orders now yield the same provider, and
the rule is simply that the casing override wins over whatever casing the
supplied config carries.

@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.

The implementation side looks good overall, left some comments on docs and tests. I don't think any of it is blocking though.

builder
.unquotedCasing
.map(builder.sqlParserConfig::withUnquotedCasing)
.orElse(builder.sqlParserConfig);

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.

I think this is a reasonable solution to the problem of setting both.

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/test/java/io/substrait/isthmus/UnquotedCasingTest.java Outdated
Comment thread isthmus/src/test/java/io/substrait/isthmus/UnquotedCasingTest.java Outdated
Comment thread isthmus/src/test/java/io/substrait/isthmus/UnquotedCasingTest.java Outdated
Comment thread isthmus/src/test/java/io/substrait/isthmus/UnquotedCasingTest.java Outdated
Address review feedback on the unquoted-casing convenience.

Docs: drop the parser-config callouts from `builder()` and the
hand-derivation warning from `unquotedCasing(...)`, and mention the
convenience from `getSqlParserConfig()` in a paragraph of its own rather
than parenthetically.

Tests: fold the builder-level assertions into
`ConverterProviderBuilderTest` as a nested `UnquotedCasing` section so
they read as ConverterProvider behaviour rather than general casing
functionality, and collapse the redundant cases. The end-to-end check
moves to `SqlToSubstraitTest`, parameterized over the casings so it
states the property directly: the `NamedScan` name follows whatever
casing the provider carries.
@nielspardon
nielspardon merged commit 0af28cc into main Aug 4, 2026
13 checks passed
@nielspardon
nielspardon deleted the feat/builder-unquoted-casing branch August 4, 2026 06:29
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