Domain Mapping#
Domain mapping transforms iterable attributes or nested collections into element-wise bindings while preserving existing variable bindings. This page covers two common patterns:
Flattening an iterable attribute (
flatten)Indexing into container attributes (capturing
__getitem__symbolically)
Setup#
We first define a small world used by both examples below.
from dataclasses import dataclass, field
from typing_extensions import List, Dict
from krrood.entity_query_language.entity import (
entity,
set_of,
an,
let,
flatten,
Symbol,
)
@dataclass
class Body(Symbol):
name: str
@dataclass
class Handle(Body):
...
@dataclass
class Container(Body):
...
@dataclass
class View(Symbol):
world: object = field(default=None, repr=False, kw_only=True)
@dataclass
class Drawer(View):
handle: Handle
container: Container
@dataclass
class World(Symbol):
id_: int
bodies: List[Body] = field(default_factory=list)
views: List[View] = field(default_factory=list)
# Build a small world
world = World(1)
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]
Flatten iterable attributes#
Flatten turns an iterable-of-iterables into a flat sequence of items while keeping the original parent binding (similar to SQL UNNEST). It is handy when a selected variable has an attribute that is a list and you want one row per element of that list.
views = let(type_=View, domain=world.views)
drawers = flatten(views.drawers) # UNNEST-like flatten of each view's drawers
query = an(set_of([views, drawers]))
rows = list(query.evaluate())
# Each solution contains both the parent view and one flattened drawer
assert len(rows) == 2
assert {r[drawers].handle.name for r in rows} == {"Handle1", "Handle3"}
assert all(r[views] is cabinet for r in rows)
print(*map(lambda r: r[drawers], rows), sep="\n")
Drawer(handle=Handle(name='Handle1'), container=Container(name='Container1'))
Drawer(handle=Handle(name='Handle3'), container=Container(name='Container3'))
Notes:
flattenworks on any expression that yields an iterable (for example, an attribute likeviews.drawers).Each solution produced by
flattenretains the original bindings (here,views), so they can be used in further constraints or selections.
Indexing into container attributes#
Indexing on symbolic variables is captured in the expression graph. You can index into containers (such as dictionaries or lists) held by your symbolic variable, and the operation is represented symbolically within the query.
@dataclass
class ScoredBody(Symbol):
name: str
props: Dict[str, int]
@dataclass
class ScoreWorld(Symbol):
bodies: List[ScoredBody]
score_world = ScoreWorld([
ScoredBody("Body1", {"score": 1}),
ScoredBody("Body2", {"score": 2}),
])
b = let(type_=ScoredBody, domain=score_world.bodies)
# Use indexing on a dict field; the indexing is preserved symbolically
query = an(entity(b, b.props["score"] == 2))
results = list(query.evaluate())
assert len(results) == 1 and results[0].name == "Body2"
print(*results, sep="\n")
ScoredBody(name='Body2', props={'score': 2})