diff --git a/pages/memgraph-zero/memgql/changelog.mdx b/pages/memgraph-zero/memgql/changelog.mdx index eb4497a36..e8473bed4 100644 --- a/pages/memgraph-zero/memgql/changelog.mdx +++ b/pages/memgraph-zero/memgql/changelog.mdx @@ -11,6 +11,31 @@ description: MemGQL release notes ## MemGQL v0.15.0 - unreleased +### 🍃 New features & Improvements + +- **One vertex in several connectors.** A label declared once per connector, + every declaration naming the same `linkKey`, is one entity: a path crosses + from one database into another at that node, with no edge to declare and no + connector named in the query. `MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book)<-[:REVIEWED]-(r:Reader)` + reads the books in one Memgraph, their reviews in another, and joins them on + the key. Whole-node returns come back as one node with the properties of + every side, connected to the relationships on both. See + [One vertex in several connectors](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors). +- **Faster cross-backend joins between connectors no graph maps.** Two + `MATCH` clauses that route to two different native connectors now send the + join keys of the filtered side to the other one as an `IN` list, instead of + reading both sides in full. +- **A clearer error when a label lives in several connectors of one graph.** + The error now names the connectors to choose from instead of the graph. + +### 🐞 Bug fixes + +- **Content in another connector is read wherever the vertex sits in the + path.** A vertex with a `mappedContentSource` that was not the first node of + its pattern, as `b` in `MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book) RETURN + b.blurb`, had its content properties come back `null`; the content + connector was never asked. + ## MemGQL v0.14.0 - October 25th, 2026 ### ⚠️ Behavior and breaking changes diff --git a/pages/memgraph-zero/memgql/connect/memgraph.mdx b/pages/memgraph-zero/memgql/connect/memgraph.mdx index cd1b0796e..9488034c0 100644 --- a/pages/memgraph-zero/memgql/connect/memgraph.mdx +++ b/pages/memgraph-zero/memgql/connect/memgraph.mdx @@ -87,6 +87,31 @@ CYPHER MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN a.name, b.name; For environment variables, see [Reference](/memgraph-zero/memgql/reference#memgraph-memgraph-memgraph-gql). +## Several Memgraph instances + +Each instance is its own connector, and one MemGQL endpoint queries them all. +With a `catalog` instance holding `(:Book)-[:WRITTEN_BY]->(:Author)` and a +`reviews` instance holding `(:Reader)-[:REVIEWED]->(:Book)`, both carry `Book`, +so the relationship type picks the instance: + +```gql +MATCH (b:Book)<-[:REVIEWED]-(r:Reader) RETURN b.title, r.name; -- runs on reviews +``` + +A query over both joins two `MATCH` clauses on a property. The filtered side +runs first and its keys go to the other instance as one `IN` list: + +```gql +MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book) +MATCH (b2:Book)<-[:REVIEWED]-(r:Reader) +WHERE a.name = 'Frank Herbert' AND b.isbn = b2.isbn +RETURN b.title, r.name; +``` + +Declare `Book` in both with a `linkKey` and the same question is one path, +`MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book)<-[:REVIEWED]-(r:Reader)`; see +[One vertex in several connectors](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors). + ## Supported GQL features GQL-specific syntax (INSERT, LOCAL_DATETIME, etc.) only works on `memgraph-gql`; the plain `memgraph` connector is Cypher passthrough. diff --git a/pages/memgraph-zero/memgql/connect/redshift.mdx b/pages/memgraph-zero/memgql/connect/redshift.mdx index f52e98ec7..e518865d4 100644 --- a/pages/memgraph-zero/memgql/connect/redshift.mdx +++ b/pages/memgraph-zero/memgql/connect/redshift.mdx @@ -160,6 +160,40 @@ MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name; MATCH (a:Person)((-[:KNOWS]->()){1,3})(b:Person) RETURN DISTINCT b.name; ``` +## Joining Redshift with a graph + +A Redshift table can be one home of an entity that also lives in a graph +database. Declare it with the same label and `linkKey` as the graph's +declaration, and a path crosses from the graph into Redshift at that node. +Next to the graph's own vertices and edges (here `Author` and `WRITTEN_BY` in +a `catalog` Memgraph), the Redshift side of the graph is: + +```json +{ "label": "Book", "mappedGraphSource": { "connector": "catalog" }, "linkKey": "isbn" }, +{ "label": "Book", + "mappedTableSource": { "connector": "warehouse", "table": "books", "metaFields": { "id": "isbn" } }, + "attributes": [ { "name": "isbn" }, { "name": "title" } ], + "linkKey": "isbn" }, +{ "label": "Sale", + "mappedTableSource": { "connector": "warehouse", "table": "sales", "metaFields": { "id": "sale_id" } }, + "attributes": [ { "name": "isbn" }, { "name": "region" }, { "name": "units", "type": "Int" } ] }, +{ "label": "SOLD_IN", + "from": "Book", + "to": "Sale", + "mappedJoinSource": { "fromKey": "isbn", "toKey": "isbn" } } +``` + +```gql +MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book)-[:SOLD_IN]->(s:Sale) +WHERE a.name = 'Frank Herbert' +RETURN b.title, s.region, s.units; +``` + +The books are found in the graph, and Redshift is asked about their `isbn` +values only, as bind parameters, with `books` joined to `sales` inside +Redshift. See +[One vertex in several connectors](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors). + ## Authentication The URI scheme selects the method: diff --git a/pages/memgraph-zero/memgql/multiple-graphs.mdx b/pages/memgraph-zero/memgql/multiple-graphs.mdx index f038a338a..49d1496fa 100644 --- a/pages/memgraph-zero/memgql/multiple-graphs.mdx +++ b/pages/memgraph-zero/memgql/multiple-graphs.mdx @@ -325,6 +325,12 @@ path pattern, and carries no properties of its own. See [Schema File → Cross-connector edges](/memgraph-zero/memgql/schema-file#cross-connector-edges) for the full rules. +When the two sides are the **same** entity kept in two databases (a book in a +catalog graph and in a reviews graph), declare it once per connector with a +`linkKey` instead, and a path crosses between them at that node with no edge to +write; see +[Schema File → One vertex in several connectors](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors). + ## Composite Queries Across Graphs The GQL standard defines composite expressions combining query branches with `UNION`, `INTERSECT`, and `EXCEPT`. Each branch can target a different graph. diff --git a/pages/memgraph-zero/memgql/reference.mdx b/pages/memgraph-zero/memgql/reference.mdx index 1e19b180f..334d28166 100644 --- a/pages/memgraph-zero/memgql/reference.mdx +++ b/pages/memgraph-zero/memgql/reference.mdx @@ -26,6 +26,9 @@ supports. | Pass a whole node through `WITH n` to a later step | ✓ | ✓ | ✓ | | `MATCH (n)-[r:R]->(m)` typed edge expansion | ✓ | ✓ | ✓ | | Multi-label pattern `(n:A&B)`; shared label over several mappings | ✓ | ✓ | ✗ | +| Path crossing connectors at a [linked vertex](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors) (`linkKey`) | ✓ | ✓ | ✓ | +| [Cross-connector edge](/memgraph-zero/memgql/schema-file#cross-connector-edges) (`mappedJoinSource`) | ✓ | ✓ | ✓ | +| Holding the [content of a vertex](/memgraph-zero/memgql/schema-file#content-in-another-connector) in another connector (`mappedContentSource`) | ✗ | ✓ | ✓ | | Untyped edge `()-[]->(b)` (union over types) | ✓ | ✗ | ✗ | | `UNION` / `UNION ALL` / `UNION DISTINCT` | ✓ | ✓ | ✗ | | `INTERSECT` / `EXCEPT` | ✓ | ✓ | ✓ | @@ -59,6 +62,11 @@ supports. | `REMOVE` (property delete) | ✓ | ✗ | ✓ | | `SET` / `REMOVE` of a **label** | ✓ | ✗ | ✗ | +The three cross-connector mapping rows hold for every connector tested against +Memgraph on the other side: PostgreSQL, MySQL, DuckDB, SQL Server, ClickHouse, +Amazon Redshift, MongoDB and Neo4j. A native graph (Memgraph, Neo4j) cannot +hold a vertex's content, which lives in a table or collection. + ### Known limitations - **Unbounded variable-length paths on SQL backends** (`()-[*]->()`) @@ -91,8 +99,12 @@ supports. individual nodes / edges instead. - **Shared labels** (a label listed by several vertex mappings, see [Vertices](/memgraph-zero/memgql/schema-file#vertices)) are unioned only - within one connector; a label shared across connectors is refused by routing - as ambiguous. A pattern matched through a shared label can be returned, + within one connector. A label several connectors carry is placed by the + relationship types next to it in the pattern (`(b:Book)<-[:REVIEWED]-()` + with `REVIEWED` stored in one connector); with nothing to place it, routing + refuses it as ambiguous and names the candidates. Declared with a + [`linkKey`](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors), + such a label is one entity, and a path may cross connectors at it. A pattern matched through a shared label can be returned, filtered, sorted and aggregated, but not traversed from (`(a:Animal)-[:R]->()`), not `INSERT`ed, and JSON keys (`a.props.key`) cannot be read through it — each is refused with a pointer to the identifying label to use instead. @@ -472,7 +484,10 @@ USE ; In `multi` mode, a query with no `USE` clause routes automatically when its schema signals (labels, relationship types, properties) match exactly one -source; zero or multiple matches hard-error. Identifier matching is **exact** +source; zero or multiple matches hard-error. A label several sources carry is +narrowed by the relationship types and properties around it, and when the +candidates are connectors of one graph the error names those connectors. +Identifier matching is **exact** (`:person` ≠ `Person`). Memgraph sources must run with `--schema-info-enabled` for property-level introspection. See [Multiple Graphs → Routing](/memgraph-zero/memgql/multiple-graphs#routing). @@ -782,6 +797,10 @@ The same mapping body is used everywhere a graph is defined: - **`MAPPING_FILE`** in single-connector mode: a bare `{ "vertices": …, "edges": … }` body (the connector comes from the environment). +A label is unique within a graph, except that the same entity may be declared +once per connector when every declaration names the same `linkKey`; see +[One vertex in several connectors](/memgraph-zero/memgql/schema-file#one-vertex-in-several-connectors). + A mapping with a required field missing (for example a relational edge without `metaFields.from` / `metaFields.to`) is rejected before the server serves queries against it. The legacy `nodes` / `id_column` / `rel_type` format is no diff --git a/pages/memgraph-zero/memgql/schema-file.mdx b/pages/memgraph-zero/memgql/schema-file.mdx index a5297d276..14bdad99b 100644 --- a/pages/memgraph-zero/memgql/schema-file.mdx +++ b/pages/memgraph-zero/memgql/schema-file.mdx @@ -253,7 +253,9 @@ A vertex maps a **label** to a backend source. Exactly one source kind is set: A vertex may also add a **`mappedContentSource`** on top of either one, to read some of its properties from a table in another connector (see -[Content in another connector](#content-in-another-connector)). +[Content in another connector](#content-in-another-connector)), or a +**`linkKey`**, to declare the same entity in several connectors (see +[One vertex in several connectors](#one-vertex-in-several-connectors)). ```json { @@ -343,7 +345,7 @@ spelling [schema discovery](#discovered-native-schema) produces. | Field | Source kind | Description | |-------|-------------|-------------| -| `label` / `labels` | both | The node label, or a list of labels whose first entry identifies the mapping (unique per graph) and whose others may be shared with other mappings. | +| `label` / `labels` | both | The node label, or a list of labels whose first entry identifies the mapping (unique per graph, unless every declaration names a `linkKey`) and whose others may be shared with other mappings. | | `mappedTableSource.connector` | relational | Connector name (alias `catalog`). | | `mappedTableSource.schema` | relational | Optional middle namespace (PostgreSQL schema, etc.). | | `mappedTableSource.database` | relational | Optional outer namespace for backends with three-part names; on SQL Server it renders `database.schema.table`, so one connection can reach views in other databases. | @@ -352,6 +354,7 @@ spelling [schema discovery](#discovered-native-schema) produces. | `mappedGraphSource.connector` | native | Connector name (Memgraph / Neo4j); the query passes through as Cypher. | | `mappedGraphSource.label` | native | Optional backend label (on an edge, backend relationship type) exposed under this element's `label`. Queries use the exposed name, MemGQL rewrites it for the backend, and returned nodes and relationships carry the exposed name. Two elements cannot share a backend name. Not renamed: strings a query computes from names (`labels(n)`, `type(r)`), the extra labels of a multi-label vertex, and property keys. | | `mappedContentSource` | either | Optional second source for some of the vertex's properties; see [Content in another connector](#content-in-another-connector). | +| `linkKey` | either | Optional property identifying the same entity in every connector that declares this label; see [One vertex in several connectors](#one-vertex-in-several-connectors). | | `attributes` | either | The properties the vertex exposes; see [Attributes](#attributes). | #### Content in another connector @@ -397,7 +400,7 @@ content connector. | `mappedContentSource` key | Required | Description | |---------------------------|----------|-------------| -| `connector` | yes | A **relational** connector, different from the vertex's own. | +| `connector` | yes | A **relational** or **MongoDB** connector, different from the vertex's own. | | `schema` | no | Middle namespace of the content table. | | `table` | yes | The content table. | | `key` | yes | A **property** declared in the vertex's own `attributes`; its value finds the content row. | @@ -410,12 +413,95 @@ instead): - Each property has exactly one home: a name may not appear both in the vertex's `attributes` and in the content `attributes`, and the `key` is declared only on the vertex. -- The content connector must be relational and differ from the vertex's +- The content connector must be relational or MongoDB and differ from the vertex's connector. Content in the same connector is just more attributes. - A query that reads content properties must be a single `MATCH … RETURN`: no `WITH`, `UNWIND`, `CALL`, `UNION` or `RETURN *`, and `OPTIONAL MATCH` may not bind the vertex whose content is read. +#### One vertex in several connectors + +The same entity often lives in more than one database: a book is in the +catalog graph, in the reviews graph, and in the sales warehouse, each time +with its own properties and relationships. Declare the label **once per +connector**, and give every declaration the same **`linkKey`**: the property +whose value says "this is the same book everywhere". + +``` + Memgraph "catalog" Memgraph "reviews" Redshift "warehouse" + ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ + │ (:Book │ │ (:Book │ │ books │ + │ isbn: "978-0441…" │ │ isbn: "978-0441…" │ │ isbn: '978-0441…' │ + │ title: "Dune") │ │ avgRating: 4.6) │ │ units_sold: 120000 │ + └───────────┬────────────┘ └───────────┬────────────┘ └───────────┬────────────┘ + └──────────── "linkKey": "isbn" ⇒ one Book ─────────────┘ +``` + +```json +{ "label": "Book", "mappedGraphSource": { "connector": "catalog" }, "linkKey": "isbn" }, +{ "label": "Book", "mappedGraphSource": { "connector": "reviews" }, "linkKey": "isbn" }, +{ "label": "Book", + "mappedTableSource": { "connector": "warehouse", "table": "books", "metaFields": { "id": "isbn" } }, + "attributes": [ { "name": "isbn" }, { "name": "unitsSold", "column": "units_sold" } ], + "linkKey": "isbn" } +``` + +Each declaration is an ordinary vertex of its connector. An edge that names +`Book` reads the declaration in its own connector: `WRITTEN_BY` in `catalog`, +`REVIEWED` in `reviews`. A path can therefore cross from one database into +another **at a book**, and the query never says where anything lives: + +```gql +MATCH (a:Author)<-[:WRITTEN_BY]-(b:Book)<-[:REVIEWED]-(r:Reader) +WHERE a.name = 'Frank Herbert' +RETURN a, b, r; +``` + +`WRITTEN_BY` lives in `catalog` and `REVIEWED` in `reviews`, so MemGQL reads +the author's books in `catalog`, sends their `isbn` values to `reviews` as one +`IN` list, and joins the two halves on `isbn`: + +``` + catalog reviews + (a Frank Herbert)──[:WRITTEN_BY]──(b ?) (b' ?)──[:REVIEWED]──(r Reader) + └── b.isbn = b'.isbn ──┘ +``` + +`RETURN b` comes back as **one** node carrying the properties of both +halves (`title` from `catalog`, `avgRating` from `reviews`), and the +`WRITTEN_BY` and `REVIEWED` relationships both point at it, so a graph client +draws one connected path. An equality filter on either side runs first in its +own database and narrows what the other side is asked for; a `LIMIT` applies to +the joined result. + +**Same label, different names in the backends.** Linking groups declarations +by their exposed `label`, but the backends may name things differently. A +table's name never matters, and a native label can be renamed with +`mappedGraphSource.label` +(`{ "label": "Book", "mappedGraphSource": { "connector": "reviews", "label": "Title" }, "linkKey": "isbn" }`). +The key is one property name on every declaration: a table can map it from any +column (`{ "name": "isbn", "column": "isbn13" }`), while on Memgraph and Neo4j +the stored property must itself be called `isbn`. Two different entities, such +as `Book` and `Author`, are not linked: connect them with a +[cross-connector edge](#cross-connector-edges). + +**Rules and limits** (each is refused with a message saying what to write +instead): + +- Every declaration names the same `linkKey`, at most one declaration per + connector, and a label declared twice without `linkKey` is a duplicate. +- On a table declaration, `linkKey` must be one of its declared attributes, as + a plain column (not a JSON `path`). +- `linkKey` does not combine with `mappedContentSource`, and a + `mappedJoinSource` edge between two declarations of the same linked label is + refused: the key already joins them. +- A `Book` with no relationship to place it + (`MATCH (b:Book {isbn: '…'}) RETURN b`) is refused, naming the connectors to + choose from. Add a relationship that only one of them stores. +- A path crosses connectors in a single `MATCH`, without `OPTIONAL MATCH`, + path variables, shortest-path search or variable-length hops across the + crossing. + ### Edges An edge maps a **relationship type** between two labels. Relational edges name @@ -854,10 +940,11 @@ Every name a schema file can contain, grouped by where it appears. | Name | Meaning | |------|---------| -| `label` | The exposed node label; unique within a graph. | +| `label` | The exposed node label; unique within a graph, except that a linked label is declared once per connector. | | `labels` | Several labels; the first identifies the mapping, the rest may be shared. | | `mappedTableSource` / `mappedGraphSource` | Where the vertex lives; exactly one is set. | | `mappedContentSource` | Optional table in another connector for some of its properties. | +| `linkKey` | The **property** identifying the same entity in every connector that declares the label. | | `attributes` | The properties read from the vertex's own source. | ### Edge