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 Comparator node 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_/contains since it remains a symbolic node and can benefit from caching.