Uh oh!
There was an error while loading. Please reload this page.
[SPARK-58798][SQL] Fix collated CHAR LCT and cover compare/IN/set-ops - #58080
[SPARK-58798][SQL] Fix collated CHAR LCT and cover compare/IN/set-ops#58080srielau wants to merge 12 commits into
Conversation
bfa4001 to
93ae68aCompare### What changes were proposed in this pull request? This is the foundation for SQL standard-aligned `CHAR(n)` / `VARCHAR(n)` support. It adds a single gate, `spark.sql.charVarchar.standardSemantics.enabled` (default `false`), under which `CHAR`/`VARCHAR` behave as first-class types with proper length semantics instead of being silently annotated `STRING`. The change is best read as a path a `CHAR`/`VARCHAR` value travels through the engine, hardened one stage at a time: 1. **Typing** - `CHAR`/`VARCHAR` may now appear as first-class types in schemas and plans, rather than being erased to `STRING` during analysis. 2. **Introduction (`CAST`)** - `CAST` / `try_cast` to `CHAR(n)` / `VARCHAR(n)` keep the target type and enforce its length: `CHAR` is padded, `VARCHAR` is trimmed, and an oversized value fails with `EXCEED_LIMIT_LENGTH` (after trailing blanks are trimmed). 3. **Combination (least common type)** - `COALESCE`, `CASE`, `UNION`, `IN`, and friends widen along `CHAR -> VARCHAR -> STRING`, taking `max(n, m)` for the length, so combining constrained values yields the tightest type that fits all of them. 4. **Transformation** - functions that rewrite string content (`upper`, `substr`, `concat` / `||`, `regexp_*`, `split`, `mask`, `reverse`, `hex`, `array_join`, `str_to_map`, `json_tuple`, ...) return plain `STRING`, because their result length is not the input's length. This is implemented as a promotion in `ImplicitTypeCasts` (`CHAR`/`VARCHAR` -> `STRING` where a plain string is expected, analogous to `SHORT` -> `INT`); the few expressions that bypass implicit casting derive the same result type directly. A registry-wide inventory test fails if any function outside an explicit pass-through allowlist returns a constrained type under the flag, so future expressions cannot silently leak a length. 5. **Persistence (scan / write)** - write-side checks preserve the declared type, and scans pad `CHAR` back to its length and reject oversized values, so what is read back matches what the schema promises. 6. **Delivery (Thrift/JDBC)** - `RowSetUtils` no longer wraps `CHAR`/`VARCHAR` (or collated `STRING`) results in quotes. Its fast path matched only the default-collation `STRING` singleton, so these types fell through to a rendering path that quoted them (`"ab"` instead of `ab`). This fix is intentionally not gated on the flag, since collated strings were mis-rendered regardless. `spark.sql.preserveCharVarcharTypeInfo` remains the pre-existing experimental path; only `standardSemantics` applies the transformation-returns-`STRING` rule and the hardening above. ### Out of scope / follow-up This PR is the **engine foundation** only. The feature is not complete without the work below; each item is intentionally deferred so this change stays reviewable. **Clients** - HiveServer2 / JDBC `ResultSetMetaData`: report `CHAR`/`VARCHAR` (with precision `n`) instead of generic `STRING` / `VARCHAR` with unbounded precision. - `DatabaseMetaData.getColumns`: fix `COLUMN_SIZE = 0` for `VARCHAR`. - Spark Connect JDBC type mapping for `CHAR`/`VARCHAR` + precision. **Language surfaces** - Schema fidelity for `CTAS` / `CREATE VIEW` / CTEs, `ALTER`, temp tables, and external file tables. - SQL `FUNCTION` parameters / `RETURNS`, and session variables (`DECLARE` / `SET`). - Format round-trips (Parquet / ORC / Avro / CSV) that preserve the logical type end to end. Parameterized lengths (`CHAR(:n)` / `VARCHAR(:n)`) are covered in #58080 (parser already binds markers in length position; that PR pins the behavior under the flag). **Known engine gap (addressed in follow-up)** - Combining two collated `CHAR`s of different lengths but the same collation used to resolve to an indeterminate collation. Fixed and covered, along with compare / `IN` / set-op LCT behavior, in #58080. **Explicitly not in this effort** - Deprecating `preserveCharVarcharTypeInfo`. - Iceberg `CHAR`/`VARCHAR` metadata. - Full ISO operator-result typing for every string operator. ### Why are the changes needed? Without this gate, `CHAR`/`VARCHAR` are effectively aliases for `STRING`: `CAST` stringifies the target, combinations always widen to `STRING`, and length is neither enforced on write nor restored on read. Standard-compliant `CHAR`/`VARCHAR` need a consistent, opt-in path through the whole engine. ### Does this PR introduce _any_ user-facing change? Yes, when `spark.sql.charVarchar.standardSemantics.enabled` is `true` (default remains `false`): - `CAST AS CHAR/VARCHAR` returns a typed result and enforces length. - `COALESCE` / `CASE` / `UNION` may return `CHAR`/`VARCHAR`. - Content-transforming string functions return `STRING`. - Scans pad `CHAR` and reject oversized values. Independently of the flag, Thrift/JDBC clients no longer see spurious quotes around `CHAR`/`VARCHAR` and collated `STRING` column values. ### How was this patch tested? - New cases in `CharVarcharTestSuite` / `BasicCharVarcharTestSuite` / `FileSourceCharVarcharTestSuite` covering `CAST`, least-common-type, transformation, `createDataFrame`, and scan pad/oversize. - Golden file `charvarchar-standard-semantics.sql` (plus analyzer results). - A parity matrix asserting the fixed-point and single-pass analyzers agree under the flag. - A registry-wide inventory test that fails if any function outside the pass-through allowlist returns `CHAR`/`VARCHAR`. - `RowSetUtilsSuite` for unquoted rendering of `CHAR`, `VARCHAR`, and collated `STRING`; verified it fails when the `RowSetUtils` fix is reverted. Closes#58033 from srielau/SPARK-58794. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Gengliang Wang <gengliang@apache.org>
### What changes were proposed in this pull request? This is the foundation for SQL standard-aligned `CHAR(n)` / `VARCHAR(n)` support. It adds a single gate, `spark.sql.charVarchar.standardSemantics.enabled` (default `false`), under which `CHAR`/`VARCHAR` behave as first-class types with proper length semantics instead of being silently annotated `STRING`. The change is best read as a path a `CHAR`/`VARCHAR` value travels through the engine, hardened one stage at a time: 1. **Typing** - `CHAR`/`VARCHAR` may now appear as first-class types in schemas and plans, rather than being erased to `STRING` during analysis. 2. **Introduction (`CAST`)** - `CAST` / `try_cast` to `CHAR(n)` / `VARCHAR(n)` keep the target type and enforce its length: `CHAR` is padded, `VARCHAR` is trimmed, and an oversized value fails with `EXCEED_LIMIT_LENGTH` (after trailing blanks are trimmed). 3. **Combination (least common type)** - `COALESCE`, `CASE`, `UNION`, `IN`, and friends widen along `CHAR -> VARCHAR -> STRING`, taking `max(n, m)` for the length, so combining constrained values yields the tightest type that fits all of them. 4. **Transformation** - functions that rewrite string content (`upper`, `substr`, `concat` / `||`, `regexp_*`, `split`, `mask`, `reverse`, `hex`, `array_join`, `str_to_map`, `json_tuple`, ...) return plain `STRING`, because their result length is not the input's length. This is implemented as a promotion in `ImplicitTypeCasts` (`CHAR`/`VARCHAR` -> `STRING` where a plain string is expected, analogous to `SHORT` -> `INT`); the few expressions that bypass implicit casting derive the same result type directly. A registry-wide inventory test fails if any function outside an explicit pass-through allowlist returns a constrained type under the flag, so future expressions cannot silently leak a length. 5. **Persistence (scan / write)** - write-side checks preserve the declared type, and scans pad `CHAR` back to its length and reject oversized values, so what is read back matches what the schema promises. 6. **Delivery (Thrift/JDBC)** - `RowSetUtils` no longer wraps `CHAR`/`VARCHAR` (or collated `STRING`) results in quotes. Its fast path matched only the default-collation `STRING` singleton, so these types fell through to a rendering path that quoted them (`"ab"` instead of `ab`). This fix is intentionally not gated on the flag, since collated strings were mis-rendered regardless. `spark.sql.preserveCharVarcharTypeInfo` remains the pre-existing experimental path; only `standardSemantics` applies the transformation-returns-`STRING` rule and the hardening above. ### Out of scope / follow-up This PR is the **engine foundation** only. The feature is not complete without the work below; each item is intentionally deferred so this change stays reviewable. **Clients** - HiveServer2 / JDBC `ResultSetMetaData`: report `CHAR`/`VARCHAR` (with precision `n`) instead of generic `STRING` / `VARCHAR` with unbounded precision. - `DatabaseMetaData.getColumns`: fix `COLUMN_SIZE = 0` for `VARCHAR`. - Spark Connect JDBC type mapping for `CHAR`/`VARCHAR` + precision. **Language surfaces** - Schema fidelity for `CTAS` / `CREATE VIEW` / CTEs, `ALTER`, temp tables, and external file tables. - SQL `FUNCTION` parameters / `RETURNS`, and session variables (`DECLARE` / `SET`). - Format round-trips (Parquet / ORC / Avro / CSV) that preserve the logical type end to end. Parameterized lengths (`CHAR(:n)` / `VARCHAR(:n)`) are covered in #58080 (parser already binds markers in length position; that PR pins the behavior under the flag). **Known engine gap (addressed in follow-up)** - Combining two collated `CHAR`s of different lengths but the same collation used to resolve to an indeterminate collation. Fixed and covered, along with compare / `IN` / set-op LCT behavior, in #58080. **Explicitly not in this effort** - Deprecating `preserveCharVarcharTypeInfo`. - Iceberg `CHAR`/`VARCHAR` metadata. - Full ISO operator-result typing for every string operator. ### Why are the changes needed? Without this gate, `CHAR`/`VARCHAR` are effectively aliases for `STRING`: `CAST` stringifies the target, combinations always widen to `STRING`, and length is neither enforced on write nor restored on read. Standard-compliant `CHAR`/`VARCHAR` need a consistent, opt-in path through the whole engine. ### Does this PR introduce _any_ user-facing change? Yes, when `spark.sql.charVarchar.standardSemantics.enabled` is `true` (default remains `false`): - `CAST AS CHAR/VARCHAR` returns a typed result and enforces length. - `COALESCE` / `CASE` / `UNION` may return `CHAR`/`VARCHAR`. - Content-transforming string functions return `STRING`. - Scans pad `CHAR` and reject oversized values. Independently of the flag, Thrift/JDBC clients no longer see spurious quotes around `CHAR`/`VARCHAR` and collated `STRING` column values. ### How was this patch tested? - New cases in `CharVarcharTestSuite` / `BasicCharVarcharTestSuite` / `FileSourceCharVarcharTestSuite` covering `CAST`, least-common-type, transformation, `createDataFrame`, and scan pad/oversize. - Golden file `charvarchar-standard-semantics.sql` (plus analyzer results). - A parity matrix asserting the fixed-point and single-pass analyzers agree under the flag. - A registry-wide inventory test that fails if any function outside the pass-through allowlist returns `CHAR`/`VARCHAR`. - `RowSetUtilsSuite` for unquoted rendering of `CHAR`, `VARCHAR`, and collated `STRING`; verified it fails when the `RowSetUtils` fix is reverted. Closes#58033 from srielau/SPARK-58794. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Gengliang Wang <gengliang@apache.org> (cherry picked from commit 763b7c9) Signed-off-by: Gengliang Wang <gengliang@apache.org>
CollationTypeCoercion treated same-collation CHAR/VARCHAR values of different lengths as a collation mismatch (sameType on the full type), yielding an indeterminate collation. At equal strength, take StringHelper.tightestCommonString instead so children cast to max(n, m). Also pin the compare / IN / UNION / INTERSECT / EXCEPT / VALUES behavior under standardSemantics: every participant, including the IN left-hand side, is cast to the string-family LCT. Casting to CHAR pads (so unequal CHAR lengths compare equal after widen); casting to VARCHAR/STRING keeps the CHAR pad.
…ndardSemantics Parameter markers in length position (`CHAR(:n)` / `VARCHAR(?)`) already bind through the parser. Pin that under standardSemantics CAST stays typed, pads/enforces length, DDL keeps the bound n, and non-integral/negative lengths fail.
93ae68a to
ec5cd75Compare
srielau
left a comment
There was a problem hiding this comment.
SQL Language review
Domain review of the collated CHAR LCT fix and compare/IN/set-op coverage.
Surface areas: fixed-point Analyzer (CollationTypeCoercion), SQL golden files, BasicCharVarcharTestSuite. Analyzer++ is not directly edited; it shares expression coercion but set-op resolution is a separate path.
Findings
Critical -- collation precedence can still narrow length.
tightestCommonStringruns only on equal strength. The unequal-priority branch still returns the entire winningStringTypeWithContext, so an explicitCHAR(2) COLLATE UTF8_LCASEagainst an implicit same-collationCHAR(4)selectsCHAR(2). That contradicts the stated invariant (same-collation operands widen tomax(n, m), never truncate). Collation strength and string-family LCT should be resolved independently.Medium -- parameterized-length test is out of scope. It does not exercise
getWinningStringType. Please drop it from this PR (or move missing CAST/error cases to a parameter suite withcheckError).Medium -- unit tests duplicate goldens; plan substring is weak. Most of the SPARK-58794 unit test repeats
charvarchar-standard-semantics.sql.analyzed.toString.contains("as varchar(4)")can match a list-element cast and does not prove the IN LHS widened. Put that three-part IN query in the analyzer golden instead.Medium -- the actual regression is not in the SPARK-58802 dual-run matrix. Please add collated mixed-length COALESCE / compare / IN, and mixed-CHAR
UNION ALL, under dual-run.Medium -- golden matrix does not match the PR description.
UNION ALLis still VARCHAR(3)/VARCHAR(8); new set-ops are uncollated; no collated mixed CHAR/VARCHAR; EXCEPT has an empty result so padding is not observed.Medium -- not standardSemantics-only.
charVarcharFirstClassTypesis also true underpreserveCharVarcharTypeInfo, andCollationTypeCoercionis always installed. Document and test preserve-only behavior, or gate if preserve must not change.Low -- load-bearing comment belongs on the method scaladoc, with a concrete SQL / type example.
Suggested order
- Split collation-strength selection from CHAR/VARCHAR length LCT; add explicit-vs-implicit mixed-length goldens.
- Dual-run the actual collated mixed-length cases.
- Align goldens with UNION ALL / collated CHAR-VARCHAR / non-empty EXCEPT.
- Remove duplicated unit coverage and the parameterized-length test.
- Call out preserve-mode impact in the PR description.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…in LCT Same-collation mixed-strength inputs were taking the stronger operand's length, which could narrow CHAR(n). Resolve max(n, m) independently and attach the winning strength afterward.
srielau
left a comment
There was a problem hiding this comment.
SQL Language re-review (64008e1)
Previous findings 1-3 and 5-7 are addressed. The same-collation strength/length split is fixed: tightestCommonString whenever collation ids match, then attach the stronger strength. Implicit vs Default mixed-length COALESCE is covered in goldens, preserve-only tests, and dual-run. Parameterized-length coverage (#4) kept with checkError -- not blocking.
One new correctness issue is reachable because that LCT now succeeds.
Remaining
Critical -- collated CHAR-to-VARCHAR LCT rewrites the inner Cast and drops padding.
changeTypedoescast.copy(dataType = newDataType). Uncollated TypeCoercion keeps a nested cast (cast(cast(a as char(2)) as varchar(2)) = ...->false). After this fix, collated CHAR vs VARCHAR goldens retarget tocast(a as varchar(2) collate UTF8_LCASE)on both sides and resulttrue. UTF8_LCASE is not RTRIM; the PR text still says VARCHAR/STRING keep the CHAR pad. Dual-run only checks collated CHAR vs CHAR, so it will not catch this.Low -- different collations at unequal strength still return the full winner (length included). Pre-existing SQL collation precedence; documented in the new scaladoc. Not a merge blocker.
Please wrap instead of retargeting when the source is CHAR and the LCT is not, flip those two goldens to false, and add that compare/IN pair to SPARK-58802 dual-run.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Retargeting CAST CHAR to VARCHAR skipped padding. Nest the Cast so collated CHAR vs VARCHAR compare and IN match the uncollated nested plan.
…dardSemantics ISO 6.13: explicit character CAST truncates rather than raising right-truncation. Numeric CAST still errors when the formatted literal does not fit. Store assignment is unchanged.
…AST truncate Dataset.to and other Project store-assignment casts used Cast onto CHAR/VARCHAR. After explicit character CAST truncates, those paths must cast to STRING then stringLengthCheck so overflow still errors.
srielau
left a comment
There was a problem hiding this comment.
SQL Language re-review (0c8e47f)
Previous CHAR-pad finding: PARTIAL. The reported collated CHAR vs VARCHAR case is fixed: analyzer nests cast(cast(a as char(2) collate UTF8_LCASE) as varchar(2) ...), execution is false. changeType still retargets every other Cast, which now interacts with truncating explicit CAST.
Findings
Critical -- retargeting still erases explicit CAST semantics. Only CHAR-to-non-CHAR is nested.
CAST('abcdef' AS VARCHAR(2))inside COALESCE/CASE/IN with a wider VARCHAR sibling can becomeCAST('abcdef' AS VARCHAR(4))and yieldabcdinstead ofab. Numeric / TRY_CAST overflow can disappear the same way. ISO 6.13 then depends on surrounding LCT.Critical -- store check skipped when
standardSemanticsandcharVarcharAsStringare both on.stringLengthCheckis gated on!conf.charVarcharAsString, but standardSemantics keeps first-class CHAR/VARCHAR when both flags are true.Dataset.tocan then skip the write-side check.TableOutputResolveruses the same gate (pre-existing); this PR copies it intoProject.High --
ToStringBasecannot tell explicit CAST from store/implicit Cast. Truncation is implemented on all string-to-CHAR/VARCHAR Casts; Project is a reach-around so INSERT/Dataset.todo not inherit it. Other write paths can drift.Medium -- CAST truncation + store-assignment rewrite is a second, user-visible change. Consider splitting from the collated-LCT fix.
Medium -- tests miss the interacting paths: truncating CAST/TRY_CAST and numeric overflow inside COALESCE/CASE/IN; combined-flag
Dataset.to/ INSERT; collated truncating CAST; dual-run of truncation+LCT.Low -- extract the Project store-assignment helper (pattern match, not
isDefined/get).
Please preserve inner constrained-string Casts in changeType (wrap, do not retarget), fix the combined-flag gate, and add those goldens. The CHAR-pad compare/IN goldens look correct now.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…de checks changeType always nests an outer LCT Cast instead of retargeting, so truncation and numeric overflow stay on the inner USER_SPECIFIED_CAST. ToStringBase truncates only when that tag is set. Write-side length checks still run when standardSemantics and charVarcharAsString are both on.
srielau
left a comment
There was a problem hiding this comment.
SQL Language re-review (dddd721)
Previous review comments are addressed for the SQL CAST / store-assignment paths.
| # | Original | Status |
|---|---|---|
| 1 | changeType retargets Cast (VARCHAR truncate / numeric overflow) | Fixed -- always Cast(existing, lct); COALESCE/CASE/IN goldens nest and return ab; numeric COALESCE still EXCEED_LIMIT_LENGTH |
| 2 | Write-side check skipped when both flags are on | Fixed -- shouldApplyWriteSideLengthCheck; Project, TableOutputResolver, default columns |
| 3 | ToStringBase cannot tell explicit CAST from store Cast | Fixed -- truncateCharVarcharOnCast / USER_SPECIFIED_CAST; outer LCT Cast is untagged |
| 4 | Split CAST truncation from LCT | Open / non-blocking -- maintainer scope |
| 5 | Missing CAST-inside-LCT tests | Fixed |
| 6 | Extract Project helper | Fixed (storeAssignCast); scaladoc nit remains |
Remaining
Medium --
Literalarm still retargets.lit.copy(dataType = newDataType)does not rematerialize CHAR pad / VARCHAR length. SQL CAST stays aCastso the new goldens are fine;Literal.create(..., CharType(2))widened to CHAR(4) keeps'a 'instead of'a '. Please wrap when the string constraint changes (untagged outer Cast), and add a rule-levelLiteral.createtest.Low --
storeAssignCastcontract is still an inline comment. Lift it to Scaladoc with the STRING-then-write-check shape.
CHAR-pad compare/IN and CAST-inside-LCT goldens look correct. Not blocking on (2) or the CAST/LCT scope split.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
CollationTypeCoercion.changeType still copies a Literal when only collation changes. When the string constraint changes, wrap in Cast so CHAR padding is re-applied. Lift the storeAssignCast contract to Scaladoc.
…iters
concat('<', CHAR, '>') also LCT-widens the delimiters, so Hive JDBC
showed extra spaces. hex() reports the coalesced CHAR(4) bytes only.
cloud-fan
left a comment
There was a problem hiding this comment.
0 blocking, 2 non-blocking, 1 nit.
The semantic fixes and regression coverage are coherent; three small performance/prose cleanups remain.
Suggestions (2)
- Non-blocking: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ToStringBase.scala:71: Cache the source-to-UTF8 converter outside the per-row lambda. -- see inline
- Non-blocking: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala:154: Compare matching struct fields by index to avoid an eager zipped tuple array. -- see inline
Nits: 1 minor item (see inline comments).
Verification
I traced explicit-cast tagging from parser/Column/Connect into Cast and both interpreted/code-generated ToStringBase paths; traced analyzer LCT wrapping and typed-Literal rematerialization; and checked store-assignment consumers against the shared write-side predicate. The generated SQL dossier and focused suite assert the resulting schemas, values, errors, padding, and analyzer parity.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…sion castToString rebuilt the source converter for every interpreted row; bind it once when the lambda is constructed. Also drop the tuple array from the struct arm of stringConstraintChanged.
cloud-fan
left a comment
There was a problem hiding this comment.
3 addressed, 0 remaining, 2 new to this AI review. (0 newly introduced, 2 late catches, 0 previously raised.)
0 blocking, 1 non-blocking, 1 nit.
The semantic fix is coherent; one small hot-path optimization and one comment grammar correction remain.
Suggestions (1)
- Non-blocking: sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java:65: Compute the CHAR source length once so truncating casts avoid a second UTF8 traversal. -- see inline
Nits: 1 minor item (see inline comments).
Verification
I traced explicit-cast tagging from SQL parsing, classic Column.cast, and Connect through Cast into both interpreted and generated ToStringBase paths. I also verified that CollationTypeCoercion preserves inner cast semantics, and that Project, TableOutputResolver, and default-column validation share the write-side length-check predicate. The generated SQL dossier confirms the corresponding schemas, values, errors, padding, and analyzer plans.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Compute numChars once and pad, return, or truncate. The previous path called varcharTypeCast then scanned the result again.
cloud-fan
left a comment
There was a problem hiding this comment.
2 addressed, 0 remaining, 0 new to this AI review.
0 blocking, 0 non-blocking, 0 nits.
The current head is coherent and the two issues from the prior AI review are addressed; I found no remaining defects.
Verification
I traced explicit-cast provenance from SQL parsing, classic Column.cast, and Connect through Cast into both interpreted and generated ToStringBase paths. I also verified that CollationTypeCoercion preserves inner cast semantics, and that Project, TableOutputResolver, and default-column validation share the write-side length-check predicate. The generated SQL dossier and focused suite cover the resulting schemas, values, errors, padding, and analyzer plans.
…esult schemas ### What changes were proposed in this pull request? Covers Connect JDBC mapping ([SPARK-58806](https://issues.apache.org/jira/browse/SPARK-58806)), plus HiveServer2 / JDBC result metadata ([SPARK-58804](https://issues.apache.org/jira/browse/SPARK-58804)) and `DatabaseMetaData.getColumns` `COLUMN_SIZE` ([SPARK-58805](https://issues.apache.org/jira/browse/SPARK-58805)). Parent: [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Independent of the remaining CHAR/VARCHAR follow-ups. Rebased onto `master` after #58033; can merge without #58080 / #58087 / #58130. Unique vs master: srielau/spark@master...serge-rielau_data/SPARK-58794-clients Expose first-class `CHAR(n)` and `VARCHAR(n)` to SQL clients when `spark.sql.charVarchar.standardSemantics.enabled` is on, and let those clients decode result rows that carry the types. Metadata: - Spark Connect JDBC maps `CharType` / `VarcharType` to `java.sql.Types.CHAR` / `VARCHAR`, with Java string values and precision / display size `n`. - Connect `DatabaseMetaData.getColumns` reports `COLUMN_SIZE` as the declared character length `n`. `CHAR_OCTET_LENGTH` is the UTF-8 maximum byte capacity `4 * n` (saturating at `Int.MaxValue`); unbounded STRING stays 0. - Connect `DatabaseMetaData.getTypeInfo` lists `CHAR` and `VARCHAR` with `CREATE_PARAMS = length`. Unbounded `STRING` remains a `Types.VARCHAR` row without create params. - HiveServer2 `getColumns` reports `COLUMN_SIZE = n` for `VARCHAR(n)` as well as `CHAR(n)`, and `CHAR_OCTET_LENGTH = 4 * n` for both (null for unbounded STRING). - HiveServer2 result metadata coverage pins Hive JDBC names (`char` / `varchar`, no length) and precision `n` for CAST results. Decode (JVM Connect client): - The Connect client process has no engine `SQLConf`, so `RowEncoder.encoderFor` still follows the local `charVarcharFirstClassTypes` flag and rejects CHAR/VARCHAR when first-class types are off. - `RowEncoder.encoderForResultSchema` always accepts CHAR/VARCHAR. `SparkResult.createEncoder` uses it only for `UnboundRowEncoder` (engine-produced result schemas). Other encoder bindings keep `encoderFor`. - Arrow serializer / deserializer treat `CharEncoder` / `VarcharEncoder` like `StringEncoder`, and deserialize via `StringHelper.plainStringType` so STRING Arrow vectors up-cast to CHAR/VARCHAR. - JDBC CAST `ResultSet.next` / `collect` needs this path; metadata-only mapping is not enough. Python Connect already maps proto `char` / `var_char` in `pyspark.sql.connect.types`; a mixed classic vs Connect test covers schema and `collect()`. The existing Connect proto already carries CHAR/VARCHAR and their lengths. ### Why are the changes needed? Spark Connect JDBC rejected first-class `CharType` and `VarcharType` as unsupported because its metadata mapping only recognized the `StringType` singleton. `getColumns` hardcoded `CHAR_OCTET_LENGTH` to 0, and `getTypeInfo` listed only unbounded STRING. HiveServer2 already identified VARCHAR catalog columns but reported `COLUMN_SIZE` as unknown. Clients therefore cannot reliably describe or size CHAR/VARCHAR columns even though Catalyst retains the type and length. Separately, a server with standard semantics sends CHAR/VARCHAR in the result schema. The JVM Connect client then failed at row decode (`UNSUPPORTED_DATA_TYPE_FOR_ENCODER`) because `RowEncoder.encoderFor` read the client's default conf (`charVarcharFirstClassTypes = false`). JDBC CAST collect and Spark Connect `SparkResult` both hit that path. ### Does this PR introduce _any_ user-facing change? Yes, when first-class CHAR/VARCHAR types are present: JDBC and HiveServer2 metadata now report the corresponding JDBC type and declared character length instead of rejecting the type or reporting an unknown size. `CHAR_OCTET_LENGTH` is the UTF-8 maximum byte capacity (`4 * n`), not the character length. Connect clients can also collect CHAR/VARCHAR result columns instead of failing to decode the schema. ### How was this patch tested? - `RowEncoderSuite`: `encoderFor` still raises `UNSUPPORTED_DATA_TYPE_FOR_ENCODER` for CHAR/VARCHAR when both first-class flags are off; `encoderForResultSchema` accepts the same schema. - `ArrowEncoderSuite`: CHAR/VARCHAR Arrow round-trip (top-level, nested struct, array). - `SparkConnectJdbcDataTypeSuite`: CAST result metadata and table `getColumns` (`CHAR(4)` / `VARCHAR(6)`, `COLUMN_SIZE`, `CHAR_OCTET_LENGTH` = 16 / 24); CAST collect after decode. - `SparkConnectDatabaseMetaDataSuite`: `getTypeInfo` rows for CHAR / VARCHAR. - `SparkMetadataOperationSuite`: HiveServer2 `getColumns` / CAST result metadata (`CHAR_OCTET_LENGTH` = `4 * n`). - `pyspark.sql.tests.connect.test_connect_basic.SparkConnectBasicTests.test_char_varchar_result_schema`: classic vs Connect schema and `collect()`. Local compile of `sql-api`, `connect-client-jdbc`, and `hive-thriftserver` passed. HiveServer2 / full Connect JDBC in this environment remain blocked (`CheckReturnValue` while compiling `connect-common`; FIPS Python multiprocessing). Please treat the suites above as the merge gate. ### Was this patch authored or co-authored using generative AI tooling? Yes (Cursor). Closes#58132 from srielau/serge-rielau_data/SPARK-58794-clients. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…esult schemas ### What changes were proposed in this pull request? Covers Connect JDBC mapping ([SPARK-58806](https://issues.apache.org/jira/browse/SPARK-58806)), plus HiveServer2 / JDBC result metadata ([SPARK-58804](https://issues.apache.org/jira/browse/SPARK-58804)) and `DatabaseMetaData.getColumns` `COLUMN_SIZE` ([SPARK-58805](https://issues.apache.org/jira/browse/SPARK-58805)). Parent: [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Independent of the remaining CHAR/VARCHAR follow-ups. Rebased onto `master` after #58033; can merge without #58080 / #58087 / #58130. Unique vs master: srielau/spark@master...serge-rielau_data/SPARK-58794-clients Expose first-class `CHAR(n)` and `VARCHAR(n)` to SQL clients when `spark.sql.charVarchar.standardSemantics.enabled` is on, and let those clients decode result rows that carry the types. Metadata: - Spark Connect JDBC maps `CharType` / `VarcharType` to `java.sql.Types.CHAR` / `VARCHAR`, with Java string values and precision / display size `n`. - Connect `DatabaseMetaData.getColumns` reports `COLUMN_SIZE` as the declared character length `n`. `CHAR_OCTET_LENGTH` is the UTF-8 maximum byte capacity `4 * n` (saturating at `Int.MaxValue`); unbounded STRING stays 0. - Connect `DatabaseMetaData.getTypeInfo` lists `CHAR` and `VARCHAR` with `CREATE_PARAMS = length`. Unbounded `STRING` remains a `Types.VARCHAR` row without create params. - HiveServer2 `getColumns` reports `COLUMN_SIZE = n` for `VARCHAR(n)` as well as `CHAR(n)`, and `CHAR_OCTET_LENGTH = 4 * n` for both (null for unbounded STRING). - HiveServer2 result metadata coverage pins Hive JDBC names (`char` / `varchar`, no length) and precision `n` for CAST results. Decode (JVM Connect client): - The Connect client process has no engine `SQLConf`, so `RowEncoder.encoderFor` still follows the local `charVarcharFirstClassTypes` flag and rejects CHAR/VARCHAR when first-class types are off. - `RowEncoder.encoderForResultSchema` always accepts CHAR/VARCHAR. `SparkResult.createEncoder` uses it only for `UnboundRowEncoder` (engine-produced result schemas). Other encoder bindings keep `encoderFor`. - Arrow serializer / deserializer treat `CharEncoder` / `VarcharEncoder` like `StringEncoder`, and deserialize via `StringHelper.plainStringType` so STRING Arrow vectors up-cast to CHAR/VARCHAR. - JDBC CAST `ResultSet.next` / `collect` needs this path; metadata-only mapping is not enough. Python Connect already maps proto `char` / `var_char` in `pyspark.sql.connect.types`; a mixed classic vs Connect test covers schema and `collect()`. The existing Connect proto already carries CHAR/VARCHAR and their lengths. ### Why are the changes needed? Spark Connect JDBC rejected first-class `CharType` and `VarcharType` as unsupported because its metadata mapping only recognized the `StringType` singleton. `getColumns` hardcoded `CHAR_OCTET_LENGTH` to 0, and `getTypeInfo` listed only unbounded STRING. HiveServer2 already identified VARCHAR catalog columns but reported `COLUMN_SIZE` as unknown. Clients therefore cannot reliably describe or size CHAR/VARCHAR columns even though Catalyst retains the type and length. Separately, a server with standard semantics sends CHAR/VARCHAR in the result schema. The JVM Connect client then failed at row decode (`UNSUPPORTED_DATA_TYPE_FOR_ENCODER`) because `RowEncoder.encoderFor` read the client's default conf (`charVarcharFirstClassTypes = false`). JDBC CAST collect and Spark Connect `SparkResult` both hit that path. ### Does this PR introduce _any_ user-facing change? Yes, when first-class CHAR/VARCHAR types are present: JDBC and HiveServer2 metadata now report the corresponding JDBC type and declared character length instead of rejecting the type or reporting an unknown size. `CHAR_OCTET_LENGTH` is the UTF-8 maximum byte capacity (`4 * n`), not the character length. Connect clients can also collect CHAR/VARCHAR result columns instead of failing to decode the schema. ### How was this patch tested? - `RowEncoderSuite`: `encoderFor` still raises `UNSUPPORTED_DATA_TYPE_FOR_ENCODER` for CHAR/VARCHAR when both first-class flags are off; `encoderForResultSchema` accepts the same schema. - `ArrowEncoderSuite`: CHAR/VARCHAR Arrow round-trip (top-level, nested struct, array). - `SparkConnectJdbcDataTypeSuite`: CAST result metadata and table `getColumns` (`CHAR(4)` / `VARCHAR(6)`, `COLUMN_SIZE`, `CHAR_OCTET_LENGTH` = 16 / 24); CAST collect after decode. - `SparkConnectDatabaseMetaDataSuite`: `getTypeInfo` rows for CHAR / VARCHAR. - `SparkMetadataOperationSuite`: HiveServer2 `getColumns` / CAST result metadata (`CHAR_OCTET_LENGTH` = `4 * n`). - `pyspark.sql.tests.connect.test_connect_basic.SparkConnectBasicTests.test_char_varchar_result_schema`: classic vs Connect schema and `collect()`. Local compile of `sql-api`, `connect-client-jdbc`, and `hive-thriftserver` passed. HiveServer2 / full Connect JDBC in this environment remain blocked (`CheckReturnValue` while compiling `connect-common`; FIPS Python multiprocessing). Please treat the suites above as the merge gate. ### Was this patch authored or co-authored using generative AI tooling? Yes (Cursor). Closes#58132 from srielau/serge-rielau_data/SPARK-58794-clients. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit 6af294e) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
### What changes were proposed in this pull request? Covers the string-family LCT lattice ([SPARK-58798](https://issues.apache.org/jira/browse/SPARK-58798)). Parent: [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Also includes explicit character CAST truncation ([SPARK-58797](https://issues.apache.org/jira/browse/SPARK-58797)). Rebased onto `master` after #58033 merged. Unique commits vs master: srielau/spark@master...SPARK-58794-lct-compare Follow-up that drills into least-common-type, comparison, and `IN` under `spark.sql.charVarchar.standardSemantics.enabled`. `CollationTypeCoercion` is not gated on `standardSemantics` alone (`charVarcharFirstClassTypes` is also true under `preserveCharVarcharTypeInfo`). The equal-collation length LCT therefore applies in preserve-only mode as well; tests cover both flags. **Bug fix.** `CollationTypeCoercion.getWinningStringType` required `sameType` at equal collation strength. Same-collation `CHAR(2)` and `CHAR(4)` therefore looked like a collation mismatch and became `IndeterminateStringType` (`string collate null`). At equal strength, take `StringHelper.tightestCommonString` instead so every child is cast to `max(n, m)` (padding, never truncating). **Explicit CAST vs store assignment (ISO 6.13 / 9.2).** Under the flag: - Character-to-`CHAR`/`VARCHAR` `CAST` / `TRY_CAST` **truncates** to `n` characters (then `CHAR` pads). Example: `CAST('abcdef' AS VARCHAR(2))` is `'ab'`, not `EXCEED_LIMIT_LENGTH`. - Numeric (and other non-string) `CAST` to `CHAR`/`VARCHAR` still **errors** when the formatted literal does not fit (`CAST(12345 AS VARCHAR(4))` -> `EXCEED_LIMIT_LENGTH` / 54006). That matches ISO 22001, not silent digit clipping. - Store assignment (`INSERT`, `Dataset.to`, encoder write-side) is unchanged: non-space overflow still errors. Project store-assignment casts to unconstrained `STRING` then `stringLengthCheck`, so they do not inherit CAST truncation. **Coverage / pinned behavior.** - Set ops: `UNION` / `UNION ALL` / `INTERSECT` / `EXCEPT`, plus multi-row `VALUES`, all share the string-family LCT. - Comparison and `IN`: every participant is cast to that LCT, including the `IN` left-hand side (`InTypeCoercion` uses `findWiderCommonType` over `value +: list`). Casting to `CHAR` pads, so unequal `CHAR` lengths compare equal after widen; casting to `VARCHAR`/`STRING` keeps the `CHAR` pad, so `CHAR 'a'` (stored as `'a '`) is not equal to `VARCHAR`/`STRING 'a'` unless the other side carries the same trailing blank. Ignoring trailing blanks is a collation concern (`RTRIM`), not a type-level `PAD SPACE` policy (design D17). - Parameterized lengths (`CHAR(:n)` / `VARCHAR(?)`): markers in length position already bind through the parser; under the flag, character CAST truncates to the bound `n`, numeric CAST still errors if it does not fit, and DDL keeps the bound `n`. Negative / non-integral lengths fail at parse after substitution. ### Why are the changes needed? #58033 left the collated mixed-length LCT gap as a known issue, and had only thin coverage of `UNION ALL` / `INTERSECT` with no compare / `IN`-LHS / `VALUES` / `EXCEPT` matrix. Without pinning the LCT-cast rule, CHAR vs CHAR vs VARCHAR equality is easy to misread as PAD SPACE. Explicit character CAST was still using the write-side length check, so `CAST('abcdef' AS VARCHAR(2))` failed the same way as `INSERT`. ISO 6.13 truncates that CAST; 9.2 keeps the INSERT error. Numeric CAST must still fail when the literal does not fit (ISO 22001). ### Does this PR introduce _any_ user-facing change? Yes, when the flag is on: - Collated `CHAR`/`VARCHAR` values of different lengths but the same collation now widen to `CHAR`/`VARCHAR(max(n,m))` instead of an indeterminate collation. Compare / `IN` / set-op results for mixed `CHAR` lengths follow the LCT-cast rule above (this matches what the engine already did for non-collated paths; the new tests lock it in). - Explicit character-to-`CHAR`/`VARCHAR` CAST truncates instead of raising `EXCEED_LIMIT_LENGTH`. `INSERT` / encoder overflow and numeric CAST overflow still error. ### How was this patch tested? - New `BasicCharVarcharTestSuite` cases covering compare, `IN` (including analyzed-plan assertion that the LHS widens), `RTRIM`, collated LCT, `UNION` / `INTERSECT` / `EXCEPT`, multi-row `VALUES`, parameterized `CHAR`/`VARCHAR` lengths, character CAST truncation, and numeric CAST overflow under the flag. - Expanded golden file `charvarchar-standard-semantics.sql`. - Locally: `BasicCharVarcharTestSuite` and the golden file regeneration/run. Closes#58080 from srielau/SPARK-58794-lct-compare. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Yicong-Huang <17627829+Yicong-Huang@users.noreply.github.com> (cherry picked from commit ac5bba0) Signed-off-by: Yicong-Huang <17627829+Yicong-Huang@users.noreply.github.com>
Yicong-Huang
commented
Aug 20, 2026
…tandardSemantics ### What changes were proposed in this pull request? Parent epic: [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Unique ticket: [SPARK-58807](https://issues.apache.org/jira/browse/SPARK-58807) (CTAS/CREATE VIEW/CTE inherit CHAR/VARCHAR). Also covers [SPARK-58808](https://issues.apache.org/jira/browse/SPARK-58808) (SQL FUNCTION assignment), [SPARK-58809](https://issues.apache.org/jira/browse/SPARK-58809) (session variables), and [SPARK-58811](https://issues.apache.org/jira/browse/SPARK-58811) (ALTER/temp/external). #58080 (SPARK-58798 LCT) is on master. Clients (JDBC / HS2 / Connect) landed in #58132 / SPARK-58806. Language-surface hardening under `spark.sql.charVarchar.standardSemantics.enabled`: **Bug fixes (format write)** - **ORC:** `CharType`/`VarcharType` extend `StringType`, so write used to stamp `spark.sql.catalyst.type=string` and file-only inference lost the constraint. Stamp `char(n)` / `varchar(n)` via `CharVarcharUtils.charVarcharTypeName`. Write stays on ORC `STRING` plus that attribute (not native ORC CHAR/VARCHAR), so ORC `maxLength` does not fight Spark store assignment. Unbounded STRING, including collated STRING, still stamps `string` (same as Avro: collation is not a file-only round-trip in this PR). - **Avro:** same subclass trap on leaves, map keys, and ser/de. Stamp `spark.sql.catalyst.type` on STRING and `spark.sql.catalyst.mapKey.type` on maps. Restore only `StringType` subtypes; a non-string stamp is `IncompatibleSchemaException`. sql/core has no avro data source; ser/de is covered with `DataFileWriter`. **Coverage** - CTAS / CREATE VIEW (CHAR and VARCHAR) / CTE inherit CHAR/VARCHAR - ALTER COLUMN equal-length CHAR/VARCHAR; V2 VARCHAR widen / CHAR to VARCHAR - Session / script variables, FETCH INTO, SQL FUNCTION params and RETURNS - EXTERNAL TABLE scan pad / overflow - CHAR/VARCHAR vs non-string compare and COALESCE - JSON / CSV with a user-specified CHAR/VARCHAR schema - ORC catalog + file-only round-trip, including cross-flag (`standardSemantics` on then off, first-class types off, `preserveCharVarcharTypeInfo` only) - Avro schema conversion, nested struct, CHAR map keys, and serializer/deserializer values ### Why are the changes needed? Without ORC/Avro catalyst-type stamping, Spark-written files cannot re-infer CHAR/VARCHAR under the flag. Language surfaces (CTAS/VIEW/CTE/vars/functions/user schemas) need explicit coverage so schema fidelity does not regress. ### Does this PR introduce _any_ user-facing change? Yes, when the flag is on: ORC and Avro preserve CHAR/VARCHAR logical types across write/read instead of collapsing to STRING. CTAS/VIEW/CTE/vars/functions behavior matches the foundation rules and is now tested. ### How was this patch tested? - `BasicCharVarcharTestSuite` / language surfaces (including Avro ser/de and ORC cross-flag) - Regenerated `charvarchar-standard-semantics.sql` goldens Closes#58087 from srielau/SPARK-58794-surfaces. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…tandardSemantics ### What changes were proposed in this pull request? Parent epic: [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794). Unique ticket: [SPARK-58807](https://issues.apache.org/jira/browse/SPARK-58807) (CTAS/CREATE VIEW/CTE inherit CHAR/VARCHAR). Also covers [SPARK-58808](https://issues.apache.org/jira/browse/SPARK-58808) (SQL FUNCTION assignment), [SPARK-58809](https://issues.apache.org/jira/browse/SPARK-58809) (session variables), and [SPARK-58811](https://issues.apache.org/jira/browse/SPARK-58811) (ALTER/temp/external). #58080 (SPARK-58798 LCT) is on master. Clients (JDBC / HS2 / Connect) landed in #58132 / SPARK-58806. Language-surface hardening under `spark.sql.charVarchar.standardSemantics.enabled`: **Bug fixes (format write)** - **ORC:** `CharType`/`VarcharType` extend `StringType`, so write used to stamp `spark.sql.catalyst.type=string` and file-only inference lost the constraint. Stamp `char(n)` / `varchar(n)` via `CharVarcharUtils.charVarcharTypeName`. Write stays on ORC `STRING` plus that attribute (not native ORC CHAR/VARCHAR), so ORC `maxLength` does not fight Spark store assignment. Unbounded STRING, including collated STRING, still stamps `string` (same as Avro: collation is not a file-only round-trip in this PR). - **Avro:** same subclass trap on leaves, map keys, and ser/de. Stamp `spark.sql.catalyst.type` on STRING and `spark.sql.catalyst.mapKey.type` on maps. Restore only `StringType` subtypes; a non-string stamp is `IncompatibleSchemaException`. sql/core has no avro data source; ser/de is covered with `DataFileWriter`. **Coverage** - CTAS / CREATE VIEW (CHAR and VARCHAR) / CTE inherit CHAR/VARCHAR - ALTER COLUMN equal-length CHAR/VARCHAR; V2 VARCHAR widen / CHAR to VARCHAR - Session / script variables, FETCH INTO, SQL FUNCTION params and RETURNS - EXTERNAL TABLE scan pad / overflow - CHAR/VARCHAR vs non-string compare and COALESCE - JSON / CSV with a user-specified CHAR/VARCHAR schema - ORC catalog + file-only round-trip, including cross-flag (`standardSemantics` on then off, first-class types off, `preserveCharVarcharTypeInfo` only) - Avro schema conversion, nested struct, CHAR map keys, and serializer/deserializer values ### Why are the changes needed? Without ORC/Avro catalyst-type stamping, Spark-written files cannot re-infer CHAR/VARCHAR under the flag. Language surfaces (CTAS/VIEW/CTE/vars/functions/user schemas) need explicit coverage so schema fidelity does not regress. ### Does this PR introduce _any_ user-facing change? Yes, when the flag is on: ORC and Avro preserve CHAR/VARCHAR logical types across write/read instead of collapsing to STRING. CTAS/VIEW/CTE/vars/functions behavior matches the foundation rules and is now tested. ### How was this patch tested? - `BasicCharVarcharTestSuite` / language surfaces (including Avro ser/de and ORC cross-flag) - Regenerated `charvarchar-standard-semantics.sql` goldens Closes#58087 from srielau/SPARK-58794-surfaces. Authored-by: Serge Rielau <serge@rielau.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit db6fc3a) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
What changes were proposed in this pull request?
Covers the string-family LCT lattice (SPARK-58798). Parent: SPARK-58794. Also includes explicit character CAST truncation (SPARK-58797).
Rebased onto
masterafter #58033 merged. Unique commits vs master: srielau/spark@master...SPARK-58794-lct-compareFollow-up that drills into least-common-type, comparison, and
INunderspark.sql.charVarchar.standardSemantics.enabled.CollationTypeCoercionis not gated onstandardSemanticsalone (charVarcharFirstClassTypesis also true underpreserveCharVarcharTypeInfo). The equal-collation length LCT therefore applies in preserve-only mode as well; tests cover both flags.Bug fix.
CollationTypeCoercion.getWinningStringTyperequiredsameTypeat equal collation strength. Same-collationCHAR(2)andCHAR(4)therefore looked like a collation mismatch and becameIndeterminateStringType(string collate null). At equal strength, takeStringHelper.tightestCommonStringinstead so every child is cast tomax(n, m)(padding, never truncating).Explicit CAST vs store assignment (ISO 6.13 / 9.2). Under the flag:
CHAR/VARCHARCAST/TRY_CASTtruncates toncharacters (thenCHARpads). Example:CAST('abcdef' AS VARCHAR(2))is'ab', notEXCEED_LIMIT_LENGTH.CASTtoCHAR/VARCHARstill errors when the formatted literal does not fit (CAST(12345 AS VARCHAR(4))->EXCEED_LIMIT_LENGTH/ 54006). That matches ISO 22001, not silent digit clipping.INSERT,Dataset.to, encoder write-side) is unchanged: non-space overflow still errors. Project store-assignment casts to unconstrainedSTRINGthenstringLengthCheck, so they do not inherit CAST truncation.Coverage / pinned behavior.
UNION/UNION ALL/INTERSECT/EXCEPT, plus multi-rowVALUES, all share the string-family LCT.IN: every participant is cast to that LCT, including theINleft-hand side (InTypeCoercionusesfindWiderCommonTypeovervalue +: list). Casting toCHARpads, so unequalCHARlengths compare equal after widen; casting toVARCHAR/STRINGkeeps theCHARpad, soCHAR 'a'(stored as'a ') is not equal toVARCHAR/STRING 'a'unless the other side carries the same trailing blank. Ignoring trailing blanks is a collation concern (RTRIM), not a type-levelPAD SPACEpolicy (design D17).CHAR(:n)/VARCHAR(?)): markers in length position already bind through the parser; under the flag, character CAST truncates to the boundn, numeric CAST still errors if it does not fit, and DDL keeps the boundn. Negative / non-integral lengths fail at parse after substitution.Why are the changes needed?
#58033 left the collated mixed-length LCT gap as a known issue, and had only thin coverage of
UNION ALL/INTERSECTwith no compare /IN-LHS /VALUES/EXCEPTmatrix. Without pinning the LCT-cast rule, CHAR vs CHAR vs VARCHAR equality is easy to misread as PAD SPACE.Explicit character CAST was still using the write-side length check, so
CAST('abcdef' AS VARCHAR(2))failed the same way asINSERT. ISO 6.13 truncates that CAST; 9.2 keeps the INSERT error. Numeric CAST must still fail when the literal does not fit (ISO 22001).Does this PR introduce any user-facing change?
Yes, when the flag is on:
CHAR/VARCHARvalues of different lengths but the same collation now widen toCHAR/VARCHAR(max(n,m))instead of an indeterminate collation. Compare /IN/ set-op results for mixedCHARlengths follow the LCT-cast rule above (this matches what the engine already did for non-collated paths; the new tests lock it in).CHAR/VARCHARCAST truncates instead of raisingEXCEED_LIMIT_LENGTH.INSERT/ encoder overflow and numeric CAST overflow still error.How was this patch tested?
BasicCharVarcharTestSuitecases covering compare,IN(including analyzed-plan assertion that the LHS widens),RTRIM, collated LCT,UNION/INTERSECT/EXCEPT, multi-rowVALUES, parameterizedCHAR/VARCHARlengths, character CAST truncation, and numeric CAST overflow under the flag.charvarchar-standard-semantics.sql.BasicCharVarcharTestSuiteand the golden file regeneration/run.