-
Notifications
You must be signed in to change notification settings - Fork 94
fix(core): validate Plan.Root output name count #812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,12 @@ | |
| import io.substrait.SubstraitVersion; | ||
| import io.substrait.extension.AdvancedExtension; | ||
| import io.substrait.relation.Rel; | ||
| import io.substrait.type.NamedFieldCountingTypeVisitor; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.immutables.value.Value; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| @Value.Immutable | ||
| public abstract class Plan { | ||
|
|
@@ -61,10 +64,31 @@ private static Version loadVersion() { | |
|
|
||
| @Value.Immutable | ||
| public abstract static class Root { | ||
| private static final Logger LOGGER = LoggerFactory.getLogger(Root.class); | ||
|
|
||
| public abstract Rel getInput(); | ||
|
|
||
| public abstract List<String> getNames(); | ||
|
|
||
| @Value.Check | ||
| protected void check() { | ||
| final int actualNameCount = getNames().size(); | ||
| if (actualNameCount == 0) { | ||
| LOGGER.warn( | ||
| "Plan.Root built without output names; this will be an error in the next release"); | ||
| return; | ||
| } | ||
|
Comment on lines
+76
to
+80
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have the feeling like we need to make the spec more clear on names being required for We probably also should flesh out the Plan Root documentation a little bit more to describe the structure and reference to the What do you think? |
||
|
|
||
| final int expectedFieldCount = | ||
| NamedFieldCountingTypeVisitor.countNames(getInput().getRecordType()); | ||
| if (actualNameCount != expectedFieldCount) { | ||
|
benbellick marked this conversation as resolved.
|
||
| throw new IllegalArgumentException( | ||
| String.format( | ||
| "Plan.Root names count (%d) must match input record type depth-first named-field count (%d)", | ||
| actualNameCount, expectedFieldCount)); | ||
| } | ||
| } | ||
|
|
||
| public static ImmutableRoot.Builder builder() { | ||
| return ImmutableRoot.builder(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| package io.substrait.relation; | ||
|
|
||
| import io.substrait.expression.Expression; | ||
| import io.substrait.type.NamedFieldCountingTypeVisitor; | ||
| import io.substrait.type.Type; | ||
| import io.substrait.type.TypeVisitor; | ||
| import io.substrait.util.VisitationContext; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
@@ -90,162 +90,4 @@ public <O, C extends VisitationContext, E extends Exception> O accept( | |
| public static ImmutableVirtualTableScan.Builder builder() { | ||
| return ImmutableVirtualTableScan.builder(); | ||
| } | ||
|
|
||
| private static class NamedFieldCountingTypeVisitor | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice, I had no idea we already had such a visitor in the code base |
||
| implements TypeVisitor<Integer, RuntimeException> { | ||
|
|
||
| private static final NamedFieldCountingTypeVisitor VISITOR = | ||
| new NamedFieldCountingTypeVisitor(); | ||
|
|
||
| private static Integer countNames(Type type) { | ||
| return type.accept(VISITOR); | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Bool type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.I8 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.I16 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.I32 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.I64 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.FP32 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.FP64 type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Str type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Binary type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Date type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Time type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.TimestampTZ type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Timestamp type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.PrecisionTimestamp type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.PrecisionTime type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.PrecisionTimestampTZ type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.IntervalYear type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.IntervalDay type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.IntervalCompound type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.UUID type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.FixedChar type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.VarChar type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.FixedBinary type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Decimal type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Struct type) throws RuntimeException { | ||
| // Only struct fields have names - the top level column names are also | ||
| // captured by this since the whole schema is wrapped in a Struct type | ||
| return type.fields().stream().mapToInt(field -> 1 + field.accept(this)).sum(); | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.ListType type) throws RuntimeException { | ||
| return type.elementType().accept(this); | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Map type) throws RuntimeException { | ||
| return type.key().accept(this) + type.value().accept(this); | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.UserDefined type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer visit(Type.Func type) throws RuntimeException { | ||
| return 0; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is a great solution. But considering that this update will result in stricter plan enforcement, I want to figure out a way where we can just warn on incorrect behavior for now, and then make it an actual error in the future.
But I'm not sure introducing a logger here where one isn't used anywhere else makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Left a comment on whether we want to make the spec more clear first. If the spec clearly says names are required then we can fire the exception straight away and we handle the change in behavior as a breaking change commit so it gets announced to consumers of substrait-java. That would then be sufficient in my opinion. Logging a warning to communicate the upcoming change is a nice gesture but who knows if consumers notice the warning message.