Skip to content

Commit f3243c8

Browse files
authored
[Backport 2.19-dev] Add wildcard support for rename command (#4019) (#4250)
* Add wildcard support for rename command (#4019) * add wildcard support for rename Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix calcite wildcard support and add tests Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add check to analyzer Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * update doc formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * remove v2 engine wildcard support Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * update doc Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * support cascading rename Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * update formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add cross cluster test Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add test for cascading rename Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add test for cascading rename Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * change behavior for renaming existing fields Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add tests and update docs Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * update docs Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * update docs Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix renaming to same name Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix behavior for consecutive wildcards/address comments Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add back import Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix doc Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix doc Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> --------- Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> Signed-off-by: ritvibhatt <53196324+ritvibhatt@users.noreply.github.com> (cherry picked from commit ab5f21a) * empty Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * add fields to failing tests Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * revert existing rename tests Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix formatting Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> * fix failure Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> --------- Signed-off-by: Ritvi Bhatt <ribhatt@amazon.com> Signed-off-by: ritvibhatt <53196324+ritvibhatt@users.noreply.github.com>
1 parent e73bc00 commit f3243c8

11 files changed

Lines changed: 730 additions & 41 deletions

File tree

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

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
import org.opensearch.sql.expression.function.PPLFuncImpTable;
139139
import org.opensearch.sql.expression.parse.RegexCommonUtils;
140140
import org.opensearch.sql.utils.ParseUtils;
141+
import org.opensearch.sql.utils.WildcardRenameUtils;
141142

142143
public class CalciteRelNodeVisitor extends AbstractNodeVisitor<RelNode, CalcitePlanContext> {
143144

@@ -487,27 +488,52 @@ public RelNode visitRename(Rename node, CalcitePlanContext context) {
487488
visitChildren(node, context);
488489
List<String> originalNames = context.relBuilder.peek().getRowType().getFieldNames();
489490
List<String> newNames = new ArrayList<>(originalNames);
491+
490492
for (org.opensearch.sql.ast.expression.Map renameMap : node.getRenameList()) {
491-
if (renameMap.getTarget() instanceof Field) {
492-
Field t = (Field) renameMap.getTarget();
493-
String newName = t.getField().toString();
494-
RexNode check = rexVisitor.analyze(renameMap.getOrigin(), context);
495-
if (check instanceof RexInputRef) {
496-
RexInputRef ref = (RexInputRef) check;
497-
newNames.set(ref.getIndex(), newName);
498-
} else {
499-
throw new SemanticCheckException(
500-
String.format("the original field %s cannot be resolved", renameMap.getOrigin()));
501-
}
502-
} else {
493+
if (!(renameMap.getTarget() instanceof Field)) {
503494
throw new SemanticCheckException(
504495
String.format("the target expected to be field, but is %s", renameMap.getTarget()));
505496
}
497+
498+
String sourcePattern = ((Field) renameMap.getOrigin()).getField().toString();
499+
String targetPattern = ((Field) renameMap.getTarget()).getField().toString();
500+
501+
if (WildcardRenameUtils.isWildcardPattern(sourcePattern)
502+
&& !WildcardRenameUtils.validatePatternCompatibility(sourcePattern, targetPattern)) {
503+
throw new SemanticCheckException(
504+
"Source and target patterns have different wildcard counts");
505+
}
506+
507+
List<String> matchingFields = WildcardRenameUtils.matchFieldNames(sourcePattern, newNames);
508+
509+
for (String fieldName : matchingFields) {
510+
String newName =
511+
WildcardRenameUtils.applyWildcardTransformation(
512+
sourcePattern, targetPattern, fieldName);
513+
if (newNames.contains(newName) && !newName.equals(fieldName)) {
514+
removeFieldIfExists(newName, newNames, context);
515+
}
516+
int fieldIndex = newNames.indexOf(fieldName);
517+
if (fieldIndex != -1) {
518+
newNames.set(fieldIndex, newName);
519+
}
520+
}
521+
522+
if (matchingFields.isEmpty() && newNames.contains(targetPattern)) {
523+
removeFieldIfExists(targetPattern, newNames, context);
524+
context.relBuilder.rename(newNames);
525+
}
506526
}
507527
context.relBuilder.rename(newNames);
508528
return context.relBuilder.peek();
509529
}
510530

531+
private void removeFieldIfExists(
532+
String fieldName, List<String> newNames, CalcitePlanContext context) {
533+
newNames.remove(fieldName);
534+
context.relBuilder.projectExcept(context.relBuilder.field(fieldName));
535+
}
536+
511537
@Override
512538
public RelNode visitSort(Sort node, CalcitePlanContext context) {
513539
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)