Skip to content

Commit 61e844b

Browse files
authored
ArrayIdentifierExpression and JsonArrayIdentifierExpression to pass type info to flat collections (#249)
* ArrayIdentifierExpression and JsonArrayIdentifierExpression to pass type info to flat collections * Added test cases * Fixed failing test cases * Fixed failing test cases
1 parent 23c12e2 commit 61e844b

File tree

5 files changed

+411
-3
lines changed

5 files changed

+411
-3
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.hypertrace.core.documentstore.expression.impl;
2+
3+
import lombok.EqualsAndHashCode;
4+
5+
/**
6+
* Represents an identifier expression for array-typed fields. This allows parsers to apply
7+
* array-specific logic (e.g., cardinality checks for EXISTS operators to exclude empty arrays).
8+
*
9+
* <p>Similar to {@link JsonIdentifierExpression}, this provides type information to parsers so they
10+
* can generate appropriate database-specific queries for array operations.
11+
*/
12+
@EqualsAndHashCode(callSuper = true)
13+
public class ArrayIdentifierExpression extends IdentifierExpression {
14+
15+
public ArrayIdentifierExpression(String name) {
16+
super(name);
17+
}
18+
19+
public static ArrayIdentifierExpression of(String name) {
20+
return new ArrayIdentifierExpression(name);
21+
}
22+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.hypertrace.core.documentstore.expression.impl;
2+
3+
import java.util.List;
4+
import lombok.EqualsAndHashCode;
5+
import org.hypertrace.core.documentstore.postgres.utils.BasicPostgresSecurityValidator;
6+
7+
/**
8+
* Represents an identifier expression for array-typed fields inside JSONB columns. This allows
9+
* parsers to apply array-specific logic (e.g., jsonb_array_length checks for EXISTS operators to
10+
* exclude empty arrays).
11+
*
12+
* <p>Example: For a JSONB column "attributes" with a nested array field "tags":
13+
*
14+
* <pre>{"attributes": {"tags": ["value1", "value2"]}}</pre>
15+
*
16+
* Use: {@code JsonArrayIdentifierExpression.of("attributes", "tags")}
17+
*/
18+
@EqualsAndHashCode(callSuper = true)
19+
public class JsonArrayIdentifierExpression extends JsonIdentifierExpression {
20+
21+
public static JsonArrayIdentifierExpression of(
22+
final String columnName, final String... pathElements) {
23+
if (pathElements == null || pathElements.length == 0) {
24+
throw new IllegalArgumentException("JSON path cannot be null or empty for array field");
25+
}
26+
return of(columnName, List.of(pathElements));
27+
}
28+
29+
public static JsonArrayIdentifierExpression of(
30+
final String columnName, final List<String> jsonPath) {
31+
// Validate inputs
32+
BasicPostgresSecurityValidator.getDefault().validateIdentifier(columnName);
33+
34+
if (jsonPath == null || jsonPath.isEmpty()) {
35+
throw new IllegalArgumentException("JSON path cannot be null or empty for array field");
36+
}
37+
38+
BasicPostgresSecurityValidator.getDefault().validateJsonPath(jsonPath);
39+
40+
List<String> unmodifiablePath = List.copyOf(jsonPath);
41+
42+
// Construct full name for compatibility: "customAttr.myAttribute"
43+
String fullName = columnName + "." + String.join(".", unmodifiablePath);
44+
return new JsonArrayIdentifierExpression(fullName, columnName, unmodifiablePath);
45+
}
46+
47+
private JsonArrayIdentifierExpression(String name, String columnName, List<String> jsonPath) {
48+
super(name, columnName, jsonPath);
49+
}
50+
}

document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JsonIdentifierExpression.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.util.List;
44
import lombok.EqualsAndHashCode;
5-
import lombok.Value;
5+
import lombok.Getter;
66
import org.hypertrace.core.documentstore.parser.FieldTransformationVisitor;
77
import org.hypertrace.core.documentstore.postgres.utils.BasicPostgresSecurityValidator;
88

@@ -13,13 +13,18 @@
1313
*
1414
* <p>This generates SQL like: customAttr -> 'myAttribute' -> 'nestedField' (returns JSON)
1515
*/
16-
@Value
16+
@Getter
1717
@EqualsAndHashCode(callSuper = true)
1818
public class JsonIdentifierExpression extends IdentifierExpression {
1919

2020
String columnName; // e.g., "customAttr" (the top-level JSONB column)
2121
List<String> jsonPath; // e.g., ["myAttribute", "nestedField"]
2222

23+
public static JsonIdentifierExpression of(final String columnName) {
24+
throw new IllegalArgumentException(
25+
"JSON path cannot be null or empty. Use of(columnName, path...) instead.");
26+
}
27+
2328
public static JsonIdentifierExpression of(final String columnName, final String... pathElements) {
2429
if (pathElements == null || pathElements.length == 0) {
2530
// In this case, use IdentifierExpression
@@ -44,7 +49,7 @@ public static JsonIdentifierExpression of(final String columnName, final List<St
4449
return new JsonIdentifierExpression(fullName, columnName, unmodifiablePath);
4550
}
4651

47-
private JsonIdentifierExpression(String name, String columnName, List<String> jsonPath) {
52+
protected JsonIdentifierExpression(String name, String columnName, List<String> jsonPath) {
4853
super(name);
4954
this.columnName = columnName;
5055
this.jsonPath = jsonPath;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.hypertrace.core.documentstore.expression.impl;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
class ArrayIdentifierExpressionTest {
9+
10+
@Test
11+
void testOfCreatesInstance() {
12+
ArrayIdentifierExpression expression = ArrayIdentifierExpression.of("tags");
13+
14+
assertEquals("tags", expression.getName());
15+
}
16+
17+
@Test
18+
void testEqualsAndHashCode() {
19+
ArrayIdentifierExpression expr1 = ArrayIdentifierExpression.of("tags");
20+
ArrayIdentifierExpression expr2 = ArrayIdentifierExpression.of("tags");
21+
22+
// Test equality - should be equal
23+
assertEquals(expr1, expr2, "Expressions with same name should be equal");
24+
25+
// Test hashCode
26+
assertEquals(
27+
expr1.hashCode(), expr2.hashCode(), "Expressions with same name should have same hashCode");
28+
}
29+
30+
@Test
31+
void testNotEqualsWithDifferentName() {
32+
ArrayIdentifierExpression expr1 = ArrayIdentifierExpression.of("tags");
33+
ArrayIdentifierExpression expr2 = ArrayIdentifierExpression.of("categories");
34+
35+
// Test inequality
36+
assertNotEquals(expr1, expr2, "Expressions with different names should not be equal");
37+
}
38+
39+
@Test
40+
void testNotEqualsWithIdentifierExpression() {
41+
ArrayIdentifierExpression arrayExpr = ArrayIdentifierExpression.of("tags");
42+
IdentifierExpression identExpr = IdentifierExpression.of("tags");
43+
44+
// Even though they have the same name, they are different types
45+
assertNotEquals(
46+
arrayExpr, identExpr, "ArrayIdentifierExpression should not equal IdentifierExpression");
47+
}
48+
49+
@Test
50+
void testInheritsFromIdentifierExpression() {
51+
ArrayIdentifierExpression expression = ArrayIdentifierExpression.of("tags");
52+
53+
// Verify it's an instance of parent class
54+
assertEquals(
55+
IdentifierExpression.class,
56+
expression.getClass().getSuperclass(),
57+
"ArrayIdentifierExpression should extend IdentifierExpression");
58+
}
59+
60+
@Test
61+
void testMultipleInstancesWithSameNameAreEqual() {
62+
ArrayIdentifierExpression expr1 = ArrayIdentifierExpression.of("categoryTags");
63+
ArrayIdentifierExpression expr2 = ArrayIdentifierExpression.of("categoryTags");
64+
ArrayIdentifierExpression expr3 = ArrayIdentifierExpression.of("categoryTags");
65+
66+
assertEquals(expr1, expr2);
67+
assertEquals(expr2, expr3);
68+
assertEquals(expr1, expr3);
69+
assertEquals(expr1.hashCode(), expr2.hashCode());
70+
assertEquals(expr2.hashCode(), expr3.hashCode());
71+
}
72+
}

0 commit comments

Comments
 (0)