Skip to content

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 None is required, so only roots that have it load; Person | None is optional. A list is required with Relationship(..., required=True).
  • Properties are stored in camel case, as TypeScript and JVM models store them: start_date as startDate. GraphProperty("name") names one, and property_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, Count and Aggregate computed in the query, SortedBy, ReadOnly, Transient, NodeStamp, NodeLabels, PropertyBag, JsonPacked, NullAsDefault and EmptyWhenAbsent, 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 JsonPacked one packed so: a set is written alike in every process, whatever order it iterates in. patch writes one so too, and edges.relate writes a set property so. One that 0.1.0 wrote, in the order it iterated in, loads and update saves it, but until a save writes it again a where that compares the whole set, which it writes sorted, doesn’t find it on the graph engines (on Postgres and Oracle a whole-set where on a set that isn’t JsonPacked doesn’t find it either way), and on Neo4j a save given from_ of a JsonPacked one 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.