You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ToSparkExpression.visit(SExpression.I8Literal, ...) and visit(SExpression.I16Literal, ...) (spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala:54-60) narrow with asInstanceOf:
On a scala.Int that is a numeric coercion, not a checked cast. The compiled visit(I8Literal) is I8Literal.value:()I → i2b → boxToByte, and visit(I16Literal) is i2s — so an out-of-range value is truncated to the low 8 or 16 bits with no error.
The wire permits it. algebra.proto declares int32 i8 = 2 and int32 i16 = 3, ProtoExpressionConverter passes both through unbounded, and Expression.I8Literal.value() / I16Literal.value() are plain int with no @Value.Check. This is the same reachability argument TypesAndLiteralsSuite already makes for an out-of-range precision: "Reachable from the wire: the literal's precision is a plain int32 in algebra.proto and core does not bound it."
A producer emitting Literal{i8: 200} yields Spark Literal(-56, ByteType), and every downstream filter, join key and aggregate computes on -56.
Why this one is worse than the interval cases
It leaves no trace at all. An overflowed interval wraps into an implausible value — a negative interval where a positive one was meant — which a user has some chance of noticing. Here the truncated value is perfectly in range for the carrier, so nothing later can distinguish it from an intended one.
It is also the same defect class as #1138 / #1140 (an int32 wire field narrowed unchecked into a narrower Spark carrier), 100 lines up in the same visitor. After #1140 the same plan fails loudly on a wild year count and silently mis-answers on a wild i8.
Where to fix
Two options, and the choice is the interesting part:
ToSparkExpression.visit(SExpression.I8Literal, ...)andvisit(SExpression.I16Literal, ...)(spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala:54-60) narrow withasInstanceOf:On a
scala.Intthat is a numeric coercion, not a checked cast. The compiledvisit(I8Literal)isI8Literal.value:()I→i2b→boxToByte, andvisit(I16Literal)isi2s— so an out-of-range value is truncated to the low 8 or 16 bits with no error.The wire permits it.
algebra.protodeclaresint32 i8 = 2andint32 i16 = 3,ProtoExpressionConverterpasses both through unbounded, andExpression.I8Literal.value()/I16Literal.value()are plainintwith no@Value.Check. This is the same reachability argumentTypesAndLiteralsSuitealready makes for an out-of-range precision: "Reachable from the wire: the literal's precision is a plain int32 in algebra.proto and core does not bound it."A producer emitting
Literal{i8: 200}yields SparkLiteral(-56, ByteType), and every downstream filter, join key and aggregate computes on-56.Why this one is worse than the interval cases
It leaves no trace at all. An overflowed interval wraps into an implausible value — a negative interval where a positive one was meant — which a user has some chance of noticing. Here the truncated value is perfectly in range for the carrier, so nothing later can distinguish it from an intended one.
It is also the same defect class as #1138 / #1140 (an
int32wire field narrowed unchecked into a narrower Spark carrier), 100 lines up in the same visitor. After #1140 the same plan fails loudly on a wild year count and silently mis-answers on a wildi8.Where to fix
Two options, and the choice is the interesting part:
core, as@Value.Checkbounds onI8Literal/I16Literal. This is the same shape as core: interval literals and types accept component values the Substrait spec disallows #1129's proposal for the interval literals and would cover every binding at once. It is also breaking for anyone relying on the current permissiveness.Either way
expr.value().toByteis not the fix — it has the same truncating semantics as the currentasInstanceOf.Found while reviewing #1140.