Skip to content

fix(core)!: MapLiteral cannot represent a map with repeated keys #1081

Description

@nielspardon

Expression.MapLiteral keeps its pairs in a Map<Literal, Literal>. The proto message it round-trips does not, so a class of valid plans cannot be imported at all.

The proto allows repeated keys

// substrait/algebra.proto - Expression.Literal.Map
message Map {
  message KeyValue {
    Literal key = 1;
    Literal value = 2;
  }
  repeated KeyValue key_values = 1;
}

key_values is a repeated field with no uniqueness constraint, so a literal map that repeats a key - {1: 'a', 1: 'b'} - is a well-formed Substrait plan. A Map<Literal, Literal> cannot hold one.

What happens today

ProtoExpressionConverter collects the pairs with Collectors.toMap:

https://github.com/substrait-io/substrait-java/blob/main/core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java#L580-L584

Input Today Expected
repeated key: {1: 'a', 1: 'b'} import fails with a bare IllegalStateException: Duplicate key ... from inside the stream collector import it faithfully, or fail with an error that names the offending key
distinct keys, producer order pairs come back reordered - toMap collects into a HashMap key_values order survives the round trip

So this is worse than lossy: a valid plan fails to import rather than round-tripping imperfectly.

Two things are not affected:

  • EmptyMapLiteral - it carries only the key and value types, no pairs.
  • Export (ExpressionProtoConverter) - it cannot produce a bad plan, because the POJO it reads from can never hold a repeated key in the first place. That is the same limitation seen from the other side: a producer built on the POJO model has no way to emit a repeated-key map literal.

Related

MapLiteral is the remaining place where the limitation lives.

Two ways out

A. Make the representation match the proto. Replace values() with an ordered list of literal key/value pairs, mirroring NestedMap.

  • Fixes both symptoms - repeated keys are representable and pair order is preserved.
  • Breaking change to a public API that predates this repo's current shape, and it touches every consumer of MapLiteral (ExpressionCreator.map, both proto converters, isthmus ExpressionRexConverter, spark ToSparkExpression). This is why it was left out of feat(core)!: support nested map expressions and import nested structs #1062.

B. Keep the Map and make the limitation explicit. Import fails with a message naming the duplicate key instead of a bare IllegalStateException, and the pairs are collected into a LinkedHashMap so distinct-key plans at least keep their order.

  • Not a breaking change, and it turns a confusing failure into a clear one.
  • Repeated-key plans still cannot be imported.

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