Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2e50f6d
Support spath with dynamic fields
ykmr1224 Jan 17, 2026
4a49d04
Add explain test and sql conversion test
ykmr1224 Jan 20, 2026
bca7443
Address comments
ykmr1224 Jan 20, 2026
e285fbf
Fix FieldResolutionResultTest
ykmr1224 Jan 20, 2026
54073d7
Address comment
ykmr1224 Jan 20, 2026
ef79687
Fix explain
ykmr1224 Jan 20, 2026
29ee620
Fix join logic to adopt dynamic fields
ykmr1224 Jan 21, 2026
cd041e1
Fix join logic
ykmr1224 Jan 21, 2026
f43e504
Fix spath.md
ykmr1224 Jan 21, 2026
f9c7583
Support fillnull and replace
ykmr1224 Jan 21, 2026
5725f2d
Update spath.md
ykmr1224 Jan 21, 2026
aae572a
Fix test failure
ykmr1224 Jan 21, 2026
9ef0bda
Fix doc and error for fillnull
ykmr1224 Jan 22, 2026
8a618f6
Accept wildcard only at the end of field list
ykmr1224 Jan 22, 2026
9449233
Minor fix
ykmr1224 Jan 22, 2026
600809e
Adopt append command to spath
ykmr1224 Jan 22, 2026
d34616d
Fix join to allow spath in only one input
ykmr1224 Jan 22, 2026
91a06f4
Fix unit test failure
ykmr1224 Jan 22, 2026
fec8f4b
Fix join inputs logic
ykmr1224 Jan 22, 2026
e5cec4d
Fix test failure
ykmr1224 Jan 22, 2026
f7ea5dd
Move helper methods
ykmr1224 Jan 22, 2026
ced9c37
Add _MAP description in the docs
ykmr1224 Jan 22, 2026
cacc0ae
Extract more from CalciteRelNodeVisitor
ykmr1224 Jan 22, 2026
dbb1ff4
Refactor IT and address comments
ykmr1224 Jan 23, 2026
55e15ec
Fix FieldResolutionResult
ykmr1224 Jan 23, 2026
eb9bd69
Fix DynamicFieldsHelper
ykmr1224 Jan 23, 2026
7204de1
Refactor DynamicFieldsHelper
ykmr1224 Jan 23, 2026
b5b114d
Fix DynamicFieldsHelper
ykmr1224 Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* Utility class for debugging operations. This class is only for debugging purpose, and not
Expand All @@ -18,7 +16,6 @@
public class DebugUtils {
// Update this to true while you are debugging. (Safe guard to avoid usage in production code. )
private static final boolean IS_DEBUG = false;
private static final Logger logger = LogManager.getLogger(DebugUtils.class);

public static <T> T debug(T obj, String message) {
verifyDebug();
Expand All @@ -39,7 +36,7 @@ private static void verifyDebug() {
}

private static void print(String format, Object... args) {
logger.info(String.format(format, args));
System.out.println(String.format(format, args));
}

private static String getCalledFrom(int pos) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ public class FieldResolutionResult {
@NonNull private final Set<String> regularFields;
@NonNull private final Wildcard wildcard;

public FieldResolutionResult(Set<String> regularFields) {
public FieldResolutionResult(Collection<String> regularFields) {
this.regularFields = new HashSet<>(regularFields);
this.wildcard = NULL_WILDCARD;
}

public FieldResolutionResult(Set<String> regularFields, Wildcard wildcard) {
public FieldResolutionResult(Collection<String> regularFields, Wildcard wildcard) {
this.regularFields = new HashSet<>(regularFields);
this.wildcard = wildcard;
}

public FieldResolutionResult(Set<String> regularFields, String wildcard) {
public FieldResolutionResult(Collection<String> regularFields, String wildcard) {
this.regularFields = new HashSet<>(regularFields);
this.wildcard = getWildcard(wildcard);
}
Expand All @@ -53,12 +53,12 @@ private static Wildcard getWildcard(String wildcard) {
}
}

public FieldResolutionResult(Set<String> regularFields, Set<String> wildcards) {
public FieldResolutionResult(Collection<String> regularFields, Collection<String> wildcards) {
this.regularFields = new HashSet<>(regularFields);
this.wildcard = createOrWildcard(wildcards);
}

private static Wildcard createOrWildcard(Set<String> patterns) {
private static Wildcard createOrWildcard(Collection<String> patterns) {
if (patterns == null || patterns.isEmpty()) {
return NULL_WILDCARD;
}
Expand All @@ -70,38 +70,50 @@ private static Wildcard createOrWildcard(Set<String> patterns) {
return new OrWildcard(wildcards);
}

/** Returns unmodifiable view of regular fields. */
public Set<String> getRegularFieldsUnmodifiable() {
return Collections.unmodifiableSet(regularFields);
}

/** Checks if result contains any wildcard patterns. */
public boolean hasWildcards() {
return wildcard != NULL_WILDCARD;
}

/** Checks if result contains partial wildcard patterns (not '*'). */
public boolean hasPartialWildcards() {
return wildcard != NULL_WILDCARD && wildcard != ANY_WILDCARD;
}

/** Checks if result contains regular fields. */
public boolean hasRegularFields() {
return !regularFields.isEmpty();
}

/** Creates new result excluding specified fields. */
public FieldResolutionResult exclude(Collection<String> fields) {
Set<String> combinedFields = new HashSet<>(this.regularFields);
combinedFields.removeAll(fields);
return new FieldResolutionResult(combinedFields, this.wildcard);
}

public FieldResolutionResult or(Set<String> fields) {
/** Creates new result combining this result with additional fields (union). */
public FieldResolutionResult or(Collection<String> fields) {
Set<String> combinedFields = new HashSet<>(this.regularFields);
combinedFields.addAll(fields);
return new FieldResolutionResult(combinedFields, this.wildcard);
}

private Set<String> and(Set<String> fields) {
private Set<String> and(Collection<String> fields) {
return fields.stream()
.filter(field -> this.getRegularFields().contains(field) || this.wildcard.matches(field))
.collect(Collectors.toSet());
}

/** Creates new result intersecting this result with another (intersection). */
public FieldResolutionResult and(FieldResolutionResult other) {
Set<String> combinedFields = this.and(other.regularFields);
Set<String> combinedFields = new HashSet<>();
combinedFields.addAll(this.and(other.regularFields));
combinedFields.addAll(other.and(this.regularFields));

Wildcard combinedWildcard = this.wildcard.and(other.wildcard);
Expand All @@ -111,6 +123,7 @@ public FieldResolutionResult and(FieldResolutionResult other) {

/** Interface for wildcard pattern matching. */
public interface Wildcard {
/** Checks if field name matches wildcard pattern. */
boolean matches(String fieldName);

default Wildcard and(Wildcard other) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
*/
public class FieldResolutionVisitor extends AbstractNodeVisitor<Node, FieldResolutionContext> {

private static final String ALL_FIELDS = "*";

/**
* Analyzes PPL query plan to determine required fields at each node.
*
Expand Down Expand Up @@ -110,10 +112,10 @@ private void acceptAndVerifyNodeVisited(Node node, FieldResolutionContext contex

@Override
public Node visitProject(Project node, FieldResolutionContext context) {
boolean isSelectAll =
node.getProjectList().stream().anyMatch(expr -> expr instanceof AllFields);
boolean isSingleSelectAll =
node.getProjectList().size() == 1 && node.getProjectList().get(0) instanceof AllFields;

if (isSelectAll) {
if (isSingleSelectAll) {
visitChildren(node, context);
} else {
Set<String> projectFields = new HashSet<>();
Expand Down Expand Up @@ -179,15 +181,14 @@ public Node visitSpath(SPath node, FieldResolutionContext context) {
return visitEval(node.rewriteAsEval(), context);
} else {
// set requirements for spath command;
context.setResult(node, context.getCurrentRequirements());
FieldResolutionResult requirements = context.getCurrentRequirements();
if (requirements.hasWildcards()) {
context.setResult(node, requirements);
if (requirements.hasPartialWildcards()) {
throw new IllegalArgumentException(
"Spath command cannot extract arbitrary fields. Please project fields explicitly by"
+ " fields command without wildcard or stats command.");
"Spath command cannot be used with partial wildcard such as `prefix*`.");
}

context.pushRequirements(context.getCurrentRequirements().or(Set.of(node.getInField())));
context.pushRequirements(requirements.or(Set.of(node.getInField())));
visitChildren(node, context);
context.popRequirements();
return node;
Expand Down Expand Up @@ -237,6 +238,8 @@ private Set<String> extractFieldsFromExpression(UnresolvedExpression expr) {

if (expr instanceof Field field) {
fields.add(field.getField().toString());
} else if (expr instanceof AllFields) {
fields.add(ALL_FIELDS);
} else if (expr instanceof QualifiedName name) {
fields.add(name.toString());
} else if (expr instanceof Alias alias) {
Expand Down Expand Up @@ -490,43 +493,56 @@ public Node visitStreamWindow(StreamWindow node, FieldResolutionContext context)

@Override
public Node visitFillNull(FillNull node, FieldResolutionContext context) {
if (node.isAgainstAllFields()) {
throw new IllegalArgumentException("Fields need to be specified with fillnull command");
}
Set<String> fields = new HashSet<>();
node.getFields().forEach(field -> fields.addAll(extractFieldsFromExpression(field)));

context.pushRequirements(context.getCurrentRequirements().or(fields));
visitChildren(node, context);
context.popRequirements();
return node;
}

@Override
public Node visitAppendCol(AppendCol node, FieldResolutionContext context) {
visitChildren(node, context);
return node;
throw new IllegalArgumentException(
"AppendCol command cannot be used together with spath command");
Comment on lines +510 to +511
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This visitor is not generic and only for spath?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is specific for spath command since we currently use it only for spath command (I made it specific for user to understand easier). Once we generalize it for schema-on-read, we need to change the message to be generic.

}

@Override
public Node visitAppend(Append node, FieldResolutionContext context) {
// dispatch requirements to subsearch and main
acceptAndVerifyNodeVisited(node.getSubSearch(), context);
visitChildren(node, context);
return node;
}

@Override
public Node visitMultisearch(Multisearch node, FieldResolutionContext context) {
visitChildren(node, context);
return node;
throw new IllegalArgumentException(
"Multisearch command cannot be used together with spath command");
}

@Override
public Node visitLookup(Lookup node, FieldResolutionContext context) {
visitChildren(node, context);
return node;
throw new IllegalArgumentException("Lookup command cannot be used together with spath command");
}

@Override
public Node visitValues(Values node, FieldResolutionContext context) {
visitChildren(node, context);
return node;
throw new IllegalArgumentException("Values command cannot be used together with spath command");
}

@Override
public Node visitReplace(Replace node, FieldResolutionContext context) {
Set<String> fields = new HashSet<>();
node.getFieldList().forEach(field -> fields.addAll(extractFieldsFromExpression(field)));

context.pushRequirements(context.getCurrentRequirements().or(fields));
visitChildren(node, context);
context.popRequirements();
return node;
}

Expand Down Expand Up @@ -619,6 +635,10 @@ private Set<String> extractFieldsFromAggregation(UnresolvedExpression expr) {
}
}
}
return fields;
return excludeAllFieldsWildcard(fields);
}

private Set<String> excludeAllFieldsWildcard(Set<String> fields) {
return fields.stream().filter(f -> !f.equals(ALL_FIELDS)).collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public List<Field> getFields() {
return getReplacementPairs().stream().map(Pair::getLeft).toList();
}

public boolean isAgainstAllFields() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear when we should call this? Any PPL command doesn't allow *?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When fillnull command is used without field list, it applies to all the fields. This method is used for dynamic fields to raise error since we cannot support it.

return !replacementForAll.isEmpty() && getReplacementPairs().isEmpty();
}

@Override
public FillNull attach(UnresolvedPlan child) {
this.child = child;
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/Join.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.ast.expression.Argument;
import org.opensearch.sql.ast.expression.Field;
import org.opensearch.sql.ast.expression.Literal;
import org.opensearch.sql.ast.expression.UnresolvedExpression;

@ToString
Expand Down Expand Up @@ -87,6 +88,14 @@ public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
return nodeVisitor.visitJoin(this, context);
}

/**
* @return `overwrite` option value in argumentMap
*/
public boolean isOverwrite() {
return getArgumentMap().get("overwrite") == null // 'overwrite' default value is true
|| getArgumentMap().get("overwrite").equals(Literal.TRUE);
}

public enum JoinType {
INNER,
LEFT,
Expand Down
Loading
Loading