fix(isthmus)!: build a character literal from the type its conversion produced - #1171
Conversation
026c0e3 to
cc63cda
Compare
nielspardon
left a comment
There was a problem hiding this comment.
Whether a mapper returning a non-character type should fall back to the Calcite-derived literal instead of throwing is worth settling first — it converted before this PR, and the same question decides #1190. Worth marking this ! either way, since the padding changes the plans existing consumers get. I have more on the padding, the width check and the tests, but let's start here.
cc63cda to
6bea6e2
Compare
nielspardon
left a comment
There was a problem hiding this comment.
The fallback reads well. Three left, and the first is also a correction to the description: TypeConverter does not apply the PRECISION_NOT_SPECIFIED rule for CHAR.
nielspardon
left a comment
There was a problem hiding this comment.
Two left, both on the width guard.
|
No need — leave the mapper unrestricted. What lags is the reverse, which is now #1219: |
…produced LiteralConverter.convert derives the Substrait type twice: once through typeConverter.toSubstrait, which consults the UserTypeMapper, and again in the switch over the Calcite type name, which does not. The second derivation is the one the literal gets, so a mapped character column ended up with a schema of the mapped type and literals of the unmapped one, and VirtualTableScan rejected the relation. A null literal took its type from the first derivation and so already agreed, which is why only rows carrying a value diverged. The character branches now build from the type already derived, covering the three forms a Calcite character type can map to. A fixedchar literal carries no length of its own -- FixedCharLiteral derives it from the text -- so the text is padded to the declared width, which is also what CHAR(n) means. That half fixes a case needing no mapper at all: a LogicalValues row field wider than its literal, the shape substrait-io#1064 was reported with, was still rejected for character types. A mapping to anything with no character literal form is reported where it happens rather than as a schema mismatch further down. Closes substrait-io#1170.
…ength The fixedchar branch rejects a value wider than the type it is declared as, because the literal carries no length of its own and the text is the length. A varchar literal does carry one, and a value longer than it goes unchecked -- by this conversion and by the POJO.
6c70660 to
ab1ee18
Compare
|
Left unrestricted, and the description carries that now rather than the question. |
|
Filed the floor as a spec issue — substrait-io/substrait#1199. One consequence the footer does not cover: an empty character type no longer round-trips. |
|
Drop the
The negatives half stands on its own — unambiguous under any reading of the spec, and it fixes a message that reported the value's length where the width was the problem. That also answers the split question: with the |
A UserTypeMapper answers with a Substrait type directly, so nothing between it and the padding here holds its width to what a fixedchar can declare. A negative width did throw, but reported the value as longer than the fixedchar<-5> it is declared as, which is not the problem. The floor stops at the negatives. type_classes.md puts a fixedchar's width in [1..2147483647] (spec v0.101.0), but Calcite types '' as a CHAR(0) and its DDL parser takes a CHAR(0) column, so refusing a zero width here would stop ordinary SQL converting. The padded-width message says UTF-16 code units, which is what it counts: a value with an astral character makes those differ from the characters the width check above it counts.
ab1ee18 to
9f3649e
Compare
|
The description no longer presents the zero width as settled -- it says the bound is #1199's and that a One thing to weigh for #1199: |
The [1..2147483647] a fixedchar's width sits in goes back to spec substrait-io#200 in 2022, so a version marker beside it reads as provenance it does not have and would need editing on every unrelated bump. The javadoc above and TypeConverter's CHAR comment already cite the same range without one.
LiteralConverter.convertderives the Substrait type twice. Once throughtypeConverter.toSubstrait(resultType), which consults theUserTypeMapper, and again in theswitchoverresultType.getSqlTypeName(), which does not — and the second derivation is the one the literal gets. So a mapped character column produced a schema of the mapped type and literals of the unmapped one, andVirtualTableScanrejected the relation:A null literal takes its type straight from the first derivation, so it already agreed; only rows carrying a value diverged. That asymmetry is what the
nullAndNonNullLiteralsCarryTheSameMappedTypetest pins.The character branches now build from the type already derived, which covers the three forms a Calcite character type can map to —
fixedchar,varcharandstring. It also removes a copy of thePRECISION_NOT_SPECIFIEDrule thatTypeConverteralready applies — theVARCHARone, which is the only arm that had it.CHARhad none, so a reflective schema'scharorCharactercolumn, which carries no precision at all, derived afixedchar<-1>: outside the[1..2147483647]the spec allows, and a width the check below then measured every value against, the empty string included. That arm now reads the unspecified precision as Calcite's default of 1, and each rule lives in one place.A
fixedcharliteral needed one thing more: it carries no length of its own, sinceExpression.FixedCharLiteralderives the type from its text. Building one from the declared type therefore means padding the text to the declared width, which is also whatCHAR(n)means —'a'in aCHAR(3)is'a '— and whatpadRightIfNeededalready does forBINARYa few lines down. A value longer than the width it is declared as is rejected rather than truncated. The width is counted in characters, as the spec gives it -- aFIXEDCHAR<L>isLcharacters where astringis a count of UTF-8 bytes -- so one astral character fills afixedchar<1>.That half fixes a case needing no mapper at all. A
LogicalValuesrow field wider than its literal — the shape #1064 was reported with — was still rejected for character types: aCHAR(3)field holding'a'producedRow field type (FixedChar{length=1}) does not match schema field type (FixedChar{length=3}). Isthmus' own SQL path does not reach it, because Calcite pads the literal itself; a plan arriving from another planner does.What this does not cover, deliberately. A mapper that maps some other family still diverges the same way — mapping
INTEGERtoi64givesRow field type (I32) does not match schema field type (I64). Closing that generally means dispatching every branch on the Substrait type rather than the Calcite type name, which is a much larger change to this method, and it still would not reach a genuineType.UserDefined: there is no way to build a literal of one from aRexLiteralwithout knowing its encoding, so that needs a literal-side hook the interface does not have. The character family is the part that can be closed without deciding either of those. A mapping outside it leaves the literal in the form Calcite declares, which is what it converted to before this change; under a virtual table that still puts the schema and its rows out of step, and closing it needs the literal-side hook above.Worth saying, since it bears on whether this use is meant to be supported at all:
UserTypeMapper.toSubstraitreturns aType, so mapping to a built-in is expressible, while its reverse takes aType.UserDefined. The consumer this came from maps Impala'sstring— reaching Calcite asVARCHAR(2147483647)— onto Substrait'sstring, which is a natural reading of the forward signature. That reading is the intended one:TypeConverterconsults the mapper before its own switch precisely because a mapper may re-use aSqlTypeName, so mapping a Calcite built-in onto a Substrait built-in is what the forward direction offers. The reverse lags behind it —toCalcitetakes aType.UserDefined, so a mapped built-in has no hook to come back through — which is #1219.A negative width is refused where the padding happens: a mapper hands the literal a Substrait type directly, so nothing between it and this method holds its width to what a
fixedcharcan declare. It threw before as well, but reported the value as longer than thefixedchar<-5>it was declared as, rather than the width as the problem.The floor stops at the negatives rather than at the
[1..2147483647]type_classes.mdgivesL. Calcite types''as aCHAR(0)and its DDL parser takes aCHAR(0)column, so afixedchar<0>reaches a plan from ordinary SQL — onorigin/main(934a60e)SELECT '' FROM t,WHERE '' = '',CHAR_LENGTH('')and'' || 'x'all convert to one — and the bound itself is under discussion in substrait-io/substrait#1199, wheredialect_schema.yamlallows amax_lengthof 0 for the same three types. A zero width therefore converts as it does today, rather than being refused now and legalized again if that issue lands the other way.Closes #1170.
BREAKING CHANGE: a Calcite
CHAR(n)literal converts to afixedchar<n>whose text is padded to the declared width, where it previously carried the unpadded text and so afixedcharof the text's own length. A character value longer than the width it is declared as is rejected rather than converted.