Predicates: Comparison and Resolution
This page lists where Graphersal's predicates (P.eq, P.neq, P.lt, P.lte, P.gt, P.gte,
P.within, P.without, P.inside, P.outside, P.between), their composition (and, or,
not, negate) and the id order of elements differ from Apache TinkerPop 3.8.2 or settle what
TinkerPop leaves open. The Deviations table at the end collects them.
One comparison core
All of these predicates compare through one rule set (TinkerPop's semantics/Comparability).
- Numbers compare by value across
int64andfloat64;NaNis incomparable with everything, itself included. - Strings compare with strings, booleans with booleans (
false < true), UUIDs with UUIDs,nullonly withnull. - A string is never read as a number, a date or a UUID.
- Incomparable means false, never an error. Every test except
neqis false for an incomparable pair. A vertex, edge, path or collection on the stream against a number is false as well; for exampleg.V().where(P.eq(111))is an empty result, not a cast error.
g.V().where(P.eq(111)).count().next() // 0, no error
g.V().has("age", P.gt("x")).count().next() // 0, a number never compares with a string
Text predicates match strings only
TextP.startingWith, endingWith, containing and their negations (also spelled
P.startingWith(..)) test a string. Any other value on the stream (a number, a UUID, a list from
fold(), a map, a path, a vertex or an edge) is not one, so the predicate is false and its
negation true. This is never an error, matching TinkerPop, where TextP only matches a String:
g.V().values("age").is(TextP.startingWith("2")).count().next() // 0
g.V().fold().is(TextP.startingWith("a")).count().next() // 0
g.V().fold().is(TextP.notStartingWith("a")).count().next() // 1
A property handle (values("name")) and a property element (properties("name")) are tested by
their stored value.
Regular expressions
TextP.regex(pattern) holds for a string in which pattern matches somewhere; TextP.notRegex(pattern)
is its complement (also spelled not_regex, and bare regex(..)/notRegex(..)). Like the other text
predicates, a value that is not a string makes regex false and notRegex true. The match is
unanchored, as Java's Matcher.find() is; write ^...$ to match the whole string.
g.V().has("name", TextP.regex("^[jp]")).values("name").to_list() // josh, peter
g.V().has("name", TextP.regex("(?i)^MAR")).values("name").to_list() // marko
Deviation from TinkerPop: Graphersal compiles the expression with the Rust
regex crate and uses that crate's dialect, not
Java's java.util.regex. Look at the crate's syntax page for what an expression may contain.
Matching takes linear time in the input. The expression is compiled once, when the predicate is
built: an invalid one is a script error (ValueError::InvalidRegex) before the traversal runs. In
Rust the predicate is P::regex("^mar")? / P::not_regex(..)?.
Resolution of a string operand
where(P): a bare string operand ofeq/neq/lt/lte/gt/gte/within/withoutis resolved in this order: a path label (as("a")), then a side effect (aggregate,store,withSideEffect), then the literal string. A side effect holding a collection compares as a whole value foreq/neqand is false for the ordering predicates. An unknown name stays a literal.has(key, P),is(P),hasValue(P),all(P),any(P): operands are always literal values; they never resolve to a label or side effect, so a name that exists as a side-effect key cannot change the meaning of a property filter.eqwith a string resolves a step label or a vertex id first (kept from the original implementation), then compares literally.
hasId(P), hasLabel(P), hasKey(P)
hasId(P.neq("1")) and its siblings evaluate the predicate instead of stringifying it. A lone
P.eq(x)/P.within(xs) is rewritten to the literal step so source-filter pushdown still applies.
- Operands are always literal. No step label or side effect is consulted, so a label named like an id cannot shadow it.
- Label and key sets. A vertex carries a set of labels, a map a set of keys. A positive
predicate (
eq,within,lt, ...) holds if any label or key satisfies it; a negated one (neq,without,notStartingWith, ...) holds only if all do. For a single-label vertex this is TinkerPop's behaviour. - A predicate mixed with other arguments (
hasId(P.eq("1"), "2")) is anArgumentMismatch. - Ids are strings. An order predicate (
P.gt,P.gte,P.lt,P.lte,P.between,P.inside,P.outside) with a number operand inhasId(P)/has(T.id, P)is an error before the query runs, not a filter that silently matches nothing; the help shows the string form. Compare with a string instead,hasId(P.gt("3")), and mind the string order:"10"sorts before"9". Equality forms (hasId(3),hasId(P.eq(3)),hasId(P.neq(3)),hasId(P.within(1, 2)),hasId(P.without(1, 2)), also nested as inhasId(P.eq(1).or(P.eq(2)))) take numbers and compare their literal text, in the DSL and the Rust API (has_id_p(P::Neq(3))):3matches the id"3", a float3.0the id"3.0".
g.V().hasId(P.neq("1")).count().next() // 5
g.V().hasId(P.gt("3")).count().next() // 3 ("4", "5", "6")
g.V().hasId(P.without(1, 2)).count().next() // 4
g.V().hasLabel(P.within("person", "x")).count().next() // 4
Element order
- TinkerPop: vertices and edges order by id; its ids are typically numbers, so
9sorts before10. - Graphersal: ids are stored as strings and compared as strings (a string is never parsed
into a number), so
"10"sorts before"9". Elements with equal or missing ids fall back to a structural comparison.
g.V().order().by(T.id).id().toList() // "1", "2", ... ; with ids up to 10: "1", "10", "2", ...
Composing predicates: and, or, not, negate
g.V().has("age", P.gt(18).and(P.lt(30)).or(P.gt(35))).values("name").toList() // marko, vadas
g.V().has("name", TextP.containing("o").and(P.lt("m"))).values("name").toList() // lop, josh
g.V().hasLabel(P.not(P.eq("person"))).count().next() // 2
g.V("1").out().aggregate("a").out().where(P.not(P.within("a"))).values("name").toList()
p.and(q)andp.or(q)are binary and left-associative:a.and(b).or(c)means(a and b) or c,a.or(b).and(c)means(a or b) and c. Evaluation is left to right and short-circuits, as TinkerPop'sAndP/OrPdo:P.eq(1).or(q)never evaluatesqfor a value equal to 1, so a runtime error on the right is not raised when the left side decides.P.not(p)(alsoP::not(p)and a barenot(p)) andp.negate()are the complement of the result for the same stream value.TextPpredicates compose withPones.- The logic is two-valued. An incomparable pair is plain
false(see above), soP.lt(NaN)is the "error" column of theComparabilityscenarios and reduces tofalse; there is no third value. - Over a label or key set (
hasLabel(P),hasKey(P)) a leaf keeps the rule above (positive: any, negated: all); a composite combines the set results:andis&&,oris||,notis!of the child's set result. - In
where(P)each child resolves a string operand on its own (step label, then side effect, then literal), and the path analysis records the union of the labels of the children (P.eq("a").or(P.eq("d"))recordsaandd). - A composite is never pushed into an index or a source filter (
.profile()showshas("age", P.gt(27).and(P.lt(33)))with no optimizer rule named); the results are identical withg.with("optimizer.disabled", [...]). - A non-predicate operand (
P.gt(1).and(__.out()),P.not("x")) is anArgumentMismatchwhose help names the predicate and the traversal combinators (__.and(..),__.or(..)). - Not part of this feature: the infix steps
g.V().has(..).and().has(..)(TinkerPop'sConnectiveStrategy) and the two-argumentwhere("a", P)withby()modulators.
Type predicates
P.typeOf(GType.X) keeps a value of the given type (GType.STRING, GType.LONG, GType.NUMBER,
GType.LIST, GType.VERTEX, ...). TinkerPop 3.8 also accepts a type name, and so does
Graphersal, for a fixed list of names:
g.V().values("name").is(P.typeOf("String")).count().next() // 6
g.V().values("age").is(typeOf("Long")).count().next() // 4
g.V().values("age").is(P.typeOf("Byte")) // script error: unknown type name
| Name | Token |
|---|---|
String | GType.STRING |
Integer, Long | GType.LONG (every integer is an int64) |
Double, Float | GType.DOUBLE (every float is a float64) |
Boolean, UUID, Number, List, Map, Vertex, Edge, Path, Graph | the token of the same name |
Both forms take the same values in both spellings (P.typeOf(GType.LONG), P::typeOf(GType::LONG),
typeOf("Long") select the same ages). The names are Java's simple class names, as TinkerPop
registers them, not the token names: "LONG" is not a type name, in TinkerPop either.
The names are case-sensitive. Any other name is a script error (ArgumentMismatch listing the
accepted names), as TinkerPop rejects a name it has not registered. In Rust, the same lookup is
GType::from_type_name.
Deviations
| Area | TinkerPop behaviour | Graphersal behaviour | Why | Scenarios affected |
|---|---|---|---|---|
P.typeOf(name) | any name in TinkerPop's registered-type cache, custom types included | a fixed list of names (above) | Graphersal has no type registry; one width per number kind | none (the suite uses "String" and an unregistered name) |
negate() / P.not(p) | Compare.negate() flips the operator (gt becomes lte), so the result differs from the complement for incomparable operands | The plain complement of the result: P.not(P.gt(1)) is true for "foo" and NaN | One rule for every predicate (text, typeOf, composites); operator flipping would need a per-variant inverse | None (every vendored scenario negates a comparable number) |
Error value in and/or | Comparability scenarios call a NaN comparison an "error" column | There is no error value: incomparable is false, the logic is two-valued | The expected results reduce to boolean and/or | 18 Comparability |
| Numeric widths in the test surface | byte, short, BigInteger, BigDecimal are distinct types | Only int64/float64 exist; the harness maps 1b/1s/1n to an integer and 1m to a float (documented in tests/tinkerpop/README.md) | Widths cannot be told apart; equality is by value | 8 Equality, Sum, AsNumber, Inject with width literals |
| Composites and indexes | n/a | A composite predicate is not index-pushable | Only a single comparison can be folded into a range | none |
| Incomparable pairs | Some comparisons raise | false for every test but neq, never an error | See "One comparison core" | Comparability, where(111) on a vertex |
| Element id order | Numeric ids order numerically | Ids compare as strings, "10" sorts before "9"; an order predicate on a number in hasId(P)/has(T.id, P) (P.gt(3)) is an error naming the string form P.gt("3") | A string is never parsed into a number; a number comparison against a string id could never hold | order().by(T.id) on ids with different digit counts |
null | Property null handling depends on the @AllowNullPropertyValues flavour | null is a real stored value | Graphersal has a null type | AddVertex/AddEdge/MergeEdge null-property scenarios |
| Property cardinality | list/set properties | One value per property key | By design | @MultiProperties scenarios |