Copilot commented on code in PR #2372:
URL: https://github.com/apache/age/pull/2372#discussion_r3039028834
##########
drivers/python/age/builder.py:
##########
@@ -171,38 +195,49 @@ def visitArray(self, ctx:AgtypeParser.ArrayContext):
return li
def handleAnnotatedValue(self, anno:str, ctx:ParserRuleContext):
+ # Each branch below constructs a model object (Vertex, Edge, Path)
+ # and populates it from the parsed dict/list. If a type check
+ # fails (e.g. the parsed value is not a dict), AGTypeError is
+ # raised and the partially-constructed object is discarded — no
+ # cleanup is needed because the caller propagates the exception.
if anno == "numeric":
return Decimal(ctx.getText())
elif anno == "vertex":
- dict = ctx.accept(self)
- vid = dict["id"]
+ d = ctx.accept(self)
+ if not isinstance(d, dict):
+ raise AGTypeError(str(ctx.getText()), "Expected dict for
vertex, got " + type(d).__name__)
+ vid = d.get("id")
vertex = None
- if self.vertexCache != None and vid in self.vertexCache :
+ if self.vertexCache is not None and vid in self.vertexCache:
vertex = self.vertexCache[vid]
else:
vertex = Vertex()
- vertex.id = dict["id"]
- vertex.label = dict["label"]
- vertex.properties = dict["properties"]
+ vertex.id = d.get("id")
+ vertex.label = d.get("label")
+ vertex.properties = d.get("properties")
- if self.vertexCache != None:
+ if self.vertexCache is not None:
self.vertexCache[vid] = vertex
Review Comment:
In `handleAnnotatedValue()` (vertex branch), switching from required key
access to `d.get(...)` can silently construct a `Vertex` with `id=None` and/or
`properties=None` when the parsed dict is missing keys (e.g., after parser
error recovery). This can also poison `vertexCache` by storing under the `None`
key and later cause runtime errors (e.g., `Vertex.__getitem__` assumes
`properties` is a dict). Consider validating required fields (`id`, `label`,
`properties`) and either raising `AGTypeError` when they’re missing/invalid or
defaulting `properties` to `{}` before constructing/caching the vertex.
##########
drivers/python/test_agtypes.py:
##########
@@ -127,6 +127,158 @@ def test_path(self):
self.assertEqual(vertexEnd.label, "Person")
self.assertEqual(vertexEnd["name"], "Joe")
+ def test_vertex_large_array_properties(self):
+ """Issue #2367: Parser should handle vertices with large array
properties."""
+ vertexExp = (
+ '{"id": 1125899906842625, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", '
+ '"tag8", "tag9", "tag10", "tag11", "tag12"]}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842625)
+ self.assertEqual(vertex.label, "TestNode")
+ self.assertEqual(vertex["name"], "test")
+ self.assertEqual(len(vertex["tags"]), 12)
+ self.assertEqual(vertex["tags"][0], "tag1")
+ self.assertEqual(vertex["tags"][11], "tag12")
+
+ def test_vertex_special_characters_in_properties(self):
+ """Issue #2367: Parser should handle properties with special
characters."""
+ vertexExp = (
+ '{"id": 1125899906842626, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"description": "A long description with unicode chars"}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842626)
+ self.assertEqual(vertex["name"], "test")
+ self.assertIn("unicode", vertex["description"])
+
+ def test_vertex_nested_properties(self):
+ """Issue #2367: Parser should handle deeply nested property
structures."""
+ vertexExp = (
+ '{"id": 1125899906842627, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"metadata": {"level1": {"level2": {"level3":
"deep_value"}}}}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842627)
+ self.assertEqual(vertex["name"], "test")
+ self.assertEqual(vertex["metadata"]["level1"]["level2"]["level3"],
"deep_value")
+
+ def test_vertex_empty_properties(self):
+ """Parser should handle vertices with empty properties dict."""
+ vertexExp = '{"id": 1125899906842628, "label": "EmptyNode",
"properties": {}}::vertex'
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842628)
+ self.assertEqual(vertex.label, "EmptyNode")
+ self.assertEqual(vertex.properties, {})
+
+ def test_vertex_null_property_values(self):
+ """Parser should handle vertices with null property values."""
+ vertexExp = (
+ '{"id": 1125899906842629, "label": "TestNode", '
+ '"properties": {"name": "test", "optional": null, "also_null":
null}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex["name"], "test")
+ self.assertIsNone(vertex["optional"])
+ self.assertIsNone(vertex["also_null"])
+
+ def test_edge_with_complex_properties(self):
+ """Parser should handle edges with complex property structures."""
+ edgeExp = (
+ '{"id": 2533274790396577, "label": "HAS_RELATION", '
+ '"end_id": 1125899906842625, "start_id": 1125899906842626, '
+ '"properties": {"weight": 3, "tags": ["a", "b", "c"], "active":
true}}::edge'
+ )
+ edge = self.parse(edgeExp)
+ self.assertEqual(edge.id, 2533274790396577)
+ self.assertEqual(edge.label, "HAS_RELATION")
+ self.assertEqual(edge.start_id, 1125899906842626)
+ self.assertEqual(edge.end_id, 1125899906842625)
+ self.assertEqual(edge["weight"], 3)
+ self.assertEqual(edge["tags"], ["a", "b", "c"])
+ self.assertEqual(edge["active"], True)
+
+ def test_path_with_multiple_edges(self):
+ """Parser should handle paths with multiple edges and complex
properties."""
+ pathExp = (
+ '[{"id": 1, "label": "A", "properties": {"name":
"start"}}::vertex, '
+ '{"id": 10, "label": "r1", "end_id": 2, "start_id": 1,
"properties": {"w": 1}}::edge, '
+ '{"id": 2, "label": "B", "properties": {"name":
"middle"}}::vertex, '
+ '{"id": 11, "label": "r2", "end_id": 3, "start_id": 2,
"properties": {"w": 2}}::edge, '
+ '{"id": 3, "label": "C", "properties": {"name":
"end"}}::vertex]::path'
+ )
+ path = self.parse(pathExp)
+ self.assertEqual(len(path), 5)
+ self.assertEqual(path[0]["name"], "start")
+ self.assertEqual(path[2]["name"], "middle")
+ self.assertEqual(path[4]["name"], "end")
+
+ def test_empty_input(self):
+ """Parser should handle empty/null input gracefully."""
+ self.assertIsNone(self.parse(''))
+ self.assertIsNone(self.parse(None))
+
+ def test_array_of_mixed_types(self):
+ """Parser should handle arrays with mixed types including nested
arrays."""
+ arrStr = '["str", 42, true, null, [1, 2, 3], {"key": "val"}]'
+ result = self.parse(arrStr)
+ self.assertEqual(result[0], "str")
+ self.assertEqual(result[1], 42)
+ self.assertEqual(result[2], True)
+ self.assertIsNone(result[3])
+ self.assertEqual(result[4], [1, 2, 3])
+ self.assertEqual(result[5], {"key": "val"})
+
+ def test_malformed_vertex_raises_agtypeerror_or_recovers(self):
+ """Issue #2367: Malformed agtype must raise AGTypeError or recover
gracefully."""
+ from age.exceptions import AGTypeError
+
+ malformed_inputs = [
+ '{"id": 1, "label":}::vertex',
+ '{"id": 1, "label": "X", "properties": {}::vertex',
+ '{::vertex',
+ '{"id": 1, "label": "X", "properties": {"key":}}::vertex',
+ ]
+ for inp in malformed_inputs:
+ try:
+ result = self.parse(inp)
+ # If the parser recovered, the result should be a usable
+ # value (None, dict, Vertex, etc.) — not a crash.
+ self.assertNotIsInstance(result, type(NotImplemented))
+ except AGTypeError:
+ pass # expected
+ except AttributeError:
+ self.fail(
+ f"Malformed input raised AttributeError instead of "
+ f"AGTypeError: {inp}"
+ )
+
+ def test_truncated_agtype_raises_agtypeerror(self):
+ """Issue #2367: Truncated agtype must raise AGTypeError, never
AttributeError."""
+ from age.exceptions import AGTypeError
+
+ truncated_inputs = [
+ '{"id": 1, "label": "X", "properties": {"name": "te',
+ '{"id": 1, "label": "X"',
+ '[{"id": 1}::vertex, {"id": 2',
+ ]
+ for inp in truncated_inputs:
+ try:
+ result = self.parse(inp)
+ # Recovery is acceptable for truncated input
+ self.assertIsNotNone(result)
+ except AGTypeError:
Review Comment:
The docstring says truncated agtype “must raise AGTypeError”, but the test
body explicitly allows successful recovery (`assertIsNotNone(result)`). Update
the docstring (or the assertions) so the test’s contract is unambiguous.
##########
drivers/python/test_agtypes.py:
##########
@@ -127,6 +127,158 @@ def test_path(self):
self.assertEqual(vertexEnd.label, "Person")
self.assertEqual(vertexEnd["name"], "Joe")
+ def test_vertex_large_array_properties(self):
+ """Issue #2367: Parser should handle vertices with large array
properties."""
+ vertexExp = (
+ '{"id": 1125899906842625, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", '
+ '"tag8", "tag9", "tag10", "tag11", "tag12"]}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842625)
+ self.assertEqual(vertex.label, "TestNode")
+ self.assertEqual(vertex["name"], "test")
+ self.assertEqual(len(vertex["tags"]), 12)
+ self.assertEqual(vertex["tags"][0], "tag1")
+ self.assertEqual(vertex["tags"][11], "tag12")
+
+ def test_vertex_special_characters_in_properties(self):
+ """Issue #2367: Parser should handle properties with special
characters."""
+ vertexExp = (
+ '{"id": 1125899906842626, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"description": "A long description with unicode chars"}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842626)
+ self.assertEqual(vertex["name"], "test")
+ self.assertIn("unicode", vertex["description"])
+
+ def test_vertex_nested_properties(self):
+ """Issue #2367: Parser should handle deeply nested property
structures."""
+ vertexExp = (
+ '{"id": 1125899906842627, "label": "TestNode", '
+ '"properties": {"name": "test", '
+ '"metadata": {"level1": {"level2": {"level3":
"deep_value"}}}}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842627)
+ self.assertEqual(vertex["name"], "test")
+ self.assertEqual(vertex["metadata"]["level1"]["level2"]["level3"],
"deep_value")
+
+ def test_vertex_empty_properties(self):
+ """Parser should handle vertices with empty properties dict."""
+ vertexExp = '{"id": 1125899906842628, "label": "EmptyNode",
"properties": {}}::vertex'
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex.id, 1125899906842628)
+ self.assertEqual(vertex.label, "EmptyNode")
+ self.assertEqual(vertex.properties, {})
+
+ def test_vertex_null_property_values(self):
+ """Parser should handle vertices with null property values."""
+ vertexExp = (
+ '{"id": 1125899906842629, "label": "TestNode", '
+ '"properties": {"name": "test", "optional": null, "also_null":
null}}::vertex'
+ )
+ vertex = self.parse(vertexExp)
+ self.assertEqual(vertex["name"], "test")
+ self.assertIsNone(vertex["optional"])
+ self.assertIsNone(vertex["also_null"])
+
+ def test_edge_with_complex_properties(self):
+ """Parser should handle edges with complex property structures."""
+ edgeExp = (
+ '{"id": 2533274790396577, "label": "HAS_RELATION", '
+ '"end_id": 1125899906842625, "start_id": 1125899906842626, '
+ '"properties": {"weight": 3, "tags": ["a", "b", "c"], "active":
true}}::edge'
+ )
+ edge = self.parse(edgeExp)
+ self.assertEqual(edge.id, 2533274790396577)
+ self.assertEqual(edge.label, "HAS_RELATION")
+ self.assertEqual(edge.start_id, 1125899906842626)
+ self.assertEqual(edge.end_id, 1125899906842625)
+ self.assertEqual(edge["weight"], 3)
+ self.assertEqual(edge["tags"], ["a", "b", "c"])
+ self.assertEqual(edge["active"], True)
+
+ def test_path_with_multiple_edges(self):
+ """Parser should handle paths with multiple edges and complex
properties."""
+ pathExp = (
+ '[{"id": 1, "label": "A", "properties": {"name":
"start"}}::vertex, '
+ '{"id": 10, "label": "r1", "end_id": 2, "start_id": 1,
"properties": {"w": 1}}::edge, '
+ '{"id": 2, "label": "B", "properties": {"name":
"middle"}}::vertex, '
+ '{"id": 11, "label": "r2", "end_id": 3, "start_id": 2,
"properties": {"w": 2}}::edge, '
+ '{"id": 3, "label": "C", "properties": {"name":
"end"}}::vertex]::path'
+ )
+ path = self.parse(pathExp)
+ self.assertEqual(len(path), 5)
+ self.assertEqual(path[0]["name"], "start")
+ self.assertEqual(path[2]["name"], "middle")
+ self.assertEqual(path[4]["name"], "end")
+
+ def test_empty_input(self):
+ """Parser should handle empty/null input gracefully."""
+ self.assertIsNone(self.parse(''))
+ self.assertIsNone(self.parse(None))
+
+ def test_array_of_mixed_types(self):
+ """Parser should handle arrays with mixed types including nested
arrays."""
+ arrStr = '["str", 42, true, null, [1, 2, 3], {"key": "val"}]'
+ result = self.parse(arrStr)
+ self.assertEqual(result[0], "str")
+ self.assertEqual(result[1], 42)
+ self.assertEqual(result[2], True)
+ self.assertIsNone(result[3])
+ self.assertEqual(result[4], [1, 2, 3])
+ self.assertEqual(result[5], {"key": "val"})
+
+ def test_malformed_vertex_raises_agtypeerror_or_recovers(self):
+ """Issue #2367: Malformed agtype must raise AGTypeError or recover
gracefully."""
+ from age.exceptions import AGTypeError
+
+ malformed_inputs = [
+ '{"id": 1, "label":}::vertex',
+ '{"id": 1, "label": "X", "properties": {}::vertex',
+ '{::vertex',
+ '{"id": 1, "label": "X", "properties": {"key":}}::vertex',
+ ]
+ for inp in malformed_inputs:
+ try:
+ result = self.parse(inp)
+ # If the parser recovered, the result should be a usable
+ # value (None, dict, Vertex, etc.) — not a crash.
+ self.assertNotIsInstance(result, type(NotImplemented))
Review Comment:
This assertion doesn’t meaningfully validate “recovered” output:
`assertNotIsInstance(result, type(NotImplemented))` will pass for essentially
any normal parse result (including `None`). To make the test effective, assert
an expected set of types/shape (e.g., `None` or a dict/list/model object) or
assert specific properties when recovery occurs, while still ensuring no
`AttributeError` is raised.
```suggestion
# parsed value with an expected shape (None, container, or
# model-like object) — not just any arbitrary object.
self.assertTrue(
result is None
or isinstance(result, (dict, list, tuple))
or hasattr(result, "__dict__")
or hasattr(result, "__getitem__"),
f"Malformed input recovered to unexpected result type "
f"{type(result).__name__}: {inp}"
)
```
##########
drivers/python/age/builder.py:
##########
@@ -171,38 +195,49 @@ def visitArray(self, ctx:AgtypeParser.ArrayContext):
return li
def handleAnnotatedValue(self, anno:str, ctx:ParserRuleContext):
+ # Each branch below constructs a model object (Vertex, Edge, Path)
+ # and populates it from the parsed dict/list. If a type check
+ # fails (e.g. the parsed value is not a dict), AGTypeError is
+ # raised and the partially-constructed object is discarded — no
+ # cleanup is needed because the caller propagates the exception.
if anno == "numeric":
return Decimal(ctx.getText())
elif anno == "vertex":
- dict = ctx.accept(self)
- vid = dict["id"]
+ d = ctx.accept(self)
+ if not isinstance(d, dict):
+ raise AGTypeError(str(ctx.getText()), "Expected dict for
vertex, got " + type(d).__name__)
+ vid = d.get("id")
vertex = None
- if self.vertexCache != None and vid in self.vertexCache :
+ if self.vertexCache is not None and vid in self.vertexCache:
vertex = self.vertexCache[vid]
else:
vertex = Vertex()
- vertex.id = dict["id"]
- vertex.label = dict["label"]
- vertex.properties = dict["properties"]
+ vertex.id = d.get("id")
+ vertex.label = d.get("label")
+ vertex.properties = d.get("properties")
- if self.vertexCache != None:
+ if self.vertexCache is not None:
self.vertexCache[vid] = vertex
return vertex
elif anno == "edge":
edge = Edge()
- dict = ctx.accept(self)
- edge.id = dict["id"]
- edge.label = dict["label"]
- edge.end_id = dict["end_id"]
- edge.start_id = dict["start_id"]
- edge.properties = dict["properties"]
+ d = ctx.accept(self)
+ if not isinstance(d, dict):
+ raise AGTypeError(str(ctx.getText()), "Expected dict for edge,
got " + type(d).__name__)
+ edge.id = d.get("id")
+ edge.label = d.get("label")
+ edge.end_id = d.get("end_id")
+ edge.start_id = d.get("start_id")
+ edge.properties = d.get("properties")
Review Comment:
Similarly for the `edge` branch: using `d.get(...)` can create an `Edge`
with missing identifiers or `properties=None`, which can later crash when the
edge is accessed as a mapping (since `Edge.__getitem__` assumes `properties` is
a dict). Recommend enforcing required keys (`id`, `label`, `start_id`,
`end_id`, `properties`) and/or defaulting `properties` to `{}`; if the parsed
value is incomplete due to recovery, raising `AGTypeError` is safer than
returning a partially-initialized model object.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]