Skip to content

spark: i8 and i16 literals silently truncate out-of-range int32 values #1236

Description

@nielspardon

ToSparkExpression.visit(SExpression.I8Literal, ...) and visit(SExpression.I16Literal, ...) (spark/src/main/scala/io/substrait/spark/expression/ToSparkExpression.scala:54-60) narrow with asInstanceOf:

Literal(expr.value().asInstanceOf[Byte], ToSparkType.convert(expr.getType))
Literal(expr.value().asInstanceOf[Short], ToSparkType.convert(expr.getType))

On a scala.Int that is a numeric coercion, not a checked cast. The compiled visit(I8Literal) is I8Literal.value:()Ii2bboxToByte, 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:

  • In core, as @Value.Check bounds on I8Literal / 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.
  • In the Spark converter, with a checked narrowing that reports the offending value. Local and non-breaking, but leaves the other consumers as they are.

Either way expr.value().toByte is not the fix — it has the same truncating semantics as the current asInstanceOf.

Found while reviewing #1140.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions