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.json

Every 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"
      }
    }
  ]
}
FieldRequiredDescription
nameyesReferenced by graphs and DDL.
typeyesBackend type (see below).
connectionyesA 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 fieldUsed for
uriConnection string for the backend.
user / passwordCredentials (override / complement the URI).
databaseDatabase name.
schemaMiddle namespace (PostgreSQL schema, MySQL / ClickHouse database, Iceberg schema).
catalogNative root for three-level backends (Iceberg catalog, SQL Server database).
pathFile-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 / edges at the top level instead of a graphs block and you get one implicit graph named default.

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" }
  ]
}
FieldSource kindDescription
mappedTableSource.connectorrelationalConnector name (alias catalog).
mappedTableSource.schemarelationalOptional middle namespace (PostgreSQL schema, etc.).
mappedTableSource.tablerelationalThe backing table.
mappedTableSource.metaFields.idrelationalPrimary-key column — the node’s identity and the join key for edges.
mappedGraphSource.connectornativeConnector 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 keyDescription
idThe edge row’s own key.
fromForeign-key column pointing at the source vertex’s id column.
toForeign-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.

FieldRequiredDescription
nameyesThe GQL property name.
columnnoThe backing column (defaults to name), e.g. property name ← column full_name.
typenoRecorded, 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 declared attribute. Route by label / relationship type, or add an explicit USE, 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 no USE is 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).