Skip to content

Commit

Permalink
Add PathUtils and unit tests.
Browse files Browse the repository at this point in the history
Signed-off-by: currantw <[email protected]>
  • Loading branch information
currantw committed Feb 4, 2025
1 parent 840da78 commit 17f6c5b
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

package org.opensearch.sql.data.model;

import java.util.Iterator;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
Expand Down Expand Up @@ -56,7 +56,7 @@ public BindingTuple bindingTuples() {

@Override
public Map<String, ExprValue> tupleValue() {
return valueMap;
return Collections.unmodifiableMap(valueMap);
}

@Override
Expand All @@ -69,23 +69,19 @@ public ExprValue keyValue(String key) {
*
* @return true for equal, otherwise false.
*/
public boolean equal(ExprValue o) {
if (!(o instanceof ExprTupleValue)) {
public boolean equal(ExprValue other) {

if (!(other instanceof ExprTupleValue)) {
return false;
} else {
ExprTupleValue other = (ExprTupleValue) o;
Iterator<Entry<String, ExprValue>> thisIterator = this.valueMap.entrySet().iterator();
Iterator<Entry<String, ExprValue>> otherIterator = other.valueMap.entrySet().iterator();
while (thisIterator.hasNext() && otherIterator.hasNext()) {
Entry<String, ExprValue> thisEntry = thisIterator.next();
Entry<String, ExprValue> otherEntry = otherIterator.next();
if (!(thisEntry.getKey().equals(otherEntry.getKey())
&& thisEntry.getValue().equals(otherEntry.getValue()))) {
return false;
}
}
return !(thisIterator.hasNext() || otherIterator.hasNext());
}

/**
* {@link Map#equals} returns true if the two maps' entry sets are equal, and works properly
* across all implementation of the {@link Map} interface. See {@link
* https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#equals-java.lang.Object-} for
* more details.
*/
return valueMap.equals(other.tupleValue());
}

/** Only compare the size of the map. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@

package org.opensearch.sql.planner.physical;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.expression.ReferenceExpression;
import org.opensearch.sql.utils.PathUtils;

/** Flattens the specified field from the input and returns the result. */
@Getter
Expand All @@ -26,8 +27,6 @@ public class ExpandOperator extends PhysicalPlan {
private final PhysicalPlan input;
private final ReferenceExpression field;

private static final Pattern PATH_SEPARATOR_PATTERN = Pattern.compile(".", Pattern.LITERAL);

private List<ExprValue> expandedRows = List.of();

@Override
Expand All @@ -43,7 +42,7 @@ public List<PhysicalPlan> getChild() {
@Override
public boolean hasNext() {
while (expandedRows.isEmpty() && input.hasNext()) {
expandedRows = expandExprValueAtPath(input.next(), field.getAttr());
expandedRows = expandValue(input.next(), field.getAttr());
}

return expandedRows.isEmpty();
Expand All @@ -58,9 +57,10 @@ public ExprValue next() {
* Expands the {@link org.opensearch.sql.data.model.ExprCollectionValue} at the specified path and
* returns the resulting value. If the value is null or missing, the unmodified value is returned.
*/
private static List<ExprValue> expandExprValueAtPath(ExprValue exprValue, String path) {

// TODO #3016: Implement expand command
return new ArrayList<>(Collections.singletonList(exprValue));
private static List<ExprValue> expandValue(ExprValue rootExprValue, String path) {
ExprValue targetExprValue = PathUtils.getExprValueAtPath(rootExprValue, path);
return targetExprValue.collectionValue().stream()
.map(v -> PathUtils.setExprValueAtPath(rootExprValue, path, v))
.collect(Collectors.toCollection(LinkedList::new));
}
}
123 changes: 123 additions & 0 deletions core/src/main/java/org/opensearch/sql/utils/PathUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.utils;

import static org.opensearch.sql.data.type.ExprCoreType.STRUCT;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import lombok.experimental.UtilityClass;
import org.opensearch.sql.data.model.ExprTupleValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.exception.SemanticCheckException;

@UtilityClass
public class PathUtils {

private final Pattern PATH_SEPARATOR_PATTERN = Pattern.compile(".", Pattern.LITERAL);

/** Returns true if a value exists at the specified path within the given root value. */
public boolean containsExprValueAtPath(ExprValue root, String path) {
List<String> pathComponents = splitPath(path);
return containsExprValueForPathComponents(root, pathComponents);
}

/**
* Returns the {@link ExprValue} at the specified path within the given root value. Returns {@code
* null} if the root value does not contain the path - see {@link
* PathUtils#containsExprValueAtPath}.
*/
public ExprValue getExprValueAtPath(ExprValue root, String path) {
List<String> pathComponents = splitPath(path);

if (!containsExprValueForPathComponents(root, pathComponents)) {
return null;
}

return getExprValueForPathComponents(root, pathComponents);
}

/**
* Sets the {@link ExprValue} at the specified path within the given root value and returns the
* result. Throws {@link SemanticCheckException} if the root value does not contain the path - see
* {@link PathUtils#containsExprValueAtPath}.
*/
public ExprValue setExprValueAtPath(ExprValue root, String path, ExprValue newValue) {
List<String> pathComponents = splitPath(path);

if (!containsExprValueForPathComponents(root, pathComponents)) {
throw new SemanticCheckException(String.format("Field path '%s' does not exist.", path));
}

return setExprValueForPathComponents(root, pathComponents, newValue);
}

/** Helper method for {@link PathUtils#containsExprValueAtPath}. */
private boolean containsExprValueForPathComponents(ExprValue root, List<String> pathComponents) {

if (pathComponents.isEmpty()) {
return true;
}

if (!root.type().equals(STRUCT)) {
return false;
}

String currentPathComponent = pathComponents.getFirst();
List<String> remainingPathComponents = pathComponents.subList(1, pathComponents.size());

Map<String, ExprValue> exprValueMap = root.tupleValue();
if (!exprValueMap.containsKey(currentPathComponent)) {
return false;
}

return containsExprValueForPathComponents(
exprValueMap.get(currentPathComponent), remainingPathComponents);
}

/** Helper method for {@link PathUtils#getExprValueAtPath}. */
private ExprValue getExprValueForPathComponents(ExprValue root, List<String> pathComponents) {

if (pathComponents.isEmpty()) {
return root;
}

String currentPathComponent = pathComponents.getFirst();
List<String> remainingPathComponents = pathComponents.subList(1, pathComponents.size());

Map<String, ExprValue> exprValueMap = root.tupleValue();
return getExprValueForPathComponents(
exprValueMap.get(currentPathComponent), remainingPathComponents);
}

/** Helper method for {@link PathUtils#setExprValueAtPath}. */
private ExprValue setExprValueForPathComponents(
ExprValue root, List<String> pathComponents, ExprValue newValue) {

if (pathComponents.isEmpty()) {
return newValue;
}

String currentPathComponent = pathComponents.getFirst();
List<String> remainingPathComponents = pathComponents.subList(1, pathComponents.size());

Map<String, ExprValue> exprValueMap = new HashMap<>(root.tupleValue());
exprValueMap.put(
currentPathComponent,
setExprValueForPathComponents(
exprValueMap.get(currentPathComponent), remainingPathComponents, newValue));

return ExprTupleValue.fromExprValueMap(exprValueMap);
}

/** Splits the given path and returns the corresponding components. */
private List<String> splitPath(String path) {
return Arrays.asList(PATH_SEPARATOR_PATTERN.split(path));
}
}
134 changes: 134 additions & 0 deletions core/src/test/java/org/opensearch/sql/utils/PathUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;
import lombok.ToString;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.model.ExprValueUtils;
import org.opensearch.sql.exception.SemanticCheckException;

@ToString
class PathUtilsTest {

// Test values
private final ExprValue value = ExprValueUtils.integerValue(0);
private final ExprValue newValue = ExprValueUtils.stringValue("value");
private final ExprValue nullValue = ExprValueUtils.nullValue();
private final ExprValue missingValue = ExprValueUtils.missingValue();

private final ExprValue struct1Value = ExprValueUtils.tupleValue(Map.of("field", value));
private final ExprValue struct2Value =
ExprValueUtils.tupleValue(
Map.of("struct1", ExprValueUtils.tupleValue(Map.of("field", value))));

private final ExprValue input =
ExprValueUtils.tupleValue(
Map.ofEntries(
Map.entry("field", value),
Map.entry("struct1", struct1Value),
Map.entry("struct2", struct2Value),
Map.entry("struct_null", nullValue),
Map.entry("struct_missing", missingValue)));

@Test
void testContainsExprValueForPathComponents() {
assertTrue(PathUtils.containsExprValueAtPath(input, "field"));
assertTrue(PathUtils.containsExprValueAtPath(input, "struct1.field"));
assertTrue(PathUtils.containsExprValueAtPath(input, "struct2.struct1.field"));

assertFalse(PathUtils.containsExprValueAtPath(input, "field_invalid"));
assertFalse(PathUtils.containsExprValueAtPath(input, "struct_null.field"));
assertFalse(PathUtils.containsExprValueAtPath(input, "struct_missing.field"));
assertFalse(PathUtils.containsExprValueAtPath(input, "field.field"));
}

@Test
void testGetExprValueForPathComponents() {
assertEquals(value, PathUtils.getExprValueAtPath(input, "field"));
assertEquals(value, PathUtils.getExprValueAtPath(input, "struct1.field"));
assertEquals(value, PathUtils.getExprValueAtPath(input, "struct2.struct1.field"));

assertNull(PathUtils.getExprValueAtPath(input, "field_invalid"));
assertNull(PathUtils.getExprValueAtPath(input, "struct_null.field"));
assertNull(PathUtils.getExprValueAtPath(input, "struct_missing.field"));
assertNull(PathUtils.getExprValueAtPath(input, "field.field"));
}

@Test
void testSetExprValueForPathComponents() {
ExprValue expected;
ExprValue actual;

expected =
ExprValueUtils.tupleValue(
Map.ofEntries(
Map.entry("field", newValue),
Map.entry("struct1", struct1Value),
Map.entry("struct2", struct2Value),
Map.entry("struct_null", nullValue),
Map.entry("struct_missing", missingValue)));
actual = PathUtils.setExprValueAtPath(input, "field", newValue);
assertEquals(expected, actual);

expected =
ExprValueUtils.tupleValue(
Map.ofEntries(
Map.entry("field", value),
Map.entry("struct1", ExprValueUtils.tupleValue(Map.of("field", newValue))),
Map.entry("struct2", struct2Value),
Map.entry("struct_null", nullValue),
Map.entry("struct_missing", missingValue)));
actual = PathUtils.setExprValueAtPath(input, "struct1.field", newValue);
assertEquals(expected, actual);

expected =
ExprValueUtils.tupleValue(
Map.ofEntries(
Map.entry("field", value),
Map.entry("struct1", struct1Value),
Map.entry(
"struct2",
ExprValueUtils.tupleValue(
Map.of("struct1", ExprValueUtils.tupleValue(Map.of("field", newValue))))),
Map.entry("struct_null", nullValue),
Map.entry("struct_missing", missingValue)));
assertEquals(expected, PathUtils.setExprValueAtPath(input, "struct2.struct1.field", newValue));

Exception ex;

ex =
assertThrows(
SemanticCheckException.class,
() -> PathUtils.setExprValueAtPath(input, "field_invalid", newValue));
assertEquals("Field path 'field_invalid' does not exist.", ex.getMessage());

ex =
assertThrows(
SemanticCheckException.class,
() -> PathUtils.setExprValueAtPath(input, "struct_null.field_invalid", newValue));
assertEquals("Field path 'struct_null.field_invalid' does not exist.", ex.getMessage());

ex =
assertThrows(
SemanticCheckException.class,
() -> PathUtils.setExprValueAtPath(input, "struct_missing.field_invalid", newValue));
assertEquals("Field path 'struct_missing.field_invalid' does not exist.", ex.getMessage());

ex =
assertThrows(
SemanticCheckException.class,
() -> PathUtils.setExprValueAtPath(input, "field.field_invalid", newValue));
assertEquals("Field path 'field.field_invalid' does not exist.", ex.getMessage());
}
}

0 comments on commit 17f6c5b

Please sign in to comment.