Skip to content

Commit ab5f21a

Browse files
authored
Add wildcard support for rename command (#4019)
* add wildcard support for rename Signed-off-by: Ritvi Bhatt <[email protected]> * fix calcite wildcard support and add tests Signed-off-by: Ritvi Bhatt <[email protected]> * fix formatting Signed-off-by: Ritvi Bhatt <[email protected]> * add check to analyzer Signed-off-by: Ritvi Bhatt <[email protected]> * update doc formatting Signed-off-by: Ritvi Bhatt <[email protected]> * remove v2 engine wildcard support Signed-off-by: Ritvi Bhatt <[email protected]> * update doc Signed-off-by: Ritvi Bhatt <[email protected]> * fix formatting Signed-off-by: Ritvi Bhatt <[email protected]> * support cascading rename Signed-off-by: Ritvi Bhatt <[email protected]> * update formatting Signed-off-by: Ritvi Bhatt <[email protected]> * add cross cluster test Signed-off-by: Ritvi Bhatt <[email protected]> * add test for cascading rename Signed-off-by: Ritvi Bhatt <[email protected]> * fix formatting Signed-off-by: Ritvi Bhatt <[email protected]> * add test for cascading rename Signed-off-by: Ritvi Bhatt <[email protected]> * change behavior for renaming existing fields Signed-off-by: Ritvi Bhatt <[email protected]> * add tests and update docs Signed-off-by: Ritvi Bhatt <[email protected]> * update docs Signed-off-by: Ritvi Bhatt <[email protected]> * update docs Signed-off-by: Ritvi Bhatt <[email protected]> * fix renaming to same name Signed-off-by: Ritvi Bhatt <[email protected]> * fix behavior for consecutive wildcards/address comments Signed-off-by: Ritvi Bhatt <[email protected]> * add back import Signed-off-by: Ritvi Bhatt <[email protected]> * fix doc Signed-off-by: Ritvi Bhatt <[email protected]> * fix doc Signed-off-by: Ritvi Bhatt <[email protected]> * fix formatting Signed-off-by: Ritvi Bhatt <[email protected]> --------- Signed-off-by: Ritvi Bhatt <[email protected]> Signed-off-by: ritvibhatt <[email protected]>
1 parent c186686 commit ab5f21a

File tree

11 files changed

+732
-41
lines changed

11 files changed

+732
-41
lines changed

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
import org.opensearch.sql.expression.function.PPLFuncImpTable;
134134
import org.opensearch.sql.expression.parse.RegexCommonUtils;
135135
import org.opensearch.sql.utils.ParseUtils;
136+
import org.opensearch.sql.utils.WildcardRenameUtils;
136137

137138
public class CalciteRelNodeVisitor extends AbstractNodeVisitor<RelNode, CalcitePlanContext> {
138139

@@ -465,25 +466,52 @@ public RelNode visitRename(Rename node, CalcitePlanContext context) {
465466
visitChildren(node, context);
466467
List<String> originalNames = context.relBuilder.peek().getRowType().getFieldNames();
467468
List<String> newNames = new ArrayList<>(originalNames);
469+
468470
for (org.opensearch.sql.ast.expression.Map renameMap : node.getRenameList()) {
469-
if (renameMap.getTarget() instanceof Field t) {
470-
String newName = t.getField().toString();
471-
RexNode check = rexVisitor.analyze(renameMap.getOrigin(), context);
472-
if (check instanceof RexInputRef ref) {
473-
newNames.set(ref.getIndex(), newName);
474-
} else {
475-
throw new SemanticCheckException(
476-
String.format("the original field %s cannot be resolved", renameMap.getOrigin()));
477-
}
478-
} else {
471+
if (!(renameMap.getTarget() instanceof Field)) {
479472
throw new SemanticCheckException(
480473
String.format("the target expected to be field, but is %s", renameMap.getTarget()));
481474
}
475+
476+
String sourcePattern = ((Field) renameMap.getOrigin()).getField().toString();
477+
String targetPattern = ((Field) renameMap.getTarget()).getField().toString();
478+
479+
if (WildcardRenameUtils.isWildcardPattern(sourcePattern)
480+
&& !WildcardRenameUtils.validatePatternCompatibility(sourcePattern, targetPattern)) {
481+
throw new SemanticCheckException(
482+
"Source and target patterns have different wildcard counts");
483+
}
484+
485+
List<String> matchingFields = WildcardRenameUtils.matchFieldNames(sourcePattern, newNames);
486+
487+
for (String fieldName : matchingFields) {
488+
String newName =
489+
WildcardRenameUtils.applyWildcardTransformation(
490+
sourcePattern, targetPattern, fieldName);
491+
if (newNames.contains(newName) && !newName.equals(fieldName)) {
492+
removeFieldIfExists(newName, newNames, context);
493+
}
494+
int fieldIndex = newNames.indexOf(fieldName);
495+
if (fieldIndex != -1) {
496+
newNames.set(fieldIndex, newName);
497+
}
498+
}
499+
500+
if (matchingFields.isEmpty() && newNames.contains(targetPattern)) {
501+
removeFieldIfExists(targetPattern, newNames, context);
502+
context.relBuilder.rename(newNames);
503+
}
482504
}
483505
context.relBuilder.rename(newNames);
484506
return context.relBuilder.peek();
485507
}
486508

509+
private void removeFieldIfExists(
510+
String fieldName, List<String> newNames, CalcitePlanContext context) {
511+
newNames.remove(fieldName);
512+
context.relBuilder.projectExcept(context.relBuilder.field(fieldName));
513+
}
514+
487515
@Override
488516
public RelNode visitSort(Sort node, CalcitePlanContext context) {
489517
visitChildren(node, context);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.utils;
7+
8+
import java.util.ArrayList;
9+
import java.util.Arrays;
10+
import java.util.Collection;
11+
import java.util.List;
12+
import java.util.regex.Matcher;
13+
import java.util.regex.Pattern;
14+
import java.util.stream.Collectors;
15+
16+
/** Utility class for handling wildcard patterns in rename operations. */
17+
public class WildcardRenameUtils {
18+
19+
/**
20+
* Check if pattern contains any supported wildcards.
21+
*
22+
* @param pattern the pattern to check
23+
* @return true if pattern contains * wildcards
24+
*/
25+
public static boolean isWildcardPattern(String pattern) {
26+
return pattern.contains("*");
27+
}
28+
29+
/**
30+
* Check if pattern is only wildcards that matches all fields.
31+
*
32+
* @param pattern the pattern to check
33+
* @return true if pattern is only made up of wildcards "*"
34+
*/
35+
public static boolean isFullWildcardPattern(String pattern) {
36+
return pattern.matches("\\*+");
37+
}
38+
39+
/**
40+
* Convert wildcard pattern to regex.
41+
*
42+
* @param pattern the wildcard pattern
43+
* @return regex pattern with capture groups
44+
*/
45+
public static String wildcardToRegex(String pattern) {
46+
String[] parts = pattern.split("\\*", -1);
47+
return Arrays.stream(parts).map(Pattern::quote).collect(Collectors.joining("(.*)"));
48+
}
49+
50+
/**
51+
* Match field names against wildcard pattern.
52+
*
53+
* @param wildcardPattern the pattern to match against
54+
* @param availableFields collection of available field names
55+
* @return list of matching field names
56+
*/
57+
public static List<String> matchFieldNames(
58+
String wildcardPattern, Collection<String> availableFields) {
59+
// Single wildcard matches all available fields
60+
if (isFullWildcardPattern(wildcardPattern)) {
61+
return new ArrayList<>(availableFields);
62+
}
63+
64+
String regexPattern = "^" + wildcardToRegex(wildcardPattern) + "$";
65+
Pattern pattern = Pattern.compile(regexPattern);
66+
67+
return availableFields.stream()
68+
.filter(field -> pattern.matcher(field).matches())
69+
.collect(Collectors.toList());
70+
}
71+
72+
/**
73+
* Apply wildcard transformation to get new field name.
74+
*
75+
* @param sourcePattern the source wildcard pattern
76+
* @param targetPattern the target wildcard pattern
77+
* @param actualFieldName the actual field name to transform
78+
* @return transformed field name
79+
* @throws IllegalArgumentException if patterns don't match or are invalid
80+
*/
81+
public static String applyWildcardTransformation(
82+
String sourcePattern, String targetPattern, String actualFieldName) {
83+
84+
if (sourcePattern.equals(targetPattern)) {
85+
return actualFieldName;
86+
}
87+
88+
if (!isFullWildcardPattern(sourcePattern) || !isFullWildcardPattern(targetPattern)) {
89+
if (sourcePattern.matches(".*\\*{2,}.*") || targetPattern.matches(".*\\*{2,}.*")) {
90+
throw new IllegalArgumentException("Consecutive wildcards in pattern are not supported");
91+
}
92+
}
93+
94+
String sourceRegex = "^" + wildcardToRegex(sourcePattern) + "$";
95+
Matcher matcher = Pattern.compile(sourceRegex).matcher(actualFieldName);
96+
97+
if (!matcher.matches()) {
98+
throw new IllegalArgumentException(
99+
String.format("Field '%s' does not match pattern '%s'", actualFieldName, sourcePattern));
100+
}
101+
102+
String result = targetPattern;
103+
104+
for (int i = 1; i <= matcher.groupCount(); i++) {
105+
String capturedValue = matcher.group(i);
106+
107+
int index = result.indexOf("*");
108+
if (index >= 0) {
109+
result = result.substring(0, index) + capturedValue + result.substring(index + 1);
110+
} else {
111+
throw new IllegalArgumentException(
112+
"Target pattern has fewer wildcards than source pattern");
113+
}
114+
}
115+
116+
return result;
117+
}
118+
119+
/**
120+
* Validate that source and target patterns have matching wildcard counts.
121+
*
122+
* @param sourcePattern the source pattern
123+
* @param targetPattern the target pattern
124+
* @return true if patterns are compatible
125+
*/
126+
public static boolean validatePatternCompatibility(String sourcePattern, String targetPattern) {
127+
int sourceWildcards = countWildcards(sourcePattern);
128+
int targetWildcards = countWildcards(targetPattern);
129+
return sourceWildcards == targetWildcards;
130+
}
131+
132+
/**
133+
* Count the number of wildcards in a pattern.
134+
*
135+
* @param pattern the pattern to analyze
136+
* @return number of wildcard characters
137+
*/
138+
private static int countWildcards(String pattern) {
139+
return (int) pattern.chars().filter(ch -> ch == '*').count();
140+
}
141+
}

0 commit comments

Comments
 (0)