This is an automated email from the ASF dual-hosted git repository. xiazcy pushed a commit to branch steps-taking-traversal in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 9ca2f633490fb4efbdcd34ad914475523174deb3 Author: Yang Xia <[email protected]> AuthorDate: Fri Jun 12 00:14:52 2026 -0700 Add runtime child-traversal resolution to predicates Extend P, TextP, NotP, and the connective predicates (AndP/OrP) to carry a child traversal whose result is resolved per-traverser at runtime instead of a literal value. P.resolve() splits the traverser, seeds and runs the child traversal, and installs the result as the comparison value for the current test cycle. Semantics: - Scalar predicates (eq/neq/gt/lt/gte/lte) take the first result; an empty result cannot be satisfied and is flagged resolved-empty so steps short-circuit. - Collection predicates (within/without) resolve to a collection; an empty result resolves to an empty set so within(empty) is false and without(empty) is true, matching literal P.within([])/P.without([]) semantics. - Multi-traversal within/without combine the first result of each child. - AndP short-circuits resolution at the first child that resolves empty. - NotP exposes getWrapped() so traversal collection does not rely on negate(). GremlinLang serializes traversal-bearing predicates as op(traversal). --- .../gremlin/process/traversal/GremlinLang.java | 19 +- .../tinkerpop/gremlin/process/traversal/NotP.java | 28 +- .../tinkerpop/gremlin/process/traversal/P.java | 425 +++++++++++++++- .../tinkerpop/gremlin/process/traversal/TextP.java | 79 ++- .../gremlin/process/traversal/util/AndP.java | 29 ++ .../process/traversal/util/ConnectiveP.java | 18 + .../gremlin/process/traversal/util/OrP.java | 13 + .../GremlinLangTraversalRoundTripTest.java | 120 +++++ .../tinkerpop/gremlin/process/traversal/PTest.java | 10 +- .../gremlin/process/traversal/PTraversalTest.java | 535 +++++++++++++++++++++ 10 files changed, 1261 insertions(+), 15 deletions(-) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java index 3bd1cb5287..289e8a6974 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java @@ -317,7 +317,11 @@ public class GremlinLang implements Cloneable, Serializable { final StringBuilder sb = new StringBuilder(); if (p instanceof TextP) { sb.append("TextP.").append(p.getPredicateName()).append("("); - sb.append(argAsString(p.getValue())); + if (p.hasTraversal()) { + sb.append(argAsString(p.getTraversalValue())); + } else { + sb.append(argAsString(p.getValue())); + } } else if (p instanceof ConnectiveP) { // ConnectiveP gets some special handling because it's reduced to and(P, P, P) and we want it // generated the way it was written which was P.and(P).and(P) @@ -337,6 +341,19 @@ public class GremlinLang implements Cloneable, Serializable { } else if (p instanceof NotP) { sb.append("P.not("); sb.append(argAsString(p.negate())); // Wrap internal P in `P.not(%s)` + } else if (p.hasTraversal()) { + // Traversal-bearing predicate: serialize as P.op(traversalGremlinLang) + sb.append("P.").append(p.getPredicateName()).append("("); + if (p.getTraversalValues() != null) { + // Multi-traversal predicate (within/without with multiple traversals) + final List<Traversal.Admin<?, ?>> traversals = p.getTraversalValues(); + for (int i = 0; i < traversals.size(); i++) { + if (i > 0) sb.append(","); + sb.append(argAsString(traversals.get(i))); + } + } else { + sb.append(argAsString(p.getTraversalValue())); + } } else { sb.append("P.").append(p.getPredicateName()).append("("); sb.append(argAsString(p.getValue())); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java index f77f0f5974..aca1d241e4 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java @@ -59,17 +59,41 @@ public class NotP<V> extends P<V> { } /** - * Returns the original unwrapped P contained within this NotP, as double negation cancels out. + * Returns the inner predicate wrapped by this NotP. + */ + public P<V> getWrapped() { + return this.originalP; + } + + /** + * Returns the original unwrapped P, since double negation cancels out. + * @apiNote Functionally identical to {@link #getWrapped()}, but fulfills the {@link java.util.function.Predicate#negate()} contract. */ @Override public P<V> negate() { - return originalP; + return getWrapped(); } public P<V> clone() { return new NotP<>(this.originalP.clone()); } + @Override + public boolean hasTraversal() { + return super.hasTraversal() || this.originalP.hasTraversal(); + } + + @Override + public boolean isResolvedEmpty() { + return this.originalP.isResolvedEmpty(); + } + + @Override + public void resolve(final Traverser.Admin<?> traverser) { + super.resolve(traverser); + this.originalP.resolve(traverser); + } + /** * A NotPBiPredicate wraps a PBiPredicate and represents its negation. */ diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java index afc41b5681..5f0606bd07 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java @@ -20,9 +20,13 @@ package org.apache.tinkerpop.gremlin.process.traversal; import org.apache.commons.collections4.CollectionUtils; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet; import org.apache.tinkerpop.gremlin.process.traversal.util.AndP; +import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; import org.apache.tinkerpop.gremlin.process.traversal.util.OrP; +import org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator; import java.io.Serializable; import java.util.ArrayList; @@ -31,7 +35,10 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -49,10 +56,23 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { protected Map<String, V> variables = new HashMap<>(); protected Collection<V> literals = Collections.EMPTY_LIST; private boolean isCollection = false; + private boolean resolvedEmpty = false; + private Traversal.Admin<?, ?> traversalValue; + private List<Traversal.Admin<?, ?>> traversalValues; public P(final PBiPredicate<V, V> biPredicate, final V value) { - setValue(value); this.biPredicate = biPredicate; + // If the value is a DefaultGraphTraversal (the type created by __.xxx() anonymous traversals), + // treat it as a child traversal rather than a literal. This handles the case where Java's + // overload resolution picks P(BiPredicate, V) instead of P(BiPredicate, Traversal) when + // the caller passes a GraphTraversal. We specifically check for DefaultGraphTraversal + // rather than Traversal to avoid catching internal traversal types like ConstantTraversal, + // ValueTraversal, and IdentityTraversal which are used as literal values in P. + if (value instanceof org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal) { + this.traversalValue = ((Traversal<?, ?>) value).asAdmin(); + } else { + setValue(value); + } } public P(final PBiPredicate<V, V> biPredicate, final GValue<V> value) { @@ -81,6 +101,29 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { this.isCollection = isCollection; } + /** + * Constructs a {@code P} with a child traversal whose result is resolved at runtime against the current + * traverser. The literals and variables are left at their defaults and will be populated when + * {@link #resolve(Traverser.Admin)} is called. + */ + public P(final PBiPredicate<V, V> biPredicate, final Traversal.Admin<?, ?> traversalValue) { + this.biPredicate = biPredicate; + this.traversalValue = traversalValue; + } + + /** + * Constructs a {@code P} with multiple child traversals whose results are unioned at runtime against the + * current traverser. Only valid for collection predicates ({@link Contains#within}, {@link Contains#without}). + * The literals and variables are left at their defaults and will be populated when + * {@link #resolve(Traverser.Admin)} is called. + * + * @since 4.0.0 + */ + public P(final PBiPredicate<V, V> biPredicate, final List<Traversal.Admin<?, ?>> traversalValues) { + this.biPredicate = biPredicate; + this.traversalValues = traversalValues; + } + public PBiPredicate<V, V> getBiPredicate() { return this.biPredicate; } @@ -162,6 +205,10 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { result ^= this.variables.hashCode(); if (null != this.literals) result ^= this.literals.hashCode(); + if (null != this.traversalValue) + result ^= this.traversalValue.hashCode(); + if (null != this.traversalValues) + result ^= this.traversalValues.hashCode(); return result; } @@ -171,7 +218,9 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { ((P) other).getClass().equals(this.getClass()) && ((P) other).getBiPredicate().equals(this.biPredicate) && ((((P) other).variables == null && this.variables == null) || (((P) other).variables != null && ((P) other).variables.equals(this.variables))) && - ((((P) other).literals == null && this.literals == null) || (((P) other).literals != null && CollectionUtils.isEqualCollection(((P) other).literals, this.literals))); + ((((P) other).literals == null && this.literals == null) || (((P) other).literals != null && CollectionUtils.isEqualCollection(((P) other).literals, this.literals))) && + Objects.equals(((P) other).traversalValue, this.traversalValue) && + Objects.equals(((P) other).traversalValues, this.traversalValues); } @Override @@ -200,7 +249,17 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { public P<V> clone() { try { - return (P<V>) super.clone(); + final P<V> clone = (P<V>) super.clone(); + if (this.traversalValue != null) { + clone.traversalValue = this.traversalValue.clone(); + } + if (this.traversalValues != null) { + clone.traversalValues = new ArrayList<>(this.traversalValues.size()); + for (final Traversal.Admin<?, ?> tv : this.traversalValues) { + clone.traversalValues.add(tv.clone()); + } + } + return clone; } catch (final CloneNotSupportedException e) { throw new IllegalStateException(e.getMessage(), e); } @@ -224,6 +283,187 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { return results; } + /** + * Determines if this predicate holds a child traversal whose result is resolved at runtime. + */ + public boolean hasTraversal() { + return this.traversalValue != null || (this.traversalValues != null && !this.traversalValues.isEmpty()); + } + + /** + * Returns {@code true} if the most recent call to {@link #resolve(Traverser.Admin)} produced no results. + * Steps should check this after calling {@code resolve()} and short-circuit appropriately rather than + * calling {@link #test(Object)}, which would compare against {@code null}. + */ + public boolean isResolvedEmpty() { + return this.resolvedEmpty; + } + + /** + * Gets the child traversal value, if one was provided. Returns {@code null} when this predicate uses + * literal values or variables. + */ + public Traversal.Admin<?, ?> getTraversalValue() { + return this.traversalValue; + } + + /** + * Gets the list of child traversal values for multi-traversal predicates (e.g., {@code within(trav1, trav2)}). + * Returns {@code null} when this predicate uses a single traversal or literal values. + * + * @since 4.0.0 + */ + public List<Traversal.Admin<?, ?>> getTraversalValues() { + return this.traversalValues; + } + + /** + * Resolves the child traversal against the given traverser, replacing the traversal value with the + * resolved literal(s) for this test cycle. If no traversal is present, this method returns immediately. + * + * <p>For all predicates, only the first result from the child traversal is used, + * consistent with {@code by(traversal)} semantics. For collection predicates + * ({@link Contains#within}, {@link Contains#without}), the first result should be a + * {@link Collection} (e.g., produced by {@code fold()}).</p> + * + * <p>When multiple traversals are present (via {@link #traversalValues}), each traversal is evaluated + * independently, the first result from each is taken, and results are combined into a single collection. + * This is only valid for collection predicates ({@link Contains#within}, {@link Contains#without}).</p> + */ + @SuppressWarnings("unchecked") + public void resolve(final Traverser.Admin<?> traverser) { + if (this.traversalValues != null && !this.traversalValues.isEmpty()) { + resolveMultipleTraversals(traverser); + return; + } + + if (this.traversalValue == null) return; + + final Traversal.Admin<Object, Object> trav = (Traversal.Admin<Object, Object>) (Traversal.Admin) this.traversalValue; + prepareChildTraversal(traverser, trav); + + try { + if (!trav.hasNext()) { + // No results from the child traversal. For collection predicates (within/without) this is a + // legitimate empty set: within(empty) -> false, without(empty) -> true. Resolve to an empty + // collection and let Contains.test() apply the correct semantics rather than short-circuiting. + // For scalar predicates (eq/gt/lt/etc.) there is no comparison value, so flag as resolved-empty + // and let the step short-circuit (cannot satisfy). + this.literals = Collections.emptyList(); + if (this.biPredicate instanceof Contains) { + this.resolvedEmpty = false; + this.isCollection = true; + } else { + this.resolvedEmpty = true; + this.isCollection = false; + } + } else { + this.resolvedEmpty = false; + final Object firstResult = trav.next(); + if (this.biPredicate instanceof Contains) { + if (firstResult instanceof Collection) { + this.literals = (Collection<V>) firstResult; + } else { + this.literals = Collections.singletonList((V) firstResult); + } + this.isCollection = true; + } else { + setValue((V) firstResult); + } + } + } finally { + CloseableIterator.closeIterator(trav); + } + } + + /** + * Resolves multiple child traversals, taking the first result from each and combining into a collection. + * Only valid for collection predicates ({@link Contains}). + */ + @SuppressWarnings("unchecked") + private void resolveMultipleTraversals(final Traverser.Admin<?> traverser) { + final List<Object> allResults = new ArrayList<>(); + + for (final Traversal.Admin<?, ?> tv : this.traversalValues) { + final Traversal.Admin<Object, Object> trav = (Traversal.Admin<Object, Object>) (Traversal.Admin) tv; + prepareChildTraversal(traverser, trav); + + try { + if (trav.hasNext()) { + final Object firstResult = trav.next(); + if (firstResult instanceof Collection) { + allResults.addAll((Collection<?>) firstResult); + } else { + allResults.add(firstResult); + } + } + } finally { + CloseableIterator.closeIterator(trav); + } + } + + // Multi-traversal resolution is only valid for collection predicates (within/without). An empty + // combined result is a legitimate empty set, so resolve to an empty collection and let Contains.test() + // apply the correct semantics (within(empty) -> false, without(empty) -> true) instead of short-circuiting. + this.resolvedEmpty = false; + this.isCollection = true; + this.literals = allResults.isEmpty() + ? Collections.emptyList() + : (Collection<V>) (Collection<?>) allResults; + } + + /** + * Prepares a child traversal for evaluation by splitting the current traverser and seeding it. + */ + @SuppressWarnings("unchecked") + private static void prepareChildTraversal(final Traverser.Admin<?> traverser, final Traversal.Admin<Object, Object> trav) { + final Traverser.Admin<Object> split = (Traverser.Admin<Object>) traverser.split(); + split.setSideEffects(trav.getSideEffects()); + split.setBulk(1L); + trav.reset(); + trav.addStart(split); + } + + //////////////// predicate traversal utilities + + /** + * Recursively integrates all child traversals found in the predicate tree into the given parent step. + * Handles {@link ConnectiveP} (recurses into children) and {@link NotP} (recurses into wrapped predicate). + */ + public static void integrateTraversals(final P<?> p, final TraversalParent parent) { + if (p instanceof ConnectiveP) { + for (final P<?> child : ((ConnectiveP<?>) p).getPredicates()) { + integrateTraversals(child, parent); + } + } else if (p instanceof NotP) { + integrateTraversals(((NotP<?>) p).getWrapped(), parent); + } else if (p.getTraversalValue() != null) { + parent.integrateChild(p.getTraversalValue()); + } else if (p.getTraversalValues() != null) { + for (final Traversal.Admin<?, ?> tv : p.getTraversalValues()) { + parent.integrateChild(tv); + } + } + } + + /** + * Recursively collects all child traversals from a predicate tree. + * Handles {@link ConnectiveP} (recurses into children) and {@link NotP} (recurses into wrapped predicate). + */ + public static void collectTraversals(final P<?> p, final List<Traversal.Admin<?, ?>> traversals) { + if (p instanceof ConnectiveP) { + for (final P<?> child : ((ConnectiveP<?>) p).getPredicates()) { + collectTraversals(child, traversals); + } + } else if (p instanceof NotP) { + collectTraversals(((NotP<?>) p).getWrapped(), traversals); + } else if (p.getTraversalValue() != null) { + traversals.add(p.getTraversalValue()); + } else if (p.getTraversalValues() != null) { + traversals.addAll(p.getTraversalValues()); + } + } + //////////////// statics /** @@ -394,6 +634,8 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { * @since 3.0.0-incubating */ public static <V> P<V> within(final V... values) { + final P<V> traversalP = handleContainsVarargs(values, Contains.within, "within"); + if (traversalP != null) return traversalP; final V[] v = null == values ? (V[]) new Object[] { null } : (V[]) values; return P.within(Arrays.asList(v)); } @@ -427,6 +669,8 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { * @since 3.0.0-incubating */ public static <V> P<V> without(final V... values) { + final P<V> traversalP = handleContainsVarargs(values, Contains.without, "without"); + if (traversalP != null) return traversalP; final V[] v = null == values ? (V[]) new Object[] { null } : values; return P.without(Arrays.asList(v)); } @@ -453,6 +697,106 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { return new P(Contains.without, value); } + /** + * Determines if values are equal using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> eq(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.eq, traversalValue.asAdmin()); + } + + /** + * Determines if values are not equal using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> neq(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.neq, traversalValue.asAdmin()); + } + + /** + * Determines if a value is less than another using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> lt(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.lt, traversalValue.asAdmin()); + } + + /** + * Determines if a value is less than or equal to another using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> lte(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.lte, traversalValue.asAdmin()); + } + + /** + * Determines if a value is greater than another using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> gt(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.gt, traversalValue.asAdmin()); + } + + /** + * Determines if a value is greater than or equal to another using a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> gte(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Compare.gte, traversalValue.asAdmin()); + } + + /** + * Determines if a value is within the results of a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> within(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Contains.within, traversalValue.asAdmin()); + } + + /** + * Determines if a value is within the union of results from multiple child traversals resolved at runtime. + * Each traversal is evaluated independently and results are combined into a single collection. + * + * @since 4.0.0 + */ + public static <V> P<V> within(final Traversal<?, ?> first, final Traversal<?, ?> second, final Traversal<?, ?>... more) { + return containsTraversals(Contains.within, first, second, more); + } + + /** + * Determines if a value is not within the results of a child traversal resolved at runtime. + * + * @since 4.0.0 + */ + public static <V> P<V> without(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new P(Contains.without, traversalValue.asAdmin()); + } + + /** + * Determines if a value is not within the union of results from multiple child traversals resolved at runtime. + * Each traversal is evaluated independently and results are combined into a single collection. + * + * @since 4.0.0 + */ + public static <V> P<V> without(final Traversal<?, ?> first, final Traversal<?, ?> second, final Traversal<?, ?>... more) { + return containsTraversals(Contains.without, first, second, more); + } + /** * Determines if a value is of a type denoted by {@code GType}. * @@ -497,4 +841,79 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { public static <V> P<V> not(final P<V> predicate) { return predicate.negate(); } + + /** + * Handles varargs traversal detection for within/without. Returns a P if traversals were found, + * or null if the values are plain literals. + */ + @SuppressWarnings("unchecked") + private static <V> P<V> handleContainsVarargs(final V[] values, final PBiPredicate predicate, final String stepName) { + if (values != null && values.length == 1 && values[0] instanceof Traversal) { + final Traversal<?, ?> trav = (Traversal<?, ?>) values[0]; + ChildTraversalValidator.validate(trav.asAdmin()); + return new P(predicate, trav.asAdmin()); + } + if (values != null && values.length > 1 && allTraversals(values)) { + final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(values.length); + for (final V v : values) { + traversals.add(((Traversal<?, ?>) v).asAdmin()); + } + for (final Traversal.Admin<?, ?> tv : traversals) { + ChildTraversalValidator.validate(tv); + } + return new P(predicate, traversals); + } + if (values != null && values.length > 1 && anyTraversals(values)) { + throw new IllegalArgumentException( + "Cannot mix traversals and literal values in " + stepName + "(). " + + "Use " + stepName + "(__.constant(val1), __.constant(val2)) to wrap all values as traversals."); + } + return null; + } + + /** + * Creates a Contains predicate from multiple validated child traversals. + */ + @SuppressWarnings("unchecked") + private static <V> P<V> containsTraversals(final PBiPredicate predicate, final Traversal<?, ?> first, + final Traversal<?, ?> second, final Traversal<?, ?>... more) { + final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(2 + (more != null ? more.length : 0)); + traversals.add(first.asAdmin()); + traversals.add(second.asAdmin()); + if (more != null) { + for (final Traversal<?, ?> tv : more) { + traversals.add(tv.asAdmin()); + } + } + for (final Traversal.Admin<?, ?> tv : traversals) { + ChildTraversalValidator.validate(tv); + } + return new P(predicate, traversals); + } + + /** + * Checks if all elements in the array are {@link Traversal} instances (specifically + * {@link org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal}). + */ + private static <V> boolean allTraversals(final V[] values) { + for (final V v : values) { + if (!(v instanceof org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal)) { + return false; + } + } + return true; + } + + /** + * Checks if any element in the array is a {@link Traversal} instance (specifically + * {@link org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal}). + */ + private static <V> boolean anyTraversals(final V[] values) { + for (final V v : values) { + if (v instanceof org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal) { + return true; + } + } + return false; + } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java index d1d8c21019..c0bd155185 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java @@ -19,6 +19,7 @@ package org.apache.tinkerpop.gremlin.process.traversal; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator; import java.util.Collection; import java.util.Map; @@ -38,6 +39,16 @@ public class TextP extends P<String> { super(biPredicate, value); } + /** + * Constructs a {@code TextP} with a child traversal whose result is resolved at runtime against the + * current traverser. The traversal must produce a {@code String} result. + * + * @since 4.0.0 + */ + public TextP(final PBiPredicate<String, String> biPredicate, final Traversal.Admin<?, ?> traversalValue) { + super(biPredicate, traversalValue); + } + protected TextP(final PBiPredicate<String, String> biPredicate, final Collection<String> literals, final Map<String, String> variables) { super(biPredicate, literals, variables, false); } @@ -76,6 +87,16 @@ public class TextP extends P<String> { return new TextP(Text.startingWith, value); } + /** + * Determines if String does start with the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP startingWith(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.startingWith, traversalValue.asAdmin()); + } + /** * Determines if String does not start with the given value. * @@ -95,7 +116,17 @@ public class TextP extends P<String> { } /** - * Determines if String does start with the given value. + * Determines if String does not start with the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP notStartingWith(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.notStartingWith, traversalValue.asAdmin()); + } + + /** + * Determines if String does end with the given value. * * @since 3.4.0 */ @@ -104,7 +135,7 @@ public class TextP extends P<String> { } /** - * Determines if String does start with the given value. + * Determines if String does end with the given value. * * @since 3.8.0 */ @@ -113,7 +144,17 @@ public class TextP extends P<String> { } /** - * Determines if String does not start with the given value. + * Determines if String does end with the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP endingWith(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.endingWith, traversalValue.asAdmin()); + } + + /** + * Determines if String does not end with the given value. * * @since 3.4.0 */ @@ -122,7 +163,7 @@ public class TextP extends P<String> { } /** - * Determines if String does not start with the given value. + * Determines if String does not end with the given value. * * @since 3.8.0 */ @@ -130,6 +171,16 @@ public class TextP extends P<String> { return new TextP(Text.notEndingWith, value); } + /** + * Determines if String does not end with the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP notEndingWith(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.notEndingWith, traversalValue.asAdmin()); + } + /** * Determines if String does contain the given value. * @@ -148,6 +199,16 @@ public class TextP extends P<String> { return new TextP(Text.containing, value); } + /** + * Determines if String does contain the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP containing(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.containing, traversalValue.asAdmin()); + } + /** * Determines if String does not contain the given value. * @@ -165,6 +226,16 @@ public class TextP extends P<String> { public static TextP notContaining(final GValue<String> value) { return new TextP(Text.notContaining, value); } + + /** + * Determines if String does not contain the value resolved from a child traversal at runtime. + * + * @since 4.0.0 + */ + public static TextP notContaining(final Traversal<?, ?> traversalValue) { + ChildTraversalValidator.validate(traversalValue.asAdmin()); + return new TextP(Text.notContaining, traversalValue.asAdmin()); + } /** * Determines if String has a match with the given regex pattern. The TinkerPop reference implementation uses diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java index db5cced9ec..1275e08f04 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java @@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.process.traversal.util; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import java.io.Serializable; @@ -56,6 +57,34 @@ public final class AndP<V> extends ConnectiveP<V> { return new OrP<>(this.predicates); } + /** + * Resolves child predicates with short-circuiting. Because a conjunction fails as soon as any child + * predicate cannot be satisfied, resolution stops at the first child that resolves empty (i.e. a scalar + * predicate whose child traversal produced no comparison value). This avoids evaluating the remaining + * child traversals, which may be expensive. Collection predicates (within/without) never resolve empty + * (they resolve to an empty collection), so they do not trigger the short-circuit. + */ + @Override + public void resolve(final Traverser.Admin<?> traverser) { + // No super.resolve(): a connective predicate carries no child traversal of its own; only its + // operands do. Resolving each operand directly is sufficient (and enables the short-circuit below). + for (final P<V> p : this.predicates) { + if (p.hasTraversal()) { + p.resolve(traverser); + if (p.isResolvedEmpty()) return; + } + } + } + + @Override + public boolean isResolvedEmpty() { + // AND short-circuits: if any child resolved empty, the conjunction cannot be satisfied + for (final P<V> p : this.predicates) { + if (p.hasTraversal() && p.isResolvedEmpty()) return true; + } + return false; + } + @Override public String toString() { return "and(" + StringFactory.removeEndBrackets(this.predicates) + ")"; diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java index ef5a1142c3..fa277d2806 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java @@ -19,6 +19,7 @@ package org.apache.tinkerpop.gremlin.process.traversal.util; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import java.util.ArrayList; @@ -134,4 +135,21 @@ public abstract class ConnectiveP<V> extends P<V> { } return allGValues; } + + @Override + public boolean hasTraversal() { + if (super.hasTraversal()) return true; + for (final P<?> p : this.predicates) { + if (p.hasTraversal()) return true; + } + return false; + } + + @Override + public void resolve(final Traverser.Admin<?> traverser) { + super.resolve(traverser); + for (final P<?> p : this.predicates) { + p.resolve(traverser); + } + } } \ No newline at end of file diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java index 2a0c27c7af..0a054033e9 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java @@ -57,6 +57,19 @@ public final class OrP<V> extends ConnectiveP<V> { return new AndP<>(this.predicates); } + @Override + public boolean isResolvedEmpty() { + // OR short-circuits: only empty if ALL traversal-bearing children resolved empty + boolean anyTraversal = false; + for (final P<V> p : this.predicates) { + if (p.hasTraversal()) { + anyTraversal = true; + if (!p.isResolvedEmpty()) return false; + } + } + return anyTraversal; + } + @Override public String toString() { return "or(" + StringFactory.removeEndBrackets(this.predicates) + ")"; diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java new file mode 100644 index 0000000000..391938b9ca --- /dev/null +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.language.grammar.GremlinAntlrToJava; +import org.apache.tinkerpop.gremlin.language.grammar.GremlinQueryParser; +import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; +import org.junit.Test; + +import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal; +import static org.junit.Assert.assertEquals; + +/** + * Property 7: GremlinLang serialization round-trip preserves traversal arguments. + * <p> + * For any step containing a child traversal argument, serializing to GremlinLang and parsing back + * SHALL produce a structurally equivalent traversal. + * <p> + * <b>Validates: Requirements 1.3, 3.6, 4.5, 6.5, 7.3</b> + */ +public class GremlinLangTraversalRoundTripTest { + + private static final GraphTraversalSource g = traversal().with(EmptyGraph.instance()); + + /** + * Serializes a traversal to GremlinLang, parses it back, and verifies structural equivalence + * by comparing the GremlinLang output of both the original and the round-tripped traversal. + */ + private void assertRoundTrip(final Traversal<?, ?> traversal) { + final String originalGremlin = traversal.asAdmin().getGremlinLang().getGremlin(); + + // Parse the GremlinLang string back into a traversal + final GremlinAntlrToJava antlr = new GremlinAntlrToJava(); + final Object parsed = GremlinQueryParser.parse(originalGremlin, antlr); + + // Get the GremlinLang of the parsed traversal + final String roundTrippedGremlin = ((Traversal<?, ?>) parsed).asAdmin().getGremlinLang().getGremlin(); + + assertEquals("GremlinLang round-trip should preserve traversal structure for: " + originalGremlin, + originalGremlin, roundTrippedGremlin); + } + + @Test + public void shouldRoundTripHasWithTraversalValue() { + // g.V().has("name", __.values("x")) + assertRoundTrip(g.V().has("name", __.values("x"))); + } + + @Test + public void shouldRoundTripHasWithPredicateTraversal() { + // g.V().has("name", P.eq(__.values("x"))) + assertRoundTrip(g.V().has("name", P.eq(__.values("x").asAdmin()))); + } + + @Test + public void shouldRoundTripVWithTraversal() { + // g.V().V(__.select("ids")) + assertRoundTrip(g.V().V(__.select("ids"))); + } + + @Test + public void shouldRoundTripPropertyWithTraversal() { + // g.V().property("key", __.select("val")) + assertRoundTrip(g.V().property("key", __.select("val"))); + } + + @Test + public void shouldRoundTripHasWithConstantTraversal() { + // g.V().has("name", __.constant("marko")) + assertRoundTrip(g.V().has("name", __.constant("marko"))); + } + + @Test + public void shouldRoundTripLiteralHasUnchanged() { + // Backward compatibility: literal has() should still round-trip correctly + assertRoundTrip(g.V().has("name", "marko")); + } + + @Test + public void shouldRoundTripLiteralPredicateUnchanged() { + // Backward compatibility: literal P.eq() should still round-trip correctly + assertRoundTrip(g.V().has("name", P.eq("marko"))); + } + + @Test + public void shouldRoundTripHasWithPGtTraversal() { + // g.V().has("age", P.gt(__.constant(30))) + assertRoundTrip(g.V().has("age", P.gt(__.constant(30).asAdmin()))); + } + + @Test + public void shouldRoundTripEWithTraversal() { + // g.V().E(__.select("edgeIds")) + assertRoundTrip(g.V().E(__.select("edgeIds"))); + } + + @Test + public void shouldRoundTripPropertyWithMapTraversal() { + // g.V().property(__.V().project("a").by("name")) - the Map-producing form + assertRoundTrip(g.V().property(__.V().project("a").by("name"))); + } +} diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java index 4a8959a1ee..80f65c59db 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java @@ -65,8 +65,8 @@ public class PTest { {P.eq(-0), +0, true}, {P.eq(0), 1, false}, {P.eq(0), null, false}, - {P.eq(null), null, true}, - {P.eq(null), 0, false}, + {P.eq((Object) null), null, true}, + {P.eq((Object) null), 0, false}, {P.eq(Double.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, false}, {P.eq(Float.POSITIVE_INFINITY), Float.NEGATIVE_INFINITY, false}, {P.eq(Float.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, false}, @@ -82,8 +82,8 @@ public class PTest { {P.neq(-0), +0, false}, {P.neq(0), 1, true}, {P.neq(0), null, true}, - {P.neq(null), null, false}, - {P.neq(null), 0, true}, + {P.neq((Object) null), null, false}, + {P.neq((Object) null), 0, true}, {P.neq(Double.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, true}, {P.neq(Float.POSITIVE_INFINITY), Float.NEGATIVE_INFINITY, true}, {P.neq(Float.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, true}, @@ -468,7 +468,7 @@ public class PTest { assertTrue(predicate.test(null)); assertFalse(predicate.test(INITIAL_VALUE)); assertEquals("eq", predicate.toString()); - assertEquals(predicate, P.eq(null)); + assertEquals(predicate, P.eq((Object) null)); assertNotEquals(predicate, P.eq(INITIAL_VALUE)); } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java new file mode 100644 index 0000000000..d52b45ae49 --- /dev/null +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java @@ -0,0 +1,535 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.gremlin.process.traversal; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_O_Traverser; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests for traversal-aware behavior in {@link P}, covering traversal detection accuracy + * and single-value predicate rejection of multiple traversal results. + */ +@RunWith(Enclosed.class) +public class PTraversalTest { + + /** + * Property 10: Traversal detection in predicates is accurate. + * <p> + * For any P instance containing a child traversal, {@code P.hasTraversal()} SHALL return true. + * For any P instance containing only literal values or GValue variables, + * {@code P.hasTraversal()} SHALL return false. + * <p> + * <b>Validates: Requirements 9.4</b> + */ + public static class TraversalDetectionTest { + + @Test + public void shouldDetectTraversalInComparisonPredicate() { + final P<Object> p = P.eq(__.identity().asAdmin()); + assertThat(p.hasTraversal(), is(true)); + } + + @Test + public void shouldDetectTraversalInCollectionPredicate() { + final P<Object> p = P.within(__.inject(1, 2, 3).asAdmin()); + assertThat(p.hasTraversal(), is(true)); + } + + @Test + public void shouldNotDetectTraversalInLiteralPredicate() { + final P<String> p = P.eq("value"); + assertThat(p.hasTraversal(), is(false)); + } + + @Test + public void shouldNotDetectTraversalInGValuePredicate() { + final P<String> p = P.eq(GValue.of("x", "value")); + assertThat(p.hasTraversal(), is(false)); + } + + @Test + public void shouldReturnTraversalValueWhenPresent() { + final Traversal.Admin<?, ?> traversal = __.inject(42).asAdmin(); + final P<Object> p = P.eq(traversal); + assertThat(p.getTraversalValue() == traversal, is(true)); + } + + @Test + public void shouldReturnNullTraversalValueForLiteral() { + final P<String> p = P.eq("value"); + assertThat(p.getTraversalValue() == null, is(true)); + } + } + + /** + * Property 3: Single-value predicate rejects multiple traversal results. + * <p> + * For any single-value predicate (eq, neq, gt, lt, gte, lte) and any child traversal that produces + * more than one result, the predicate SHALL throw an IllegalArgumentException. + * For any collection predicate (within, without) and any child traversal producing multiple results, + * the predicate SHALL accept all results as the collection value. + * <p> + * <b>Validates: Requirements 2.5</b> + */ + public static class SingleValuePredicateRejectionTest { + + private Traverser.Admin<?> createTraverser(final Object value) { + return new B_O_Traverser<>(value, 1L); + } + + // --- Single-value predicates should throw on multiple results --- + + // --- Single-value predicates take first result, ignore extras (consistent with by()) --- + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForEq() { + final P<Object> p = P.eq(__.union(__.constant(1), __.constant(2)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForNeq() { + final P<Object> p = P.neq(__.union(__.constant(1), __.constant(2)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(false)); + assertThat(p.test(2), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForGt() { + final P<Object> p = P.gt(__.union(__.constant(10), __.constant(20)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(11), is(true)); + assertThat(p.test(10), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForLt() { + final P<Object> p = P.lt(__.union(__.constant(10), __.constant(20)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(9), is(true)); + assertThat(p.test(10), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForGte() { + final P<Object> p = P.gte(__.union(__.constant(10), __.constant(20)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(10), is(true)); + assertThat(p.test(9), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldTakeFirstResultForLte() { + final P<Object> p = P.lte(__.union(__.constant(10), __.constant(20)).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(10), is(true)); + assertThat(p.test(11), is(false)); + } + + // --- Collection predicates should accept multiple results --- + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptMultipleResultsForWithin() { + // within(traversal) takes first result only. Use fold() to get a collection. + final P<Object> p = P.within(__.inject(1, 2, 3).fold().asAdmin()); + p.resolve(createTraverser("start")); + // After resolve, the predicate should have the collection value and be testable + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(true)); + assertThat(p.test(3), is(true)); + assertThat(p.test(4), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptMultipleResultsForWithout() { + // without(traversal) takes first result only. Use fold() to get a collection. + final P<Object> p = P.without(__.inject(1, 2, 3).fold().asAdmin()); + p.resolve(createTraverser("start")); + // After resolve, without should exclude the resolved values + assertThat(p.test(1), is(false)); + assertThat(p.test(2), is(false)); + assertThat(p.test(3), is(false)); + assertThat(p.test(4), is(true)); + } + + // --- Single-value predicates should succeed with exactly one result --- + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptSingleResultForEq() { + final P<Object> p = P.eq(__.constant(42).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(42), is(true)); + assertThat(p.test(99), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptSingleResultForNeq() { + final P<Object> p = P.neq(__.constant(42).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(42), is(false)); + assertThat(p.test(99), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptSingleResultForGt() { + final P<Object> p = P.gt(__.constant(10).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(11), is(true)); + assertThat(p.test(10), is(false)); + assertThat(p.test(9), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptSingleResultForLt() { + final P<Object> p = P.lt(__.constant(10).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(9), is(true)); + assertThat(p.test(10), is(false)); + assertThat(p.test(11), is(false)); + } + + // --- Collection predicates should also work with single result --- + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptSingleResultForWithin() { + final P<Object> p = P.within(__.constant(42).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(42), is(true)); + assertThat(p.test(99), is(false)); + } + } + + /** + * Tests for multi-traversal support in within() and without(). + * <p> + * When multiple traversals are passed to within(trav1, trav2, ...), each traversal is evaluated + * independently and results are unioned into a single collection for the Contains test. + */ + public static class MultiTraversalTest { + + private Traverser.Admin<?> createTraverser(final Object value) { + return new B_O_Traverser<>(value, 1L); + } + + // --- Detection --- + + @Test + public void shouldDetectMultipleTraversalsInWithin() { + final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + assertThat(p.hasTraversal(), is(true)); + } + + @Test + public void shouldDetectMultipleTraversalsInWithout() { + final P<Object> p = P.without(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + assertThat(p.hasTraversal(), is(true)); + } + + @Test + public void shouldReturnTraversalValuesListForMultiTraversal() { + final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + assertThat(p.getTraversalValues() != null, is(true)); + assertThat(p.getTraversalValues().size(), is(2)); + } + + @Test + public void shouldReturnNullTraversalValueForMultiTraversal() { + // Single traversalValue should be null when using multi-traversal form + final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + assertThat(p.getTraversalValue() == null, is(true)); + } + + // --- Resolution and testing --- + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveMultipleTraversalsForWithin() { + // within(__.constant(1), __.constant(2)) should union results: [1, 2] + final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(true)); + assertThat(p.test(3), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveMultipleTraversalsForWithout() { + // without(__.constant(1), __.constant(2)) should union results: [1, 2] + final P<Object> p = P.without(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(false)); + assertThat(p.test(2), is(false)); + assertThat(p.test(3), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveMultipleTraversalsWithMultipleResultsEach() { + // within(__.inject(1,2).fold(), __.inject(3,4).fold()) - each fold() produces a list, unpacked into union + final P<Object> p = P.within(__.inject(1, 2).fold().asAdmin(), __.inject(3, 4).fold().asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(true)); + assertThat(p.test(3), is(true)); + assertThat(p.test(4), is(true)); + assertThat(p.test(5), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldHandleEmptyResultFromOneTraversal() { + // within(__.inject(1,2).fold(), __.limit(0)) where second produces nothing + // Should still match on results from first traversal + final P<Object> p = P.within(__.inject(1, 2).fold().asAdmin(), __.limit(0).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.isResolvedEmpty(), is(false)); + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(true)); + assertThat(p.test(3), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldHandleAllEmptyResults() { + // within(__.limit(0), __.limit(0)) where both produce nothing. A collection predicate resolves to + // an empty set rather than flagging resolved-empty, so within() simply matches nothing. + final P<Object> p = P.within(__.limit(0).asAdmin(), __.limit(0).asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.isResolvedEmpty(), is(false)); + assertThat(p.test(1), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveThreeTraversals() { + // within(__.constant("a"), __.constant("b"), __.constant("c")) + final P<Object> p = P.within(__.constant("a").asAdmin(), __.constant("b").asAdmin(), __.constant("c").asAdmin()); + p.resolve(createTraverser("start")); + assertThat(p.test("a"), is(true)); + assertThat(p.test("b"), is(true)); + assertThat(p.test("c"), is(true)); + assertThat(p.test("d"), is(false)); + } + + // --- Clone independence --- + + @SuppressWarnings("unchecked") + @Test + public void shouldCloneMultiTraversalPredicate() { + final P<Object> original = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin()); + final P<Object> clone = original.clone(); + + // Clone should have independent traversal values + assertThat(clone.hasTraversal(), is(true)); + assertThat(clone.getTraversalValues() != null, is(true)); + assertThat(clone.getTraversalValues().size(), is(2)); + + // Modifying clone should not affect original + clone.resolve(createTraverser("start")); + assertThat(clone.test(1), is(true)); + // Original should still be unresolved (literals empty) + assertThat(original.getTraversalValues().size(), is(2)); + } + + // --- Varargs detection --- + + @SuppressWarnings("unchecked") + @Test + public void shouldDetectMultipleTraversalsInVarargs() { + // This tests the varargs path: P.within(trav1, trav2) going through within(V... values) + final Traversal<?, ?> trav1 = __.constant(10); + final Traversal<?, ?> trav2 = __.constant(20); + final P<Object> p = (P<Object>) P.within(trav1, trav2); + assertThat(p.hasTraversal(), is(true)); + assertThat(p.getTraversalValues() != null, is(true)); + assertThat(p.getTraversalValues().size(), is(2)); + p.resolve(createTraverser("start")); + assertThat(p.test(10), is(true)); + assertThat(p.test(20), is(true)); + assertThat(p.test(30), is(false)); + } + + // --- collectTraversals and integrateTraversals --- + + @Test + public void shouldCollectTraversalsFromMultiTraversalPredicate() { + final P<Object> p = P.within(__.constant(1).asAdmin(), __.constant(2).asAdmin(), __.constant(3).asAdmin()); + final java.util.List<Traversal.Admin<?, ?>> collected = new java.util.ArrayList<>(); + P.collectTraversals(p, collected); + assertThat(collected.size(), is(3)); + } + } + + /** + * Covers the empty-result semantics of collection (within/without) versus scalar predicates and the + * behavior of connective predicates (AndP/OrP) and NotP when their operands carry child traversals. + * <p> + * Regression coverage for: within(empty) -> false, without(empty) -> true (collection predicates resolve + * to an empty collection rather than short-circuiting), scalar predicates remain resolved-empty, and the + * shared-state safety of resolving the same predicate across many traversers sequentially. + */ + public static class EmptyResolutionAndConnectiveTest { + + private Traverser.Admin<?> createTraverser(final Object value) { + return new B_O_Traverser<>(value, 1L); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldPassWithoutWhenTraversalResolvesEmpty() { + // without(empty set) -> nothing to exclude -> everything passes + final P<Object> p = P.without(__.<Object>limit(0).asAdmin()); + p.resolve(createTraverser("anything")); + assertThat(p.isResolvedEmpty(), is(false)); + assertThat(p.test("anything"), is(true)); + assertThat(p.test(42), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldFailWithinWhenTraversalResolvesEmpty() { + // within(empty set) -> nothing matches -> everything fails + final P<Object> p = P.within(__.<Object>limit(0).asAdmin()); + p.resolve(createTraverser("anything")); + assertThat(p.isResolvedEmpty(), is(false)); + assertThat(p.test("anything"), is(false)); + assertThat(p.test(42), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldPassWithoutWhenFoldedTraversalIsEmpty() { + // explicit empty collection via fold() of nothing + final P<Object> p = P.without(__.<Object>limit(0).fold().asAdmin()); + p.resolve(createTraverser("anything")); + assertThat(p.test("anything"), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldFailWithinWhenMultiTraversalResolvesAllEmpty() { + final P<Object> p = P.within(__.<Object>limit(0).asAdmin(), __.<Object>limit(0).asAdmin()); + p.resolve(createTraverser("anything")); + assertThat(p.isResolvedEmpty(), is(false)); + assertThat(p.test(1), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldRemainResolvedEmptyForScalarPredicateWithEmptyTraversal() { + // eq(empty) has no comparison value -> resolved empty so the step can short-circuit + final P<Object> p = P.eq(__.<Object>limit(0).asAdmin()); + p.resolve(createTraverser("anything")); + assertThat(p.isResolvedEmpty(), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveAndPWithTraversalOperands() { + final P<Object> p = (P<Object>) (P) P.gt(__.constant(10).asAdmin()).and(P.lt(__.constant(20).asAdmin())); + p.resolve(createTraverser("start")); + assertThat(p.test(15), is(true)); + assertThat(p.test(10), is(false)); + assertThat(p.test(25), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldShortCircuitAndPResolveWhenScalarChildEmpty() { + // first child resolves empty (no comparison value); AndP cannot be satisfied + final P<Object> p = (P<Object>) (P) P.eq(__.<Object>limit(0).asAdmin()).and(P.gt(__.constant(5).asAdmin())); + p.resolve(createTraverser("start")); + assertThat(p.isResolvedEmpty(), is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveOrPWithTraversalOperands() { + final P<Object> p = (P<Object>) (P) P.eq(__.constant(1).asAdmin()).or(P.eq(__.constant(2).asAdmin())); + p.resolve(createTraverser("start")); + assertThat(p.test(1), is(true)); + assertThat(p.test(2), is(true)); + assertThat(p.test(3), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldReturnFalseForOrPWhenResolvedNonEmptyButNonMatching() { + // a non-empty within() resolution that simply does not contain the test value must still return false + final P<Object> p = (P<Object>) (P) P.within(__.inject(1, 2, 3).fold().asAdmin()) + .or(P.within(__.inject(4, 5, 6).fold().asAdmin())); + p.resolve(createTraverser("start")); + assertThat(p.test(2), is(true)); + assertThat(p.test(5), is(true)); + assertThat(p.test(9), is(false)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldResolveNotPWrappingTraversalPredicate() { + final P<Object> p = P.eq(__.constant(42).asAdmin()).negate(); + p.resolve(createTraverser("start")); + assertThat(p.test(42), is(false)); + assertThat(p.test(99), is(true)); + } + + @Test + public void shouldExposeWrappedPredicateFromNotP() { + final P<Object> inner = P.eq(__.constant(42).asAdmin()); + final NotP<Object> notP = new NotP<>(inner); + assertThat(notP.getWrapped() == inner, is(true)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldProduceConsistentResultsAcrossManySequentialResolves() { + // resolve() mutates the predicate per traverser; verify repeated resolve+test cycles are stable + final P<Object> p = P.gt(__.constant(10).asAdmin()); + for (int i = 0; i < 1000; i++) { + p.resolve(createTraverser("start" + i)); + assertThat(p.test(11), is(true)); + assertThat(p.test(5), is(false)); + } + } + } +}
