Comparators#
EQL features a bunch of comparators built into the language: ==, !=, <, <=, >, >=, and membership via in/contains.
They are optimized and can be negated and composed with logical operators.
from dataclasses import dataclass
from typing_extensions import List
from krrood.entity_query_language.entity import (
entity, an, let, Symbol,
in_, contains, not_, and_, or_,
)
@dataclass
class Body(Symbol):
name: str
weight: int
tags: List[str]
@dataclass
class World:
id_: int
bodies: List[Body]
world = World(
1,
[
Body("Container1", 10, ["metal", "blue"]),
Body("Container2", 15, ["plastic", "red"]),
Body("Handle1", 5, ["metal"]),
Body("Handle2", 7, ["wood", "brown"]),
],
)
Equality and inequality: == and !=#
Use Python’s comparison operators. EQL overloads these on symbolic variables to produce comparator nodes.
b = let(Body, domain=world.bodies)
query = an(entity(b, b.name == "Container1"))
print(*query.evaluate(), sep="\n")
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Inequality != works similarly:
b = let(Body, domain=world.bodies)
query = an(entity(b, b.name != "Container1"))
print(*query.evaluate(), sep="\n")
# => all bodies except the one with name == 'Container1'
Body(name='Container2', weight=15, tags=['plastic', 'red'])
Body(name='Handle1', weight=5, tags=['metal'])
Body(name='Handle2', weight=7, tags=['wood', 'brown'])
You can compare attributes between two variables as well:
left = let(Body, domain=world.bodies)
right = let(Body, domain=world.bodies)
# Same name, but different instances allowed by domain (not enforced here)
query = an(entity(left, left.name == right.name))
print(*query.evaluate(), sep="\n")
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Body(name='Container2', weight=15, tags=['plastic', 'red'])
Body(name='Handle1', weight=5, tags=['metal'])
Body(name='Handle2', weight=7, tags=['wood', 'brown'])
Ordering: <, <=, >, >=#
These work for numeric and comparable attributes.
b = let(Body, domain=world.bodies)
heavy = an(entity(b, b.weight >= 10))
print(*heavy.evaluate(), sep="\n")
# => bodies with weight >= 10
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Body(name='Container2', weight=15, tags=['plastic', 'red'])
Chaining with logical operators (implicit AND when multiple conditions are given):
b = let(Body, domain=world.bodies)
query = an(
entity(
b,
b.weight >= 10,
b.name.startswith("C"), # attribute/property comparisons can be mixed
)
)
print(*query.evaluate(), sep="\n")
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Body(name='Container2', weight=15, tags=['plastic', 'red'])
Membership: contains, and in_#
Membership has to be checked using EQL operators in_(item, container) or contains(container, item).
Writing item in literal_list will be evaluated immediately by Python and not produce a symbolic comparator.
b = let(Body, domain=world.bodies)
query = an(entity(b, in_(b.name, {"Container1", "Handle1"})))
print(*query.evaluate(), sep="\n")
b = let(Body, domain=world.bodies)
query = an(entity(b, contains({"metal", "wood"}, b.tags[0])))
print(*query.evaluate(), sep="\n")
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Body(name='Handle1', weight=5, tags=['metal'])
Body(name='Container1', weight=10, tags=['metal', 'blue'])
Body(name='Handle1', weight=5, tags=['metal'])
Body(name='Handle2', weight=7, tags=['wood', 'brown'])
Tip: Membership works with any container type whose Python operator.contains is defined (lists, sets, strings, etc.). For strings, you can check substrings: "Con" in b.name.
Evaluation order and performance notes#
Comparators are represented by the
Comparatornode in the expression tree. EQL automatically reorders which side gets evaluated first based on currently bound variables to reduce search space.When you negate a comparator (
not_), EQL swaps the underlying Python operation (for example,==becomes!=) to keep the expression efficient instead of post-filtering results.Membership over large literal containers is efficient when you use
in_/containssince it remains a symbolic node and can benefit from caching.