• v0.10.0 ba405b633c

    v0.10.0 Stable

    x released this 2026-10-21 10:54:05 +00:00 | 6 commits to main since this release

    VSKI v0.10.0

    Light Graphs: retiring the Neo4J sidecar

    Most tenants that needed graph workloads ran Neo4J alongside VSKI.
    Looking at actual usage, 99% of those use cases - agent graphs,
    knowledge linking, dependency walking - never touched the advanced
    features (Cypher, clustering, multi-hop pattern matching at scale) and
    never exceeded 100k edges. What they did incur was the cost of running
    and maintaining a separate database service: another deployment unit,
    another backup/replication story, another source of truth to keep
    consistent with VSKI data, another thing on-call has to know.

    Light graphs move that workload into VSKI itself. Graphs are now a
    first-class entity type alongside collections - same
    (dbName, name) namespacing, same access-rule model, same single
    SQLite storage engine, same replication - so the graph and the content
    it indexes live in one place. For the workloads tenants actually run,
    this removes the Neo4J dependency and its engineering overhead
    entirely; no new infrastructure ships with it.

    How it works

    A graph is a named, db-scoped registry entry (same tombstone/revive
    semantics as collections) whose data lives in two physical tables in
    the tenant database: graph_<name>_nodes and graph_<name>_edges
    (labelled, directed edges with optional numeric weight, cascading
    deletes on endpoints). SQLite stays the source of truth - no separate
    service, nothing new to operate.

    Traversal performance comes from an in-memory index: on first access
    each graph's topology (ids, edge labels, weights - never properties)
    is loaded into a compact adjacency index with nodes and edges
    interned to uint32 slots. A fully-warm 100k-node / 100k-edge graph
    occupies ~20-25 MB and a worst-case BFS runs in single-digit
    milliseconds. Mutations serialize per graph (check + SQL write +
    index update under one lock), so size caps cannot be raced and the
    index cannot drift from committed state. Cold graphs are evicted LRU
    (GRAPH_INDEX_MAX_GRAPHS, default 32) and reload from SQLite on next
    touch; replicas revalidate against the database epoch and reload
    transparently.

    API

    Registry (mirrors /api/collections): GET/POST /api/graphs,
    GET/PATCH/DELETE /api/graphs/:name (soft delete keeps data;
    re-creating the same name revives the graph with its contents).

    Data & traversal:

    • GET/POST /api/graphs/:name/nodes, GET/PATCH/DELETE .../nodes/:nodeId
    • GET/POST /api/graphs/:name/edges, GET/PATCH/DELETE .../edges/:edgeId
    • GET .../nodes/:nodeId/neighbors?direction=out|in|both&labels=a,b&limit=
    • GET .../nodes/:nodeId/traverse?depth=N&direction=&labels=&limit= (BFS, max depth 10)
    • GET .../path?from=&to=&maxHops=&cost=hops|weight&labels= (BFS or Dijkstra over edge weights)
    • GET .../subgraph?nodes=a,b&hops=N - neighborhood around seed nodes
    • GET .../stats - node/edge counts, label histogram, degree stats, effective caps

    Nodes and edges accept single objects or arrays on create; ids are
    optional (15-char generated). Deleting a node cascades its edges.

    Rules

    The five collection rule fields apply with collection semantics: empty
    rule denies non-admins, "*" is public, admins bypass. List endpoints
    evaluate rules per item (non-trivial rules scan ahead to fill the page;
    the response carries "filtered": true and counts only visible items
    when that happened). Traversal endpoints are gated by viewRule.

    Caps

    GRAPH_MAX_EDGES / GRAPH_MAX_NODES (default 100000 each) bound every
    graph server-wide; a graph may declare a lower per-graph cap via
    options.maxEdges / options.maxNodes. Exceeding the cap is a clean
    400 (graph edge cap exceeded (...)) from the per-graph write lock.

    The caps exist to keep the memory story boring: every graph is bounded,
    so the total index footprint is bounded by the number of warm graphs,
    and nothing about the feature changes VSKI's single-binary, single-file
    storage profile.

    SDKs

    • TypeScript/JS (@vski/sdk v2.1.0): new client.graph(name, db?)
      / client.db(name).graph(name) namespace (node/edge CRUD,
      neighbors, traverse, path, subgraph, stats) and
      client.settings.graphs registry namespace. New types:
      GraphConfig, GraphNode, GraphEdge, GraphStats, and the
      traversal result types.
    • Python (vski-sdk v0.4.0): matching surface - client.graph(name, db) / client.db(name).graph(name) (get_nodes/create_nodes/
      get_node/update_node/delete_node, the same five for edges,
      neighbors, traverse, path, subgraph, stats) plus the
      client.settings.graphs registry namespace and GraphConfig/
      GraphNode/GraphEdge/GraphStats TypedDicts. The sync wrapper
      mirrors both (SyncVskiClient.graph(...)). The e2e harness (including
      the replica suite) now honours VSKI_BINARY/VSKI_DIR overrides, so
      it runs against any server checkout; the suite passes on pytest 8 and 9.

    Fixes

    • Collections: the drop index/view/FTS routes
      (DELETE /api/collections/:name/indexes/:indexName,
      .../views/:viewName, .../fts/:ftsIndexName) were documented and
      implemented but never registered by the router - every call 404ed. They
      are wired now; the SDK drop_view/drop_index/drop_fts_index
      methods work against them.
    • Python SDK docs: set_views was documented as full-list
      replacement, but the server only ever adds views - slugs missing
      from the new list stay until dropped individually. The docstring now
      states the additive semantics and points at drop_view (which takes
      the full materialised <collection>_view_<slug> name).

    Compatibility

    • No breaking changes. The graphs registry table and the unique index
      uq_graphs_db_name are created idempotently in default.db on boot;
      tenant tables are created per graph on demand.
    • Existing Neo4J deployments are unaffected; migration to light graphs
      is per-workload (create the graph, replay nodes/edges through the
      SDK or a workflow) and can be done incrementally.
    • Graph mutations publish realtime events on the db:<graph> topic
      (subscription UI support follows in the dashboard update).
    • Dashboard: graph management pages are not part of this release; use
      the SDK or the SQL console until the follow-up web update.
    Downloads