Skip to content

VSKI v0.13.0

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.

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).

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.

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 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.

  • 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 path now accepts direction=out|in|both.

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/createEdges take an optional { upsert } second argument; updateEdge takes weightDelta; path takes direction; getNodes takes ids and prop, getEdges takes array source/target and prop; neighbors/traverse/subgraph take nodeLabels; GraphStats adds nodeLabels and edgeWeightSums. All additive.
  • Python (vski-sdk v0.6.0): create_nodes/create_edges take upsert=; update_edge takes weight_delta=; get_nodes takes ids= and prop=, get_edges takes list-valued source=/target= and prop=; neighbors/traverse/subgraph take node_labels=; path takes direction=; GraphStats adds nodeLabels and edgeWeightSums. 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.

  • 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 in graph-sync keep working unchanged.