redis 5.3.0b5__py3-none-any.whl → 6.0.0b2__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.
- redis/__init__.py +2 -11
- redis/_parsers/base.py +14 -2
- redis/asyncio/client.py +27 -14
- redis/asyncio/cluster.py +85 -59
- redis/asyncio/connection.py +76 -23
- redis/asyncio/lock.py +26 -5
- redis/asyncio/sentinel.py +11 -1
- redis/asyncio/utils.py +1 -1
- redis/auth/token.py +6 -2
- redis/backoff.py +15 -0
- redis/client.py +23 -14
- redis/cluster.py +112 -48
- redis/commands/cluster.py +1 -11
- redis/commands/core.py +219 -207
- redis/commands/helpers.py +0 -70
- redis/commands/redismodules.py +5 -17
- redis/commands/search/aggregation.py +3 -1
- redis/commands/search/commands.py +41 -14
- redis/commands/search/dialect.py +3 -0
- redis/commands/search/profile_information.py +14 -0
- redis/commands/search/query.py +5 -1
- redis/commands/vectorset/__init__.py +46 -0
- redis/commands/vectorset/commands.py +367 -0
- redis/commands/vectorset/utils.py +94 -0
- redis/connection.py +76 -27
- redis/exceptions.py +4 -1
- redis/lock.py +24 -4
- redis/ocsp.py +2 -1
- redis/sentinel.py +3 -1
- redis/utils.py +114 -1
- {redis-5.3.0b5.dist-info → redis-6.0.0b2.dist-info}/METADATA +57 -23
- {redis-5.3.0b5.dist-info → redis-6.0.0b2.dist-info}/RECORD +35 -39
- {redis-5.3.0b5.dist-info → redis-6.0.0b2.dist-info}/WHEEL +1 -2
- redis/commands/graph/__init__.py +0 -263
- redis/commands/graph/commands.py +0 -313
- redis/commands/graph/edge.py +0 -91
- redis/commands/graph/exceptions.py +0 -3
- redis/commands/graph/execution_plan.py +0 -211
- redis/commands/graph/node.py +0 -88
- redis/commands/graph/path.py +0 -78
- redis/commands/graph/query_result.py +0 -588
- redis-5.3.0b5.dist-info/top_level.txt +0 -1
- /redis/commands/search/{indexDefinition.py → index_definition.py} +0 -0
- {redis-5.3.0b5.dist-info → redis-6.0.0b2.dist-info/licenses}/LICENSE +0 -0
redis/commands/graph/commands.py
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
from redis import DataError
|
|
2
|
-
from redis.exceptions import ResponseError
|
|
3
|
-
|
|
4
|
-
from .exceptions import VersionMismatchException
|
|
5
|
-
from .execution_plan import ExecutionPlan
|
|
6
|
-
from .query_result import AsyncQueryResult, QueryResult
|
|
7
|
-
|
|
8
|
-
PROFILE_CMD = "GRAPH.PROFILE"
|
|
9
|
-
RO_QUERY_CMD = "GRAPH.RO_QUERY"
|
|
10
|
-
QUERY_CMD = "GRAPH.QUERY"
|
|
11
|
-
DELETE_CMD = "GRAPH.DELETE"
|
|
12
|
-
SLOWLOG_CMD = "GRAPH.SLOWLOG"
|
|
13
|
-
CONFIG_CMD = "GRAPH.CONFIG"
|
|
14
|
-
LIST_CMD = "GRAPH.LIST"
|
|
15
|
-
EXPLAIN_CMD = "GRAPH.EXPLAIN"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class GraphCommands:
|
|
19
|
-
"""RedisGraph Commands"""
|
|
20
|
-
|
|
21
|
-
def commit(self):
|
|
22
|
-
"""
|
|
23
|
-
Create entire graph.
|
|
24
|
-
"""
|
|
25
|
-
if len(self.nodes) == 0 and len(self.edges) == 0:
|
|
26
|
-
return None
|
|
27
|
-
|
|
28
|
-
query = "CREATE "
|
|
29
|
-
for _, node in self.nodes.items():
|
|
30
|
-
query += str(node) + ","
|
|
31
|
-
|
|
32
|
-
query += ",".join([str(edge) for edge in self.edges])
|
|
33
|
-
|
|
34
|
-
# Discard leading comma.
|
|
35
|
-
if query[-1] == ",":
|
|
36
|
-
query = query[:-1]
|
|
37
|
-
|
|
38
|
-
return self.query(query)
|
|
39
|
-
|
|
40
|
-
def query(self, q, params=None, timeout=None, read_only=False, profile=False):
|
|
41
|
-
"""
|
|
42
|
-
Executes a query against the graph.
|
|
43
|
-
For more information see `GRAPH.QUERY <https://redis.io/commands/graph.query>`_. # noqa
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
|
|
47
|
-
q : str
|
|
48
|
-
The query.
|
|
49
|
-
params : dict
|
|
50
|
-
Query parameters.
|
|
51
|
-
timeout : int
|
|
52
|
-
Maximum runtime for read queries in milliseconds.
|
|
53
|
-
read_only : bool
|
|
54
|
-
Executes a readonly query if set to True.
|
|
55
|
-
profile : bool
|
|
56
|
-
Return details on results produced by and time
|
|
57
|
-
spent in each operation.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
# maintain original 'q'
|
|
61
|
-
query = q
|
|
62
|
-
|
|
63
|
-
# handle query parameters
|
|
64
|
-
query = self._build_params_header(params) + query
|
|
65
|
-
|
|
66
|
-
# construct query command
|
|
67
|
-
# ask for compact result-set format
|
|
68
|
-
# specify known graph version
|
|
69
|
-
if profile:
|
|
70
|
-
cmd = PROFILE_CMD
|
|
71
|
-
else:
|
|
72
|
-
cmd = RO_QUERY_CMD if read_only else QUERY_CMD
|
|
73
|
-
command = [cmd, self.name, query, "--compact"]
|
|
74
|
-
|
|
75
|
-
# include timeout is specified
|
|
76
|
-
if isinstance(timeout, int):
|
|
77
|
-
command.extend(["timeout", timeout])
|
|
78
|
-
elif timeout is not None:
|
|
79
|
-
raise Exception("Timeout argument must be a positive integer")
|
|
80
|
-
|
|
81
|
-
# issue query
|
|
82
|
-
try:
|
|
83
|
-
response = self.execute_command(*command)
|
|
84
|
-
return QueryResult(self, response, profile)
|
|
85
|
-
except ResponseError as e:
|
|
86
|
-
if "unknown command" in str(e) and read_only:
|
|
87
|
-
# `GRAPH.RO_QUERY` is unavailable in older versions.
|
|
88
|
-
return self.query(q, params, timeout, read_only=False)
|
|
89
|
-
raise e
|
|
90
|
-
except VersionMismatchException as e:
|
|
91
|
-
# client view over the graph schema is out of sync
|
|
92
|
-
# set client version and refresh local schema
|
|
93
|
-
self.version = e.version
|
|
94
|
-
self._refresh_schema()
|
|
95
|
-
# re-issue query
|
|
96
|
-
return self.query(q, params, timeout, read_only)
|
|
97
|
-
|
|
98
|
-
def merge(self, pattern):
|
|
99
|
-
"""
|
|
100
|
-
Merge pattern.
|
|
101
|
-
"""
|
|
102
|
-
query = "MERGE "
|
|
103
|
-
query += str(pattern)
|
|
104
|
-
|
|
105
|
-
return self.query(query)
|
|
106
|
-
|
|
107
|
-
def delete(self):
|
|
108
|
-
"""
|
|
109
|
-
Deletes graph.
|
|
110
|
-
For more information see `DELETE <https://redis.io/commands/graph.delete>`_. # noqa
|
|
111
|
-
"""
|
|
112
|
-
self._clear_schema()
|
|
113
|
-
return self.execute_command(DELETE_CMD, self.name)
|
|
114
|
-
|
|
115
|
-
# declared here, to override the built in redis.db.flush()
|
|
116
|
-
def flush(self):
|
|
117
|
-
"""
|
|
118
|
-
Commit the graph and reset the edges and the nodes to zero length.
|
|
119
|
-
"""
|
|
120
|
-
self.commit()
|
|
121
|
-
self.nodes = {}
|
|
122
|
-
self.edges = []
|
|
123
|
-
|
|
124
|
-
def bulk(self, **kwargs):
|
|
125
|
-
"""Internal only. Not supported."""
|
|
126
|
-
raise NotImplementedError(
|
|
127
|
-
"GRAPH.BULK is internal only. "
|
|
128
|
-
"Use https://github.com/redisgraph/redisgraph-bulk-loader."
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
def profile(self, query):
|
|
132
|
-
"""
|
|
133
|
-
Execute a query and produce an execution plan augmented with metrics
|
|
134
|
-
for each operation's execution. Return a string representation of a
|
|
135
|
-
query execution plan, with details on results produced by and time
|
|
136
|
-
spent in each operation.
|
|
137
|
-
For more information see `GRAPH.PROFILE <https://redis.io/commands/graph.profile>`_. # noqa
|
|
138
|
-
"""
|
|
139
|
-
return self.query(query, profile=True)
|
|
140
|
-
|
|
141
|
-
def slowlog(self):
|
|
142
|
-
"""
|
|
143
|
-
Get a list containing up to 10 of the slowest queries issued
|
|
144
|
-
against the given graph ID.
|
|
145
|
-
For more information see `GRAPH.SLOWLOG <https://redis.io/commands/graph.slowlog>`_. # noqa
|
|
146
|
-
|
|
147
|
-
Each item in the list has the following structure:
|
|
148
|
-
1. A unix timestamp at which the log entry was processed.
|
|
149
|
-
2. The issued command.
|
|
150
|
-
3. The issued query.
|
|
151
|
-
4. The amount of time needed for its execution, in milliseconds.
|
|
152
|
-
"""
|
|
153
|
-
return self.execute_command(SLOWLOG_CMD, self.name)
|
|
154
|
-
|
|
155
|
-
def config(self, name, value=None, set=False):
|
|
156
|
-
"""
|
|
157
|
-
Retrieve or update a RedisGraph configuration.
|
|
158
|
-
For more information see `<https://redis.io/commands/graph.config-get/>`__.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
|
|
162
|
-
name : str
|
|
163
|
-
The name of the configuration
|
|
164
|
-
value :
|
|
165
|
-
The value we want to set (can be used only when `set` is on)
|
|
166
|
-
set : bool
|
|
167
|
-
Turn on to set a configuration. Default behavior is get.
|
|
168
|
-
"""
|
|
169
|
-
params = ["SET" if set else "GET", name]
|
|
170
|
-
if value is not None:
|
|
171
|
-
if set:
|
|
172
|
-
params.append(value)
|
|
173
|
-
else:
|
|
174
|
-
raise DataError(
|
|
175
|
-
"``value`` can be provided only when ``set`` is True"
|
|
176
|
-
) # noqa
|
|
177
|
-
return self.execute_command(CONFIG_CMD, *params)
|
|
178
|
-
|
|
179
|
-
def list_keys(self):
|
|
180
|
-
"""
|
|
181
|
-
Lists all graph keys in the keyspace.
|
|
182
|
-
For more information see `GRAPH.LIST <https://redis.io/commands/graph.list>`_. # noqa
|
|
183
|
-
"""
|
|
184
|
-
return self.execute_command(LIST_CMD)
|
|
185
|
-
|
|
186
|
-
def execution_plan(self, query, params=None):
|
|
187
|
-
"""
|
|
188
|
-
Get the execution plan for given query,
|
|
189
|
-
GRAPH.EXPLAIN returns an array of operations.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
query: the query that will be executed
|
|
193
|
-
params: query parameters
|
|
194
|
-
"""
|
|
195
|
-
query = self._build_params_header(params) + query
|
|
196
|
-
|
|
197
|
-
plan = self.execute_command(EXPLAIN_CMD, self.name, query)
|
|
198
|
-
if isinstance(plan[0], bytes):
|
|
199
|
-
plan = [b.decode() for b in plan]
|
|
200
|
-
return "\n".join(plan)
|
|
201
|
-
|
|
202
|
-
def explain(self, query, params=None):
|
|
203
|
-
"""
|
|
204
|
-
Get the execution plan for given query,
|
|
205
|
-
GRAPH.EXPLAIN returns ExecutionPlan object.
|
|
206
|
-
For more information see `GRAPH.EXPLAIN <https://redis.io/commands/graph.explain>`_. # noqa
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
query: the query that will be executed
|
|
210
|
-
params: query parameters
|
|
211
|
-
"""
|
|
212
|
-
query = self._build_params_header(params) + query
|
|
213
|
-
|
|
214
|
-
plan = self.execute_command(EXPLAIN_CMD, self.name, query)
|
|
215
|
-
return ExecutionPlan(plan)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
class AsyncGraphCommands(GraphCommands):
|
|
219
|
-
async def query(self, q, params=None, timeout=None, read_only=False, profile=False):
|
|
220
|
-
"""
|
|
221
|
-
Executes a query against the graph.
|
|
222
|
-
For more information see `GRAPH.QUERY <https://oss.redis.com/redisgraph/master/commands/#graphquery>`_. # noqa
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
|
|
226
|
-
q : str
|
|
227
|
-
The query.
|
|
228
|
-
params : dict
|
|
229
|
-
Query parameters.
|
|
230
|
-
timeout : int
|
|
231
|
-
Maximum runtime for read queries in milliseconds.
|
|
232
|
-
read_only : bool
|
|
233
|
-
Executes a readonly query if set to True.
|
|
234
|
-
profile : bool
|
|
235
|
-
Return details on results produced by and time
|
|
236
|
-
spent in each operation.
|
|
237
|
-
"""
|
|
238
|
-
|
|
239
|
-
# maintain original 'q'
|
|
240
|
-
query = q
|
|
241
|
-
|
|
242
|
-
# handle query parameters
|
|
243
|
-
query = self._build_params_header(params) + query
|
|
244
|
-
|
|
245
|
-
# construct query command
|
|
246
|
-
# ask for compact result-set format
|
|
247
|
-
# specify known graph version
|
|
248
|
-
if profile:
|
|
249
|
-
cmd = PROFILE_CMD
|
|
250
|
-
else:
|
|
251
|
-
cmd = RO_QUERY_CMD if read_only else QUERY_CMD
|
|
252
|
-
command = [cmd, self.name, query, "--compact"]
|
|
253
|
-
|
|
254
|
-
# include timeout is specified
|
|
255
|
-
if isinstance(timeout, int):
|
|
256
|
-
command.extend(["timeout", timeout])
|
|
257
|
-
elif timeout is not None:
|
|
258
|
-
raise Exception("Timeout argument must be a positive integer")
|
|
259
|
-
|
|
260
|
-
# issue query
|
|
261
|
-
try:
|
|
262
|
-
response = await self.execute_command(*command)
|
|
263
|
-
return await AsyncQueryResult().initialize(self, response, profile)
|
|
264
|
-
except ResponseError as e:
|
|
265
|
-
if "unknown command" in str(e) and read_only:
|
|
266
|
-
# `GRAPH.RO_QUERY` is unavailable in older versions.
|
|
267
|
-
return await self.query(q, params, timeout, read_only=False)
|
|
268
|
-
raise e
|
|
269
|
-
except VersionMismatchException as e:
|
|
270
|
-
# client view over the graph schema is out of sync
|
|
271
|
-
# set client version and refresh local schema
|
|
272
|
-
self.version = e.version
|
|
273
|
-
self._refresh_schema()
|
|
274
|
-
# re-issue query
|
|
275
|
-
return await self.query(q, params, timeout, read_only)
|
|
276
|
-
|
|
277
|
-
async def execution_plan(self, query, params=None):
|
|
278
|
-
"""
|
|
279
|
-
Get the execution plan for given query,
|
|
280
|
-
GRAPH.EXPLAIN returns an array of operations.
|
|
281
|
-
|
|
282
|
-
Args:
|
|
283
|
-
query: the query that will be executed
|
|
284
|
-
params: query parameters
|
|
285
|
-
"""
|
|
286
|
-
query = self._build_params_header(params) + query
|
|
287
|
-
|
|
288
|
-
plan = await self.execute_command(EXPLAIN_CMD, self.name, query)
|
|
289
|
-
if isinstance(plan[0], bytes):
|
|
290
|
-
plan = [b.decode() for b in plan]
|
|
291
|
-
return "\n".join(plan)
|
|
292
|
-
|
|
293
|
-
async def explain(self, query, params=None):
|
|
294
|
-
"""
|
|
295
|
-
Get the execution plan for given query,
|
|
296
|
-
GRAPH.EXPLAIN returns ExecutionPlan object.
|
|
297
|
-
|
|
298
|
-
Args:
|
|
299
|
-
query: the query that will be executed
|
|
300
|
-
params: query parameters
|
|
301
|
-
"""
|
|
302
|
-
query = self._build_params_header(params) + query
|
|
303
|
-
|
|
304
|
-
plan = await self.execute_command(EXPLAIN_CMD, self.name, query)
|
|
305
|
-
return ExecutionPlan(plan)
|
|
306
|
-
|
|
307
|
-
async def flush(self):
|
|
308
|
-
"""
|
|
309
|
-
Commit the graph and reset the edges and the nodes to zero length.
|
|
310
|
-
"""
|
|
311
|
-
await self.commit()
|
|
312
|
-
self.nodes = {}
|
|
313
|
-
self.edges = []
|
redis/commands/graph/edge.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
from ..helpers import quote_string
|
|
2
|
-
from .node import Node
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Edge:
|
|
6
|
-
"""
|
|
7
|
-
An edge connecting two nodes.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
def __init__(self, src_node, relation, dest_node, edge_id=None, properties=None):
|
|
11
|
-
"""
|
|
12
|
-
Create a new edge.
|
|
13
|
-
"""
|
|
14
|
-
if src_node is None or dest_node is None:
|
|
15
|
-
# NOTE(bors-42): It makes sense to change AssertionError to
|
|
16
|
-
# ValueError here
|
|
17
|
-
raise AssertionError("Both src_node & dest_node must be provided")
|
|
18
|
-
|
|
19
|
-
self.id = edge_id
|
|
20
|
-
self.relation = relation or ""
|
|
21
|
-
self.properties = properties or {}
|
|
22
|
-
self.src_node = src_node
|
|
23
|
-
self.dest_node = dest_node
|
|
24
|
-
|
|
25
|
-
def to_string(self):
|
|
26
|
-
res = ""
|
|
27
|
-
if self.properties:
|
|
28
|
-
props = ",".join(
|
|
29
|
-
key + ":" + str(quote_string(val))
|
|
30
|
-
for key, val in sorted(self.properties.items())
|
|
31
|
-
)
|
|
32
|
-
res += "{" + props + "}"
|
|
33
|
-
|
|
34
|
-
return res
|
|
35
|
-
|
|
36
|
-
def __str__(self):
|
|
37
|
-
# Source node.
|
|
38
|
-
if isinstance(self.src_node, Node):
|
|
39
|
-
res = str(self.src_node)
|
|
40
|
-
else:
|
|
41
|
-
res = "()"
|
|
42
|
-
|
|
43
|
-
# Edge
|
|
44
|
-
res += "-["
|
|
45
|
-
if self.relation:
|
|
46
|
-
res += ":" + self.relation
|
|
47
|
-
if self.properties:
|
|
48
|
-
props = ",".join(
|
|
49
|
-
key + ":" + str(quote_string(val))
|
|
50
|
-
for key, val in sorted(self.properties.items())
|
|
51
|
-
)
|
|
52
|
-
res += "{" + props + "}"
|
|
53
|
-
res += "]->"
|
|
54
|
-
|
|
55
|
-
# Dest node.
|
|
56
|
-
if isinstance(self.dest_node, Node):
|
|
57
|
-
res += str(self.dest_node)
|
|
58
|
-
else:
|
|
59
|
-
res += "()"
|
|
60
|
-
|
|
61
|
-
return res
|
|
62
|
-
|
|
63
|
-
def __eq__(self, rhs):
|
|
64
|
-
# Type checking
|
|
65
|
-
if not isinstance(rhs, Edge):
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
|
-
# Quick positive check, if both IDs are set.
|
|
69
|
-
if self.id is not None and rhs.id is not None and self.id == rhs.id:
|
|
70
|
-
return True
|
|
71
|
-
|
|
72
|
-
# Source and destination nodes should match.
|
|
73
|
-
if self.src_node != rhs.src_node:
|
|
74
|
-
return False
|
|
75
|
-
|
|
76
|
-
if self.dest_node != rhs.dest_node:
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
# Relation should match.
|
|
80
|
-
if self.relation != rhs.relation:
|
|
81
|
-
return False
|
|
82
|
-
|
|
83
|
-
# Quick check for number of properties.
|
|
84
|
-
if len(self.properties) != len(rhs.properties):
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
# Compare properties.
|
|
88
|
-
if self.properties != rhs.properties:
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
return True
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ProfileStats:
|
|
5
|
-
"""
|
|
6
|
-
ProfileStats, runtime execution statistics of operation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
def __init__(self, records_produced, execution_time):
|
|
10
|
-
self.records_produced = records_produced
|
|
11
|
-
self.execution_time = execution_time
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Operation:
|
|
15
|
-
"""
|
|
16
|
-
Operation, single operation within execution plan.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, name, args=None, profile_stats=None):
|
|
20
|
-
"""
|
|
21
|
-
Create a new operation.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
name: string that represents the name of the operation
|
|
25
|
-
args: operation arguments
|
|
26
|
-
profile_stats: profile statistics
|
|
27
|
-
"""
|
|
28
|
-
self.name = name
|
|
29
|
-
self.args = args
|
|
30
|
-
self.profile_stats = profile_stats
|
|
31
|
-
self.children = []
|
|
32
|
-
|
|
33
|
-
def append_child(self, child):
|
|
34
|
-
if not isinstance(child, Operation) or self is child:
|
|
35
|
-
raise Exception("child must be Operation")
|
|
36
|
-
|
|
37
|
-
self.children.append(child)
|
|
38
|
-
return self
|
|
39
|
-
|
|
40
|
-
def child_count(self):
|
|
41
|
-
return len(self.children)
|
|
42
|
-
|
|
43
|
-
def __eq__(self, o: object) -> bool:
|
|
44
|
-
if not isinstance(o, Operation):
|
|
45
|
-
return False
|
|
46
|
-
|
|
47
|
-
return self.name == o.name and self.args == o.args
|
|
48
|
-
|
|
49
|
-
def __str__(self) -> str:
|
|
50
|
-
args_str = "" if self.args is None else " | " + self.args
|
|
51
|
-
return f"{self.name}{args_str}"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class ExecutionPlan:
|
|
55
|
-
"""
|
|
56
|
-
ExecutionPlan, collection of operations.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
def __init__(self, plan):
|
|
60
|
-
"""
|
|
61
|
-
Create a new execution plan.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
plan: array of strings that represents the collection operations
|
|
65
|
-
the output from GRAPH.EXPLAIN
|
|
66
|
-
"""
|
|
67
|
-
if not isinstance(plan, list):
|
|
68
|
-
raise Exception("plan must be an array")
|
|
69
|
-
|
|
70
|
-
if isinstance(plan[0], bytes):
|
|
71
|
-
plan = [b.decode() for b in plan]
|
|
72
|
-
|
|
73
|
-
self.plan = plan
|
|
74
|
-
self.structured_plan = self._operation_tree()
|
|
75
|
-
|
|
76
|
-
def _compare_operations(self, root_a, root_b):
|
|
77
|
-
"""
|
|
78
|
-
Compare execution plan operation tree
|
|
79
|
-
|
|
80
|
-
Return: True if operation trees are equal, False otherwise
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
# compare current root
|
|
84
|
-
if root_a != root_b:
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
# make sure root have the same number of children
|
|
88
|
-
if root_a.child_count() != root_b.child_count():
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
# recursively compare children
|
|
92
|
-
for i in range(root_a.child_count()):
|
|
93
|
-
if not self._compare_operations(root_a.children[i], root_b.children[i]):
|
|
94
|
-
return False
|
|
95
|
-
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
def __str__(self) -> str:
|
|
99
|
-
def aggraget_str(str_children):
|
|
100
|
-
return "\n".join(
|
|
101
|
-
[
|
|
102
|
-
" " + line
|
|
103
|
-
for str_child in str_children
|
|
104
|
-
for line in str_child.splitlines()
|
|
105
|
-
]
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
def combine_str(x, y):
|
|
109
|
-
return f"{x}\n{y}"
|
|
110
|
-
|
|
111
|
-
return self._operation_traverse(
|
|
112
|
-
self.structured_plan, str, aggraget_str, combine_str
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def __eq__(self, o: object) -> bool:
|
|
116
|
-
"""Compares two execution plans
|
|
117
|
-
|
|
118
|
-
Return: True if the two plans are equal False otherwise
|
|
119
|
-
"""
|
|
120
|
-
# make sure 'o' is an execution-plan
|
|
121
|
-
if not isinstance(o, ExecutionPlan):
|
|
122
|
-
return False
|
|
123
|
-
|
|
124
|
-
# get root for both plans
|
|
125
|
-
root_a = self.structured_plan
|
|
126
|
-
root_b = o.structured_plan
|
|
127
|
-
|
|
128
|
-
# compare execution trees
|
|
129
|
-
return self._compare_operations(root_a, root_b)
|
|
130
|
-
|
|
131
|
-
def _operation_traverse(self, op, op_f, aggregate_f, combine_f):
|
|
132
|
-
"""
|
|
133
|
-
Traverse operation tree recursively applying functions
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
op: operation to traverse
|
|
137
|
-
op_f: function applied for each operation
|
|
138
|
-
aggregate_f: aggregation function applied for all children of a single operation
|
|
139
|
-
combine_f: combine function applied for the operation result and the children result
|
|
140
|
-
""" # noqa
|
|
141
|
-
# apply op_f for each operation
|
|
142
|
-
op_res = op_f(op)
|
|
143
|
-
if len(op.children) == 0:
|
|
144
|
-
return op_res # no children return
|
|
145
|
-
else:
|
|
146
|
-
# apply _operation_traverse recursively
|
|
147
|
-
children = [
|
|
148
|
-
self._operation_traverse(child, op_f, aggregate_f, combine_f)
|
|
149
|
-
for child in op.children
|
|
150
|
-
]
|
|
151
|
-
# combine the operation result with the children aggregated result
|
|
152
|
-
return combine_f(op_res, aggregate_f(children))
|
|
153
|
-
|
|
154
|
-
def _operation_tree(self):
|
|
155
|
-
"""Build the operation tree from the string representation"""
|
|
156
|
-
|
|
157
|
-
# initial state
|
|
158
|
-
i = 0
|
|
159
|
-
level = 0
|
|
160
|
-
stack = []
|
|
161
|
-
current = None
|
|
162
|
-
|
|
163
|
-
def _create_operation(args):
|
|
164
|
-
profile_stats = None
|
|
165
|
-
name = args[0].strip()
|
|
166
|
-
args.pop(0)
|
|
167
|
-
if len(args) > 0 and "Records produced" in args[-1]:
|
|
168
|
-
records_produced = int(
|
|
169
|
-
re.search("Records produced: (\\d+)", args[-1]).group(1)
|
|
170
|
-
)
|
|
171
|
-
execution_time = float(
|
|
172
|
-
re.search("Execution time: (\\d+.\\d+) ms", args[-1]).group(1)
|
|
173
|
-
)
|
|
174
|
-
profile_stats = ProfileStats(records_produced, execution_time)
|
|
175
|
-
args.pop(-1)
|
|
176
|
-
return Operation(
|
|
177
|
-
name, None if len(args) == 0 else args[0].strip(), profile_stats
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
# iterate plan operations
|
|
181
|
-
while i < len(self.plan):
|
|
182
|
-
current_op = self.plan[i]
|
|
183
|
-
op_level = current_op.count(" ")
|
|
184
|
-
if op_level == level:
|
|
185
|
-
# if the operation level equal to the current level
|
|
186
|
-
# set the current operation and move next
|
|
187
|
-
child = _create_operation(current_op.split("|"))
|
|
188
|
-
if current:
|
|
189
|
-
current = stack.pop()
|
|
190
|
-
current.append_child(child)
|
|
191
|
-
current = child
|
|
192
|
-
i += 1
|
|
193
|
-
elif op_level == level + 1:
|
|
194
|
-
# if the operation is child of the current operation
|
|
195
|
-
# add it as child and set as current operation
|
|
196
|
-
child = _create_operation(current_op.split("|"))
|
|
197
|
-
current.append_child(child)
|
|
198
|
-
stack.append(current)
|
|
199
|
-
current = child
|
|
200
|
-
level += 1
|
|
201
|
-
i += 1
|
|
202
|
-
elif op_level < level:
|
|
203
|
-
# if the operation is not child of current operation
|
|
204
|
-
# go back to it's parent operation
|
|
205
|
-
levels_back = level - op_level + 1
|
|
206
|
-
for _ in range(levels_back):
|
|
207
|
-
current = stack.pop()
|
|
208
|
-
level -= levels_back
|
|
209
|
-
else:
|
|
210
|
-
raise Exception("corrupted plan")
|
|
211
|
-
return stack[0]
|
redis/commands/graph/node.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
from ..helpers import quote_string
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Node:
|
|
5
|
-
"""
|
|
6
|
-
A node within the graph.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
def __init__(self, node_id=None, alias=None, label=None, properties=None):
|
|
10
|
-
"""
|
|
11
|
-
Create a new node.
|
|
12
|
-
"""
|
|
13
|
-
self.id = node_id
|
|
14
|
-
self.alias = alias
|
|
15
|
-
if isinstance(label, list):
|
|
16
|
-
label = [inner_label for inner_label in label if inner_label != ""]
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
label is None
|
|
20
|
-
or label == ""
|
|
21
|
-
or (isinstance(label, list) and len(label) == 0)
|
|
22
|
-
):
|
|
23
|
-
self.label = None
|
|
24
|
-
self.labels = None
|
|
25
|
-
elif isinstance(label, str):
|
|
26
|
-
self.label = label
|
|
27
|
-
self.labels = [label]
|
|
28
|
-
elif isinstance(label, list) and all(
|
|
29
|
-
[isinstance(inner_label, str) for inner_label in label]
|
|
30
|
-
):
|
|
31
|
-
self.label = label[0]
|
|
32
|
-
self.labels = label
|
|
33
|
-
else:
|
|
34
|
-
raise AssertionError(
|
|
35
|
-
"label should be either None, string or a list of strings"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
self.properties = properties or {}
|
|
39
|
-
|
|
40
|
-
def to_string(self):
|
|
41
|
-
res = ""
|
|
42
|
-
if self.properties:
|
|
43
|
-
props = ",".join(
|
|
44
|
-
key + ":" + str(quote_string(val))
|
|
45
|
-
for key, val in sorted(self.properties.items())
|
|
46
|
-
)
|
|
47
|
-
res += "{" + props + "}"
|
|
48
|
-
|
|
49
|
-
return res
|
|
50
|
-
|
|
51
|
-
def __str__(self):
|
|
52
|
-
res = "("
|
|
53
|
-
if self.alias:
|
|
54
|
-
res += self.alias
|
|
55
|
-
if self.labels:
|
|
56
|
-
res += ":" + ":".join(self.labels)
|
|
57
|
-
if self.properties:
|
|
58
|
-
props = ",".join(
|
|
59
|
-
key + ":" + str(quote_string(val))
|
|
60
|
-
for key, val in sorted(self.properties.items())
|
|
61
|
-
)
|
|
62
|
-
res += "{" + props + "}"
|
|
63
|
-
res += ")"
|
|
64
|
-
|
|
65
|
-
return res
|
|
66
|
-
|
|
67
|
-
def __eq__(self, rhs):
|
|
68
|
-
# Type checking
|
|
69
|
-
if not isinstance(rhs, Node):
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
# Quick positive check, if both IDs are set.
|
|
73
|
-
if self.id is not None and rhs.id is not None and self.id != rhs.id:
|
|
74
|
-
return False
|
|
75
|
-
|
|
76
|
-
# Label should match.
|
|
77
|
-
if self.label != rhs.label:
|
|
78
|
-
return False
|
|
79
|
-
|
|
80
|
-
# Quick check for number of properties.
|
|
81
|
-
if len(self.properties) != len(rhs.properties):
|
|
82
|
-
return False
|
|
83
|
-
|
|
84
|
-
# Compare properties.
|
|
85
|
-
if self.properties != rhs.properties:
|
|
86
|
-
return False
|
|
87
|
-
|
|
88
|
-
return True
|