Skip to content

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)

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.

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.

Terminal window
pip install "hypermesh[virtual]"

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:

ShapeOne hyperedge is…Use when
rowa single row, with members read from distinct columnseach row already is an event (e.g. a login: machine + user + ip)
groupall rows sharing a group keymembers are spread across rows (e.g. an alert with many entities)
stara fact row joined out to dimension tablesa star schema (e.g. an order → customer, product)

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 DuckDBBackend
from 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']

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"),
],
)

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.

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.

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.