Skip to content

Commit fadc330

Browse files
authored
update internal cached mapping upon graph version change (#94)
* update internal cached mapping upon graph version change * test cache sync * maintain graph version * print require version on arity exception
1 parent 94e2631 commit fadc330

File tree

4 files changed

+199
-60
lines changed

4 files changed

+199
-60
lines changed

redisgraph/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class VersionMismatchException(Exception):
2+
def __init__(self, version):
3+
self.version = version
4+

redisgraph/graph.py

+77-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from .util import *
2+
import redis
23
from .query_result import QueryResult
4+
from .exceptions import VersionMismatchException
5+
36
class Graph(object):
47
"""
58
Graph, collection of nodes and edges.
@@ -9,39 +12,65 @@ def __init__(self, name, redis_con):
912
"""
1013
Create a new graph.
1114
"""
12-
self.name = name
15+
self.name = name # Graph key
1316
self.redis_con = redis_con
1417
self.nodes = {}
1518
self.edges = []
16-
self._labels = [] # List of node labels.
17-
self._properties = [] # List of properties.
18-
self._relationshipTypes = [] # List of relation types.
19+
self._labels = [] # List of node labels.
20+
self._properties = [] # List of properties.
21+
self._relationshipTypes = [] # List of relation types.
22+
self.version = 0 # Graph version
23+
24+
def _clear_schema(self):
25+
self._labels = []
26+
self._properties = []
27+
self._relationshipTypes = []
28+
29+
def _refresh_schema(self):
30+
self._clear_schema()
31+
self._refresh_labels()
32+
self._refresh_relations()
33+
self._refresh_attributes()
34+
35+
def _refresh_labels(self):
36+
lbls = self.labels()
37+
38+
# Unpack data.
39+
self._labels = [None] * len(lbls)
40+
for i, l in enumerate(lbls):
41+
self._labels[i] = l[0]
42+
43+
def _refresh_relations(self):
44+
rels = self.relationshipTypes()
45+
46+
# Unpack data.
47+
self._relationshipTypes = [None] * len(rels)
48+
for i, r in enumerate(rels):
49+
self._relationshipTypes[i] = r[0]
50+
51+
def _refresh_attributes(self):
52+
props = self.propertyKeys()
53+
54+
# Unpack data.
55+
self._properties = [None] * len(props)
56+
for i, p in enumerate(props):
57+
self._properties[i] = p[0]
1958

2059
def get_label(self, idx):
2160
try:
2261
label = self._labels[idx]
2362
except IndexError:
24-
# Refresh graph labels.
25-
lbls = self.labels()
26-
# Unpack data.
27-
self._labels = [None] * len(lbls)
28-
for i, l in enumerate(lbls):
29-
self._labels[i] = l[0]
30-
63+
# Refresh labels.
64+
self._refresh_labels()
3165
label = self._labels[idx]
3266
return label
3367

3468
def get_relation(self, idx):
3569
try:
3670
relationshipType = self._relationshipTypes[idx]
3771
except IndexError:
38-
# Refresh graph relations.
39-
rels = self.relationshipTypes()
40-
# Unpack data.
41-
self._relationshipTypes = [None] * len(rels)
42-
for i, r in enumerate(rels):
43-
self._relationshipTypes[i] = r[0]
44-
72+
# Refresh relationship types.
73+
self._refresh_relations()
4574
relationshipType = self._relationshipTypes[idx]
4675
return relationshipType
4776

@@ -50,12 +79,7 @@ def get_property(self, idx):
5079
propertie = self._properties[idx]
5180
except IndexError:
5281
# Refresh properties.
53-
props = self.propertyKeys()
54-
# Unpack data.
55-
self._properties = [None] * len(props)
56-
for i, p in enumerate(props):
57-
self._properties[i] = p[0]
58-
82+
self._refresh_attributes()
5983
propertie = self._properties[idx]
6084
return propertie
6185

@@ -121,16 +145,40 @@ def query(self, q, params=None, timeout=None):
121145
"""
122146
Executes a query against the graph.
123147
"""
148+
149+
# maintain original 'q'
150+
query = q
151+
152+
# handle query parameters
124153
if params is not None:
125-
q = self.build_params_header(params) + q
154+
query = self.build_params_header(params) + query
155+
156+
# construct query command
157+
# ask for compact result-set format
158+
# specify known graph version
159+
command = ["GRAPH.QUERY", self.name, query, "--compact", "version", self.version]
126160

127-
command = ["GRAPH.QUERY", self.name, q, "--compact"]
161+
# include timeout is specified
128162
if timeout:
129163
if not isinstance(timeout, int):
130164
raise Exception("Timeout argument must be a positive integer")
131165
command += ["timeout", timeout]
132-
response = self.redis_con.execute_command(*command)
133-
return QueryResult(self, response)
166+
167+
# issue query
168+
try:
169+
response = self.redis_con.execute_command(*command)
170+
return QueryResult(self, response)
171+
except redis.exceptions.ResponseError as e:
172+
if "wrong number of arguments" in str(e):
173+
print ("Note: RedisGraph Python requires server version 2.2.8 or above")
174+
raise e
175+
except VersionMismatchException as e:
176+
# client view over the graph schema is out of sync
177+
# set client version and refresh local schema
178+
self.version = e.version
179+
self._refresh_schema()
180+
# re-issue query
181+
return self.query(q, params, timeout)
134182

135183
def _execution_plan_to_string(self, plan):
136184
return "\n".join(plan)
@@ -151,6 +199,7 @@ def delete(self):
151199
"""
152200
Deletes graph.
153201
"""
202+
self._clear_schema()
154203
return self.redis_con.execute_command("GRAPH.DELETE", self.name)
155204

156205
def merge(self, pattern):

redisgraph/query_result.py

+48-32
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
from .node import Node
22
from .edge import Edge
33
from .path import Path
4+
from .exceptions import VersionMismatchException
45
from prettytable import PrettyTable
56
from redis import ResponseError
67

8+
LABELS_ADDED = 'Labels added'
9+
NODES_CREATED = 'Nodes created'
10+
NODES_DELETED = 'Nodes deleted'
11+
RELATIONSHIPS_DELETED = 'Relationships deleted'
12+
PROPERTIES_SET = 'Properties set'
13+
RELATIONSHIPS_CREATED = 'Relationships created'
14+
INDICES_CREATED = "Indices created"
15+
INDICES_DELETED = "Indices deleted"
16+
CACHED_EXECUTION = "Cached execution"
17+
INTERNAL_EXECUTION_TIME = 'internal execution time'
18+
19+
STATS = [LABELS_ADDED, NODES_CREATED, PROPERTIES_SET, RELATIONSHIPS_CREATED,
20+
NODES_DELETED, RELATIONSHIPS_DELETED, INDICES_CREATED, INDICES_DELETED,
21+
CACHED_EXECUTION, INTERNAL_EXECUTION_TIME]
722

823
class ResultSetColumnTypes(object):
924
COLUMN_UNKNOWN = 0
1025
COLUMN_SCALAR = 1
1126
COLUMN_NODE = 2 # Unused as of RedisGraph v2.1.0, retained for backwards compatibility.
1227
COLUMN_RELATION = 3 # Unused as of RedisGraph v2.1.0, retained for backwards compatibility.
1328

14-
1529
class ResultSetScalarTypes(object):
1630
VALUE_UNKNOWN = 0
1731
VALUE_NULL = 1
@@ -25,31 +39,33 @@ class ResultSetScalarTypes(object):
2539
VALUE_PATH = 9
2640

2741
class QueryResult(object):
28-
LABELS_ADDED = 'Labels added'
29-
NODES_CREATED = 'Nodes created'
30-
NODES_DELETED = 'Nodes deleted'
31-
RELATIONSHIPS_DELETED = 'Relationships deleted'
32-
PROPERTIES_SET = 'Properties set'
33-
RELATIONSHIPS_CREATED = 'Relationships created'
34-
INDICES_CREATED = "Indices created"
35-
INDICES_DELETED = "Indices deleted"
36-
CACHED_EXECUTION = "Cached execution"
37-
INTERNAL_EXECUTION_TIME = 'internal execution time'
3842

3943
def __init__(self, graph, response):
4044
self.graph = graph
4145
self.header = []
4246
self.result_set = []
4347

44-
# If we encountered a run-time error, the last response element will be an exception.
45-
if isinstance(response[-1], ResponseError):
46-
raise response[-1]
48+
# incase of an error an exception will be raised
49+
self._check_for_errors(response)
4750

4851
if len(response) == 1:
4952
self.parse_statistics(response[0])
5053
else:
51-
self.parse_results(response)
54+
# start by parsing statistics, matches the one we have
5255
self.parse_statistics(response[-1]) # Last element.
56+
self.parse_results(response)
57+
58+
def _check_for_errors(self, response):
59+
if isinstance(response[0], ResponseError):
60+
error = response[0]
61+
if str(error) == "version mismatch":
62+
version = response[1]
63+
error = VersionMismatchException(version)
64+
raise error
65+
66+
# If we encountered a run-time error, the last response element will be an exception.
67+
if isinstance(response[-1], ResponseError):
68+
raise response[-1]
5369

5470
def parse_results(self, raw_result_set):
5571
self.header = self.parse_header(raw_result_set)
@@ -63,10 +79,12 @@ def parse_results(self, raw_result_set):
6379
def parse_statistics(self, raw_statistics):
6480
self.statistics = {}
6581

66-
stats = [self.LABELS_ADDED, self.NODES_CREATED, self.PROPERTIES_SET, self.RELATIONSHIPS_CREATED,
67-
self.NODES_DELETED, self.RELATIONSHIPS_DELETED, self.INDICES_CREATED, self.INDICES_DELETED,
68-
self.CACHED_EXECUTION, self.INTERNAL_EXECUTION_TIME]
69-
for s in stats:
82+
# decode statistics
83+
for idx, stat in enumerate(raw_statistics):
84+
if isinstance(stat, bytes):
85+
raw_statistics[idx] = stat.decode()
86+
87+
for s in STATS:
7088
v = self._get_value(s, raw_statistics)
7189
if v is not None:
7290
self.statistics[s] = v
@@ -223,53 +241,51 @@ def is_empty(self):
223241
@staticmethod
224242
def _get_value(prop, statistics):
225243
for stat in statistics:
226-
if isinstance(stat, bytes):
227-
stat = stat.decode()
228244
if prop in stat:
229245
return float(stat.split(': ')[1].split(' ')[0])
230246

231-
232247
return None
233248

234249
def _get_stat(self, stat):
235250
return self.statistics[stat] if stat in self.statistics else 0
236251

237252
@property
238253
def labels_added(self):
239-
return self._get_stat(self.LABELS_ADDED)
254+
return self._get_stat(LABELS_ADDED)
240255

241256
@property
242257
def nodes_created(self):
243-
return self._get_stat(self.NODES_CREATED)
258+
return self._get_stat(NODES_CREATED)
244259

245260
@property
246261
def nodes_deleted(self):
247-
return self._get_stat(self.NODES_DELETED)
262+
return self._get_stat(NODES_DELETED)
248263

249264
@property
250265
def properties_set(self):
251-
return self._get_stat(self.PROPERTIES_SET)
266+
return self._get_stat(PROPERTIES_SET)
252267

253268
@property
254269
def relationships_created(self):
255-
return self._get_stat(self.RELATIONSHIPS_CREATED)
270+
return self._get_stat(RELATIONSHIPS_CREATED)
256271

257272
@property
258273
def relationships_deleted(self):
259-
return self._get_stat(self.RELATIONSHIPS_DELETED)
274+
return self._get_stat(RELATIONSHIPS_DELETED)
260275

261276
@property
262277
def indices_created(self):
263-
return self._get_stat(self.INDICES_CREATED)
278+
return self._get_stat(INDICES_CREATED)
264279

265280
@property
266281
def indices_deleted(self):
267-
return self._get_stat(self.INDICES_DELETED)
282+
return self._get_stat(INDICES_DELETED)
268283

269284
@property
270285
def cached_execution(self):
271-
return self._get_stat(self.CACHED_EXECUTION) == 1
286+
return self._get_stat(CACHED_EXECUTION) == 1
272287

273288
@property
274289
def run_time_ms(self):
275-
return self._get_stat(self.INTERNAL_EXECUTION_TIME)
290+
return self._get_stat(INTERNAL_EXECUTION_TIME)
291+

0 commit comments

Comments
 (0)