VSKI v0.13.0
Light graphs: calendar-grade indexing
Section titled “Light graphs: calendar-grade indexing”Light graphs landed in v0.10 as a relationship index for collections, and the first heavy users all converged on the same ask: index events by time. “What did this person do last week”, “what changed on this date”, “how many hours accumulated on this edge” - the graph data model already fit (nodes and edges carry labels, weights and free-form JSON properties), but the ingest and query primitives were missing. v0.13.0 closes that gap without growing the engine: no new tables, no migrations, one new index.
Idempotent ingest: upsert=true
Section titled “Idempotent ingest: upsert=true”Indexing pipelines re-run. POST /graphs/:name/nodes|edges?upsert=true
(now also graph.createNodes(data, { upsert: true }) in the JS SDK)
replaces an existing id wholesale instead of erroring - the graph
equivalent of MERGE, and the primitive every timetree ingester needs.
Upserts hitting an existing id additionally check the graph’s updateRule
(the item already passed createRule like any create). The whole batch is
written in a single transaction: on failure nothing is persisted (batch
creates previously could persist a prefix of the batch).
Atomic accumulation: weightDelta
Section titled “Atomic accumulation: weightDelta”PATCH /graphs/:name/edges/:id accepts weightDelta (mutually exclusive
with weight): the server reads the current weight under the graph’s
write lock and adds to it, so concurrent increments cannot lose updates -
the classic “hours accumulated”, “stock consumed” counter. Weights also
feed a new stats aggregate: GET .../stats now reports
edgeWeightSums per edge label (computed in SQL, where NULL weights are
distinguishable from 0) alongside a new nodeLabels histogram.
Property filters: prop=path op value
Section titled “Property filters: prop=path op value”The JSON properties maps are no longer opaque. List endpoints accept
repeatable prop filters - ref.collection=users, capacity>=5,
status!=draft, title~meeting, archived=null - compiled to SQLite
json_extract comparisons with bound parameters (numeric-looking values
compare numerically, ~ is a substring match, null tests for
absence). This makes properties.ref and capacity/stock metadata
first-class query targets. Nodes also accept ids=a,b,c (max 500) and
edge source/target accept comma lists, so “everything touching last
week’s day nodes” is one request.
Node types in the index: nodeLabels
Section titled “Node types in the index: nodeLabels”Node labels are now interned in the in-memory topology index (alongside
edge labels and weights; ~4 bytes/node). neighbors, traverse and
subgraph accept nodeLabels=a,b to restrict visited/returned nodes by
type - “documents linked to this day, not the people”. Node label PATCHes
keep the index in sync; a SQL index on nodes(label) is created for new
graphs and backfilled idempotently for existing ones.
Correctness fixes
Section titled “Correctness fixes”- Edge weight PATCHes left the in-memory index stale on the master: the index entry was rebuilt only when the label changed, and with the pre-update weight - weighted shortest paths kept answering with the old cost until restart. Weight and label changes now sync the index immediately.
- Shortest path ignored edge direction - it always relaxed both
adjacency lists, so directed graphs got undirected routes. The default
stays both-ways (documented behavior), but
pathnow acceptsdirection=out|in|both.
Calendar timetree, packaged
Section titled “Calendar timetree, packaged”The Calendar Timetree recipe
documents the pattern end to end: week --includes_day--> day backbone
nodes, record --on_day--> day activity edges with magnitudes on the
weight, deterministic ids so re-ingest is a no-op. The new
calendar-index example plugin automates it - subscribe it to a
collection with a date field and it maintains the tree on every
create/update/delete (record deletes cascade their edges; the calendar
backbone stays), with backfill and week_of helper functions. It uses
native upsert (hence vski-sdk >= 0.6.0) and needs read rules on the
source collection plus write rules on the target graph for its service
account (the recipe shows both).
The server stays calendar-agnostic: any bucketing (sprints, fiscal quarters) is just more backbone nodes with the same shape.
- JS/TS:
createNodes/createEdgestake an optional{ upsert }second argument;updateEdgetakesweightDelta;pathtakesdirection;getNodestakesidsandprop,getEdgestakes arraysource/targetandprop;neighbors/traverse/subgraphtakenodeLabels;GraphStatsaddsnodeLabelsandedgeWeightSums. All additive. - Python (
vski-sdkv0.6.0):create_nodes/create_edgestakeupsert=;update_edgetakesweight_delta=;get_nodestakesids=andprop=,get_edgestakes list-valuedsource=/target=andprop=;neighbors/traverse/subgraphtakenode_labels=;pathtakesdirection=;GraphStatsaddsnodeLabelsandedgeWeightSums. The sync wrapper mirrors everything automatically.
47_graph_test.ts grows a “Calendar indexing primitives” step (upsert
replacement + duplicate rejection, weightDelta accumulation and
exclusivity, weight-patch freshness on weighted paths, directed path,
property/ids/multi-value filters, nodeLabels on neighbors and subgraph,
stats sums); wiki_graph_verify.ts pins every new claim the wiki pages
make; graphdb unit tests cover SetEdgeWeight, SetNodeLabel,
directed paths and node-label filters. On the Python side, the SDK’s
test_47_graphs.py mirrors the new surface and test_53_calendar_index.py
exercises the calendar-index plugin end to end (tree build, re-ingest
idempotency, delete cascade, week_of, backfill). A pre-existing flaky
SchemaFromMap test (map iteration order) is now order-independent.
Compatibility
Section titled “Compatibility”- No breaking API or storage changes; everything new is opt-in query/body parameters. Path defaults to the historical both-ways walk; plain POST stays insert-only.
- No migrations: the node-label index is
CREATE INDEX IF NOT EXISTS, applied on graph creation and backfilled lazily (master only) when an existing graph’s index loads. - Upsert replaces
label/properties(and edge endpoints/weight) wholesale - it does not merge. Pipelines that relied on the create-then-update fallback ingraph-synckeep working unchanged.