Grouped reads
Group a fragment’s nodes by keys, with numbers about each group, in one statement:
from drivine import avg, count, hop, key, path_key, refs
q = refs(Review)top = await gom.groups.load( Review, keys={"page": key(q.page)}, # or path_key(Page, [hop("REVIEWS")]): the node aggregates={"votes": count(q.helpful.eq(True)), "rating": avg(q.rating)}, where=q.published.is_not_null(), having=[("votes", "gte", 3)], order_by=[("votes", "desc")], limit=10,)# [{"page": "/pricing", "votes": 12, "rating": 4.5}, ...]
await gom.groups.count_by(Review, "page") # {"/pricing": 14, ...}Aggregates are count, sum_, avg, min_ and max_, each with an optional filter. Ties in an order are broken by
the keys, so a page of groups is the same every time.
- Models are Pydantic. A loaded fragment is validated, so a bad value fails at the load.
- The annotation says what a relationship holds: a fragment, a relationship fragment or a view, one or a list. A
single relationship typed without
Noneis required, so only roots that have it load;Person | Noneis optional. A list is required withRelationship(..., required=True). - Properties are stored in camel case, as TypeScript and JVM models store them:
start_dateasstartDate.GraphProperty("name")names one, andproperty_names="as_written"keeps a class’s fields as they are written. - A subclass is loaded as itself. A fragment’s labels are inherited, and a node loads as the most specific subclass whose labels it carries.
- Recursion:
Relationship("HAS_LOCATION", max_depth=3)on a list of the view’s own class expands three levels;depth={"sub_locations": 5}overrides it for one load. - More markers:
GraphPath([hop(...), ...])for the nodes at the end of several hops,CountandAggregatecomputed in the query,SortedBy,ReadOnly,Transient,NodeStamp,NodeLabels,PropertyBag,JsonPacked,NullAsDefaultandEmptyWhenAbsent, as in TypeScript’s Drivine. - A set field is stored as a list of its items, sorted where they order (as the set iterates where they don’t), and
a
JsonPackedone packed so: a set is written alike in every process, whatever order it iterates in.patchwrites one so too, andedges.relatewrites a set property so. One that 0.1.0 wrote, in the order it iterated in, loads andupdatesaves it, but until a save writes it again awherethat compares the whole set, which it writes sorted, doesn’t find it on the graph engines (on Postgres and Oracle a whole-setwhereon a set that isn’tJsonPackeddoesn’t find it either way), and on Neo4j a save givenfrom_of aJsonPackedone is refused. - A flat
PropertyBag(flat=True)stores each entry under its bare key and loads every property no field or other bag is stored under. A save refuses, before it writes anything, an entry whose key a field (the id among them) is stored under, one under another bag’s prefix, or one beginning__drivine., Drivine’s own, such as the stamp.
The statements are TypeScript’s, character for character: the tests check them against those TypeScript’s Drivine writes for the same models.

