Example with concatenate
#
concatenate
is used to combine nested iterables found in a variable’s domain into a single iterable value while preserving any outer bindings. Conceptually, if you have a variable bound to a collection of collections (e.g., a view with a list of drawers), concatenate
produces one value that is the concatenation of all those inner collections. This is useful for:
Performing membership checks (in_/not_) against a single concatenated iterable.
Passing a single, aggregated collection downstream in your query.
Below is a minimal, self-contained example that mirrors the behavior tested in the suite (similar to test_merge
).
from dataclasses import dataclass, field
from typing import List
from entity_query_language import symbolic_mode, let, concatenate, not_, in_, entity, an, From
from entity_query_language.predicate import symbol
# Minimal dataset for the example
@symbol
@dataclass
class Body:
name: str
@symbol
@dataclass
class Handle(Body):
...
@symbol
@dataclass
class Container(Body):
...
@symbol
@dataclass
class View:
world: object = field(default=None, repr=False, kw_only=True)
@symbol
@dataclass
class Drawer(View):
handle: Handle
container: Container
@symbol
@dataclass
class World:
bodies: List[Body] = field(default_factory=list)
views: List[View] = field(default_factory=list)
# Build a small world
world = World()
container1 = Container(name="Container1")
container3 = Container(name="Container3")
handle1 = Handle(name="Handle1")
handle3 = Handle(name="Handle3")
world.bodies.extend([container1, container3, handle1, handle3])
# Two drawers
drawer1 = Drawer(handle=handle1, container=container1)
drawer2 = Drawer(handle=handle3, container=container3)
# A simple view-like class with an iterable attribute `drawers`
class CabinetLike(View):
def __init__(self, drawers):
super().__init__()
self.drawers = list(drawers)
cabinet = CabinetLike([drawer1, drawer2])
world.views = [cabinet]
# Example 1: Use concatenate to collect all drawers into a single iterable domain
with symbolic_mode():
views = let(type_=View, domain=world.views)
all_drawers = concatenate(views.drawers) # <-- concatenate nested iterables into one iterable domain
# Select the concatenated iterable only (single row expected)
query1 = an(entity(all_drawers))
rows1 = list(query1.evaluate())
# rows1 is a list with a single UnificationDict; `all_drawers` maps to the concatenated list of drawers
assert len(rows1) == 1
assert {d.handle.name for d in rows1[0]} == {"Handle1", "Handle3"}
# Example 2: Test membership using in_/not_ with the concatenated iterable
with symbolic_mode():
# A variable ranging over drawers in the world (simulate another source of drawers)
d = Drawer(From([drawer1, drawer2]))
views = CabinetLike(From(world.views))
all_drawers = concatenate(views.drawers)
# Find drawers that are NOT in the concatenated list (expect none in this tiny world)
query2 = an(entity(d, not_(in_(d, all_drawers))))
rows2 = list(query2.evaluate())
assert len(rows2) == 0
with symbolic_mode():
d = let(type_=Drawer, domain=[drawer1, drawer2])
views = let(type_=View, domain=world.views)
all_drawers = concatenate(views.drawers)
# Find drawers that ARE in the concatenated list (expect both)
query3 = an(entity(d, in_(d, all_drawers)))
rows3 = list(query3.evaluate())
assert {r.handle.name for r in rows3} == {"Handle1", "Handle3"}
Notes:
concatenate
merges inner iterables into a single iterable from the bound variable’s domain while keeping outer iterable, this is equivalent to creating a new variable with the concatenated domain, because this loses the binding to the original variable elements.Use
in_(x, concatenated)
andnot_(in_(x, concatenated))
to test membership against the concatenated collection.If you need one result per inner element while keeping parent bindings, use
flatten
instead.