gestaltdb 0.3.0__py3-none-any.whl

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.
gestaltdb/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """GestaltDB package."""
2
+
3
+ from .sampling import SamplingHop, SamplingPattern
4
+ from .ingestion import EdgeList, NodeList
5
+ from .cypher import QueryResult
6
+
7
+ __all__ = ["EdgeList", "NodeList", "QueryResult", "SamplingHop", "SamplingPattern"]
gestaltdb/cypher.py ADDED
@@ -0,0 +1,104 @@
1
+ """Minimal read-only Cypher support for GestaltDB.
2
+
3
+ The supported subset maps directly to existing typed adjacency and sampling APIs:
4
+
5
+ MATCH (a {id: "node-id"})-[:TYPE1]->(b)<-[:TYPE2]-(c) RETURN a.name, b LIMIT 10
6
+ CALL pg.sample_typed_paths(["node-id"], [{"edge_type": "TYPE", "sample_size": 2}]) YIELD path RETURN path
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from .cypher_ast import MatchQuery, MultiMatchQuery, NodeScanQuery, RelationshipScanQuery, SampleTypedPathsCall
14
+ from .cypher_plan import LogicalPlan, plan_query
15
+ from .cypher_parser import parse as _parse_query, split_top_level_args as _split_top_level_args
16
+ from .cypher_runtime import QueryContext, execute_match, execute_multi_match, execute_node_scan, execute_relationship_scan
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class QueryResult:
21
+ """Tabular query result returned by ``GraphDB.query``.
22
+
23
+ ``columns`` contains projected column names in return order. ``records`` is
24
+ a list of dictionaries keyed by column name.
25
+
26
+ Examples:
27
+ >>> result = QueryResult(columns=("n",), records=[{"n": "node"}])
28
+ >>> len(result)
29
+ 1
30
+ >>> list(result)[0]["n"]
31
+ 'node'
32
+ """
33
+
34
+ columns: tuple[str, ...]
35
+ records: list[dict[str, object]]
36
+
37
+ def __iter__(self):
38
+ """Iterate over result records."""
39
+ return iter(self.records)
40
+
41
+ def __len__(self):
42
+ """Return the number of result records."""
43
+ return len(self.records)
44
+
45
+
46
+ def parse(query: str) -> MatchQuery | SampleTypedPathsCall | NodeScanQuery | RelationshipScanQuery | MultiMatchQuery:
47
+ """Parse the supported Cypher subset.
48
+
49
+ Args:
50
+ query: Cypher query text.
51
+
52
+ Returns:
53
+ Parsed query object.
54
+
55
+ Raises:
56
+ ValueError: If the query is outside the supported subset.
57
+
58
+ Examples:
59
+ >>> parse('MATCH (n:Drug) RETURN n').label
60
+ 'Drug'
61
+ """
62
+ return _parse_query(query)
63
+
64
+
65
+ def plan(query: str) -> LogicalPlan:
66
+ """Return the logical plan for a supported Cypher query."""
67
+ return plan_query(parse(query))
68
+
69
+
70
+ def execute(graph, query: str, parameters: dict[str, object] | None = None) -> QueryResult:
71
+ """Execute a supported Cypher query against a ``GraphDB`` instance.
72
+
73
+ Args:
74
+ graph: ``GraphDB`` instance used for indexed lookups and traversal.
75
+ query: Cypher query text.
76
+
77
+ Returns:
78
+ ``QueryResult`` with projected records.
79
+
80
+ Examples:
81
+ >>> execute(graph_db, 'MATCH (n:Drug) RETURN n') # doctest: +SKIP
82
+ """
83
+ parsed = parse(query)
84
+ plan_query(parsed)
85
+ parameters = parameters or {}
86
+ if isinstance(parsed, SampleTypedPathsCall):
87
+ paths = graph.sample_typed_paths(parsed.seed_ids, parsed.pattern)
88
+ if parsed.limit is not None:
89
+ paths = paths[:parsed.limit]
90
+ return QueryResult(
91
+ columns=parsed.returns,
92
+ records=[{"path": path} for path in paths],
93
+ )
94
+ if isinstance(parsed, NodeScanQuery):
95
+ records = execute_node_scan(parsed, QueryContext(graph=graph, parameters=parameters))
96
+ return QueryResult(columns=parsed.returns, records=records)
97
+ if isinstance(parsed, RelationshipScanQuery):
98
+ records = execute_relationship_scan(parsed, QueryContext(graph=graph, parameters=parameters))
99
+ return QueryResult(columns=parsed.returns, records=records)
100
+ if isinstance(parsed, MultiMatchQuery):
101
+ records = execute_multi_match(parsed, QueryContext(graph=graph, parameters=parameters))
102
+ return QueryResult(columns=parsed.returns, records=records)
103
+ records = execute_match(parsed, QueryContext(graph=graph, parameters=parameters))
104
+ return QueryResult(columns=parsed.returns, records=records)
@@ -0,0 +1,180 @@
1
+ """AST objects for the GestaltDB Cypher subset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Parameter:
10
+ """Cypher query parameter reference, such as ``$name``."""
11
+
12
+ name: str
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class PropertyRef:
17
+ """Reference to a variable property, such as ``n.name``."""
18
+
19
+ variable: str
20
+ property_name: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ComparisonExpression:
25
+ """Binary comparison expression for the current Cypher subset."""
26
+
27
+ left: PropertyRef
28
+ operator: str
29
+ right: object
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class InExpression:
34
+ """Membership predicate, such as ``n.kind IN ["drug"]``."""
35
+
36
+ left: PropertyRef
37
+ values: object
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class NullPredicate:
42
+ """Null check predicate."""
43
+
44
+ expression: PropertyRef
45
+ negated: bool = False
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class AndExpression:
50
+ """Conjunction of boolean expressions."""
51
+
52
+ expressions: tuple[object, ...]
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class OrderItem:
57
+ """One ORDER BY item."""
58
+
59
+ expression: str
60
+ descending: bool = False
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class TraversalHop:
65
+ """One typed relationship expansion in a parsed ``MATCH`` pattern."""
66
+
67
+ rel_var: str | None
68
+ edge_type: str
69
+ target_var: str
70
+ direction: str = "out"
71
+ edge_types: tuple[str, ...] = ()
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class MatchQuery:
76
+ """Parsed anchored typed path query."""
77
+
78
+ source_var: str
79
+ source_id: str
80
+ hops: tuple[TraversalHop, ...]
81
+ returns: tuple[str, ...]
82
+ limit: int | None = None
83
+ where: object | None = None
84
+ projections: tuple[str, ...] = ()
85
+ order_by: tuple[OrderItem, ...] = ()
86
+ skip: int | None = None
87
+ distinct: bool = False
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class NodePatternClause:
92
+ """One node pattern in a multi-clause ``MATCH`` query."""
93
+
94
+ variable: str
95
+ label: str | None = None
96
+ property_name: str | None = None
97
+ property_value: object = None
98
+ labels: tuple[str, ...] = ()
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class RelationshipPatternClause:
103
+ """One relationship pattern in a multi-clause ``MATCH`` query."""
104
+
105
+ source_var: str
106
+ rel_var: str | None
107
+ edge_type: str
108
+ target_var: str
109
+ direction: str = "out"
110
+ edge_types: tuple[str, ...] = ()
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class AnchoredPatternClause:
115
+ """One anchored traversal pattern in a multi-clause ``MATCH`` query."""
116
+
117
+ source_var: str
118
+ source_id: str
119
+ hops: tuple[TraversalHop, ...]
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class MultiMatchQuery:
124
+ """Parsed query containing multiple ``MATCH`` clauses."""
125
+
126
+ clauses: tuple[object, ...]
127
+ returns: tuple[str, ...]
128
+ where: object | None = None
129
+ projections: tuple[str, ...] = ()
130
+ order_by: tuple[OrderItem, ...] = ()
131
+ skip: int | None = None
132
+ limit: int | None = None
133
+ distinct: bool = False
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class SampleTypedPathsCall:
138
+ """Parsed ``pg.sample_typed_paths`` procedure call."""
139
+
140
+ seed_ids: list[str]
141
+ pattern: list[dict[str, object]]
142
+ returns: tuple[str, ...] = ("path",)
143
+ limit: int | None = None
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class NodeScanQuery:
148
+ """Parsed indexed node label scan query."""
149
+
150
+ variable: str
151
+ label: str | None
152
+ property_name: str | None
153
+ property_value: object
154
+ returns: tuple[str, ...]
155
+ limit: int | None = None
156
+ where: object | None = None
157
+ labels: tuple[str, ...] = ()
158
+ projections: tuple[str, ...] = ()
159
+ order_by: tuple[OrderItem, ...] = ()
160
+ skip: int | None = None
161
+ distinct: bool = False
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class RelationshipScanQuery:
166
+ """Parsed unanchored typed relationship scan query."""
167
+
168
+ source_var: str
169
+ rel_var: str | None
170
+ edge_type: str
171
+ target_var: str
172
+ returns: tuple[str, ...]
173
+ direction: str = "out"
174
+ edge_types: tuple[str, ...] = ()
175
+ where: object | None = None
176
+ projections: tuple[str, ...] = ()
177
+ order_by: tuple[OrderItem, ...] = ()
178
+ skip: int | None = None
179
+ limit: int | None = None
180
+ distinct: bool = False