ProtoPlanConverter.from reads every PlanRel as a root without asking which arm of the oneof is set, so a plan carrying a bare relation is rejected whole. ProtoPlanConverter.java:97-99 at v0.103.0, and the same three lines on main at ee9f3d20:
for (PlanRel planRel : plan.getRelationsList()) {
io.substrait.proto.RelRoot root = planRel.getRoot();
Rel rel = relConverter.from(root.getInput());
On the rel arm getRoot() returns the default instance, so relConverter is handed an empty Rel and reports that faithfully. The message names RELTYPE_NOT_SET because that is what it was given, not what the plan holds:
ROOT -> 1 root(s)
REL -> UnsupportedOperationException: Unsupported RelTypeCase of RELTYPE_NOT_SET
substrait-go produces this shape: its Plan.ToProto calls ToProtoPlanRel per relation and emits PlanRel_Rel for every relation that is not a root, at plan/plan.go:132-135. So this is two implementations that cannot exchange a plan, rather than a shape only a hand-built proto reaches.
Only reading is affected. PlanProtoConverter walks plan.getRoots() and always calls setRoot, so substrait-java never writes the arm it cannot read.
The fix worth agreeing on before writing it: skipping bare relations would turn today's loud failure into a quiet one, dropping relations and leaving any ReferenceRel pointing at them dangling. Checking the arm and refusing with an accurate message is smaller and loses nothing. Holding them properly means somewhere for them to live in io.substrait.plan.Plan, which today has only roots. Which of the two would you want?
Reproducer
import io.substrait.proto.*;
import io.substrait.plan.ProtoPlanConverter;
public final class Repro {
public static void main(String[] args) {
Rel read = Rel.newBuilder().setRead(ReadRel.newBuilder()
.setNamedTable(ReadRel.NamedTable.newBuilder().addNames("t"))).build();
for (PlanRel pr : new PlanRel[] {
PlanRel.newBuilder().setRoot(RelRoot.newBuilder().setInput(read)).build(),
PlanRel.newBuilder().setRel(read).build()}) {
Plan p = Plan.newBuilder().addRelations(pr).build();
try {
System.out.printf("%-6s -> %d root(s)%n", pr.getRelTypeCase(),
new ProtoPlanConverter().from(p).getRoots().size());
} catch (Throwable t) {
System.out.printf("%-6s -> %s: %s%n", pr.getRelTypeCase(),
t.getClass().getSimpleName(), t.getMessage());
}
}
}
}
ProtoPlanConverter.fromreads everyPlanRelas a root without asking which arm of the oneof is set, so a plan carrying a bare relation is rejected whole.ProtoPlanConverter.java:97-99at v0.103.0, and the same three lines on main atee9f3d20:On the
relarmgetRoot()returns the default instance, sorelConverteris handed an emptyReland reports that faithfully. The message namesRELTYPE_NOT_SETbecause that is what it was given, not what the plan holds:substrait-go produces this shape: its
Plan.ToProtocallsToProtoPlanRelper relation and emitsPlanRel_Relfor every relation that is not a root, atplan/plan.go:132-135. So this is two implementations that cannot exchange a plan, rather than a shape only a hand-built proto reaches.Only reading is affected.
PlanProtoConverterwalksplan.getRoots()and always callssetRoot, so substrait-java never writes the arm it cannot read.The fix worth agreeing on before writing it: skipping bare relations would turn today's loud failure into a quiet one, dropping relations and leaving any
ReferenceRelpointing at them dangling. Checking the arm and refusing with an accurate message is smaller and loses nothing. Holding them properly means somewhere for them to live inio.substrait.plan.Plan, which today has onlyroots. Which of the two would you want?Reproducer