Schema File
MemGQL federates one or more backends behind a single Bolt endpoint and exposes them as named graphs you query with GQL / openCypher. A schema file is the one place that declares both halves of that setup:
A connector is a connection. A graph is a mapping laid over connectors. You query graphs; routing figures out which backend to hit.
Everything in the file can also be created at runtime with DDL; the schema file is the boot-time equivalent, and runtime changes persist back to it.
Booting from a schema file
bolt_server --schema=/path/to/schema.jsonEvery connector connects and every graph registers at boot — no CONNECT, no
USE required to start querying. SCHEMA_FILE=<path> is honored as an
alternative to the flag. Runtime DDL writes the updated catalog back to this
file atomically, so a restart reloads the same state with no replay
(credentials are kept on disk but masked in EXPORT SCHEMA).
The file is validated at boot: a malformed mapping — a relational edge missing
metaFields.from / metaFields.to, or the legacy nodes / id_column format —
is rejected with an actionable error before the server starts serving.
The file has two top-level sections:
{
"connectors": [ ],
"graphs": [ ]
}A minimal schema
The smallest useful file is one connector and one vertex — a label mapped to a table by its primary-key column:
{
"connectors": [
{ "name": "pg", "type": "postgres", "connection": { "uri": "postgresql://demo:demo@localhost:5432/demo" } }
],
"graphs": [
{
"name": "store",
"vertices": [
{ "label": "Product",
"mappedTableSource": { "connector": "pg", "table": "products", "metaFields": { "id": "pid" } },
"attributes": [ { "name": "pid", "type": "Int" }, { "name": "title" } ] }
]
}
]
}Boot it, then query the label by name — no USE required:
MATCH (p:Product) RETURN p.title LIMIT 5;The sections below are the full field reference for each block.
connectors
A connector is a pure connection — how to reach a backend, and nothing about
graph shape. catalogs is accepted as an alias for connectors.
{
"connectors": [
{
"name": "pg",
"type": "postgres",
"connection": {
"uri": "postgresql://user:pass@host:5432/db"
}
}
]
}| Field | Required | Description |
|---|---|---|
name | yes | Referenced by graphs and DDL. |
type | yes | Backend type (see below). |
connection | yes | A native connection block (see fields below). JDBC-style keys (jdbc, driverClass, …) are rejected. |
Every connection field is optional — supply what a given backend needs:
connection field | Used for |
|---|---|
uri | Connection string for the backend. |
user / password | Credentials (override / complement the URI). |
database | Database name. |
schema | Middle namespace (PostgreSQL schema, MySQL / ClickHouse database, Iceberg schema). |
catalog | Native root for three-level backends (Iceberg catalog, SQL Server database). |
path | File-backed backends (DuckDB). |
Supported type values: memgraph, neo4j, postgres (postgresql),
mysql, sqlserver, oracle, duckdb, iceberg, iceberg-direct,
clickhouse, pinot.
graphs
Each graph is a set of vertices and edges laid over the shared connectors. A
single graph may span several connectors.
{
"graphs": [
{ "name": "store", "vertices": [ ], "edges": [ ] }
]
}Single-graph shorthand: put
vertices/edgesat the top level instead of agraphsblock and you get one implicit graph nameddefault.
Vertices
A vertex maps a label to a backend source. Exactly one source kind is set:
mappedTableSource— a relational table (PostgreSQL, MySQL, Oracle, SQL Server, DuckDB, ClickHouse, Iceberg). Rows become nodes.mappedGraphSource— a native graph backend (Memgraph / Neo4j). Queries pass through as Cypher; only the connector is declared.
{
"label": "Customer",
"mappedTableSource": {
"connector": "pg",
"schema": "public",
"table": "customers",
"metaFields": { "id": "cid" }
},
"attributes": [
{ "name": "cid", "type": "Int" },
{ "name": "name", "column": "full_name" },
{ "name": "email", "type": "String" }
]
}{
"label": "User",
"mappedGraphSource": { "connector": "mg" },
"attributes": [
{ "name": "uid", "type": "Int" },
{ "name": "handle" }
]
}| Field | Source kind | Description |
|---|---|---|
mappedTableSource.connector | relational | Connector name (alias catalog). |
mappedTableSource.schema | relational | Optional middle namespace (PostgreSQL schema, etc.). |
mappedTableSource.table | relational | The backing table. |
mappedTableSource.metaFields.id | relational | Primary-key column — the node’s identity and the join key for edges. |
mappedGraphSource.connector | native | Connector name (Memgraph / Neo4j); the query passes through as Cypher. |
Edges
An edge maps a relationship type between two labels. Relational edges name
the foreign-key columns via metaFields.from / metaFields.to; native-graph
edges pass through.
{
"label": "PURCHASED",
"from": "Customer",
"to": "Product",
"mappedTableSource": {
"connector": "pg",
"table": "orders",
"metaFields": { "id": "oid", "from": "cid", "to": "pid" }
},
"attributes": [ { "name": "qty", "type": "Int" } ]
}{ "label": "FOLLOWS", "from": "User", "to": "User", "mappedGraphSource": { "connector": "mg" } }A relational edge adds two more metaFields on top of id:
metaFields key | Description |
|---|---|
id | The edge row’s own key. |
from | Foreign-key column pointing at the source vertex’s id column. |
to | Foreign-key column pointing at the target vertex’s id column. |
from and to are required on relational edges — they define the traversal
(from)-[:LABEL]->(to). Loading fails with an actionable error if either is
missing. Native-graph edges (mappedGraphSource) need only from / to
labels; the traversal is resolved by the backend.
Attributes
attributes declare the properties a label exposes.
| Field | Required | Description |
|---|---|---|
name | yes | The GQL property name. |
column | no | The backing column (defaults to name), e.g. property name ← column full_name. |
type | no | Recorded, not enforced at query time (default String). |
Allowed type values: Boolean, Byte, Short, Int, Long, HugeInt,
Float, Double, Decimal, String, Date, DateTime.
attributes are optional and need not be exhaustive: a property you don’t list
still resolves to a same-named column at query time (p.sku → column sku).
Declare the ones you want to rename (column), give a type, or route
by USE-free.
Note: For USE-free routing to match a query by property (e.g.
WHERE c.email = …), the property must be a declaredattribute. Route by label / relationship type, or add an explicitUSE, when a property is not declared. See USE-free routing.
Complete example
One engine over Memgraph (a social graph) and PostgreSQL (a store),
sharing a uid / cid id space so you can query each on its own or join across
them.
{
"connectors": [
{ "name": "mg", "type": "memgraph", "connection": { "uri": "bolt://localhost:7687" } },
{ "name": "pg", "type": "postgres", "connection": { "uri": "postgresql://demo:demo@localhost:5432/demo" } }
],
"graphs": [
{
"name": "social",
"vertices": [
{ "label": "User",
"mappedGraphSource": { "connector": "mg" },
"attributes": [ { "name": "uid", "type": "Int" }, { "name": "handle" }, { "name": "name" } ] }
],
"edges": [
{ "label": "FOLLOWS", "from": "User", "to": "User", "mappedGraphSource": { "connector": "mg" } }
]
},
{
"name": "store",
"vertices": [
{ "label": "Customer",
"mappedTableSource": { "connector": "pg", "table": "customers", "metaFields": { "id": "cid" } },
"attributes": [ { "name": "cid", "type": "Int" }, { "name": "name", "column": "full_name" }, { "name": "email" } ] },
{ "label": "Product",
"mappedTableSource": { "connector": "pg", "table": "products", "metaFields": { "id": "pid" } },
"attributes": [ { "name": "pid", "type": "Int" }, { "name": "title" }, { "name": "price", "type": "Double" } ] }
],
"edges": [
{ "label": "PURCHASED", "from": "Customer", "to": "Product",
"mappedTableSource": { "connector": "pg", "table": "orders", "metaFields": { "id": "oid", "from": "cid", "to": "pid" } },
"attributes": [ { "name": "qty", "type": "Int" } ] }
]
}
]
}Boot it, then query graphs by name — no USE needed:
-- USE-free: each label routes to its owning graph / backend
MATCH (u:User) RETURN u.handle, u.name ORDER BY u.uid LIMIT 5; -- → social (Memgraph)
MATCH (c:Customer)-[:PURCHASED]->(p:Product) RETURN c.name, p.title; -- → store (PostgreSQL)
-- pin a query to a graph explicitly
USE store MATCH (p:Product) WHERE p.price > 100 RETURN p.title ORDER BY p.price DESC;The shared uid / cid id space also lets one query join across both
backends. See Multiple Graphs for the
full query model — USE-free routing, cross-backend joins, and composites — and
Reference for
the runtime DDL that builds the same catalog live.
How it works
- Connector = connection, graph = mapping. A graph owns a per-connector
mapping (keyed internally
<graph>/<connector>), so one graph can span multiple backends. - USE-free routing. At boot — and on
REFRESH SCHEMA— the engine builds a label → source index from the graphs, connectors, and introspected native schemas. A query with noUSEis routed by the labels (and declared properties) it mentions; ambiguity yields an actionable error rather than a silent guess. - Per-query mapping. Right before a routed query runs, the target connection’s executor is stamped with that graph’s mapping, so relational backends translate GQL → SQL against the right tables/columns while native backends pass Cypher straight through.
- Cross-backend federation. When one statement spans graphs, each part runs on its own backend and the results are joined in-engine (hash join).