HyperMesh Virtual
HyperMesh Virtual lets you treat tables that already live in your warehouse, lakehouse, or a Parquet file as a temporal hypergraph — without copying the data into the engine. You describe how rows map to hyperedges once (a spec), then run HyperCypher queries; Virtual compiles them to SQL, runs them on your backend, and hydrates the results into the same canonical hyperedge records the rest of HyperMesh uses.
HyperCypher → logical plan → SQL (DuckDB / …) → rows → HyperedgeRecord (your query) (compiler) (your warehouse) (canonical IR)Why this exists
Section titled “Why this exists”The atomic unit in HyperMesh is a multi-party event at a point in time. That shape is everywhere in warehouse data already — an alert that ties together a machine, a user, and a file; a login row with a source, a destination, and an IP; an order joined to a customer and a product. Virtual reads that structure in place so you get hypergraph queries over data you can’t (or don’t want to) move:
- Zero-copy — the bytes stay in your warehouse; nothing is re-ingested.
- Deterministic compilation — the same query + spec always produce the same SQL (golden-snapshot tested), so plans are reviewable and cacheable.
- One result model — virtual results are
HyperedgeRecords, identical to native results, so downstream analytics and tooling don’t care where the data came from.
Install
Section titled “Install”Virtual is an optional extra (it pulls in DuckDB + the Lark parser). The core
engine and REST API import cleanly without it; the /v1/virtual/* routes only
mount when the extra is present.
pip install "hypermesh[virtual]"The mental model
Section titled “The mental model”A VirtualHypergraphSpec is the contract between your tables and the
hypergraph. It names:
- a backend (e.g. DuckDB pointed at a Parquet file or an attached database),
- the node labels and which table/column identifies them, and
- one or more edge specs, each with a shape that says how rows become hyperedges.
There are three edge shapes:
| Shape | One hyperedge is… | Use when |
|---|---|---|
row | a single row, with members read from distinct columns | each row already is an event (e.g. a login: machine + user + ip) |
group | all rows sharing a group key | members are spread across rows (e.g. an alert with many entities) |
star | a fact row joined out to dimension tables | a star schema (e.g. an order → customer, product) |
A first virtual graph (Python)
Section titled “A first virtual graph (Python)”The example below maps a network-login Parquet file (row shape: one login =
one hyperedge with three members).
from hypermesh_virtual.model.spec import ( MemberSpec, VirtualEdgeSpec, VirtualHypergraphSpec, VirtualNodeSpec,)from hypermesh_virtual.backends.duckdb import DuckDBBackendfrom hypermesh_virtual.runtime.executor import VirtualExecutor
spec = VirtualHypergraphSpec( id="net", name="Network Logins", backend={"type": "duckdb", "path": "/data/logins.parquet"}, nodes=[ VirtualNodeSpec("Machine", "logins", "x"), VirtualNodeSpec("User", "logins", "x"), VirtualNodeSpec("IP", "logins", "x"), ], edges=[ VirtualEdgeSpec( label="Login", shape="row", table="logins", ts_column="event_ts", row_member_columns={"m": "src_machine", "u": "dst_user", "ip": "remote_ip"}, members=[ MemberSpec("m", "Machine", role="src"), MemberSpec("u", "User", role="dst"), MemberSpec("ip", "IP", role="via"), ], ) ],)
backend = DuckDBBackend(":memory:", attach_files=["/data/logins.parquet"])backend.connect()records = list(VirtualExecutor(backend, spec).run( "MATCH (e:Login){m:Machine, u:User, ip:IP} WHERE m.id = 'i-1' RETURN e"))for r in records: print([m.vertex_local_id for m in r.members])# → ['machine:i-1', 'user:alice', 'ip:10.0.0.1']Group shape
Section titled “Group shape”When members are spread across rows that share a key, use group:
VirtualEdgeSpec( label="Alert", shape="group", table="security_alerts", group_key="alert_id", # all rows with the same alert_id → one hyperedge member_value="entity_id", # the column holding each member's id member_kind="entity_kind", # the column holding each member's type ts_column="event_ts", members=[ MemberSpec("m", "Machine", role="machine"), MemberSpec("u", "User", role="user"), MemberSpec("f", "File", role="file"), MemberSpec("ip", "IP", role="ip"), ],)Star shape
Section titled “Star shape”For a star schema, declare star_joins so fact foreign keys are resolved to
human-readable dimension values:
VirtualEdgeSpec( label="Order", shape="star", table="orders", ts_column="order_ts", star_joins=[ {"member_name": "c", "dim_table": "customers", "fact_fk": "customer_id", "dim_pk": "id", "dim_value": "name", "node_label": "Customer"}, {"member_name": "p", "dim_table": "products", "fact_fk": "product_id", "dim_pk": "id", "dim_value": "title", "node_label": "Product"}, ], members=[ MemberSpec("c", "Customer", role="customer"), MemberSpec("p", "Product", role="product"), ],)Members hydrate from the dimension value columns (e.g. customer:ACME,
product:Widget), not the raw foreign keys.
Results caching
Section titled “Results caching”Each spec carries a spec_version. Query results are cached on disk and keyed by
(spec_id, spec_version, query), so edits automatically bust stale entries. Pass
refresh=True (Python) or "refresh": true (REST) to bypass the cache, and
editing a spec via the REST API invalidates its cached results.
Auto-proposing a spec
Section titled “Auto-proposing a spec”You don’t have to write a spec by hand. Point the proposer at a backend and it
introspects the schema and proposes node/edge specs with a per-edge rationale
(optionally refined by an LLM). See POST /v1/virtual/propose.
Where to next
Section titled “Where to next”- HyperCypher & query coverage — the query language, every supported pattern, and the current limits.
- REST API — manage specs and run queries over HTTP.