@@ -255,6 +255,152 @@ def test_relation_with_owns(self) -> None:
255255
256256 assert "since: attributes.Since" in source
257257
258+ def test_relation_optional_attribute (self ) -> None :
259+ """Render relation with optional attribute (no @card constraint)."""
260+ schema = parse_tql_schema ("""
261+ define
262+ attribute sequence_index, value integer;
263+ entity milestone,
264+ plays task_grouping:milestone;
265+ entity task,
266+ plays task_grouping:task;
267+
268+ define
269+ relation task_grouping,
270+ relates milestone @card(1),
271+ relates task @card(0..),
272+ owns sequence_index;
273+ """ )
274+ attr_names = build_class_name_map (schema .attributes )
275+ entity_names = build_class_name_map (schema .entities )
276+ relation_names = build_class_name_map (schema .relations )
277+ source = render_relations (schema , attr_names , entity_names , relation_names )
278+
279+ # Without @card constraint, attribute should be optional
280+ assert "sequence_index: attributes.Sequence_index | None = None" in source
281+
282+ def test_relation_required_attribute (self ) -> None :
283+ """Render relation with required attribute (@card(1))."""
284+ schema = parse_tql_schema ("""
285+ define
286+ attribute weight, value double;
287+ entity node,
288+ plays edge:endpoint;
289+
290+ define
291+ relation edge,
292+ relates endpoint,
293+ owns weight @card(1);
294+ """ )
295+ attr_names = build_class_name_map (schema .attributes )
296+ entity_names = build_class_name_map (schema .entities )
297+ relation_names = build_class_name_map (schema .relations )
298+ source = render_relations (schema , attr_names , entity_names , relation_names )
299+
300+ # With @card(1), attribute should be required (no | None = None)
301+ assert "weight: attributes.Weight" in source
302+ assert "weight: attributes.Weight | None" not in source
303+
304+ def test_relation_key_attribute (self ) -> None :
305+ """Render relation with @key attribute."""
306+ schema = parse_tql_schema ("""
307+ define
308+ attribute edge_id, value string;
309+ entity node,
310+ plays connection:endpoint;
311+
312+ define
313+ relation connection,
314+ relates endpoint,
315+ owns edge_id @key;
316+ """ )
317+ attr_names = build_class_name_map (schema .attributes )
318+ entity_names = build_class_name_map (schema .entities )
319+ relation_names = build_class_name_map (schema .relations )
320+ source = render_relations (schema , attr_names , entity_names , relation_names )
321+
322+ # With @key, attribute should use Flag(Key)
323+ assert "edge_id: attributes.Edge_id = Flag(Key)" in source
324+ assert "from type_bridge import" in source
325+ assert "Flag" in source
326+ assert "Key" in source
327+
328+ def test_relation_multi_value_attribute (self ) -> None :
329+ """Render relation with multi-value attribute (@card(0..))."""
330+ schema = parse_tql_schema ("""
331+ define
332+ attribute tag, value string;
333+ entity item,
334+ plays tagging:item;
335+
336+ define
337+ relation tagging,
338+ relates item,
339+ owns tag @card(0..);
340+ """ )
341+ attr_names = build_class_name_map (schema .attributes )
342+ entity_names = build_class_name_map (schema .entities )
343+ relation_names = build_class_name_map (schema .relations )
344+ source = render_relations (schema , attr_names , entity_names , relation_names )
345+
346+ # With @card(0..), attribute should be a list
347+ assert "list[attributes.Tag]" in source
348+ assert "Card" in source
349+
350+ def test_relation_inherits_key_from_parent (self ) -> None :
351+ """Child relation inherits @key constraint from parent."""
352+ schema = parse_tql_schema ("""
353+ define
354+ attribute rel_id, value string;
355+ entity node,
356+ plays base_rel:endpoint,
357+ plays child_rel:endpoint;
358+
359+ define
360+ relation base_rel @abstract,
361+ relates endpoint,
362+ owns rel_id @key;
363+
364+ define
365+ relation child_rel sub base_rel;
366+ """ )
367+ attr_names = build_class_name_map (schema .attributes )
368+ entity_names = build_class_name_map (schema .entities )
369+ relation_names = build_class_name_map (schema .relations )
370+ source = render_relations (schema , attr_names , entity_names , relation_names )
371+
372+ # Child should inherit @key from parent
373+ assert "class Child_rel(Base_rel):" in source
374+ assert "rel_id: attributes.Rel_id = Flag(Key)" in source
375+
376+ def test_relation_inherits_cardinality_from_parent (self ) -> None :
377+ """Child relation inherits cardinality constraint from parent."""
378+ schema = parse_tql_schema ("""
379+ define
380+ attribute weight, value double;
381+ entity node,
382+ plays base_edge:endpoint,
383+ plays weighted_edge:endpoint;
384+
385+ define
386+ relation base_edge @abstract,
387+ relates endpoint,
388+ owns weight @card(1);
389+
390+ define
391+ relation weighted_edge sub base_edge;
392+ """ )
393+ attr_names = build_class_name_map (schema .attributes )
394+ entity_names = build_class_name_map (schema .entities )
395+ relation_names = build_class_name_map (schema .relations )
396+ source = render_relations (schema , attr_names , entity_names , relation_names )
397+
398+ # Child should inherit required cardinality from parent
399+ # The parent declares weight as required, child should not have it optional
400+ assert "class Weighted_edge(Base_edge):" in source
401+ # weight should NOT be in child since it's inherited from parent
402+ # But if it were re-declared, it should still be required
403+
258404
259405class TestComingSoonAnnotationStubs :
260406 """Tests for coming-soon annotation stubs (TODO comments in generated code)."""
0 commit comments