diff --git a/docs/cypher.rst b/docs/cypher.rst index 35b6608..f604fa0 100644 --- a/docs/cypher.rst +++ b/docs/cypher.rst @@ -28,8 +28,8 @@ Legend: ✅ supported, 🟡 partially supported, ❌ not supported. - Notes * - Node and edge property storage - ✅ - - 🟡 - - Cypher can return bound ``Node`` and ``Edge`` objects, but property projections such as ``RETURN n.name`` are not implemented yet. + - ✅ + - Cypher can return bound ``Node`` and ``Edge`` objects and project properties such as ``RETURN n.name`` or ``RETURN r.score``. * - Native node labels - ✅ - ✅ @@ -60,12 +60,12 @@ Legend: ✅ supported, 🟡 partially supported, ❌ not supported. - Cypher supports repeated outgoing typed hops from an anchored start node. * - Reverse typed traversal - ✅ - - ❌ - - DB API supports ``direction="in"``. Cypher support for ``<-[:TYPE]-`` is not implemented yet. + - ✅ + - DB API supports ``direction="in"``. Cypher supports ``<-[:TYPE]-`` from an anchored node. * - Undirected typed traversal - ✅ - - ❌ - - DB API supports ``direction="any"``. Cypher support for ``-[:TYPE]-`` is not implemented yet. + - ✅ + - DB API supports ``direction="any"``. Cypher supports ``-[:TYPE]-`` from an anchored node. * - Untyped BFS traversal - ✅ - ❌ @@ -86,6 +86,10 @@ Legend: ✅ supported, 🟡 partially supported, ❌ not supported. - 🟡 - ❌ - DB API has exact-match index lookup helpers, but Cypher ``WHERE`` parsing is future work. + * - Result limiting + - ✅ + - ✅ + - Cypher supports ``LIMIT`` on label scans, anchored typed traversals, and ``pg.sample_typed_paths`` calls. * - Mutating Cypher queries - ✅ - ❌ @@ -123,6 +127,21 @@ it for performance-sensitive lookup. If a property index is not registered, Cypher still restricts the search to the label index and then filters decoded nodes in Python. +Property Projections and Limits +------------------------------- + +Use dot notation in ``RETURN`` to project values from bound nodes and +relationships. Missing properties return ``None``. The special fields ``id`` and +``labels`` are available on nodes; ``id``, ``source``, and ``target`` are +available on relationships. + +.. code-block:: python + + result = graph_db.query('MATCH (d:Drug) RETURN d.id, d.name LIMIT 10') + + for record in result: + print(record["d.id"], record["d.name"]) + Anchored One-Hop Traversal -------------------------- @@ -157,7 +176,15 @@ Relationship variables can be bound and returned. ) for record in result: - print(record["r"].get_id, record["r"].properties) + print(record["r"].get_id, record["r"].properties) + +Relationship properties can be projected directly. + +.. code-block:: python + + result = graph_db.query( + 'MATCH (d {id: "drug-1"})-[r:drug-to-disease]->(x) RETURN r.id, r.type LIMIT 1' + ) Anchored Multi-Hop Traversal ---------------------------- @@ -178,7 +205,31 @@ Relationship variables can be used across multiple hops as well. .. code-block:: python result = graph_db.query( - 'MATCH (d {id: "drug-1"})-[r1:drug-to-protein]->(p)-[r2:protein-to-disease]->(x) RETURN r1, r2, x' + 'MATCH (d {id: "drug-1"})-[r1:drug-to-protein]->(p)-[r2:protein-to-disease]->(x) RETURN r1, r2, x' + ) + +Reverse and Undirected Traversal +-------------------------------- + +Anchored typed traversals can follow outgoing, incoming, or either-direction +relationships. + +.. code-block:: python + + incoming = graph_db.query( + 'MATCH (p {id: "protein-1"})<-[:drug-to-protein]-(d) RETURN p, d' + ) + + undirected = graph_db.query( + 'MATCH (p {id: "protein-1"})-[:drug-to-protein]-(n) RETURN n' + ) + +Direction can vary by hop: + +.. code-block:: python + + result = graph_db.query( + 'MATCH (x {id: "disease-1"})<-[:protein-to-disease]-(p)<-[:drug-to-protein]-(d) RETURN x, p, d' ) Sampling Procedure @@ -206,8 +257,6 @@ Unsupported Cypher features raise ``ValueError`` with a message describing the supported subset. The current Cypher API does not yet support: - Multiple labels in one node pattern, such as ``(n:Drug:Approved)``. -- Reverse or undirected patterns such as ``<-[:TYPE]-`` or ``-[:TYPE]-``. - Unanchored all-node scans such as ``MATCH (n) RETURN n``. - ``WHERE`` predicates. -- Property projections such as ``RETURN n.name``. -- ``LIMIT``, ``ORDER BY``, aggregation, joins across separate patterns, or mutation clauses. +- ``ORDER BY``, aggregation, joins across separate patterns, or mutation clauses. diff --git a/src/pygraphdb/cypher.py b/src/pygraphdb/cypher.py index f690254..8142f68 100644 --- a/src/pygraphdb/cypher.py +++ b/src/pygraphdb/cypher.py @@ -2,7 +2,7 @@ The supported subset maps directly to existing typed adjacency and sampling APIs: - MATCH (a {id: "node-id"})-[:TYPE1]->(b)-[:TYPE2]->(c) RETURN a, b, c + MATCH (a {id: "node-id"})-[:TYPE1]->(b)<-[:TYPE2]-(c) RETURN a.name, b LIMIT 10 CALL pg.sample_typed_paths(["node-id"], [{"edge_type": "TYPE", "sample_size": 2}]) YIELD path RETURN path """ @@ -15,47 +15,59 @@ _IDENTIFIER = r"[A-Za-z_][A-Za-z0-9_]*" _IDENTIFIER_RE = re.compile(rf"^{_IDENTIFIER}$") +_RETURN_ITEM = rf"{_IDENTIFIER}(?:\.{_IDENTIFIER})?" +_RETURN_ITEMS = rf"{_RETURN_ITEM}(?:\s*,\s*{_RETURN_ITEM})*" _ANCHOR_RE = re.compile( rf"^\s*\((?P{_IDENTIFIER})\s*" rf"\{{\s*id\s*:\s*(?P['\"])(?P.*?)(?P=quote)\s*\}}\)" ) -_HOP_RE = re.compile( +_OUT_HOP_RE = re.compile( rf"^\s*-\s*\[(?:(?P{_IDENTIFIER})\s*)?:(?P[^\]\s]+)\]\s*->\s*" rf"\((?P{_IDENTIFIER})\)" ) +_IN_HOP_RE = re.compile( + rf"^\s*<-\s*\[(?:(?P{_IDENTIFIER})\s*)?:(?P[^\]\s]+)\]\s*-\s*" + rf"\((?P{_IDENTIFIER})\)" +) +_ANY_HOP_RE = re.compile( + rf"^\s*-\s*\[(?:(?P{_IDENTIFIER})\s*)?:(?P[^\]\s]+)\]\s*-\s*" + rf"\((?P{_IDENTIFIER})\)" +) _RETURN_RE = re.compile( - rf"^\s*RETURN\s+(?P{_IDENTIFIER}(?:\s*,\s*{_IDENTIFIER})*)\s*;?\s*$", + rf"^\s*RETURN\s+(?P{_RETURN_ITEMS})(?:\s+LIMIT\s+(?P\d+))?\s*;?\s*$", re.IGNORECASE | re.DOTALL, ) _NODE_SCAN_RE = re.compile( rf"^\s*MATCH\s+\((?P{_IDENTIFIER}):(?P