-
Notifications
You must be signed in to change notification settings - Fork 176
Implement Append
command with Calcite
#4123
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
Changes from all commits
8267a9b
58e87bb
15f5ce7
96f2c43
318f67b
4c14bb1
6fcd711
f078f34
60ad92d
6cf443e
cece01b
915fca4
f52a545
3f744bd
11cae8e
ff3586c
7487c29
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 |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.sql.ast; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
import org.opensearch.sql.ast.tree.Append; | ||
import org.opensearch.sql.ast.tree.AppendCol; | ||
import org.opensearch.sql.ast.tree.Join; | ||
import org.opensearch.sql.ast.tree.Lookup; | ||
import org.opensearch.sql.ast.tree.Relation; | ||
import org.opensearch.sql.ast.tree.TableFunction; | ||
import org.opensearch.sql.ast.tree.UnresolvedPlan; | ||
import org.opensearch.sql.ast.tree.Values; | ||
|
||
/** AST nodes visitor simplifies subsearch ast tree with empty source input. */ | ||
public class EmptySourcePropagateVisitor extends AbstractNodeVisitor<UnresolvedPlan, Void> { | ||
|
||
public static final UnresolvedPlan EMPTY_SOURCE = new Values(Collections.emptyList()); | ||
|
||
@Override | ||
public UnresolvedPlan visitValues(Values node, Void context) { | ||
return node; | ||
} | ||
|
||
@Override | ||
public UnresolvedPlan visitRelation(Relation node, Void context) { | ||
return node; | ||
} | ||
|
||
// Assume future table functions like inputLookup, makeresult command will use this unresolved | ||
// plan | ||
@Override | ||
public UnresolvedPlan visitTableFunction(TableFunction node, Void context) { | ||
return node; | ||
} | ||
|
||
@Override | ||
public UnresolvedPlan visitChildren(Node node, Void context) { | ||
assert node instanceof UnresolvedPlan; | ||
UnresolvedPlan unresolvedPlan = (UnresolvedPlan) node; | ||
|
||
if (unresolvedPlan.getChild().size() == 1) { | ||
return isEmptySource(((List<UnresolvedPlan>) unresolvedPlan.getChild()).get(0)) | ||
? EMPTY_SOURCE | ||
: unresolvedPlan; | ||
} | ||
return super.visitChildren(node, context); | ||
} | ||
|
||
@Override | ||
public UnresolvedPlan visitAppend(Append node, Void context) { | ||
UnresolvedPlan subSearch = node.getSubSearch().accept(this, context); | ||
UnresolvedPlan child = node.getChild().get(0).accept(this, context); | ||
return new Append(subSearch).attach(child); | ||
} | ||
|
||
@Override | ||
public UnresolvedPlan visitAppendCol(AppendCol node, Void context) { | ||
UnresolvedPlan subSearch = node.getSubSearch().accept(this, context); | ||
UnresolvedPlan child = node.getChild().get(0).accept(this, context); | ||
return new AppendCol(node.isOverride(), subSearch).attach(child); | ||
} | ||
|
||
// TODO: Revisit lookup logic here but for now we don't see use case yet | ||
@Override | ||
public UnresolvedPlan visitLookup(Lookup node, Void context) { | ||
UnresolvedPlan lookupRelation = node.getLookupRelation().accept(this, context); | ||
UnresolvedPlan child = node.getChild().get(0).accept(this, context); | ||
// Lookup is a LEFT join. | ||
// If left child is expected to be 0 row, it outputs 0 row | ||
// If right child is expected to be 0 row, the output is the left child; | ||
if (isEmptySource(child)) { | ||
return EMPTY_SOURCE; | ||
} | ||
return isEmptySource(lookupRelation) ? child : node; | ||
} | ||
|
||
// Not see use case yet | ||
@Override | ||
public UnresolvedPlan visitJoin(Join node, Void context) { | ||
UnresolvedPlan left = node.getLeft().accept(this, context); | ||
UnresolvedPlan right = node.getRight().accept(this, context); | ||
|
||
boolean leftEmpty = isEmptySource(left); | ||
boolean rightEmpty = isEmptySource(right); | ||
|
||
switch (node.getJoinType()) { | ||
case INNER: | ||
case CROSS: | ||
return leftEmpty || rightEmpty ? EMPTY_SOURCE : node; | ||
case LEFT: | ||
case SEMI: | ||
case ANTI: | ||
if (leftEmpty) { | ||
return EMPTY_SOURCE; | ||
} | ||
return rightEmpty ? left : node; | ||
case RIGHT: | ||
if (rightEmpty) { | ||
return EMPTY_SOURCE; | ||
} | ||
return leftEmpty ? right : node; | ||
case FULL: | ||
if (leftEmpty) { | ||
return right; | ||
} | ||
return rightEmpty ? left : node; | ||
default: | ||
return node; | ||
} | ||
} | ||
|
||
private boolean isEmptySource(UnresolvedPlan plan) { | ||
return plan instanceof Values | ||
&& (((Values) plan).getValues() == null || ((Values) plan).getValues().isEmpty()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.sql.ast.tree; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import java.util.List; | ||
import lombok.EqualsAndHashCode; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.Setter; | ||
import lombok.ToString; | ||
import org.opensearch.sql.ast.AbstractNodeVisitor; | ||
import org.opensearch.sql.ast.Node; | ||
|
||
/** Logical plan node of Append, the interface for union all columns in queries. */ | ||
@Getter | ||
@Setter | ||
@ToString | ||
@EqualsAndHashCode(callSuper = false) | ||
@RequiredArgsConstructor | ||
public class Append extends UnresolvedPlan { | ||
|
||
private final UnresolvedPlan subSearch; | ||
|
||
private UnresolvedPlan child; | ||
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. what's the different between 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. The This is for matching optional For example, in future, this query is a valid query: Another reason is to use searchPlan to handle an edge case of appending empty subresults. Some other pipeline language has similar functionality to allow subsearch outputs empty result. For example, Calcite is a strong schema engine, parsing a RelNode like
will throw exception when either building RelNode or at runtime. Because Project cannot find any 'a' column from the input. I'm open to this discussion, we have two options here:
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 refactor the code a bit to avoid confusing name of 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 choose to throw exception for empty subsearch input for now because it needs some dirty work to preprocess ast tree to achieve a not useful use case. Not supporting it by throwing exception is elegant. 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.
@songkant-aws I think the the previous implementation looks good to me except the confusing name of 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 changed back the empty source support, and for nested case. |
||
|
||
@Override | ||
public UnresolvedPlan attach(UnresolvedPlan child) { | ||
this.child = child; | ||
return this; | ||
} | ||
|
||
@Override | ||
public List<? extends Node> getChild() { | ||
return this.child == null ? ImmutableList.of() : ImmutableList.of(this.child); | ||
} | ||
|
||
@Override | ||
public <T, C> T accept(AbstractNodeVisitor<T, C> visitor, C context) { | ||
return visitor.visitAppend(this, context); | ||
} | ||
} |
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.
None of tests/ITs cover the
visitLookup
andvisitJoin
you added here. Please add tests/ITs for empty source forlookup
/join
Uh oh!
There was an error while loading. Please reload this page.
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.
Added tests. For now, our join or lookup command syntax enforces the searchCommand to be existent in the right child. So I can't cover the case of empty source of right child. Empty source for left child test cases are added.