@@ -14302,8 +14302,7 @@ def test_issue_996_case_r_when_null_searched_form_still_works() -> None:
1430214302# Issue #1395: sequential MATCH reply-author row-shaping joins (IC8 / IS7)
1430314303# ---------------------------------------------------------------------------
1430414304
14305- def test_issue_1395_sequential_match_recent_replies_row_shaping_ic8() -> None:
14306- """IC8 shape: two non-optional MATCH clauses preserve reply-author projection fields."""
14305+ def _mk_issue_1395_reply_author_ic8_graph(*, cudf_mode: bool = False) -> _CypherTestGraph:
1430714306 nodes = pd.DataFrame({
1430814307 "id": [
1430914308 "viewer",
@@ -14337,7 +14336,17 @@ def test_issue_1395_sequential_match_recent_replies_row_shaping_ic8() -> None:
1433714336 "HAS_CREATOR",
1433814337 ],
1433914338 })
14340- g = _mk_graph(nodes, edges)
14339+ if cudf_mode:
14340+ pytest.importorskip("cudf")
14341+ import cudf # type: ignore
14342+ nodes = cudf.DataFrame.from_pandas(nodes)
14343+ edges = cudf.DataFrame.from_pandas(edges)
14344+ return _mk_graph(nodes, edges)
14345+
14346+
14347+ def test_issue_1395_sequential_match_recent_replies_row_shaping_ic8() -> None:
14348+ """IC8 shape: two non-optional MATCH clauses preserve reply-author projection fields."""
14349+ g = _mk_issue_1395_reply_author_ic8_graph()
1434114350
1434214351 query = (
1434314352 "MATCH (:Person {id: $personId})<-[:HAS_CREATOR]-(message:Message) "
@@ -14374,6 +14383,86 @@ def test_issue_1395_sequential_match_recent_replies_row_shaping_ic8() -> None:
1437414383 ]
1437514384
1437614385
14386+ def test_issue_1395_sequential_match_recent_replies_row_shaping_ic8_on_cudf() -> None:
14387+ """cuDF parity: IC8 sequential MATCH row-shaping stays aligned on GPU engine."""
14388+ g = _mk_issue_1395_reply_author_ic8_graph(cudf_mode=True)
14389+
14390+ query = (
14391+ "MATCH (:Person {id: $personId})<-[:HAS_CREATOR]-(message:Message) "
14392+ "MATCH (message)<-[:REPLY_OF]-(comment:Comment)-[:HAS_CREATOR]->(commentAuthor:Person) "
14393+ "RETURN "
14394+ "commentAuthor.id AS commentAuthorId, "
14395+ "commentAuthor.firstName AS commentAuthorFirstName, "
14396+ "commentAuthor.lastName AS commentAuthorLastName, "
14397+ "comment.creationDate AS commentCreationDate, "
14398+ "comment.id AS commentId, "
14399+ "comment.content AS commentContent "
14400+ "ORDER BY commentCreationDate DESC, commentId ASC "
14401+ "LIMIT 20"
14402+ )
14403+ result = g.gfql(query, params={"personId": "viewer"}, engine="cudf")
14404+
14405+ assert type(result._nodes).__module__.startswith("cudf")
14406+ assert result._nodes.to_pandas().to_dict(orient="records") == [
14407+ {
14408+ "commentAuthorId": "author1",
14409+ "commentAuthorFirstName": "Ann",
14410+ "commentAuthorLastName": "One",
14411+ "commentCreationDate": 110.0,
14412+ "commentId": "c1",
14413+ "commentContent": "reply-1",
14414+ },
14415+ {
14416+ "commentAuthorId": "author2",
14417+ "commentAuthorFirstName": "Bob",
14418+ "commentAuthorLastName": "Two",
14419+ "commentCreationDate": 105.0,
14420+ "commentId": "c2",
14421+ "commentContent": "reply-2",
14422+ },
14423+ ]
14424+
14425+
14426+ def test_issue_1395_sequential_match_where_boundary_lock_ic8() -> None:
14427+ """Boundary lock: intermediate WHERE stays explicitly unsupported for sequential MATCH merge."""
14428+ g = _mk_issue_1395_reply_author_ic8_graph()
14429+
14430+ query = (
14431+ "MATCH (:Person {id: $personId})<-[:HAS_CREATOR]-(message:Message) "
14432+ "WHERE message.creationDate >= 95 "
14433+ "MATCH (message)<-[:REPLY_OF]-(comment:Comment)-[:HAS_CREATOR]->(commentAuthor:Person) "
14434+ "RETURN comment.id AS commentId, commentAuthor.id AS commentAuthorId "
14435+ "ORDER BY commentId"
14436+ )
14437+ with pytest.raises(
14438+ GFQLValidationError,
14439+ match="WHERE on intermediate MATCH clauses is not yet supported for sequential MATCH merge",
14440+ ):
14441+ _ = g.gfql(query, params={"personId": "viewer"})
14442+
14443+
14444+ def test_issue_1395_sequential_match_equivalent_to_single_match_comma_pattern() -> None:
14445+ """Shape equivalence: sequential MATCH and single-MATCH comma pattern return identical rows."""
14446+ g = _mk_issue_1395_reply_author_ic8_graph()
14447+
14448+ query_sequential = (
14449+ "MATCH (:Person {id: $personId})<-[:HAS_CREATOR]-(message:Message) "
14450+ "MATCH (message)<-[:REPLY_OF]-(comment:Comment)-[:HAS_CREATOR]->(commentAuthor:Person) "
14451+ "RETURN comment.id AS commentId, commentAuthor.id AS commentAuthorId "
14452+ "ORDER BY commentId"
14453+ )
14454+ query_single_match = (
14455+ "MATCH (:Person {id: $personId})<-[:HAS_CREATOR]-(message:Message), "
14456+ "(message)<-[:REPLY_OF]-(comment:Comment)-[:HAS_CREATOR]->(commentAuthor:Person) "
14457+ "RETURN comment.id AS commentId, commentAuthor.id AS commentAuthorId "
14458+ "ORDER BY commentId"
14459+ )
14460+
14461+ seq_rows = g.gfql(query_sequential, params={"personId": "viewer"})._nodes.to_dict(orient="records")
14462+ comma_rows = g.gfql(query_single_match, params={"personId": "viewer"})._nodes.to_dict(orient="records")
14463+ assert seq_rows == comma_rows
14464+
14465+
1437714466def test_issue_1395_sequential_match_message_replies_row_shaping_is7() -> None:
1437814467 """IS7 shape: sequential MATCH keeps comment + replyAuthor + messageAuthor rows aligned."""
1437914468 nodes = pd.DataFrame({
0 commit comments