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 +7 -0
- gestaltdb/cypher.py +104 -0
- gestaltdb/cypher_ast.py +180 -0
- gestaltdb/cypher_parser.py +584 -0
- gestaltdb/cypher_plan.py +216 -0
- gestaltdb/cypher_runtime.py +590 -0
- gestaltdb/graphdb.py +2240 -0
- gestaltdb/ingestion.py +233 -0
- gestaltdb/kvstores.py +1414 -0
- gestaltdb/sampling.py +198 -0
- gestaltdb/serializers.py +230 -0
- gestaltdb-0.3.0.dist-info/METADATA +244 -0
- gestaltdb-0.3.0.dist-info/RECORD +15 -0
- gestaltdb-0.3.0.dist-info/WHEEL +5 -0
- gestaltdb-0.3.0.dist-info/top_level.txt +1 -0
gestaltdb/__init__.py
ADDED
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)
|
gestaltdb/cypher_ast.py
ADDED
|
@@ -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
|