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

Run MemGQL in Docker: mount the schema file and pass its container path.

docker run --rm -p 7688:7688 \
  -e BOLT_LISTEN_ADDR=0.0.0.0:7688 \
  -v "$(pwd)/schema.json:/data/schema.json" \
  memgraph/memgql:latest --schema=/data/schema.json

--schema (or the SCHEMA_FILE env var) is all you need; it implies multi-connector mode. Set BOLT_LISTEN_ADDR=0.0.0.0:7688 so the Bolt port is reachable from outside the container (the default binds loopback only), and run MemGQL on the same Docker network as the backends its connectors reference.

Every connector connects and every graph registers at boot, so no CONNECT or USE is required to start querying. Runtime DDL writes the updated catalog back to this file atomically, so a restart reloads the same state with no replay (credentials stay on disk but are masked in EXPORT SCHEMA).

The file is validated at boot. A malformed mapping (for example 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:

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, Fabric warehouse/lakehouse item).
pathFile-backed backends (DuckDB).
sslRootCertPEM bundle of CA certificates to trust instead of the system roots (PostgreSQL).
trustServerCertificateEncrypt without verifying the server’s certificate (PostgreSQL). Mutually exclusive with sslRootCert.

Supported type values: memgraph, neo4j, postgres (postgresql), mysql, sqlserver, oracle, duckdb, iceberg, iceberg-direct, clickhouse, pinot, snowflake, mongodb, fabric.

For PostgreSQL, the TLS mode stays in the URI (sslmode=), where libpq users expect it; sslRootCert / trustServerCertificate declare what a URI cannot express. See TLS connections.

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. Also exposed as a property, so RETURN n includes it and n.<id_column> reads it by name.
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; and a mappedJoinSource edge links two labels that live in different connectors (see Cross-connector edges). Exactly one source kind is set.

{
  "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. Exposed as a property, like a vertex’s.
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.

Cross-connector edges

A third source kind, mappedJoinSource, declares an edge whose two endpoints live in different connectors. It has no backing table anywhere: the relationship is equality between one property on each endpoint.

{
  "label": "MANUFACTURED_BY",
  "from": "Component",
  "to": "Manufacturer",
  "mappedJoinSource": {
    "fromKey": "manufacturer_code",
    "toKey": "code"
  }
}

With Component mapped to a PostgreSQL table and Manufacturer to a Memgraph connector, one pattern now traverses the boundary:

MATCH (c:Component)-[:MANUFACTURED_BY]->(m:Manufacturer) RETURN c.sku, m.name;
mappedJoinSource keyDescription
fromKeyProperty on the from vertex. A GQL property name, not a raw column; it resolves through that vertex’s own mapping.
toKeyProperty on the to vertex, resolved the same way.

This is the only edge form that works when one endpoint is a native-graph backend, which has no joinable edge table to point at. Under the hood the traversal is rewritten into the cross-backend join that already runs — one part per connector plus the join equality — so piping, per-part caching and the local join all apply unchanged.

RETURN c, r, m packs real nodes and a relationship, so graph clients such as Memgraph Lab can draw and expand the result. Element ids are synthesized from the connector, label and key, so two backends that both number rows from 1 don’t collide.

Rules and limits (each is refused with a message saying what to write instead):

  • Both endpoints must live in different connectors — within one connector use mappedTableSource with metaFields.from / to.
  • Each key must be a declared attribute of its endpoint vertex (native-graph vertices are exempt, since the query passes through).
  • The edge must be traversed in a direction: -[:R]-> or <-[:R]-, not -[:R]-.
  • No variable-length traversal, no OPTIONAL MATCH, and one MATCH with one path pattern per query.
  • The edge carries no properties, so it cannot be filtered — filter the endpoints instead.
  • Every node in the pattern needs a label, so each side can be routed.
  • Aggregating across the join (for example count() over both sides) is not supported yet; return the rows and count client-side.

A table-backed edge whose endpoints sit in different connectors is still skipped at load time with a warning pointing at mappedJoinSource.

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).
pathnoLocation inside a JSON document column (see below).

Allowed type values: Boolean, Byte, Short, Int, Long, HugeInt, Float, Double, Decimal, String, Date, DateTime, Json.

JSON / JSONB columns

A document column (PostgreSQL JSONB, MySQL JSON, …) can be mapped two ways, and they compose — use both on the same column.

Declared path. Pin a value inside the document to its own typed property. column names the document column, path the location inside it, and type the type of the value found there:

{
  "name": "voltage",
  "column": "props",
  "path": "electrical.voltage",
  "type": "Double"
}
MATCH (c:Component) WHERE c.voltage > 240 RETURN c.voltage;

pushes down to the source as an extraction cast to the declared type. The cast is what makes the comparison numeric — without it the database compares text, where '90' > '240'. path is a dotted string; for keys that themselves contain a dot, write an explicit segment list: "path": ["spec.v2", "voltage"].

Passthrough. Mark the column Json and it stays a document: RETURN c.props returns a map, and undeclared keys are reachable with ordinary property syntax — c.props.rohs, c.props.electrical.voltage — each pushed down as an extraction on the source. This is the option for heterogeneous blobs where declaring every key up front isn’t possible. An undeclared key carries no declared type, so the comparison supplies one: compared against a number it is cast like a declared numeric path, compared against a string it stays text. A key the document doesn’t have reads as null rather than failing the query, and so does one whose parent is missing or isn’t an object (c.props.nosuch.deeper). Filtering on an absent key matches no rows — including a numeric comparison, where the cast applies to nothing.

{ "name": "props", "type": "Json" }

Limits. path attributes are read-only — an INSERT through a JSON path is rejected rather than clobbering the surrounding document. JSON path support is per backend: PostgreSQL, MySQL, DuckDB, SQL Server, Microsoft Fabric and Snowflake. A mapping that declares a path against any other connector fails to load, so the mismatch surfaces at boot / CREATE GRAPH rather than on every query.

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 queries by.

Note: for routing to match a query by property (e.g. WHERE c.email = …), the property must be a declared attribute. Route by label or relationship type, or add an explicit USE, when a property isn’t declared. See routing.

Note: caching a graph follows the same rule. The cache holds a label’s id and its declared attributes, so a query reading an undeclared property is served from the source rather than from the cache. A label declaring no attributes still caches its ids and topology, which is what traversals and graph algorithms run on.

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 by label:

MATCH (u:User) RETURN u.handle, u.name ORDER BY u.uid LIMIT 5;
MATCH (c:Customer)-[:PURCHASED]->(p:Product) RETURN c.name, p.title;
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 (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 maps labels and relationship types onto one or more connectors, so a single graph can span backends.
  • Routing by label. At boot (and on REFRESH SCHEMA) the engine tracks which graph defines each label. A query with no USE routes by the labels and declared properties it mentions; anything ambiguous returns an actionable error rather than a silent guess.