This is an automated email from the ASF dual-hosted git repository. singhpk234 pushed a commit to branch feature/serialize-bound-expression in repository https://gitbox.apache.org/repos/asf/iceberg.git
commit 11269fceac9324610fd17c5933504b27a6b7f016 Author: Prashant Kumar Singh <[email protected]> AuthorDate: Fri Sep 26 02:49:25 2025 +0000 Add ref impl --- .../iceberg/expressions/ResolvedTransform.java | 2 +- .../iceberg/expressions/TestResolvedReference.java | 20 +- .../iceberg/expressions/ExpressionParser.java | 11 +- .../TestEnhancedExpressionParserWithFieldIds.java | 299 +++++------ .../TestExpressionParserWithResolvedReference.java | 572 +++++++++++---------- 5 files changed, 466 insertions(+), 438 deletions(-) diff --git a/api/src/main/java/org/apache/iceberg/expressions/ResolvedTransform.java b/api/src/main/java/org/apache/iceberg/expressions/ResolvedTransform.java index 287fc6d994..07065b2cd7 100644 --- a/api/src/main/java/org/apache/iceberg/expressions/ResolvedTransform.java +++ b/api/src/main/java/org/apache/iceberg/expressions/ResolvedTransform.java @@ -85,4 +85,4 @@ public class ResolvedTransform<S, T> implements UnboundTerm<T>, Term { public int hashCode() { return 31 * ref.hashCode() + transform.hashCode(); } -} \ No newline at end of file +} diff --git a/api/src/test/java/org/apache/iceberg/expressions/TestResolvedReference.java b/api/src/test/java/org/apache/iceberg/expressions/TestResolvedReference.java index 54c5694138..9b03a4e8ea 100644 --- a/api/src/test/java/org/apache/iceberg/expressions/TestResolvedReference.java +++ b/api/src/test/java/org/apache/iceberg/expressions/TestResolvedReference.java @@ -65,7 +65,7 @@ public class TestResolvedReference { @Test public void testResolvedReferenceBindIgnoresCaseSensitivity() { ResolvedReference<Integer> ref = new ResolvedReference<>("A", 34); - + // Should work regardless of case sensitivity since we use fieldId BoundTerm<Integer> bound1 = ref.bind(SCHEMA.asStruct(), true); BoundTerm<Integer> bound2 = ref.bind(SCHEMA.asStruct(), false); @@ -79,7 +79,7 @@ public class TestResolvedReference { @Test public void testResolvedReferenceBindWithInvalidFieldId() { ResolvedReference<Integer> ref = new ResolvedReference<>("invalid", 999); - + assertThatThrownBy(() -> ref.bind(SCHEMA.asStruct(), true)) .isInstanceOf(ValidationException.class) .hasMessageContaining("Cannot find field 'invalid' in struct"); @@ -89,14 +89,14 @@ public class TestResolvedReference { public void testResolvedReferenceRef() { ResolvedReference<Integer> ref = new ResolvedReference<>("a", 34); NamedReference<?> namedRef = ref.ref(); - + assertThat(namedRef.name()).isEqualTo("a"); } @Test public void testResolvedReferenceToString() { ResolvedReference<Integer> ref = new ResolvedReference<>("a", 34); - + assertThat(ref.toString()).isEqualTo("ref(name=\"a\", fieldId=\"34\")"); } @@ -105,10 +105,10 @@ public class TestResolvedReference { // Test that ResolvedReference works in expression predicates Expression expr = Expressions.equal(Expressions.ref("a", 34), 5); assertThat(expr).isInstanceOf(UnboundPredicate.class); - + UnboundPredicate<?> predicate = (UnboundPredicate<?>) expr; assertThat(predicate.term()).isInstanceOf(ResolvedReference.class); - + ResolvedReference<?> resolvedRef = (ResolvedReference<?>) predicate.term(); assertThat(resolvedRef.name()).isEqualTo("a"); assertThat(resolvedRef.fieldId()).isEqualTo(34); @@ -119,14 +119,14 @@ public class TestResolvedReference { // Test that unbinding a bound reference returns a NamedReference for compatibility Expression expr = Expressions.equal(Expressions.ref("a", 34), 5); Expression boundExpr = Binder.bind(SCHEMA.asStruct(), expr, true); - + assertThat(boundExpr).isInstanceOf(BoundPredicate.class); BoundPredicate<?> boundPred = (BoundPredicate<?>) boundExpr; - + UnboundTerm<?> unbound = ExpressionUtil.unbind(boundPred.term()); assertThat(unbound).isInstanceOf(NamedReference.class); - + NamedReference<?> namedRef = (NamedReference<?>) unbound; assertThat(namedRef.name()).isEqualTo("a"); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/apache/iceberg/expressions/ExpressionParser.java b/core/src/main/java/org/apache/iceberg/expressions/ExpressionParser.java index 396520f997..91c5355092 100644 --- a/core/src/main/java/org/apache/iceberg/expressions/ExpressionParser.java +++ b/core/src/main/java/org/apache/iceberg/expressions/ExpressionParser.java @@ -252,7 +252,10 @@ public class ExpressionParser { } else if (term instanceof ResolvedTransform) { ResolvedTransform<?, ?> transform = (ResolvedTransform<?, ?>) term; if (includeFieldIds) { - transformWithFieldId(transform.transform().toString(), transform.resolvedRef().name(), transform.resolvedRef().fieldId()); + transformWithFieldId( + transform.transform().toString(), + transform.resolvedRef().name(), + transform.resolvedRef().fieldId()); } else { transform(transform.transform().toString(), transform.ref().name()); } @@ -260,7 +263,8 @@ public class ExpressionParser { } else if (term instanceof BoundTransform) { BoundTransform<?, ?> transform = (BoundTransform<?, ?>) term; if (includeFieldIds) { - transformWithFieldId(transform.transform().toString(), transform.ref().name(), transform.ref().fieldId()); + transformWithFieldId( + transform.transform().toString(), transform.ref().name(), transform.ref().fieldId()); } else { transform(transform.transform().toString(), transform.ref().name()); } @@ -289,7 +293,8 @@ public class ExpressionParser { gen.writeEndObject(); } - private void transformWithFieldId(String transform, String name, int fieldId) throws IOException { + private void transformWithFieldId(String transform, String name, int fieldId) + throws IOException { gen.writeStartObject(); gen.writeStringField(TYPE, TRANSFORM); gen.writeStringField(TRANSFORM, transform); diff --git a/core/src/test/java/org/apache/iceberg/expressions/TestEnhancedExpressionParserWithFieldIds.java b/core/src/test/java/org/apache/iceberg/expressions/TestEnhancedExpressionParserWithFieldIds.java index 9d5817e82d..e5e2d28a32 100644 --- a/core/src/test/java/org/apache/iceberg/expressions/TestEnhancedExpressionParserWithFieldIds.java +++ b/core/src/test/java/org/apache/iceberg/expressions/TestEnhancedExpressionParserWithFieldIds.java @@ -28,17 +28,16 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.function.Supplier; import org.apache.iceberg.Schema; -import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.types.Types; import org.apache.iceberg.util.JsonUtil; import org.junit.jupiter.api.Test; /** - * Test demonstrating how ExpressionParser could be enhanced to support field IDs - * for ResolvedReference serialization/deserialization. - * - * This is a proof of concept showing the enhanced JSON format that would preserve - * field ID information during serialization round-trips. + * Test demonstrating how ExpressionParser could be enhanced to support field IDs for + * ResolvedReference serialization/deserialization. + * + * <p>This is a proof of concept showing the enhanced JSON format that would preserve field ID + * information during serialization round-trips. */ public class TestEnhancedExpressionParserWithFieldIds { @@ -58,44 +57,46 @@ public class TestEnhancedExpressionParserWithFieldIds { public void testEnhancedJsonFormatWithFieldIds() { // Test the enhanced JSON format that would support field IDs Expression resolvedExpr = Expressions.equal(Expressions.ref("data", 101), "test"); - + // Generate enhanced JSON manually to show the concept String enhancedJson = generateEnhancedJson(resolvedExpr); - + // Expected enhanced JSON structure with field ID - String expectedJson = "{\n" + - " \"type\" : \"eq\",\n" + - " \"term\" : {\n" + - " \"type\" : \"resolved-reference\",\n" + - " \"name\" : \"data\",\n" + - " \"field-id\" : 101\n" + - " },\n" + - " \"value\" : \"test\"\n" + - "}"; - + String expectedJson = + "{\n" + + " \"type\" : \"eq\",\n" + + " \"term\" : {\n" + + " \"type\" : \"resolved-reference\",\n" + + " \"name\" : \"data\",\n" + + " \"field-id\" : 101\n" + + " },\n" + + " \"value\" : \"test\"\n" + + "}"; + assertThat(enhancedJson).isEqualTo(expectedJson); } @Test public void testEnhancedParsingWithFieldIds() { // Test parsing enhanced JSON that includes field IDs - String enhancedJson = "{\n" + - " \"type\" : \"eq\",\n" + - " \"term\" : {\n" + - " \"type\" : \"resolved-reference\",\n" + - " \"name\" : \"data\",\n" + - " \"field-id\" : 101\n" + - " },\n" + - " \"value\" : \"test\"\n" + - "}"; - + String enhancedJson = + "{\n" + + " \"type\" : \"eq\",\n" + + " \"term\" : {\n" + + " \"type\" : \"resolved-reference\",\n" + + " \"name\" : \"data\",\n" + + " \"field-id\" : 101\n" + + " },\n" + + " \"value\" : \"test\"\n" + + "}"; + // Parse using enhanced parser (concept) Expression parsed = parseEnhancedJson(enhancedJson); - + assertThat(parsed).isInstanceOf(UnboundPredicate.class); UnboundPredicate<?> predicate = (UnboundPredicate<?>) parsed; assertThat(predicate.term()).isInstanceOf(ResolvedReference.class); - + ResolvedReference<?> resolvedRef = (ResolvedReference<?>) predicate.term(); assertThat(resolvedRef.name()).isEqualTo("data"); assertThat(resolvedRef.fieldId()).isEqualTo(101); @@ -104,18 +105,19 @@ public class TestEnhancedExpressionParserWithFieldIds { @Test public void testBackwardCompatibilityWithExistingFormat() { // Test that enhanced parser can handle existing JSON format without field IDs - String standardJson = "{\n" + - " \"type\" : \"eq\",\n" + - " \"term\" : \"data\",\n" + - " \"value\" : \"test\"\n" + - "}"; - + String standardJson = + "{\n" + + " \"type\" : \"eq\",\n" + + " \"term\" : \"data\",\n" + + " \"value\" : \"test\"\n" + + "}"; + Expression parsed = parseEnhancedJson(standardJson); - + assertThat(parsed).isInstanceOf(UnboundPredicate.class); UnboundPredicate<?> predicate = (UnboundPredicate<?>) parsed; assertThat(predicate.term()).isInstanceOf(NamedReference.class); - + NamedReference<?> namedRef = (NamedReference<?>) predicate.term(); assertThat(namedRef.name()).isEqualTo("data"); } @@ -123,28 +125,29 @@ public class TestEnhancedExpressionParserWithFieldIds { @Test public void testComplexExpressionWithMixedReferences() { // Test complex expression with both ResolvedReference and NamedReference - Expression mixedExpr = Expressions.and( - Expressions.equal(Expressions.ref("data", 101), "test"), // ResolvedReference - Expressions.isNull("active")); // NamedReference - + Expression mixedExpr = + Expressions.and( + Expressions.equal(Expressions.ref("data", 101), "test"), // ResolvedReference + Expressions.isNull("active")); // NamedReference + String enhancedJson = generateEnhancedJson(mixedExpr); - + // Should contain both reference types in JSON assertThat(enhancedJson).contains("\"resolved-reference\""); assertThat(enhancedJson).contains("\"field-id\" : 101"); assertThat(enhancedJson).contains("\"active\""); - + // Parse back and verify Expression parsed = parseEnhancedJson(enhancedJson); assertThat(parsed).isInstanceOf(And.class); - + And andExpr = (And) parsed; - + // Left side should be ResolvedReference UnboundPredicate<?> leftPred = (UnboundPredicate<?>) andExpr.left(); assertThat(leftPred.term()).isInstanceOf(ResolvedReference.class); - - // Right side should be NamedReference + + // Right side should be NamedReference UnboundPredicate<?> rightPred = (UnboundPredicate<?>) andExpr.right(); assertThat(rightPred.term()).isInstanceOf(NamedReference.class); } @@ -152,26 +155,27 @@ public class TestEnhancedExpressionParserWithFieldIds { @Test public void testFieldIdPreservationThroughRoundTrip() { // Test that field IDs are preserved through complete round-trip - Expression original = Expressions.and( - Expressions.greaterThan(Expressions.ref("id", 100), 50L), - Expressions.equal(Expressions.ref("data", 101), "test")); - + Expression original = + Expressions.and( + Expressions.greaterThan(Expressions.ref("id", 100), 50L), + Expressions.equal(Expressions.ref("data", 101), "test")); + // Generate enhanced JSON String json = generateEnhancedJson(original); - + // Parse back Expression parsed = parseEnhancedJson(json); - + // Verify structure is preserved assertThat(parsed).isInstanceOf(And.class); And andExpr = (And) parsed; - + // Check left predicate (id > 50) UnboundPredicate<?> leftPred = (UnboundPredicate<?>) andExpr.left(); ResolvedReference<?> leftRef = (ResolvedReference<?>) leftPred.term(); assertThat(leftRef.name()).isEqualTo("id"); assertThat(leftRef.fieldId()).isEqualTo(100); - + // Check right predicate (data = "test") UnboundPredicate<?> rightPred = (UnboundPredicate<?>) andExpr.right(); ResolvedReference<?> rightRef = (ResolvedReference<?>) rightPred.term(); @@ -182,13 +186,15 @@ public class TestEnhancedExpressionParserWithFieldIds { // Helper methods to demonstrate enhanced JSON generation and parsing private String generateEnhancedJson(Expression expr) { - return JsonUtil.generate(gen -> { - try { - generateEnhanced(expr, gen); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }, true); + return JsonUtil.generate( + gen -> { + try { + generateEnhanced(expr, gen); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, + true); } private void generateEnhanced(Expression expr, JsonGenerator gen) throws IOException { @@ -205,7 +211,8 @@ public class TestEnhancedExpressionParserWithFieldIds { } // Enhanced JSON visitor that supports field IDs - private static class EnhancedJsonVisitor extends ExpressionVisitors.CustomOrderExpressionVisitor<Void> { + private static class EnhancedJsonVisitor + extends ExpressionVisitors.CustomOrderExpressionVisitor<Void> { private final JsonGenerator gen; EnhancedJsonVisitor(JsonGenerator gen) { @@ -227,103 +234,109 @@ public class TestEnhancedExpressionParserWithFieldIds { @Override public Void alwaysTrue() { - return generate(() -> { - try { - gen.writeBoolean(true); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + return generate( + () -> { + try { + gen.writeBoolean(true); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @Override public Void alwaysFalse() { - return generate(() -> { - try { - gen.writeBoolean(false); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + return generate( + () -> { + try { + gen.writeBoolean(false); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @Override public Void not(Supplier<Void> result) { - return generate(() -> { - try { - gen.writeStartObject(); - gen.writeStringField("type", "not"); - gen.writeFieldName("child"); - toJson(result); - gen.writeEndObject(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + return generate( + () -> { + try { + gen.writeStartObject(); + gen.writeStringField("type", "not"); + gen.writeFieldName("child"); + toJson(result); + gen.writeEndObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @Override public Void and(Supplier<Void> leftResult, Supplier<Void> rightResult) { - return generate(() -> { - try { - gen.writeStartObject(); - gen.writeStringField("type", "and"); - gen.writeFieldName("left"); - toJson(leftResult); - gen.writeFieldName("right"); - toJson(rightResult); - gen.writeEndObject(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + return generate( + () -> { + try { + gen.writeStartObject(); + gen.writeStringField("type", "and"); + gen.writeFieldName("left"); + toJson(leftResult); + gen.writeFieldName("right"); + toJson(rightResult); + gen.writeEndObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @Override public Void or(Supplier<Void> leftResult, Supplier<Void> rightResult) { - return generate(() -> { - try { - gen.writeStartObject(); - gen.writeStringField("type", "or"); - gen.writeFieldName("left"); - toJson(leftResult); - gen.writeFieldName("right"); - toJson(rightResult); - gen.writeEndObject(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + return generate( + () -> { + try { + gen.writeStartObject(); + gen.writeStringField("type", "or"); + gen.writeFieldName("left"); + toJson(leftResult); + gen.writeFieldName("right"); + toJson(rightResult); + gen.writeEndObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @Override public <T> Void predicate(UnboundPredicate<T> pred) { - return generate(() -> { - try { - gen.writeStartObject(); - gen.writeStringField("type", pred.op().toString().toLowerCase().replace("_", "-")); - gen.writeFieldName("term"); - writeTerm(pred.term()); - - if (pred.literals() != null && !pred.literals().isEmpty()) { - if (pred.literals().size() == 1) { - gen.writeFieldName("value"); - writeLiteral(pred.literals().get(0)); - } else { - gen.writeFieldName("values"); - gen.writeStartArray(); - for (Literal<T> literal : pred.literals()) { - writeLiteral(literal); + return generate( + () -> { + try { + gen.writeStartObject(); + gen.writeStringField("type", pred.op().toString().toLowerCase().replace("_", "-")); + gen.writeFieldName("term"); + writeTerm(pred.term()); + + if (pred.literals() != null && !pred.literals().isEmpty()) { + if (pred.literals().size() == 1) { + gen.writeFieldName("value"); + writeLiteral(pred.literals().get(0)); + } else { + gen.writeFieldName("values"); + gen.writeStartArray(); + for (Literal<T> literal : pred.literals()) { + writeLiteral(literal); + } + gen.writeEndArray(); + } } - gen.writeEndArray(); + + gen.writeEndObject(); + } catch (IOException e) { + throw new UncheckedIOException(e); } - } - - gen.writeEndObject(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + }); } private void writeTerm(UnboundTerm<?> term) throws IOException { @@ -385,12 +398,10 @@ public class TestEnhancedExpressionParserWithFieldIds { return Expressions.not(parseEnhancedExpression(JsonUtil.get("child", node))); case "eq": return Expressions.equal( - parseEnhancedTerm(JsonUtil.get("term", node)), - parseValue(JsonUtil.get("value", node))); + parseEnhancedTerm(JsonUtil.get("term", node)), parseValue(JsonUtil.get("value", node))); case "gt": return Expressions.greaterThan( - parseEnhancedTerm(JsonUtil.get("term", node)), - parseValue(JsonUtil.get("value", node))); + parseEnhancedTerm(JsonUtil.get("term", node)), parseValue(JsonUtil.get("value", node))); case "is-null": return Expressions.isNull(parseEnhancedTerm(JsonUtil.get("term", node))); default: @@ -430,4 +441,4 @@ public class TestEnhancedExpressionParserWithFieldIds { } return node.asText(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/apache/iceberg/expressions/TestExpressionParserWithResolvedReference.java b/core/src/test/java/org/apache/iceberg/expressions/TestExpressionParserWithResolvedReference.java index 97981a6471..8a2b0dd139 100644 --- a/core/src/test/java/org/apache/iceberg/expressions/TestExpressionParserWithResolvedReference.java +++ b/core/src/test/java/org/apache/iceberg/expressions/TestExpressionParserWithResolvedReference.java @@ -53,30 +53,31 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedReferenceExpressionSerialization() { // Create expressions using ResolvedReference - Expression[] resolvedExpressions = new Expression[] { - Expressions.equal(Expressions.ref("id", 100), 42L), - Expressions.lessThan(Expressions.ref("data", 101), "test"), - Expressions.greaterThanOrEqual(Expressions.ref("i", 103), 10), - Expressions.isNull(Expressions.ref("f", 105)), - Expressions.notNull(Expressions.ref("date", 107)), - Expressions.startsWith(Expressions.ref("s", 110), "prefix"), - Expressions.in(Expressions.ref("l", 104), 1L, 2L, 3L), - Expressions.notIn(Expressions.ref("b", 102), true, false), - Expressions.isNaN(Expressions.ref("d", 106)), - Expressions.notNaN(Expressions.ref("f", 105)) - }; + Expression[] resolvedExpressions = + new Expression[] { + Expressions.equal(Expressions.ref("id", 100), 42L), + Expressions.lessThan(Expressions.ref("data", 101), "test"), + Expressions.greaterThanOrEqual(Expressions.ref("i", 103), 10), + Expressions.isNull(Expressions.ref("f", 105)), + Expressions.notNull(Expressions.ref("date", 107)), + Expressions.startsWith(Expressions.ref("s", 110), "prefix"), + Expressions.in(Expressions.ref("l", 104), 1L, 2L, 3L), + Expressions.notIn(Expressions.ref("b", 102), true, false), + Expressions.isNaN(Expressions.ref("d", 106)), + Expressions.notNaN(Expressions.ref("f", 105)) + }; for (Expression expr : resolvedExpressions) { // Verify the expression uses ResolvedReference assertThat(expr).isInstanceOf(UnboundPredicate.class); UnboundPredicate<?> predicate = (UnboundPredicate<?>) expr; assertThat(predicate.term()).isInstanceOf(ResolvedReference.class); - + // Test JSON serialization String json = ExpressionParser.toJson(expr, true); assertThat(json).isNotNull(); assertThat(json).contains("\"type\""); - + // Test that JSON contains the field name (not field ID since parser doesn't support it yet) ResolvedReference<?> resolvedRef = (ResolvedReference<?>) predicate.term(); assertThat(json).contains(resolvedRef.name()); @@ -88,20 +89,20 @@ public class TestExpressionParserWithResolvedReference { // Test that ResolvedReference expressions can be serialized and parsed back Expression resolvedExpr = Expressions.equal(Expressions.ref("id", 100), 42L); Expression namedExpr = Expressions.equal(Expressions.ref("id"), 42L); - + // Both should produce the same JSON since parser only uses names String resolvedJson = ExpressionParser.toJson(resolvedExpr, true); String namedJson = ExpressionParser.toJson(namedExpr, true); assertThat(resolvedJson).isEqualTo(namedJson); - + // Parse back and verify equivalence Expression parsedFromResolved = ExpressionParser.fromJson(resolvedJson, SCHEMA); Expression parsedFromNamed = ExpressionParser.fromJson(namedJson, SCHEMA); - + // Both parsed expressions should be equivalent assertThat(ExpressionUtil.equivalent(parsedFromResolved, parsedFromNamed, STRUCT_TYPE, true)) .isTrue(); - + // The parsed expression should be equivalent to the original named reference expression assertThat(ExpressionUtil.equivalent(namedExpr, parsedFromResolved, STRUCT_TYPE, true)) .isTrue(); @@ -110,59 +111,55 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedReferenceComplexExpressions() { // Test complex expressions with ResolvedReference - Expression complexExpr = Expressions.and( - Expressions.or( - Expressions.equal(Expressions.ref("data", 101), "test"), - Expressions.isNull(Expressions.ref("data", 101))), - Expressions.greaterThanOrEqual(Expressions.ref("id", 100), 100L)); - + Expression complexExpr = + Expressions.and( + Expressions.or( + Expressions.equal(Expressions.ref("data", 101), "test"), + Expressions.isNull(Expressions.ref("data", 101))), + Expressions.greaterThanOrEqual(Expressions.ref("id", 100), 100L)); + // Serialize to JSON String json = ExpressionParser.toJson(complexExpr, true); assertThat(json).contains("\"type\" : \"and\""); assertThat(json).contains("\"type\" : \"or\""); assertThat(json).contains("\"data\""); assertThat(json).contains("\"id\""); - + // Parse back Expression parsed = ExpressionParser.fromJson(json, SCHEMA); - + // Create equivalent expression with NamedReference for comparison - Expression namedEquivalent = Expressions.and( - Expressions.or( - Expressions.equal("data", "test"), - Expressions.isNull("data")), - Expressions.greaterThanOrEqual("id", 100L)); - + Expression namedEquivalent = + Expressions.and( + Expressions.or(Expressions.equal("data", "test"), Expressions.isNull("data")), + Expressions.greaterThanOrEqual("id", 100L)); + // Should be equivalent - assertThat(ExpressionUtil.equivalent(namedEquivalent, parsed, STRUCT_TYPE, true)) - .isTrue(); + assertThat(ExpressionUtil.equivalent(namedEquivalent, parsed, STRUCT_TYPE, true)).isTrue(); } @Test public void testResolvedReferenceTransformExpressions() { - // Test transform expressions - using NamedReference for transforms since + // Test transform expressions - using NamedReference for transforms since // ResolvedReference needs proper type parameters for transform methods - Expression dayTransform = Expressions.equal( - Expressions.day("date"), "2023-01-15"); - Expression bucketTransform = Expressions.equal( - Expressions.bucket("id", 10), 5); - + Expression dayTransform = Expressions.equal(Expressions.day("date"), "2023-01-15"); + Expression bucketTransform = Expressions.equal(Expressions.bucket("id", 10), 5); + // Test serialization String dayJson = ExpressionParser.toJson(dayTransform, true); String bucketJson = ExpressionParser.toJson(bucketTransform, true); - + assertThat(dayJson).contains("\"transform\" : \"day\""); assertThat(dayJson).contains("\"term\" : \"date\""); assertThat(bucketJson).contains("\"transform\" : \"bucket[10]\""); assertThat(bucketJson).contains("\"term\" : \"id\""); - + // Test round-trip Expression parsedDay = ExpressionParser.fromJson(dayJson, SCHEMA); Expression parsedBucket = ExpressionParser.fromJson(bucketJson, SCHEMA); - + // Should maintain equivalence after round-trip - assertThat(ExpressionUtil.equivalent(dayTransform, parsedDay, STRUCT_TYPE, true)) - .isTrue(); + assertThat(ExpressionUtil.equivalent(dayTransform, parsedDay, STRUCT_TYPE, true)).isTrue(); assertThat(ExpressionUtil.equivalent(bucketTransform, parsedBucket, STRUCT_TYPE, true)) .isTrue(); } @@ -171,22 +168,22 @@ public class TestExpressionParserWithResolvedReference { public void testResolvedReferenceBindingAfterParsing() { // Test that expressions with ResolvedReference bind correctly after parsing Expression original = Expressions.equal(Expressions.ref("id", 100), 42L); - + // Serialize and parse String json = ExpressionParser.toJson(original, true); Expression parsed = ExpressionParser.fromJson(json, SCHEMA); - + // Both should bind successfully Expression originalBound = Binder.bind(STRUCT_TYPE, original, true); Expression parsedBound = Binder.bind(STRUCT_TYPE, parsed, true); - + // Both bound expressions should be identical assertThat(originalBound).isInstanceOf(BoundPredicate.class); assertThat(parsedBound).isInstanceOf(BoundPredicate.class); - + BoundPredicate<?> originalBoundPred = (BoundPredicate<?>) originalBound; BoundPredicate<?> parsedBoundPred = (BoundPredicate<?>) parsedBound; - + // Should reference the same field assertThat(originalBoundPred.ref().fieldId()).isEqualTo(100); assertThat(parsedBoundPred.ref().fieldId()).isEqualTo(100); @@ -197,26 +194,27 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedReferenceWithDifferentTypes() { // Test ResolvedReference with various data types - Expression[] typedExpressions = new Expression[] { - Expressions.equal(Expressions.ref("b", 102), true), - Expressions.equal(Expressions.ref("i", 103), 42), - Expressions.equal(Expressions.ref("l", 104), 42L), - Expressions.equal(Expressions.ref("f", 105), 3.14f), - Expressions.equal(Expressions.ref("d", 106), 3.14159), - Expressions.equal(Expressions.ref("s", 110), "test string"), - Expressions.equal(Expressions.ref("uuid", 111), UUID.randomUUID()), - Expressions.equal(Expressions.ref("dec_11_2", 115), new BigDecimal("123.45")) - }; - + Expression[] typedExpressions = + new Expression[] { + Expressions.equal(Expressions.ref("b", 102), true), + Expressions.equal(Expressions.ref("i", 103), 42), + Expressions.equal(Expressions.ref("l", 104), 42L), + Expressions.equal(Expressions.ref("f", 105), 3.14f), + Expressions.equal(Expressions.ref("d", 106), 3.14159), + Expressions.equal(Expressions.ref("s", 110), "test string"), + Expressions.equal(Expressions.ref("uuid", 111), UUID.randomUUID()), + Expressions.equal(Expressions.ref("dec_11_2", 115), new BigDecimal("123.45")) + }; + for (Expression expr : typedExpressions) { // Test serialization doesn't break with different types String json = ExpressionParser.toJson(expr, true); assertThat(json).isNotNull(); - + // Test parsing back Expression parsed = ExpressionParser.fromJson(json, SCHEMA); assertThat(parsed).isNotNull(); - + // Test binding Expression bound = Binder.bind(STRUCT_TYPE, parsed, true); assertThat(bound).isInstanceOf(BoundPredicate.class); @@ -228,20 +226,21 @@ public class TestExpressionParserWithResolvedReference { // Test the exact JSON structure produced by ResolvedReference Expression expr = Expressions.equal(Expressions.ref("data", 101), "test"); String json = ExpressionParser.toJson(expr, true); - + // The JSON should look like a regular reference since parser doesn't support field IDs yet - String expectedStructure = "{\n" + - " \"type\" : \"eq\",\n" + - " \"term\" : \"data\",\n" + - " \"value\" : \"test\"\n" + - "}"; - + String expectedStructure = + "{\n" + + " \"type\" : \"eq\",\n" + + " \"term\" : \"data\",\n" + + " \"value\" : \"test\"\n" + + "}"; + assertThat(json).isEqualTo(expectedStructure); - + // Verify it parses back correctly Expression parsed = ExpressionParser.fromJson(json); assertThat(parsed).isInstanceOf(UnboundPredicate.class); - + UnboundPredicate<?> predicate = (UnboundPredicate<?>) parsed; assertThat(predicate.term()).isInstanceOf(NamedReference.class); assertThat(predicate.term().ref().name()).isEqualTo("data"); @@ -250,58 +249,57 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedReferenceEquivalenceAfterSerialization() { // Test that ResolvedReference expressions maintain equivalence after serialization - Expression resolvedExpr = Expressions.and( - Expressions.greaterThan(Expressions.ref("id", 100), 50L), - Expressions.lessThan(Expressions.ref("id", 100), 200L)); - - Expression namedExpr = Expressions.and( - Expressions.greaterThan("id", 50L), - Expressions.lessThan("id", 200L)); - + Expression resolvedExpr = + Expressions.and( + Expressions.greaterThan(Expressions.ref("id", 100), 50L), + Expressions.lessThan(Expressions.ref("id", 100), 200L)); + + Expression namedExpr = + Expressions.and(Expressions.greaterThan("id", 50L), Expressions.lessThan("id", 200L)); + // Serialize both String resolvedJson = ExpressionParser.toJson(resolvedExpr, true); String namedJson = ExpressionParser.toJson(namedExpr, true); - + // Should produce identical JSON assertThat(resolvedJson).isEqualTo(namedJson); - + // Parse both back Expression parsedResolved = ExpressionParser.fromJson(resolvedJson, SCHEMA); Expression parsedNamed = ExpressionParser.fromJson(namedJson, SCHEMA); - + // All should be equivalent - assertThat(ExpressionUtil.equivalent(resolvedExpr, namedExpr, STRUCT_TYPE, true)) - .isTrue(); - assertThat(ExpressionUtil.equivalent(parsedResolved, parsedNamed, STRUCT_TYPE, true)) - .isTrue(); - assertThat(ExpressionUtil.equivalent(resolvedExpr, parsedResolved, STRUCT_TYPE, true)) - .isTrue(); + assertThat(ExpressionUtil.equivalent(resolvedExpr, namedExpr, STRUCT_TYPE, true)).isTrue(); + assertThat(ExpressionUtil.equivalent(parsedResolved, parsedNamed, STRUCT_TYPE, true)).isTrue(); + assertThat(ExpressionUtil.equivalent(resolvedExpr, parsedResolved, STRUCT_TYPE, true)).isTrue(); } @Test public void testResolvedReferenceTransformExpressionEquivalence() { - // Test expressions that reference transforms where the transform terms are equivalent to resolved references - // Since UnboundTransform only accepts NamedReference, we test equivalence through binding/unbinding - + // Test expressions that reference transforms where the transform terms are equivalent to + // resolved references + // Since UnboundTransform only accepts NamedReference, we test equivalence through + // binding/unbinding + // Create transform expressions using NamedReference (current approach) Expression dayTransformNamed = Expressions.equal(Expressions.day("date"), "2023-01-15"); Expression bucketTransformNamed = Expressions.equal(Expressions.bucket("id", 10), 5); Expression truncateTransformNamed = Expressions.equal(Expressions.truncate("data", 4), "test"); - + // Create equivalent expressions using ResolvedReference for the predicate terms Expression dayWithResolvedRef = Expressions.equal(Expressions.ref("date", 107), "2023-01-15"); Expression bucketWithResolvedRef = Expressions.equal(Expressions.ref("id", 100), 5L); Expression truncateWithResolvedRef = Expressions.equal(Expressions.ref("data", 101), "test"); - + // Bind all expressions Expression boundDayTransform = Binder.bind(STRUCT_TYPE, dayTransformNamed, true); Expression boundBucketTransform = Binder.bind(STRUCT_TYPE, bucketTransformNamed, true); Expression boundTruncateTransform = Binder.bind(STRUCT_TYPE, truncateTransformNamed, true); - + Expression boundDayResolved = Binder.bind(STRUCT_TYPE, dayWithResolvedRef, true); Expression boundBucketResolved = Binder.bind(STRUCT_TYPE, bucketWithResolvedRef, true); Expression boundTruncateResolved = Binder.bind(STRUCT_TYPE, truncateWithResolvedRef, true); - + // Verify all expressions bound successfully assertThat(boundDayTransform).isInstanceOf(BoundPredicate.class); assertThat(boundBucketTransform).isInstanceOf(BoundPredicate.class); @@ -309,40 +307,42 @@ public class TestExpressionParserWithResolvedReference { assertThat(boundDayResolved).isInstanceOf(BoundPredicate.class); assertThat(boundBucketResolved).isInstanceOf(BoundPredicate.class); assertThat(boundTruncateResolved).isInstanceOf(BoundPredicate.class); - + // Test transform expressions in complex expressions with resolved references - Expression complexTransformExpr = Expressions.and( - Expressions.equal(Expressions.day("date"), "2023-01-15"), - Expressions.equal(Expressions.ref("id", 100), 42L)); - + Expression complexTransformExpr = + Expressions.and( + Expressions.equal(Expressions.day("date"), "2023-01-15"), + Expressions.equal(Expressions.ref("id", 100), 42L)); + Expression boundComplexExpr = Binder.bind(STRUCT_TYPE, complexTransformExpr, true); assertThat(boundComplexExpr).isInstanceOf(And.class); - + // Verify serialization works for transform expressions that coexist with resolved references String complexJson = ExpressionParser.toJson(complexTransformExpr, true); assertThat(complexJson).contains("\"transform\" : \"day\""); assertThat(complexJson).contains("\"term\" : \"date\""); assertThat(complexJson).contains("\"term\" : \"id\""); - + // Verify round-trip maintains correctness Expression parsedComplex = ExpressionParser.fromJson(complexJson, SCHEMA); Expression boundParsedComplex = Binder.bind(STRUCT_TYPE, parsedComplex, true); - + // Both bound expressions should reference the same fields assertThat(boundComplexExpr.toString()).isEqualTo(boundParsedComplex.toString()); } - @Test + @Test public void testResolvedReferenceInComplexTransformExpressions() { // Test complex expressions that combine transforms with resolved references - Expression complexExpr = Expressions.or( - Expressions.and( - Expressions.equal(Expressions.bucket("id", 8), 3), - Expressions.equal(Expressions.ref("data", 101), "test")), - Expressions.and( - Expressions.equal(Expressions.day("date"), "2023-01-15"), - Expressions.isNull(Expressions.ref("f", 105)))); - + Expression complexExpr = + Expressions.or( + Expressions.and( + Expressions.equal(Expressions.bucket("id", 8), 3), + Expressions.equal(Expressions.ref("data", 101), "test")), + Expressions.and( + Expressions.equal(Expressions.day("date"), "2023-01-15"), + Expressions.isNull(Expressions.ref("f", 105)))); + // Test serialization String json = ExpressionParser.toJson(complexExpr, true); assertThat(json).contains("\"transform\" : \"bucket[8]\""); @@ -351,21 +351,21 @@ public class TestExpressionParserWithResolvedReference { assertThat(json).contains("\"term\" : \"date\""); assertThat(json).contains("\"term\" : \"data\""); assertThat(json).contains("\"term\" : \"f\""); - + // Test that parsing back maintains structure Expression parsed = ExpressionParser.fromJson(json, SCHEMA); assertThat(parsed).isInstanceOf(Or.class); - + // Test binding works correctly Expression bound = Binder.bind(STRUCT_TYPE, parsed, true); assertThat(bound).isInstanceOf(Or.class); - + // Verify equivalence with original assertThat(ExpressionUtil.equivalent(complexExpr, parsed, STRUCT_TYPE, true)).isTrue(); - + // Test that mixed transform and resolved reference expressions bind to same fields Expression originalBound = Binder.bind(STRUCT_TYPE, complexExpr, true); - + // Both bound expressions should be structurally equivalent assertThat(originalBound.toString()).isEqualTo(bound.toString()); } @@ -374,38 +374,36 @@ public class TestExpressionParserWithResolvedReference { public void testTransformExpressionsWithResolvedReference() { // Test expressions that reference transforms which in turn reference resolved references // Create transforms using the new ResolvedReference-based factory methods - Expression bucketExpr = Expressions.equal( - Expressions.bucket(Expressions.ref("id", 100), 8), 3); - Expression dayExpr = Expressions.equal( - Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); - Expression hourExpr = Expressions.equal( - Expressions.hour(Expressions.ref("ts", 108)), 10); - Expression truncateExpr = Expressions.equal( - Expressions.truncate(Expressions.ref("data", 101), 4), "test"); - + Expression bucketExpr = Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3); + Expression dayExpr = + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); + Expression hourExpr = Expressions.equal(Expressions.hour(Expressions.ref("ts", 108)), 10); + Expression truncateExpr = + Expressions.equal(Expressions.truncate(Expressions.ref("data", 101), 4), "test"); + // Verify the expressions are created correctly assertThat(bucketExpr).isInstanceOf(UnboundPredicate.class); assertThat(dayExpr).isInstanceOf(UnboundPredicate.class); assertThat(hourExpr).isInstanceOf(UnboundPredicate.class); assertThat(truncateExpr).isInstanceOf(UnboundPredicate.class); - + // Verify the terms are ResolvedTransform instances UnboundPredicate<?> bucketPred = (UnboundPredicate<?>) bucketExpr; UnboundPredicate<?> dayPred = (UnboundPredicate<?>) dayExpr; UnboundPredicate<?> hourPred = (UnboundPredicate<?>) hourExpr; UnboundPredicate<?> truncatePred = (UnboundPredicate<?>) truncateExpr; - + assertThat(bucketPred.term()).isInstanceOf(ResolvedTransform.class); assertThat(dayPred.term()).isInstanceOf(ResolvedTransform.class); assertThat(hourPred.term()).isInstanceOf(ResolvedTransform.class); assertThat(truncatePred.term()).isInstanceOf(ResolvedTransform.class); - + // Verify that ResolvedTransform preserves the ResolvedReference with field IDs ResolvedTransform<?, ?> bucketTransform = (ResolvedTransform<?, ?>) bucketPred.term(); ResolvedTransform<?, ?> dayTransform = (ResolvedTransform<?, ?>) dayPred.term(); ResolvedTransform<?, ?> hourTransform = (ResolvedTransform<?, ?>) hourPred.term(); ResolvedTransform<?, ?> truncateTransform = (ResolvedTransform<?, ?>) truncatePred.term(); - + assertThat(bucketTransform.resolvedRef().fieldId()).isEqualTo(100); assertThat(bucketTransform.resolvedRef().name()).isEqualTo("id"); assertThat(dayTransform.resolvedRef().fieldId()).isEqualTo(107); @@ -414,13 +412,13 @@ public class TestExpressionParserWithResolvedReference { assertThat(hourTransform.resolvedRef().name()).isEqualTo("ts"); assertThat(truncateTransform.resolvedRef().fieldId()).isEqualTo(101); assertThat(truncateTransform.resolvedRef().name()).isEqualTo("data"); - + // Test serialization String bucketJson = ExpressionParser.toJson(bucketExpr, true); String dayJson = ExpressionParser.toJson(dayExpr, true); String hourJson = ExpressionParser.toJson(hourExpr, true); String truncateJson = ExpressionParser.toJson(truncateExpr, true); - + // Verify JSON contains the expected transform and term information assertThat(bucketJson).contains("\"transform\" : \"bucket[8]\""); assertThat(bucketJson).contains("\"term\" : \"id\""); @@ -430,36 +428,36 @@ public class TestExpressionParserWithResolvedReference { assertThat(hourJson).contains("\"term\" : \"ts\""); assertThat(truncateJson).contains("\"transform\" : \"truncate[4]\""); assertThat(truncateJson).contains("\"term\" : \"data\""); - + // Test parsing back Expression parsedBucket = ExpressionParser.fromJson(bucketJson, SCHEMA); Expression parsedDay = ExpressionParser.fromJson(dayJson, SCHEMA); Expression parsedHour = ExpressionParser.fromJson(hourJson, SCHEMA); Expression parsedTruncate = ExpressionParser.fromJson(truncateJson, SCHEMA); - + // Verify equivalence after round-trip assertThat(ExpressionUtil.equivalent(bucketExpr, parsedBucket, STRUCT_TYPE, true)).isTrue(); assertThat(ExpressionUtil.equivalent(dayExpr, parsedDay, STRUCT_TYPE, true)).isTrue(); assertThat(ExpressionUtil.equivalent(hourExpr, parsedHour, STRUCT_TYPE, true)).isTrue(); assertThat(ExpressionUtil.equivalent(truncateExpr, parsedTruncate, STRUCT_TYPE, true)).isTrue(); - + // Test binding works correctly Expression boundBucket = Binder.bind(STRUCT_TYPE, parsedBucket, true); Expression boundDay = Binder.bind(STRUCT_TYPE, parsedDay, true); Expression boundHour = Binder.bind(STRUCT_TYPE, parsedHour, true); Expression boundTruncate = Binder.bind(STRUCT_TYPE, parsedTruncate, true); - + assertThat(boundBucket).isInstanceOf(BoundPredicate.class); assertThat(boundDay).isInstanceOf(BoundPredicate.class); assertThat(boundHour).isInstanceOf(BoundPredicate.class); assertThat(boundTruncate).isInstanceOf(BoundPredicate.class); - + // Verify bound expressions reference correct field IDs BoundPredicate<?> boundBucketPred = (BoundPredicate<?>) boundBucket; BoundPredicate<?> boundDayPred = (BoundPredicate<?>) boundDay; BoundPredicate<?> boundHourPred = (BoundPredicate<?>) boundHour; BoundPredicate<?> boundTruncatePred = (BoundPredicate<?>) boundTruncate; - + assertThat(boundBucketPred.term()).isInstanceOf(BoundTransform.class); assertThat(boundDayPred.term()).isInstanceOf(BoundTransform.class); assertThat(boundHourPred.term()).isInstanceOf(BoundTransform.class); @@ -469,17 +467,18 @@ public class TestExpressionParserWithResolvedReference { @Test public void testComplexExpressionsWithResolvedReferenceTransforms() { // Test complex expressions combining transforms created from ResolvedReference - Expression complexExpr = Expressions.and( - Expressions.or( - Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), - Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15")), + Expression complexExpr = Expressions.and( - Expressions.equal(Expressions.truncate(Expressions.ref("data", 101), 4), "test"), - Expressions.isNull(Expressions.ref("f", 105)))); - + Expressions.or( + Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15")), + Expressions.and( + Expressions.equal(Expressions.truncate(Expressions.ref("data", 101), 4), "test"), + Expressions.isNull(Expressions.ref("f", 105)))); + // Test serialization of complex expression String json = ExpressionParser.toJson(complexExpr, true); - + // Verify all transforms and terms are present in JSON assertThat(json).contains("\"transform\" : \"bucket[8]\""); assertThat(json).contains("\"transform\" : \"day\""); @@ -488,18 +487,18 @@ public class TestExpressionParserWithResolvedReference { assertThat(json).contains("\"term\" : \"date\""); assertThat(json).contains("\"term\" : \"data\""); assertThat(json).contains("\"term\" : \"f\""); - + // Test parsing back maintains structure Expression parsed = ExpressionParser.fromJson(json, SCHEMA); assertThat(parsed).isInstanceOf(And.class); - + // Test binding works correctly for the complex expression Expression bound = Binder.bind(STRUCT_TYPE, parsed, true); assertThat(bound).isInstanceOf(And.class); - + // Verify equivalence after round-trip assertThat(ExpressionUtil.equivalent(complexExpr, parsed, STRUCT_TYPE, true)).isTrue(); - + // Test that the bound expression maintains correct structure Expression originalBound = Binder.bind(STRUCT_TYPE, complexExpr, true); assertThat(originalBound.toString()).isEqualTo(bound.toString()); @@ -507,46 +506,50 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedReferenceTransformCompatibilityWithNamedReference() { - // Test that transforms created with ResolvedReference are equivalent to those created with NamedReference - + // Test that transforms created with ResolvedReference are equivalent to those created with + // NamedReference + // Create equivalent transforms using both approaches - Expression bucketWithResolved = Expressions.equal( - Expressions.bucket(Expressions.ref("id", 100), 8), 3); - Expression bucketWithNamed = Expressions.equal( - Expressions.bucket("id", 8), 3); - - Expression dayWithResolved = Expressions.equal( - Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); - Expression dayWithNamed = Expressions.equal( - Expressions.day("date"), "2023-01-15"); - + Expression bucketWithResolved = + Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3); + Expression bucketWithNamed = Expressions.equal(Expressions.bucket("id", 8), 3); + + Expression dayWithResolved = + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); + Expression dayWithNamed = Expressions.equal(Expressions.day("date"), "2023-01-15"); + // Test that both serialize to the same JSON String resolvedBucketJson = ExpressionParser.toJson(bucketWithResolved, true); String namedBucketJson = ExpressionParser.toJson(bucketWithNamed, true); String resolvedDayJson = ExpressionParser.toJson(dayWithResolved, true); String namedDayJson = ExpressionParser.toJson(dayWithNamed, true); - + assertThat(resolvedBucketJson).isEqualTo(namedBucketJson); assertThat(resolvedDayJson).isEqualTo(namedDayJson); - + // Test that parsing back produces equivalent expressions Expression parsedResolvedBucket = ExpressionParser.fromJson(resolvedBucketJson, SCHEMA); Expression parsedNamedBucket = ExpressionParser.fromJson(namedBucketJson, SCHEMA); Expression parsedResolvedDay = ExpressionParser.fromJson(resolvedDayJson, SCHEMA); Expression parsedNamedDay = ExpressionParser.fromJson(namedDayJson, SCHEMA); - + // All should be equivalent - assertThat(ExpressionUtil.equivalent(bucketWithResolved, bucketWithNamed, STRUCT_TYPE, true)).isTrue(); - assertThat(ExpressionUtil.equivalent(dayWithResolved, dayWithNamed, STRUCT_TYPE, true)).isTrue(); - assertThat(ExpressionUtil.equivalent(parsedResolvedBucket, parsedNamedBucket, STRUCT_TYPE, true)).isTrue(); - assertThat(ExpressionUtil.equivalent(parsedResolvedDay, parsedNamedDay, STRUCT_TYPE, true)).isTrue(); - + assertThat(ExpressionUtil.equivalent(bucketWithResolved, bucketWithNamed, STRUCT_TYPE, true)) + .isTrue(); + assertThat(ExpressionUtil.equivalent(dayWithResolved, dayWithNamed, STRUCT_TYPE, true)) + .isTrue(); + assertThat( + ExpressionUtil.equivalent(parsedResolvedBucket, parsedNamedBucket, STRUCT_TYPE, true)) + .isTrue(); + assertThat(ExpressionUtil.equivalent(parsedResolvedDay, parsedNamedDay, STRUCT_TYPE, true)) + .isTrue(); + // Test that binding produces identical results Expression boundResolvedBucket = Binder.bind(STRUCT_TYPE, parsedResolvedBucket, true); Expression boundNamedBucket = Binder.bind(STRUCT_TYPE, parsedNamedBucket, true); Expression boundResolvedDay = Binder.bind(STRUCT_TYPE, parsedResolvedDay, true); Expression boundNamedDay = Binder.bind(STRUCT_TYPE, parsedNamedDay, true); - + assertThat(boundResolvedBucket.toString()).isEqualTo(boundNamedBucket.toString()); assertThat(boundResolvedDay.toString()).isEqualTo(boundNamedDay.toString()); } @@ -555,40 +558,40 @@ public class TestExpressionParserWithResolvedReference { public void testResolvedTransformPreservesFieldIdInformation() { // Test that ResolvedTransform preserves field ID information through binding // This is the key advantage over UnboundTransform with NamedReference - + // Create a transform expression using ResolvedReference ResolvedReference<Long> idRef = Expressions.ref("id", 100); Expression bucketExpr = Expressions.equal(Expressions.bucket(idRef, 8), 3); - + // Verify it's a ResolvedTransform UnboundPredicate<?> predicate = (UnboundPredicate<?>) bucketExpr; assertThat(predicate.term()).isInstanceOf(ResolvedTransform.class); - + ResolvedTransform<?, ?> resolvedTransform = (ResolvedTransform<?, ?>) predicate.term(); - + // Verify the ResolvedReference is preserved with field ID assertThat(resolvedTransform.resolvedRef().fieldId()).isEqualTo(100); assertThat(resolvedTransform.resolvedRef().name()).isEqualTo("id"); - + // Test binding works correctly with field ID information Expression bound = Binder.bind(STRUCT_TYPE, bucketExpr, true); assertThat(bound).isInstanceOf(BoundPredicate.class); - + BoundPredicate<?> boundPredicate = (BoundPredicate<?>) bound; assertThat(boundPredicate.term()).isInstanceOf(BoundTransform.class); - + BoundTransform<?, ?> boundTransform = (BoundTransform<?, ?>) boundPredicate.term(); assertThat(boundTransform.ref().fieldId()).isEqualTo(100); assertThat(boundTransform.ref().name()).isEqualTo("id"); - + // Compare with equivalent NamedReference approach to show they bind to same field Expression namedBucketExpr = Expressions.equal(Expressions.bucket("id", 8), 3); Expression boundNamed = Binder.bind(STRUCT_TYPE, namedBucketExpr, true); - + // Both should resolve to the same field ID since they reference the same field BoundPredicate<?> boundNamedPredicate = (BoundPredicate<?>) boundNamed; BoundTransform<?, ?> boundNamedTransform = (BoundTransform<?, ?>) boundNamedPredicate.term(); - + assertThat(boundTransform.ref().fieldId()).isEqualTo(boundNamedTransform.ref().fieldId()); assertThat(boundTransform.toString()).isEqualTo(boundNamedTransform.toString()); } @@ -596,43 +599,42 @@ public class TestExpressionParserWithResolvedReference { @Test public void testResolvedTransformExpressionParserIntegration() { // Test that expressions with ResolvedTransform integrate correctly with ExpressionParser - + // Create expressions using ResolvedTransform - Expression bucketExpr = Expressions.equal( - Expressions.bucket(Expressions.ref("id", 100), 8), 3); - Expression dayExpr = Expressions.equal( - Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); - + Expression bucketExpr = Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3); + Expression dayExpr = + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); + // Test that they can be serialized (even though they become NamedReference in JSON) String bucketJson = ExpressionParser.toJson(bucketExpr, true); String dayJson = ExpressionParser.toJson(dayExpr, true); - + assertThat(bucketJson).contains("\"transform\" : \"bucket[8]\""); assertThat(bucketJson).contains("\"term\" : \"id\""); assertThat(dayJson).contains("\"transform\" : \"day\""); assertThat(dayJson).contains("\"term\" : \"date\""); - + // Test parsing back (will create UnboundTransform with NamedReference) Expression parsedBucket = ExpressionParser.fromJson(bucketJson, SCHEMA); Expression parsedDay = ExpressionParser.fromJson(dayJson, SCHEMA); - + // The parsed expressions will have UnboundTransform (not ResolvedTransform) // but they should still be functionally equivalent when bound UnboundPredicate<?> parsedBucketPred = (UnboundPredicate<?>) parsedBucket; UnboundPredicate<?> parsedDayPred = (UnboundPredicate<?>) parsedDay; - + assertThat(parsedBucketPred.term()).isInstanceOf(UnboundTransform.class); assertThat(parsedDayPred.term()).isInstanceOf(UnboundTransform.class); - + // Both original and parsed should bind to the same fields Expression originalBucketBound = Binder.bind(STRUCT_TYPE, bucketExpr, true); Expression parsedBucketBound = Binder.bind(STRUCT_TYPE, parsedBucket, true); Expression originalDayBound = Binder.bind(STRUCT_TYPE, dayExpr, true); Expression parsedDayBound = Binder.bind(STRUCT_TYPE, parsedDay, true); - + assertThat(originalBucketBound.toString()).isEqualTo(parsedBucketBound.toString()); assertThat(originalDayBound.toString()).isEqualTo(parsedDayBound.toString()); - + // Test equivalence assertThat(ExpressionUtil.equivalent(bucketExpr, parsedBucket, STRUCT_TYPE, true)).isTrue(); assertThat(ExpressionUtil.equivalent(dayExpr, parsedDay, STRUCT_TYPE, true)).isTrue(); @@ -641,65 +643,68 @@ public class TestExpressionParserWithResolvedReference { @Test public void testMixedResolvedTransformAndResolvedReferenceExpressions() { // Test complex expressions mixing ResolvedTransform and direct ResolvedReference - Expression complexExpr = Expressions.and( - Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), - Expressions.or( - Expressions.equal(Expressions.ref("data", 101), "test"), - Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"))); - + Expression complexExpr = + Expressions.and( + Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), + Expressions.or( + Expressions.equal(Expressions.ref("data", 101), "test"), + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"))); + // Verify structure contains both ResolvedTransform and ResolvedReference assertThat(complexExpr).isInstanceOf(And.class); And andExpr = (And) complexExpr; - + // First part should be ResolvedTransform UnboundPredicate<?> bucketPred = (UnboundPredicate<?>) andExpr.left(); assertThat(bucketPred.term()).isInstanceOf(ResolvedTransform.class); - + // Second part is OR with ResolvedReference and ResolvedTransform Or orExpr = (Or) andExpr.right(); UnboundPredicate<?> dataPred = (UnboundPredicate<?>) orExpr.left(); UnboundPredicate<?> dayPred = (UnboundPredicate<?>) orExpr.right(); - + assertThat(dataPred.term()).isInstanceOf(ResolvedReference.class); assertThat(dayPred.term()).isInstanceOf(ResolvedTransform.class); - + // Test serialization and parsing String json = ExpressionParser.toJson(complexExpr, true); Expression parsed = ExpressionParser.fromJson(json, SCHEMA); - + // Test binding works correctly Expression bound = Binder.bind(STRUCT_TYPE, parsed, true); Expression originalBound = Binder.bind(STRUCT_TYPE, complexExpr, true); - + assertThat(bound.toString()).isEqualTo(originalBound.toString()); assertThat(ExpressionUtil.equivalent(complexExpr, parsed, STRUCT_TYPE, true)).isTrue(); } @Test public void testBoundExpressionSerializationWithResolvedReference() { - // Test that bound expressions serialize to JSON with ResolvedReference terms when includeFieldIds=true - + // Test that bound expressions serialize to JSON with ResolvedReference terms when + // includeFieldIds=true + // Create and bind simple expressions Expression simpleExpr = Expressions.equal(Expressions.ref("id", 100), 42L); Expression boundSimple = Binder.bind(STRUCT_TYPE, simpleExpr, true); - + // Test serialization without field IDs (existing behavior) String jsonWithoutFieldIds = ExpressionParser.toJson(boundSimple, true); assertThat(jsonWithoutFieldIds).contains("\"term\" : \"id\""); assertThat(jsonWithoutFieldIds).doesNotContain("fieldId"); - + // Test serialization with field IDs (new behavior) String jsonWithFieldIds = ExpressionParser.toJson(boundSimple, true, true); assertThat(jsonWithFieldIds).contains("\"type\" : \"ref\""); assertThat(jsonWithFieldIds).contains("\"name\" : \"id\""); assertThat(jsonWithFieldIds).contains("\"fieldId\" : 100"); - + // Test complex expressions - Expression complexExpr = Expressions.and( - Expressions.equal(Expressions.ref("data", 101), "test"), - Expressions.greaterThan(Expressions.ref("id", 100), 50L)); + Expression complexExpr = + Expressions.and( + Expressions.equal(Expressions.ref("data", 101), "test"), + Expressions.greaterThan(Expressions.ref("id", 100), 50L)); Expression boundComplex = Binder.bind(STRUCT_TYPE, complexExpr, true); - + String complexJsonWithFieldIds = ExpressionParser.toJson(boundComplex, true, true); assertThat(complexJsonWithFieldIds).contains("\"fieldId\" : 100"); assertThat(complexJsonWithFieldIds).contains("\"fieldId\" : 101"); @@ -709,31 +714,31 @@ public class TestExpressionParserWithResolvedReference { @Test public void testBoundTransformExpressionSerializationWithResolvedReference() { - // Test that bound transform expressions serialize to JSON with ResolvedReference terms when includeFieldIds=true - + // Test that bound transform expressions serialize to JSON with ResolvedReference terms when + // includeFieldIds=true + // Create transform expressions using ResolvedTransform - Expression bucketExpr = Expressions.equal( - Expressions.bucket(Expressions.ref("id", 100), 8), 3); - Expression dayExpr = Expressions.equal( - Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); - + Expression bucketExpr = Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3); + Expression dayExpr = + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15"); + // Bind the expressions Expression boundBucket = Binder.bind(STRUCT_TYPE, bucketExpr, true); Expression boundDay = Binder.bind(STRUCT_TYPE, dayExpr, true); - + // Test serialization without field IDs (existing behavior) String bucketJsonNoFieldIds = ExpressionParser.toJson(boundBucket, true); assertThat(bucketJsonNoFieldIds).contains("\"transform\" : \"bucket[8]\""); assertThat(bucketJsonNoFieldIds).contains("\"term\" : \"id\""); assertThat(bucketJsonNoFieldIds).doesNotContain("fieldId"); - + // Test serialization with field IDs (new behavior) String bucketJsonWithFieldIds = ExpressionParser.toJson(boundBucket, true, true); assertThat(bucketJsonWithFieldIds).contains("\"transform\" : \"bucket[8]\""); assertThat(bucketJsonWithFieldIds).contains("\"type\" : \"ref\""); assertThat(bucketJsonWithFieldIds).contains("\"name\" : \"id\""); assertThat(bucketJsonWithFieldIds).contains("\"fieldId\" : 100"); - + String dayJsonWithFieldIds = ExpressionParser.toJson(boundDay, true, true); assertThat(dayJsonWithFieldIds).contains("\"transform\" : \"day\""); assertThat(dayJsonWithFieldIds).contains("\"type\" : \"ref\""); @@ -744,83 +749,89 @@ public class TestExpressionParserWithResolvedReference { @Test public void testComplexBoundTransformExpressionSerializationWithResolvedReference() { // Test complex bound expressions with mixed transforms and references - Expression complexExpr = Expressions.and( - Expressions.or( - Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), - Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15")), + Expression complexExpr = Expressions.and( - Expressions.equal(Expressions.truncate(Expressions.ref("data", 101), 4), "test"), - Expressions.isNull(Expressions.ref("f", 105)))); - + Expressions.or( + Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), + Expressions.equal(Expressions.day(Expressions.ref("date", 107)), "2023-01-15")), + Expressions.and( + Expressions.equal(Expressions.truncate(Expressions.ref("data", 101), 4), "test"), + Expressions.isNull(Expressions.ref("f", 105)))); + // Bind the complex expression Expression bound = Binder.bind(STRUCT_TYPE, complexExpr, true); - + // Test serialization with field IDs String jsonWithFieldIds = ExpressionParser.toJson(bound, true, true); - + // Verify all field IDs are present assertThat(jsonWithFieldIds).contains("\"fieldId\" : 100"); // id field - assertThat(jsonWithFieldIds).contains("\"fieldId\" : 107"); // date field + assertThat(jsonWithFieldIds).contains("\"fieldId\" : 107"); // date field assertThat(jsonWithFieldIds).contains("\"fieldId\" : 101"); // data field assertThat(jsonWithFieldIds).contains("\"fieldId\" : 105"); // f field - + // Verify all field names are present assertThat(jsonWithFieldIds).contains("\"name\" : \"id\""); assertThat(jsonWithFieldIds).contains("\"name\" : \"date\""); assertThat(jsonWithFieldIds).contains("\"name\" : \"data\""); assertThat(jsonWithFieldIds).contains("\"name\" : \"f\""); - + // Verify transforms are serialized correctly assertThat(jsonWithFieldIds).contains("\"transform\" : \"bucket[8]\""); assertThat(jsonWithFieldIds).contains("\"transform\" : \"day\""); assertThat(jsonWithFieldIds).contains("\"transform\" : \"truncate[4]\""); - + // Verify ResolvedReference structure for both transforms and direct references // Count occurrences of "type" : "ref" to ensure all references use ResolvedReference format - long refTypeCount = jsonWithFieldIds.lines() - .mapToLong(line -> { - int count = 0; - int index = 0; - String pattern = "\"type\" : \"ref\""; - while ((index = line.indexOf(pattern, index)) != -1) { - count++; - index += pattern.length(); - } - return count; - }) - .sum(); - + long refTypeCount = + jsonWithFieldIds + .lines() + .mapToLong( + line -> { + int count = 0; + int index = 0; + String pattern = "\"type\" : \"ref\""; + while ((index = line.indexOf(pattern, index)) != -1) { + count++; + index += pattern.length(); + } + return count; + }) + .sum(); + assertThat(refTypeCount).isEqualTo(4); // Should have 4 ResolvedReference objects } @Test public void testBoundExpressionRoundTripWithResolvedReference() { // Test that bound expressions with ResolvedReference can be serialized and maintain information - + // Create expressions using both ResolvedTransform and ResolvedReference - Expression originalExpr = Expressions.and( - Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), - Expressions.equal(Expressions.ref("data", 101), "test")); - + Expression originalExpr = + Expressions.and( + Expressions.equal(Expressions.bucket(Expressions.ref("id", 100), 8), 3), + Expressions.equal(Expressions.ref("data", 101), "test")); + // Bind the expression Expression bound = Binder.bind(STRUCT_TYPE, originalExpr, true); - + // Serialize with field IDs String jsonWithFieldIds = ExpressionParser.toJson(bound, true, true); - + // Verify the JSON structure contains complete ResolvedReference information assertThat(jsonWithFieldIds).contains("\"type\" : \"ref\""); assertThat(jsonWithFieldIds).contains("\"fieldId\" : 100"); assertThat(jsonWithFieldIds).contains("\"fieldId\" : 101"); - + // Note: The current parser doesn't support parsing ResolvedReference format back to expressions - // This test documents the serialization capability for external systems that need field ID information - + // This test documents the serialization capability for external systems that need field ID + // information + // Verify that the bound expression maintains the same field IDs after serialization BoundPredicate<?> boundPred = (BoundPredicate<?>) ((And) bound).left(); BoundTransform<?, ?> boundTransform = (BoundTransform<?, ?>) boundPred.term(); assertThat(boundTransform.ref().fieldId()).isEqualTo(100); - + BoundPredicate<?> boundDataPred = (BoundPredicate<?>) ((And) bound).right(); BoundReference<?> boundDataRef = (BoundReference<?>) boundDataPred.term(); assertThat(boundDataRef.fieldId()).isEqualTo(101); @@ -829,35 +840,36 @@ public class TestExpressionParserWithResolvedReference { @Test public void testBoundExpressionFieldIdPreservationAcrossAllTypes() { // Test that all data types preserve field IDs correctly in bound expression serialization - + // Create expressions for fields that support meaningful comparisons - Expression[] expressions = new Expression[] { - Expressions.equal(Expressions.ref("id", 100), 42L), - Expressions.equal(Expressions.ref("data", 101), "test"), - Expressions.equal(Expressions.ref("b", 102), true), - Expressions.equal(Expressions.ref("i", 103), 42), - Expressions.equal(Expressions.ref("l", 104), 42L), - Expressions.equal(Expressions.ref("f", 105), 3.14f), - Expressions.equal(Expressions.ref("d", 106), 3.14159), - Expressions.equal(Expressions.ref("s", 110), "test-string"), - Expressions.greaterThan(Expressions.ref("id", 100), 10L) - }; - + Expression[] expressions = + new Expression[] { + Expressions.equal(Expressions.ref("id", 100), 42L), + Expressions.equal(Expressions.ref("data", 101), "test"), + Expressions.equal(Expressions.ref("b", 102), true), + Expressions.equal(Expressions.ref("i", 103), 42), + Expressions.equal(Expressions.ref("l", 104), 42L), + Expressions.equal(Expressions.ref("f", 105), 3.14f), + Expressions.equal(Expressions.ref("d", 106), 3.14159), + Expressions.equal(Expressions.ref("s", 110), "test-string"), + Expressions.greaterThan(Expressions.ref("id", 100), 10L) + }; + for (Expression expr : expressions) { // Bind each expression Expression bound = Binder.bind(STRUCT_TYPE, expr, true); - + // Serialize with field IDs String json = ExpressionParser.toJson(bound, true, true); - + // Extract expected field ID from the original ResolvedReference UnboundPredicate<?> unboundPred = (UnboundPredicate<?>) expr; ResolvedReference<?> resolvedRef = (ResolvedReference<?>) unboundPred.term(); int expectedFieldId = resolvedRef.fieldId(); - + // Verify the field ID is preserved in the JSON assertThat(json).contains("\"fieldId\" : " + expectedFieldId); assertThat(json).contains("\"type\" : \"ref\""); } } -} \ No newline at end of file +}
