Skip to content

Commit 23bfa1f

Browse files
Fix rel props projection
1 parent 63b32ad commit 23bfa1f

File tree

3 files changed

+66
-9
lines changed

3 files changed

+66
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
### Bug Fixes
77
1. Fix a bug where disconnected nodes were not projected.
88
2. Fix a bug where multiple nodes containing the identifying name could be nondeterministically matched. The user must now specify the exact name.
9-
3. Support weighted degree centrality.
10-
4. Support projecting specific node labels.
9+
3. Support weighted degree centrality.
10+
4. Support projecting specific node labels.
11+
5. Fix a bug where projection would fail loading non-numbers relationship properties.
1112

1213
### Other Changes
1314

mcp_server/src/mcp_server_neo4j_gds/gds.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ def create_projection_query(node_labels):
206206

207207

208208
def validate_rel_properties(gds: GraphDataScience, rel_properties, node_labels=None):
209+
if node_labels is None:
210+
node_labels = []
209211
if len(node_labels) > 0:
210212
match_rel_query = f"""
211213
MATCH (n)-[r]->(m)
@@ -218,17 +220,26 @@ def validate_rel_properties(gds: GraphDataScience, rel_properties, node_labels=N
218220
valid_rel_properties = {}
219221
for i in range(len(rel_properties)):
220222
pi = gds.run_cypher(
221-
f"{match_rel_query} RETURN distinct r.{rel_properties[i]} IS :: STRING AS ISSTRING"
223+
f"""
224+
{match_rel_query}
225+
WITH r.{rel_properties[i]} AS prop
226+
WITH
227+
CASE
228+
WHEN prop IS :: FLOAT THEN 1
229+
WHEN prop IS :: INTEGER THEN 1
230+
ELSE 2
231+
END AS INVALID_PROP_TYPE
232+
RETURN distinct(INVALID_PROP_TYPE)
233+
"""
222234
)
223-
if pi.shape[0] == 1 and bool(pi["ISSTRING"][0]) is False:
224-
valid_rel_properties[rel_properties[i]] = f"r.{rel_properties[i]}"
235+
if pi.shape[0] == 1 and int(pi["INVALID_PROP_TYPE"][0]) == 1:
236+
valid_rel_properties[rel_properties[i]] = f"toFloat(r.{rel_properties[i]}"
225237
return valid_rel_properties
226238

227239

228240
def validate_node_properties(gds: GraphDataScience, node_properties, node_labels=None):
229241
if node_labels is None:
230242
node_labels = []
231-
232243
projectable_properties = {}
233244

234245
for i in range(len(node_properties)):
@@ -282,14 +293,14 @@ def validate_node_properties(gds: GraphDataScience, node_properties, node_labels
282293

283294

284295
def create_source_projection_properties(projectable_properties):
285-
return create_projection_properties(projectable_properties, "n")
296+
return create_node_projection_properties(projectable_properties, "n")
286297

287298

288299
def create_target_projection_properties(projectable_properties):
289-
return create_projection_properties(projectable_properties, "m")
300+
return create_node_projection_properties(projectable_properties, "m")
290301

291302

292-
def create_projection_properties(projectable_properties, variable):
303+
def create_node_projection_properties(projectable_properties, variable):
293304
valid_node_properties = {}
294305
for prop in projectable_properties:
295306
property_type = projectable_properties[prop]

mcp_server/tests/test_projection.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,51 @@ def test_node_projection_properties_with_node_labels(neo4j_container):
141141
assert "prop" not in projection_properties_foo
142142

143143

144+
@pytest.mark.asyncio
145+
def test_rel_projection_properties(neo4j_container):
146+
"""Import test data into Neo4j."""
147+
# Set environment variables for the import script
148+
os.environ["NEO4J_URI"] = neo4j_container
149+
os.environ["NEO4J_USERNAME"] = NEO4J_USER
150+
os.environ["NEO4J_PASSWORD"] = NEO4J_PASSWORD
151+
152+
driver = GraphDatabase.driver(neo4j_container, auth=(NEO4J_USER, NEO4J_PASSWORD))
153+
existing_count1 = -1
154+
existing_count2 = -2
155+
gds = GraphDataScience(driver)
156+
with driver.session() as session:
157+
session.run("CREATE (:Foo)-[:R{ prop_ok1: 2.0}]->(:Foo)")
158+
session.run("CREATE (:Foo)-[:R{ prop_ok2: 2}]->(:Foo)")
159+
session.run("CREATE (:Foo)-[:R{ prop_bad: 2.0}]->(:Foo)")
160+
session.run("CREATE (:Foo)-[:R{ prop_bad: 'Foo'}]->(:Foo)")
161+
162+
res = session.run("MATCH (n) WHERE 'Foo' IN labels(n) RETURN count(n) as count")
163+
existing_count1 = res.single()["count"]
164+
165+
# do validations
166+
from mcp_server.src.mcp_server_neo4j_gds.gds import validate_rel_properties
167+
168+
projection_properties = validate_rel_properties(
169+
gds, ["prop_ok1", "prop_ok2", "prop_bad"]
170+
)
171+
172+
# remove data
173+
with driver.session() as session:
174+
session.run("MATCH (n:Foo) DETACH DELETE n")
175+
176+
res = session.run("MATCH (n) WHERE 'Foo' IN labels(n) RETURN count(n) as count")
177+
existing_count2 = res.single()["count"]
178+
179+
driver.close()
180+
181+
# assertions at the end to ensure failures do not affect other tests
182+
assert existing_count1 == 8
183+
assert existing_count2 == 0
184+
assert "prop_ok1" in projection_properties
185+
assert "prop_ok2" in projection_properties
186+
assert "prop_bad" not in projection_properties
187+
188+
144189
@pytest.mark.asyncio
145190
def test_rel_projection_properties_with_node_labels(neo4j_container):
146191
"""Import test data into Neo4j."""

0 commit comments

Comments
 (0)