Virtual Graph schema specification
|
This is the public preview documentation of Neo4j Virtual Graph. To provide feedback, please use the feedback form you were granted access to. During the public preview, we advise you not to use sensitive or production data with Virtual Graph. |
A Virtual Graph schema is a specification that defines what Virtual Graph’s core mapping logic needs to operate: it describes the schema as the engine sees it, and lists what you must provide rather than how you encode it.
This page provides an overview of the different entities in Virtual Graphs alongside the constraints that apply to them. Constraints constitute what the engine needs in order to map a query. There are three classes of constraints a schema must satisfy:
-
Well-formedness: the static shape of the schema configuration.
-
Existence: the tables and columns in the relational database.
-
Integrity: the values in the relational database.
The Constraint rationale section provides more in-depth information about the different constraints.
The Virtual Graph engine assumes that all constraints hold. It does not specify when, where, or whether any of it is validated. Specific schemas may differ in how they satisfy constraints.
Entities
Graph
A graph entity is the top-level contract defining the complete Virtual Graph. A graph is defined by:
-
The set of node definitions in the schema, one per label.
-
The set of relationship definitions in the schema, one per relationship type.
Constraints
Well-formedness constraints:
-
Finite distinct sets: Both sets must be finite and exhaustively known at configuration time. Label names must be unique across all node definitions, and relationship types must be unique across all relationship definitions.
-
Valid node references: Every node referenced as a relationship’s source or target must be present in the set of node definitions.
Node
A node mapping describes how each row of a relational table maps to a node with a given label. A node is defined by:
-
A node label, e.g.,
PersonorAccount. -
An underlying table name, e.g.,
peopleoraccounts. -
A set of properties belonging to the node.
-
A key, defined as a set of column names, used to uniquely identify a row in the underlying table, e.g.,
{"id"}or{"account_type", "account_number"}.
Constraints
Well-formedness constraints:
-
Finite distinct sets: The set of properties must be finite and exhaustive. Property names must be distinct.
-
One node-label: A node possesses exactly one label.
-
Non-empty keys: The key set must contain at least one column.
-
Neo4j identifiers: The label name must be valid.
-
SQL identifiers: The table name and the column names in the key set must be valid and case-sensitive.
Existence constraints:
-
Valid table: The underlying table must exist in the database.
-
Valid column: The key columns and the property columns must exist in the underlying table.
Integrity constraints:
-
Not-null keys: None of the values in the key columns can be null.
-
Strict uniqueness: The combination of values in the key columns uniquely identifies exactly one row in the table.
Relationship
A relationship mapping describes how each row of a relational table maps to a relationship of a given type between two nodes. A relationship is defined by:
-
A relationship type, e.g.,
KNOWSorOWNS_ACCOUNT. -
An underlying table name, e.g., "person_knows" for
KNOWS, oraccount_ownersforOWNS_ACCOUNT. -
A source node, e.g., the person node for both
KNOWSandOWNS_ACCOUNT. -
A mapping from the source node’s key columns to columns in the underlying relationship table, e.g.,
{"id": "person_1"}forKNOWSor{"id": "person_id"}forOWNS_ACCOUNT. -
A target node, e.g., the person node for
KNOWS, or the account node forOWNS_ACCOUNT. -
A mapping from the target node’s key columns to columns in the underlying relationship table, e.g.,
{"id": "person_2"}forKNOWS, or{"account_type": "owned_type", "account_number": "owned_number"}forOWNS_ACCOUNT. -
A set of properties which belong to the relationship.
-
A key, defined as a set of column names, used to uniquely identify a row in the underlying table, e.g.,
{"person_1", "person_2"}forKNOWS, or{"ownership_id"}forOWNS_ACCOUNT.
For example, assume that a schema contains a person node for the label :Person backed by a table of people with a key {"id"} and an account node for the label :Account backed by a table of accounts with key {"account_type", "account_number"}.
In order to map an account_owners table joining an account and its owner as a (:Person)-[:OWNS_ACCOUNT]→(:Account) relationship, the person’s key column ID is mapped to the column person_id, and the account’s key columns account_type and account_number are mapped to columns owned_type and owned_number respectively.
Constraints
Well-formedness constraints:
-
Finite distinct sets: The set of properties must be finite and exhaustive. Property names must be distinct.
-
Exhaustive key mapping: The key mapping for both the source and target nodes must map every column present in the respective node’s key set.
-
Non-empty keys: The key set must contain at least one column.
-
Neo4j identifiers: The relationship type must be valid.
-
SQL identifiers: The table name and the column names in the key set and the source/target mappings must be valid and are case-sensitive.
Existence constraints:
-
Valid table: The underlying table must exist in the database.
-
Valid column: The key columns, the property columns, and the source and target mapping columns must exist in the underlying table.
Integrity constraints:
-
Not-null keys: None of the values in the key columns can be null.
-
Strict uniqueness: The combination of values in the key columns uniquely identifies exactly one row in the table.
-
Referential integrity: The combination of values in the mapped relationship columns must match the corresponding key values of an existing row in the referenced node’s table.
Property
The mapping of a node or relationship property to a column name. A property is defined by:
-
The name of the property in the graph entity, e.g.,
id`,name,createdAt. -
The name of the corresponding column in the underlying table, e.g.,
id,name,created_at.
Property types are intentionally omitted from the schema configuration. The core mapping logic does not rely on property type information. They are discovered dynamically at runtime when the engine consumes rows via JDBC.
Constraints
Well-formedness constraints:
-
Neo4j identifiers: The property name must be valid.
-
SQL identifiers: The column name must be valid and is case-sensitive.
Constraint rationale
Well-formedness constraints
These constraints dictate the static shape of the schema configuration. They can be settled from the configuration alone, without consulting the database.
Finite distinct sets
The sets of nodes and relationships defined in the graph schema must be finite and exhaustive.
This is necessary to resolve patterns like MATCH (n).
Unlike a native graph database that can perform an AllNodesScan, the engine must treat this pattern as the union of all known node labels (e.g., MATCH (n:User|Account|Card)).
Label names must be strictly unique across all node definitions.
This ensures there is exactly one unambiguous specification for resolving a pattern like MATCH (n:User).
If a label is not present in the schema, the pattern simply yields no results.
The same applies to relationship types.
Properties must also be exhaustively declared per entity.
This allows the engine to materialise full entities, such as in the query MATCH (n:User) RETURN n.
This instructs the engine to select all underlying columns corresponding to the properties defined for the User node.
Property names must be distinct within an entity to guarantee a single, deterministic column mapping, for example in predicates such as MATCH (n:User) WHERE n.id = 42.
A natural representation for these finite sets is a map or dictionary data structure, keyed by their distinct identifiers (nodes by label, relationships by type, properties by name).
One node-label
In a Virtual Graph, a node possesses exactly one label.
This strict one-to-one mapping simplifies both graph modelling and the core mapping logic.
Crucially, it also unlocks specific execution optimizations.
Because node labels are mutually exclusive, a disjunctive pattern like (n:User|Account|Card) can be resolved using UNION ALL across the users, accounts, and cards tables, without the need for deduplication.
Valid node references
The graph schema must not contain dangling references. When a relationship defines a source or target node mapping, that node must exist in the schema’s defined set of node definitions. This ensures the engine can resolve all the entities in a pattern and construct SQL joins.
Exhaustive key mapping
When a relationship maps its underlying columns to a source or target node, the mapping must explicitly include every column defined in that node’s key set. This is a pre-requisite for referential integrity.
Non-empty keys
Because keys are used to uniquely identify rows, allowing an empty set of columns for a key would mathematically limit the underlying table to a maximum of one row. This is a degenerate case that is not worth supporting with complex branching logic. A key must consist of at least one column.
Neo4j identifiers
To ensure graph entity and property names can be safely addressed in user queries, a valid Neo4j identifier is strictly defined as any non-empty string that does not contain a backtick character (`).
Because Cypher® permits escaping identifiers with backticks, names containing spaces, special characters, or reserved keywords are fully supported. Users can query these by enclosing them in backticks:
MATCH (n:`User Account`)-[:`MATCH`]->()
RETURN n.`🏷️`
SQL identifiers
Column and table names map directly to the underlying relational database. The engine safely quotes these identifiers when generating SQL queries, meaning they are treated as case-sensitive by the backing database. This permits the use of any character (except quote characters) and allows reserved keywords to act as identifiers.
Existence constraints
These constraints relate the schema to the tables and columns present in the relational database. The engine assumes they hold: it generates SQL naming those tables and columns regardless, and the database rejects the query if they are absent.
Integrity constraints
These constraints govern the actual data present in the relational database. The engine relies on them to preserve Cypher semantics. If they don’t hold, in most cases the query still runs, but the resulting graph behaviour is unspecified.
Not-null keys
Standard SQL operates on three-valued logic where NULL = NULL evaluates to UNKNOWN, not TRUE.
If keys contained null values, the engine could not rely on standard equality predicates (e.g., a.id = b.id) and would be forced to generate complex evaluation clauses (e.g., a.id = b.id OR (a.id IS NULL AND b.id IS NULL)).
Make sure that keys are free of nulls to maintain performant, predictable join behaviour.
Strict uniqueness
Make sure that the combination of key values points to exactly one distinct row in the underlying table. This is necessary for generating stable element IDs.
An entity’s element ID encodes its node label or relationship type alongside the values that make up its key.
This allows the engine to resolve queries like MATCH (n) WHERE elementId(n) = 'abc-123'.
The ID is decoded into predicates based on the schema definition.
In the following example, the key of the Account node is defined in terms of its account type and account number:
SELECT …
FROM accounts
WHERE account_type = 'basic' AND account_number = 42
This underlying query must return exactly one row in order to respect global element ID uniqueness.
A relationship key cannot systematically be derived from its source and target mappings.
The endpoints often identify a relationship, but not always: several rows may connect the same pair of nodes.
For example, two KNOWS rows between the same pair of people may be recorded at different times.
They must surface as two distinct relationships with distinct element IDs.
The endpoint mappings determine what a relationship connects; the key determines which relationship it is.
The key is therefore defined explicitly.
The combination of non-empty keys, not-null keys, and strict uniqueness naturally maps to the concept of a primary key. The engine acts under the assumption that these keys behave exactly like primary keys, but it does not enforce the presence of an actual primary key constraint on the database itself. Backing these columns with a database index is strongly recommended: the engine generates equality predicates over key columns to resolve element IDs, and over mapped columns to join relationships to their endpoints, so unindexed keys turn both into full table scans.
Referential integrity
Make sure that the mapped relationship columns contain values that correspond to existing node rows. Specifically, the combination of values in the relationship’s mapped columns must match the corresponding key values of an existing row in the referenced node’s table.
This requirement naturally maps to the concept of a foreign key constraint.
The engine acts under the assumption that these mappings behave exactly like foreign keys, but it does not enforce the presence of an actual foreign key constraint on the database itself, as most cloud database providers do not offer them.
You are responsible for this condition to hold.
If this constraint is violated, a relationship could be left with a missing end node, leading to semantically invalid graph behavior.
For example, the engine might successfully find a relationship by its ID using MATCH ()-[r:KNOWS]→() WHERE elementId(r) = 'def-456' RETURN r, but subsequently return no results when attempting to traverse to its end node via MATCH ()-[r:KNOWS]→(p) WHERE elementId(r) = 'def-456' RETURN r, p.
Optimisations
The following sections outline the optimization strategies which the Virtual Graph engine utilizes.
Inference
Because a graph schema is strictly constrained, the engine can perform significant inference when processing a query.
For example, given the pattern (p1)-[knows:KNOWS]→(p2)-[owns:OWNS_ACCOUNT]-(account):
-
knows:KNOWSimpliesp1:Personandp2:Person. -
In turn,
p2:Personimplies that theownsrelationship is directed left-to-right:(p2)-[owns:OWNS_ACCOUNT]→(account). -
This, in turn, implies
account:Account.
The pattern effectively becomes: (p1:Person)-[knows:KNOWS]→(p2:Person)-[owns:OWNS_ACCOUNT]→(account:Account).
It can now be solved with simple joins, and no unions are necessary.
Join elimination
The combination of strict uniqueness and referential integrity ensures that a single relationship points to exactly one node row at either end.
This allows for two significant execution optimizations:
-
If no properties are needed from the endpoint of a relationship, its element ID can be calculated directly from the values in the relationship table, skipping a join entirely. For example, the query
MATCH (:Person)-[:OWNS_ACCOUNT]→(a:Account) RETURN elementId(a)can be solved by simply reading theaccount_ownerstable underlyingOWNS_ACCOUNT. The element ID for node a is calculated based on the relationship’sowned_typeandowned_numbercolumns, bypassing theaccountstable. -
When two relationship patterns are juxtaposed, provided no properties are needed from the intermediate node, the engine can connect the two relationship tables directly with a single join. For example, the query
MATCH (:Person)-[:KNOWS]→(:Person)-[:OWNS_ACCOUNT]→(:Account) RETURN count(*)can be solved with only one join between theperson_knowsandaccount_ownerstables.