What
A numeric type mapping's GenerateNonNullSqlLiteral writes a bare number. Calcite types a bare
exact-numeric literal as INTEGER (or DECIMAL, once it has a decimal point) regardless of the
column it sits beside, so the literal's SQL type is not the mapping's StoreType. In a comparison
that is invisible — Calcite coerces the operands — but a projected literal reaches the reader as
whatever Calcite typed it, and the shaper asks for the model type. The read then fails:
Cannot convert value of type 'Integer' to 'Byte'
byte is fixed in #26 by writing CAST(<value> AS TINYINT UNSIGNED), which is the same cast
CalciteQuerySqlGenerator.VisitSqlParameter already writes around a byte parameter — which is why
a parameter reached the reader as a byte where a constant did not.
Still open
The rest of the family has the same shape and no GenerateNonNullSqlLiteral override:
| mapping |
store type |
literal written |
Calcite's type for it |
reader wants |
CalciteSByteTypeMapping |
TINYINT |
5 |
INTEGER |
java.lang.Byte |
CalciteShortTypeMapping |
SMALLINT |
5 |
INTEGER |
java.lang.Short |
CalciteLongTypeMapping |
BIGINT |
5 |
INTEGER |
java.lang.Long |
CalciteUShortTypeMapping |
SMALLINT UNSIGNED |
5 |
INTEGER |
org.joou.UShort |
CalciteUIntTypeMapping |
INTEGER UNSIGNED |
5 |
INTEGER |
org.joou.UInteger |
CalciteULongTypeMapping |
BIGINT UNSIGNED |
5 |
INTEGER |
org.joou.ULong |
CalciteFloatTypeMapping |
REAL |
5 / 5.25 |
INTEGER / DECIMAL |
java.lang.Float |
CalciteDoubleTypeMapping |
DOUBLE |
5.0 |
DECIMAL |
java.lang.Double |
CalciteIntTypeMapping is the one that is already right: a bare literal is INTEGER.
CalciteDecimalTypeMapping is a special case. It overrides the literal already, to keep a 19-digit
integral value inside Calcite's maximum DECIMAL precision of 19, and a CAST(… AS DECIMAL(19, 4))
would put that value back over the limit. It needs its own answer, not this one.
CalciteIntTypeMapping must stay bare for a second reason: VisitLimitOffsetValue sends a constant
row count straight to the literal generator, and Calcite's parser does not accept a CAST in a
FETCH / OFFSET position.
Wider than literals
Typing the literal closes the constant case, not the general one. Calcite promotes operand types in
arithmetic, COALESCE and CASE, so a + b over two TINYINT UNSIGNED columns is an INTEGER
expression even when both columns are correctly typed — and the shaper still asks for byte.
CalciteQuerySqlGenerator already casts back to the store type in the places this was hit
(VisitLeftShift, VisitRightShift, VisitCountFunction); whether that should generalize to any
expression whose result mapping is narrower than what Calcite infers is the real question here.
Note on the reader
This is not a regression in Apache.Calcite.Data. Its accessors used to take any java.lang.Number
that fit, which is looser than ADO.NET means by GetByte — SqlDataReader.GetByte throws where the
column is not a byte. Tightening them (ikvmnet/calcite-dotnet@63f24cf) removed a leniency this
provider had been leaning on without saying so. The generated SQL was already wrong; the reader had
been quietly covering for it.
What
A numeric type mapping's
GenerateNonNullSqlLiteralwrites a bare number. Calcite types a bareexact-numeric literal as
INTEGER(orDECIMAL, once it has a decimal point) regardless of thecolumn it sits beside, so the literal's SQL type is not the mapping's
StoreType. In a comparisonthat is invisible — Calcite coerces the operands — but a projected literal reaches the reader as
whatever Calcite typed it, and the shaper asks for the model type. The read then fails:
byteis fixed in #26 by writingCAST(<value> AS TINYINT UNSIGNED), which is the same castCalciteQuerySqlGenerator.VisitSqlParameteralready writes around a byte parameter — which is whya parameter reached the reader as a byte where a constant did not.
Still open
The rest of the family has the same shape and no
GenerateNonNullSqlLiteraloverride:CalciteSByteTypeMappingTINYINT5INTEGERjava.lang.ByteCalciteShortTypeMappingSMALLINT5INTEGERjava.lang.ShortCalciteLongTypeMappingBIGINT5INTEGERjava.lang.LongCalciteUShortTypeMappingSMALLINT UNSIGNED5INTEGERorg.joou.UShortCalciteUIntTypeMappingINTEGER UNSIGNED5INTEGERorg.joou.UIntegerCalciteULongTypeMappingBIGINT UNSIGNED5INTEGERorg.joou.ULongCalciteFloatTypeMappingREAL5/5.25INTEGER/DECIMALjava.lang.FloatCalciteDoubleTypeMappingDOUBLE5.0DECIMALjava.lang.DoubleCalciteIntTypeMappingis the one that is already right: a bare literal isINTEGER.CalciteDecimalTypeMappingis a special case. It overrides the literal already, to keep a 19-digitintegral value inside Calcite's maximum
DECIMALprecision of 19, and aCAST(… AS DECIMAL(19, 4))would put that value back over the limit. It needs its own answer, not this one.
CalciteIntTypeMappingmust stay bare for a second reason:VisitLimitOffsetValuesends a constantrow count straight to the literal generator, and Calcite's parser does not accept a
CASTin aFETCH/OFFSETposition.Wider than literals
Typing the literal closes the constant case, not the general one. Calcite promotes operand types in
arithmetic,
COALESCEandCASE, soa + bover twoTINYINT UNSIGNEDcolumns is anINTEGERexpression even when both columns are correctly typed — and the shaper still asks for
byte.CalciteQuerySqlGeneratoralready casts back to the store type in the places this was hit(
VisitLeftShift,VisitRightShift,VisitCountFunction); whether that should generalize to anyexpression whose result mapping is narrower than what Calcite infers is the real question here.
Note on the reader
This is not a regression in
Apache.Calcite.Data. Its accessors used to take anyjava.lang.Numberthat fit, which is looser than ADO.NET means by
GetByte—SqlDataReader.GetBytethrows where thecolumn is not a byte. Tightening them (
ikvmnet/calcite-dotnet@63f24cf) removed a leniency thisprovider had been leaning on without saying so. The generated SQL was already wrong; the reader had
been quietly covering for it.