fix(isthmus)!: keep the declared length of a wide character or binary type - #1169
Conversation
895bc23 to
cdca654
Compare
|
The ceiling question you flag belongs to #1124 — |
|
Measured on this branch, since the ceiling reaching values turns out to have sharper consequences than a wrong type.
The other cases are unaffected: That reads to me as an argument for taking the limit from |
|
The padding on that path is Calcite's, agreed. One thing to weigh before you pick, though it doesn't change the direction: a cap on |
cdca654 to
880090a
Compare
|
Keeping The reasoning I landed on: the number here is the spec's bound on a length, and a narrower one is a dialect's, which is Rebased on |
nielspardon
left a comment
There was a problem hiding this comment.
Mark this breaking and add the footer — public toCalcite now throws where it previously returned a narrowed type, and SQL→Substrait output types change, which is the shape #1120 and #1121 both carried as fix(isthmus)!:. Keeping Integer.MAX_VALUE with the reasoning now in the description reads right to me. The three comments below are asymmetries in the raise I would fix before it lands.
nielspardon
left a comment
There was a problem hiding this comment.
Five smaller things below, plus two I will leave unless you want them: assertTrue(e.getMessage().contains("65536")) passes on the VARCHAR(65536) already in that message rather than on the bound it means to pin, and both public toCalcite overloads still document only @throws UnsupportedOperationException now that two conditions raise IllegalArgumentException.
|
Took both of the two you left rather than leaving them: the length test asserts |
|
looks like this needs to be reconciled with latest main |
…type SubstraitTypeSystem overrides getMaxPrecision for the temporal types and DECIMAL; CHAR, VARCHAR and BINARY fell through to RelDataTypeSystemImpl, whose default caps them at 65536. The type factory then narrowed anything wider without raising an error, so a plan declaring varchar<2147483647> came back as VARCHAR(65536) and a consumer building a Calcite tree from it had no way to notice. fixedchar and fixed_binary lose their length the same way. Those three are the types whose length crosses the boundary, and Substrait declares each as a 32-bit integer, so they now report Integer.MAX_VALUE. VARBINARY is left alone: Substrait's binary carries no length for it to lose. Closes substrait-io#1167.
The cap reaches the conversion from SQL too, which the tests did not say: Calcite narrows a declared width to its maximum silently, so a cast wider than the default used to leave the conversion as a varchar<65536>.
The conversion takes whatever RelDataTypeFactory it is handed, and a factory caps a width at its type system's maximum without saying so, so raising the maximum fixes the narrowing only for the factory isthmus builds. A fixedchar, varchar or fixed_binary the factory narrows is now reported instead of returned.
Calcite unifies a union of fixed-width binaries of different widths into a VARBINARY of the widest, so the cap this branch removes from BINARY was still reimposed one step later: leastRestrictive(BINARY(100000), BINARY(5)) returned VARBINARY(65536), narrower than one of its own inputs. Substrait's binary carries no length of its own, which is why the type was left alone before, but the width is lost inside Calcite's type unification rather than at the boundary.
… asked The check compared the factory's answer with the length asked for, which can only report what the factory survives. With assertions off -- the normal production JVM -- the factory stores a negative width and reports it back, so varChar(-5) certified itself as VARCHAR(-5); with them on, Calcite raised a bare AssertionError in place of the message. A length of -1 is Calcite's unspecified precision besides, so varChar(-1) passed and became a Substrait string, while fixedChar(-1) was reported against a bound its length is below. The floor stops at the negatives. The spec puts a fixedchar's width at 1 or more (spec v0.101.0), but Calcite types the empty character literal as a CHAR(0), so a zero length reaches this conversion from ordinary SQL.
fixed_binary is the dialect enum's FIXED_BINARY. The spec and this repo's own StringTypeVisitor both spell the type itself without the underscore.
The guard added for a declared length left a decimal to the same silent narrowing it was added for: decimal<38,10> through a factory on Calcite's default type system came back DECIMAL(19,10) and read back as a precision the plan never declared. The temporal visits already report this through requireSupportedPrecision, so the decimal one now does too. Both public toCalcite overloads document the IllegalArgumentException the length and precision checks raise, and the length test asserts the bound the message reports rather than a number that also appears in the type it narrowed to.
getMaxPrecision(VARCHAR) is also Calcite's overflow threshold in ReturnTypes.DYADIC_STRING_SUM_PRECISION, so raising it changes the emitted type for plans carrying no wide type at all: a || b over two varchar<40000> columns converts to varchar<80000> where it converted to string. The type-mapping switch in the parameterized conversion test throws on an unmatched name rather than falling through to fixedBinary, so a row added for another type fails as the test's own gap rather than as a defect in the type system.
Found checking the ends of the precision guard rather than in review. A negative precision went the way varChar(-1) did: -1 is Calcite's unspecified precision, so decimal<-1,0> converted to a bare DECIMAL and read back as the type system's maximum, through any factory rather than only a foreign one. A scale past the maximum was narrowed the same silent way a precision was -- decimal<19,25> through a factory on Calcite's default type system came back with a scale of 19. Calcite reports a zero precision and a negative scale itself, so the guards here are the two ends it leaves.
65f4036 to
e691d97
Compare
|
Rebased onto
|
nielspardon
left a comment
There was a problem hiding this comment.
Everything from round 2 is in, and the description now carries the zero-literal and concatenation paths. Two small things left: a stray Javadoc block in the new test, and a scale check no valid decimal can reach.
| /** | ||
| * The cap reaches the conversion from SQL as well. Calcite narrows a declared width to its | ||
| * maximum silently rather than reporting that it cannot hold it, so before this a cast wider than | ||
| * the default came out of the conversion as a {@code varchar<65536>}. | ||
| */ |
There was a problem hiding this comment.
Delete this block — it documents aWideVarcharDeclaredInSqlKeepsItsLength at the bottom of the file, not this test. Of two consecutive doc comments only the second applies, so the first is dead text.
| /** | |
| * The cap reaches the conversion from SQL as well. Calcite narrows a declared width to its | |
| * maximum silently rather than reporting that it cannot hold it, so before this a cast wider than | |
| * the default came out of the conversion as a {@code varchar<65536>}. | |
| */ |
There was a problem hiding this comment.
Moved rather than deleted -- it is aWideVarcharDeclaredInSqlKeepsItsLength's own javadoc, added with that test in the SQL-cast commit, and the decimal test landed between the two. The test at the bottom of the file had none left, so the block goes back to it.
…ctory The scale check compared against the type system's maxScale, which no spec-valid decimal can exceed: the spec puts scale at 0 <= S <= P <= 38, and both type systems here set maxScale equal to maxPrecision, so a scale within the precision is within the maximum too. The bound that was missing is the spec's own -- decimal<19,25> has a scale above its precision, and Calcite built DECIMAL(19,25) from it without complaint. The javadoc of the SQL-cast test goes back to that test as well; it was left behind when the decimal test was added above it.
nielspardon
left a comment
There was a problem hiding this comment.
Both fixes read right, and moving the stray Javadoc onto its own test beats deleting it. One thing left — the other half of the spec clause the scale bound came from.
The precision was checked only against the type factory's maximum, so a type system that allows more than the spec does let a wider decimal through: with a DECIMAL maximum of 76, decimal<45,2> converted to DECIMAL(45,2) and the outbound conversion then refused it with "unsupported decimal precision 45", leaving a type that converts in and cannot come back. Both ends of that clause now read one constant rather than two spellings of 38.
SubstraitTypeSystemoverridesgetMaxPrecisionfor the temporal types andDECIMAL;CHAR,VARCHARandBINARYfell through toRelDataTypeSystemImpl, whose default caps them at 65536. The type factory then narrowed anything wider without raising an error, so a plan declaringvarchar<2147483647>came back asVARCHAR(65536)and a consumer building a Calcite tree from it had no way to notice. The boundary is exact: 65536 round-trips, 65537 does not.The cap reaches the conversion from SQL too, which the issue does not mention: Calcite narrows a declared width there the same silent way, so
SELECT CAST(a AS VARCHAR(100000))came out of the conversion as avarchar<65536>. It now comes out asvarchar<100000>. What SQL is accepted does not change -- Calcite never rejected the wider declaration, it just did not keep it.Those three are the types whose length crosses the boundary — holding
fixedchar,varcharandfixedbinary— and Substrait declares each length as a 32-bit integer, so they now reportInteger.MAX_VALUE.VARBINARYis raised with them for a different reason: Substrait'sbinarycarries no length of its own, but Calcite unifies a union of fixed-width binaries of different widths into aVARBINARYof the widest, so leaving it at the default reimposed the cap one step later —leastRestrictive(BINARY(100000), BINARY(5))returnedVARBINARY(65536), narrower than one of its own inputs. That shape was unreachable before this change, since noBINARYwider than 65536 could exist. Raising it opens no new failure at the extreme: aVARBINARYis not padded, socast(x'01' AS VARBINARY(2147483647))projects fine, where the fixed-widthBINARY(2147483647)runs out of memory exactly asCHARdoes. The issue was filed for the character types;fixedbinaryturned out to have the same defect at the same boundary.One consequence, and where the ceiling belongs.
CHARandBINARYare fixed-width, so Calcite pads a literal cast to them out to the declared length, and the padding is now allowed to reach the declared width. At the extreme,RelBuilder.project(cast('a' AS CHAR(2147483647)))throwsOutOfMemoryError: Required length exceeds implementation limit, where the old cap silently made itCHAR(65536)and it converted;CHAR(100000)is fine, and the varying types are unaffected at any width. No literal need appear in the plan for this either:RexBuilder.zeroValueallocatesnew byte[precision]for aBINARY, somakeZeroLiteral(BINARY(2147483647))throws the same way and a planner rule reaches it from a column type alone, where the old cap made it a 65536-byte array. AVARBINARY's zero value isByteString.EMPTY, so raising that type costs nothing here.Integer.MAX_VALUEstays here because it is the spec's own bound on a length -- a narrower one is a dialect's answer, whichSupportedType.maxLength()already carries forFIXED_CHAR,VARCHARandFIXED_BINARY, and reading it is #1124. A cap on this type system would not bound all of it in any case:UserTypeMapper.toSubstraitreturns a Substrait type without going through the type factory at all. The padding isthmus does itself, on theLogicalValuespath where Calcite does none, is bounded in #1171.One more thing the raise changes, and it reaches plans carrying no wide type at all:
getMaxPrecision(VARCHAR)is Calcite's overflow threshold for concatenation as well, inReturnTypes.DYADIC_STRING_SUM_PRECISION. A sum of widths past it falls back to an unparameterised type, soa || bover twovarchar<40000>columns converts tovarchar<80000>where it converted tostring;CONCATdoes the same. Neither operand is anywhere near the old cap.A
RelDataTypeFactorynarrows a width past its own maximum without saying so, and this conversion takes whatever factory it is handed, so raising the maximum fixes the narrowing only for the factory isthmus builds. A declared length the factory cannot hold is now reported instead of returned quietly, and a negative one is refused before the factory is asked at all. Asking cannot tell the two apart: with assertions off — the normal production JVM — the factory stores the negative and reports it back, sovarChar(-5)certified itself asVARCHAR(-5), while under-eaCalcite raised a bareAssertionErrorin place of the message. A length of -1 is Calcite's unspecified precision besides, sovarChar(-1)passed and became a SubstraitstringwherefixedChar(-1)was reported against a bound its length is below. A zero length is left to the factory, for the reason #1171 leaves it alone: ordinary SQL produces aCHAR(0)today, and substrait-io/substrait#1199 has the floor open. A decimal went the same silent way and now reports too:decimal<38,10>through a factory on Calcite's default type system came backDECIMAL(19,10)and read back as a precision the plan never declared. Its two ends go with it, both found checking the guard's boundaries rather than in review:decimal<-1,0>converted to a bareDECIMALand read back as the maximum through any factory,-1being Calcite's unspecified precision here as well, and a scale above the precision was built as asked --decimal<19,25>came out asDECIMAL(19,25). That one is bounded by the spec rather than by the factory: scale sits in0 <= S <= P, and no maximum catches it, since both type systems here setmaxScaleequal tomaxPrecision, so a scale within the precision is within the maximum too. Calcite reports a zero precision and a negative scale itself, so those are left to it. The precision's own ceiling is the spec's too, not the factory's: a type system allowing more than 38 -- agetMaxPrecision(DECIMAL)of 76 -- letdecimal<45,2>convert toDECIMAL(45,2), whichtoSubstraitthen refused with "unsupported decimal precision 45", so the type converted in with no way back. Both ends of that clause read one constant now.The conversion test asserts the resulting precision directly rather than through
testType, whose expected type is built with the same type factory and would be narrowed alongside the value under test.Closes #1167.
BREAKING CHANGE:
TypeConverter.toCalcitethrowsIllegalArgumentExceptionwhere it previously returned a narrowed type — for a declared length the given type factory cannot hold, and for a negative length, which a factory running without assertions accepted as a type of that width. It also throws for a decimal precision the given factory cannot hold and for a negative one, where it returned a narrowed type, for a scale above the precision, which it built as asked, and for a precision above the 38 the spec allows, which only a type factory more permissive than the spec could reach. Converted output types change with the raised bound:SELECT CAST(a AS VARCHAR(100000))converts tovarchar<100000>where it converted tovarchar<65536>,a || bover twovarchar<40000>columns converts tovarchar<80000>where it converted tostring, and the least restrictive type ofBINARY(100000)andBINARY(5)isVARBINARY(100000)where it wasVARBINARY(65536).