Class reference
Query
Querying a document
query() turns a Document into something you interrogate. It returns a lazy, composable Query: you chain filters and ordering, then end with a terminal that walks the document. Nothing is evaluated until the terminal runs, and filter-only pipelines stop early. It is read-only, a query never mutates the document.
auto open_highs = doc.query("/tracker/*")
.where(byType("issue") && field("status").eq("open") && field("sev").eq("high"))
.orderBy("area");
for (const Element* e : open_highs.toList()) std::puts(e->path().str().c_str());# Every open HIGH issue, by area, the whole pipeline is lazy until .to_list()/iteration.
open_highs = doc.query("/tracker/*").where(type="issue", status="open", sev="high").order_by("area")
for issue in open_highs:
print(issue.path)query() is additive: find() still returns its eager list. Pass a path glob to scope the source, or no argument for the whole document. A Query borrows its document, do not keep one past the document's life, and do not mutate the document while iterating. (In C++, query() on a temporary document is a compile error.)
where: filtering
where narrows the result. Chain it more than once; the conditions combine with AND.
Python accepts three forms, freely mixed:
doc.query().where(type="issue", status="open") # keyword sugar: type token + property equality
doc.query().where(kg.field("sev") == "high") # a field expression
doc.query().where(kg.field("weight") > 3) # comparisons
doc.query().where(lambda h: h.get_int("weight") > 3) # an escape-hatch lambda, given a Handle
C++ uses predicate builders composed with &&, ||, !:
doc.query().where(byType("issue") && field("status").eq("open"));
doc.query().where(field("sev").eq("high") || field("sev").eq("med"));
doc.query().where([](const Element& e){ return e.type() == "issue"; }); // escape hatch
Field predicates
field("name") builds a predicate from a property. Methods are canonical in both languages; Python also allows the operators.
| reads | |
|---|---|
eq(v) / == | equal |
ne(v) / != | present and unequal |
lt le gt ge / < <= > >= | ordered comparison |
in_([...]) (Python) / in({...}) (C++) | equal to any |
contains(s) · startswith(s) | substring / prefix (string properties) |
exists() | the property is present |
Other builders: by_type(t) / byType(t) (the element's type token), under(path) (strict descendants), name_is(name) / nameIs, meta(key).eq(v), has_meta(key) / hasMeta.
Comparison semantics (the rules worth knowing)
- Numbers compare by value, across dtypes.
field("scale").eq(1)matches a property stored as a float1.0, the query layer widens both sides. (Prism'sValueonly compares exactly; the query layer adds this.) - Strings compare as text. A number-vs-string comparison never matches (it is not an error).
- Missing is not a match. A missing or wrong-typed property makes a positive predicate false, it never throws, so
field("sev").ne("high")matches elements that have asevand it isn'thigh; an element with nosevis not matched. To also catch the missing ones, negate the equality:~field("sev").eq("high")(Python) /!field("sev").eq("high")(C++) is true whensevis absent. - Time: animated properties are read at time 0 in v1.
Ordering, limiting
doc.query().where(byType("issue")).orderBy("weight"); // ascending, stable
doc.query().where(byType("issue")).orderBy("weight").reverse(); // descending
doc.query().where(byType("issue")).limit(10);doc.query().where(type="issue").order_by("weight") # ascending, stable
doc.query().where(type="issue").order_by("weight").reverse() # descending
doc.query().where(type="issue").limit(10)order_by(field) is ascending and stable; chain reverse() for descending. Elements whose key is missing or incomparable sort last, keeping document order among themselves. limit(n) takes the first n.
Terminals
A terminal evaluates the pipeline against the document as of that call (a Query is a recipe, not a frozen snapshot, call to_list() to freeze).
| terminal | returns |
|---|---|
iterate / to_list() | the matched elements (Handles in Python, const Element* in C++) |
paths() | their paths |
first() | the first match, or None/nullptr |
one() | exactly one match, else raises (PrismError / std::invalid_argument) |
count() | how many matched |
any() | whether any matched (short-circuits) |
group_by(field) | a dict/map of string key → matched elements (string properties or the type token) |
In Python a Query is list-like, len(q), q[0], and iteration all work.
auto n_open = doc.query().where(byType("issue") && field("status").eq("open")).count();
auto by_status = doc.query().where(byType("issue")).groupBy("status"); // {"open": [...], "done": [...]}
const Element* first_bug = doc.query().where(byType("issue") && field("kind").eq("bug")).first();n_open = doc.query().where(type="issue", status="open").count()
by_status = doc.query().where(type="issue").group_by("status") # {"open": [...], "done": [...]}
first_bug = doc.query().where(type="issue", kind="bug").first()Laziness
Building a query scans nothing. A filter-only pipeline runs in a single pass and short-circuits, first() and any() stop at the first match. A buffering stage (order_by, reverse) consumes the whole result before the terminal. Re-running a terminal re-walks the document, so two terminals on one query can differ if the document changed between them; to_list() freezes a snapshot.
The query is implemented natively in
kinogaki-core(C++) and mirrored in the Python package; both are exercised by one shared fixture so they stay in lockstep.