ShelfDB is built around lazy query pipelines.
Instead of reading or mutating data immediately, you build a query step by step and execute it
only when you call .run().
This model is shared across embedded mode and server mode.
Mental model
Start with a shelf:
query = db.shelf("note")
Or, in server mode:
query = client.shelf("note")
At this point, nothing has run yet. The query object only describes work to do.
Each method appends another step:
query = (
db.shelf("note")
.filter(lambda item: item[1].get("published"))
.slice(0, 20)
)
Execution happens here:
results = list(query.run())
For async server mode, execution is the same idea with await:
results = await query.run()
Read operations
These methods narrow or inspect a selection:
key(key)selects a single keykey_range(start, end)selects keys in the half-open range[start, end)keys_in(keys)fetches exact keys in the order you requested them; call it directly on a shelffilter(func)filters matching itemsslice(start, stop, step=None)slices the current selectionfirst(filter_=None)returns the first matching item orNonecount()returns the number of matching items
Example:
top_two = list(
db.shelf("note")
.filter(lambda item: item[1]["title"].startswith("note-"))
.slice(0, 2)
.run()
)
Exact-key lookups keep the input order:
batch = list(db.shelf("note").keys_in(["note-3", "note-1"]).run())
Write operations
These methods change stored documents:
put(key, data)inserts or replaces one documentput_many(items)inserts or replaces many documents and returnsNoneupdate(data)merges fields into each selected documentreplace(data)replaces each selected document completelyedit(func)transforms each selected document using a functiondelete()removes matching items
Example:
updated = list(
db.shelf("note")
.key("note-1")
.update({"published": True})
.run()
)
Write many documents at once:
db.shelf("note").put_many(
[
("note-1", {"title": "One"}),
("note-2", {"title": "Two"}),
]
).run()
put_many() and keys_in() consume iterable inputs when the query runs. If you need to reuse
the same data across runs, pass a list or tuple.
Result shapes
Local query results are one-shot iterators that yield server-style items in the form:
["key", data]
That is why local filters still look like this:
lambda item: item[1]["title"] == "First note"
If you want to keep the full local result, wrap it in list(...).
Terminal write operations such as put_many() return None.
Remote results are normalized into plain Python values so they can travel over the wire safely.
For example, a remote first() result looks like this:
["note-1", {"title": "First note"}]
ShelfDB does not sort inside query chains. If you need a custom order, sort the returned Python
values yourself with sorted(...).
Queries are reusable
You can keep a query object and run it more than once:
published = db.shelf("note").filter(lambda item: item[1].get("published"))
count_before = published.count().run()
db.shelf("note").put("note-2", {"published": True}).run()
count_after = published.count().run()
Because the query is lazy, each .run() uses the current database state.
Common mistakes
Iterating before .run()
This fails:
query = db.shelf("note")
list(query)
Run first instead:
list(query.run())
Expecting local run() to be reusable
Local multi-item run() results are one-shot iterators. Materialize them with list(...) if you
need to iterate more than once.
Forgetting to await async remote execution
Async server mode uses the same query chain, but you must await query.run().
Using strict mutators on an empty selection
replace(), update(), edit(), and delete() run inside an implicit write transaction when
you are not already inside db.transaction(write=True), so a failure rolls back the whole
query. replace(), update(), and edit() still expect at least one existing item. If nothing
matches, they raise an error. delete() is different: deleting a missing item just returns an
empty result.