Skip to content

digital-substrate/dsviper-query

Repository files navigation

dsviper-query

A consumer-side query layer over the dsviper binding. It adds nothing to the binding — native Python functional tools (generators, itertools, comprehensions, sorted/set/dict) are already expressive over the Viper value model.

Unlike its Node twin @digitalsubstrate/dsviper-query, this package needs no bridges. Viper values carry __eq__/__hash__/__lt__, so set/dict/sorted operate on them directly — value-identity and ordering come for free (there is no canonicalKey/hashKey, which the JS side must supply because Map/Set key by identity). The whole layer is query(), the free verbs, a fluent Query, and a Mongo-like match().

The idea

A value reaching a Python chain is either a native scalar (already decoded — int, float, str, …) or a wrapped Viper value (containers, keys, ids). The stateless spine works on both with stock tools and needs no library:

from dsviper_query import query

# query() is a lazy (key, document) generator; a comprehension chains over it directly.
top = [key for key, doc in query(state, attachment) if doc["score"] > 1000][:10]

The stateful / relational verbs (distinct, group_by, join, union/intersect/ except_, to_dict) work directly on wrapped values — the JS identity wall does not exist here. Python's set/dict call __hash__ and __eq__, so value-equal values collapse (equal ValueMaps are one entry, not two) and width-variants stay distinct (Int8(1) != Int64(1)). No primitive key:

from dsviper_query import distinct, group_by, order_by, join

unique   = distinct(values)                                    # keys on the value itself
by_kind  = group_by(values, lambda v: v["kind"])               # dict[key, items]
ordered  = order_by(values, lambda v: v["score"])              # native <, total order
enriched = join(left, right, lambda l: l["id"], lambda r: r["id"], lambda l, r: (l, r))

order_by rides the runtime's trans-type total order (via native <), so even a heterogeneous collection sorts deterministically, and None keys sort last.

The DSL

On top of the verbs sits an ergonomic layer — pure sugar over the same tools, adding no power, only reach.

from_(iterable) — a fluent, lazy chain. Wrap any iterable (typically query(...)) and chain the spine and the verbs; keys()/values() project the (key, doc) halves; terminals (to_list, group_by, first, count, …) consume it. A Query is single-use — every operation consumes the underlying lazy iterator, so a second operation on the same Query raises Query already consumed rather than silently yielding an empty stream. Re-derive a fresh Query from the source per chain.

from dsviper_query import from_, query

names = (from_(query(state, attachment))
         .values()                                  # drop the key half
         .where({"dept": "eng", "age": {"$gte": 30}})
         .order_by(lambda doc: doc["age"])
         .map(lambda doc: doc["name"])
         .take(5)
         .to_list())

match(spec) — a declarative, Mongo-like filter compiled to a (doc) -> bool predicate (also accepted directly by .where). Implicit equality per field, operators $eq $ne $gt $gte $lt $lte $in $nin $exists $regex $not $where, and combinators $and $or $nor $not. Dotted paths resolve through nested dict access (or get_in/at for a wrapped document; a missing path is simply unmatched, never raises). Equality is the value's native == (type-aware), ordering its native < (total).

from dsviper_query import match

wanted = match({
    "$or": [{"status": "active"}, {"score": {"$gt": 1000}}],
    "addr.city": {"$in": ["paris", "london"]},
})
hits = from_(query(state, attachment)).values().where(wanted).to_list()

match is strict — a malformed spec raises at compile time rather than silently matching nothing: an unknown operator, an unknown top-level $-combinator, a condition mixing an operator and a plain key (a typo'd $op), or an ill-typed operand ($and/$or/$nor want a list, $in/$nin an iterable, $where a callable). An absent field never matches an order comparison ($gt/$gte/$lt/$lte); use $exists to test presence.

The ambition is deliberately minimal: the operators above, the combinators, and a $where escape hatch — no $options, $elemMatch, $size, or $type. Reach for a $where (or a plain predicate) beyond that.

API

Export Kind Notes
query(state, attachment, *, key_pred=None, encoded=True) source a lazy (key, document) generator over an immutable CommitState; nil-skipped; key_pred pushes down before get()
rows(attachment_getting, attachment, …) source the engine given an AttachmentGetting directly (the testable seam)
from_(iterable) / Query fluent chainable, lazy, single-use wrapper over the verbs
match(spec, *, field=get_field) filter compiles a Mongo-like spec to a (doc) -> bool predicate
get_field(doc, path) accessor dotted-path read (nested dict, or get_in/at for wrapped); MISSING on a miss
compare_values(a, b) primitive total order — native <, None last
distinct, order_by, group_by, join, union, intersect, except_, to_dict verbs stdlib one-liners on hashable/comparable values

query accepts only an immutable CommitState — it is the sole type carrying the immutability the lazy scan is sound over (its keys()/get() see one frozen snapshot at a fixed commit_id). Mutable sources (CommitMutableState, Database) are rejected: their store can change between keys() and get(). To query live state, snapshot first — CommitState.state(commit_database, commit_id) — then query that.

Requirements

  • Python >= 3.10.
  • dsviper >= 1.2.18 (the trans-type total order that order_by / $gt ride).

Status

Experimental (0.x). The surface may change. The match/from_ DSL is pure sugar over the verbs — native Python tools plus the value dunders already express the whole algebra; the DSL only makes common shapes terser.

License

MIT

About

Consumer-side query layer over the dsviper binding — native Python functional chaining, a fluent Query, and a Mongo-like match(). No bridges: Viper values are hashable/comparable.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages