flowquery 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowquery"
3
- version = "1.0.11"
3
+ version = "1.0.12"
4
4
  description = "A declarative query language for data processing pipelines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Dict, Optional, Union
5
+ from typing import Any, Dict, Optional, Union
6
6
 
7
7
  from ..parsing.ast_node import ASTNode
8
8
  from .node import Node
@@ -54,6 +54,30 @@ class Database:
54
54
  """Gets a relationship from the database."""
55
55
  return Database._relationships.get(relationship.type) if relationship.type else None
56
56
 
57
+ async def schema(self) -> list[dict[str, Any]]:
58
+ """Returns the graph schema with node/relationship labels and sample data."""
59
+ result: list[dict[str, Any]] = []
60
+
61
+ for label, physical_node in Database._nodes.items():
62
+ records = await physical_node.data()
63
+ entry: dict[str, Any] = {"kind": "node", "label": label}
64
+ if records:
65
+ sample = {k: v for k, v in records[0].items() if k != "id"}
66
+ if sample:
67
+ entry["sample"] = sample
68
+ result.append(entry)
69
+
70
+ for rel_type, physical_rel in Database._relationships.items():
71
+ records = await physical_rel.data()
72
+ entry_rel: dict[str, Any] = {"kind": "relationship", "type": rel_type}
73
+ if records:
74
+ sample = {k: v for k, v in records[0].items() if k not in ("left_id", "right_id")}
75
+ if sample:
76
+ entry_rel["sample"] = sample
77
+ result.append(entry_rel)
78
+
79
+ return result
80
+
57
81
  async def get_data(self, element: Union['Node', 'Relationship']) -> Union['NodeData', 'RelationshipData']:
58
82
  """Gets data for a node or relationship."""
59
83
  if isinstance(element, Node):
@@ -27,6 +27,7 @@ from .range_ import Range
27
27
  from .reducer_element import ReducerElement
28
28
  from .replace import Replace
29
29
  from .round_ import Round
30
+ from .schema import Schema
30
31
  from .size import Size
31
32
  from .split import Split
32
33
  from .stringify import Stringify
@@ -71,5 +72,6 @@ __all__ = [
71
72
  "ToJson",
72
73
  "Type",
73
74
  "Functions",
75
+ "Schema",
74
76
  "PredicateSum",
75
77
  ]
@@ -0,0 +1,36 @@
1
+ """Schema introspection function."""
2
+
3
+ from typing import Any, AsyncGenerator
4
+
5
+ from .async_function import AsyncFunction
6
+ from .function_metadata import FunctionDef
7
+
8
+
9
+ @FunctionDef({
10
+ "description": (
11
+ "Returns the graph schema listing all nodes and relationships "
12
+ "with a sample of their data."
13
+ ),
14
+ "category": "async",
15
+ "parameters": [],
16
+ "output": {
17
+ "description": "Schema entry with kind, label/type, and optional sample data",
18
+ "type": "object",
19
+ },
20
+ "examples": [
21
+ "CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample",
22
+ ],
23
+ })
24
+ class Schema(AsyncFunction):
25
+ """Returns the graph schema of the database.
26
+
27
+ Lists all nodes and relationships with their labels/types and a sample
28
+ of their data (excluding id from nodes, left_id and right_id from relationships).
29
+ """
30
+
31
+ async def generate(self) -> AsyncGenerator[Any, None]:
32
+ # Import at runtime to avoid circular dependency
33
+ from ...graph.database import Database
34
+ entries = await Database.get_instance().schema()
35
+ for entry in entries:
36
+ yield entry
@@ -1539,4 +1539,49 @@ class TestRunner:
1539
1539
  await match.run()
1540
1540
  results = match.results
1541
1541
  assert len(results) == 1
1542
- assert results[0]["name"] == "Employee 1"
1542
+ assert results[0]["name"] == "Employee 1"
1543
+
1544
+ @pytest.mark.asyncio
1545
+ async def test_schema_returns_nodes_and_relationships_with_sample_data(self):
1546
+ """Test schema() returns nodes and relationships with sample data."""
1547
+ await Runner(
1548
+ """
1549
+ CREATE VIRTUAL (:Animal) AS {
1550
+ UNWIND [
1551
+ {id: 1, species: 'Cat', legs: 4},
1552
+ {id: 2, species: 'Dog', legs: 4}
1553
+ ] AS record
1554
+ RETURN record.id AS id, record.species AS species, record.legs AS legs
1555
+ }
1556
+ """
1557
+ ).run()
1558
+ await Runner(
1559
+ """
1560
+ CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
1561
+ UNWIND [
1562
+ {left_id: 2, right_id: 1, speed: 'fast'}
1563
+ ] AS record
1564
+ RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
1565
+ }
1566
+ """
1567
+ ).run()
1568
+
1569
+ runner = Runner(
1570
+ "CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
1571
+ )
1572
+ await runner.run()
1573
+ results = runner.results
1574
+
1575
+ animal = next((r for r in results if r.get("kind") == "node" and r.get("label") == "Animal"), None)
1576
+ assert animal is not None
1577
+ assert animal["sample"] is not None
1578
+ assert "id" not in animal["sample"]
1579
+ assert "species" in animal["sample"]
1580
+ assert "legs" in animal["sample"]
1581
+
1582
+ chases = next((r for r in results if r.get("kind") == "relationship" and r.get("type") == "CHASES"), None)
1583
+ assert chases is not None
1584
+ assert chases["sample"] is not None
1585
+ assert "left_id" not in chases["sample"]
1586
+ assert "right_id" not in chases["sample"]
1587
+ assert "speed" in chases["sample"]