@@ -1567,6 +1567,127 @@ def test_filter_properties_required_field_missing(
15671567 assert "required" not in result [0 ]["properties" ][0 ]
15681568
15691569
1570+ def test_enforce_required_for_constraint_properties_sets_required_true (
1571+ schema_from_text : SchemaFromTextExtractor ,
1572+ ) -> None :
1573+ node_types : list [dict [str , Any ]] = [
1574+ {
1575+ "label" : "Person" ,
1576+ "properties" : [
1577+ {"name" : "name" , "type" : "STRING" , "required" : False },
1578+ {"name" : "email" , "type" : "STRING" , "required" : False },
1579+ ],
1580+ }
1581+ ]
1582+ constraints = [
1583+ {"type" : "UNIQUENESS" , "node_type" : "Person" , "property_name" : "name" }
1584+ ]
1585+
1586+ schema_from_text ._enforce_required_for_constraint_properties (
1587+ node_types , constraints
1588+ )
1589+
1590+ # name should now be required=true
1591+ assert node_types [0 ]["properties" ][0 ]["required" ] is True
1592+ # email should remain required=false
1593+ assert node_types [0 ]["properties" ][1 ]["required" ] is False
1594+
1595+
1596+ def test_enforce_required_for_constraint_properties_already_true (
1597+ schema_from_text : SchemaFromTextExtractor ,
1598+ ) -> None :
1599+ node_types : list [dict [str , Any ]] = [
1600+ {
1601+ "label" : "Person" ,
1602+ "properties" : [
1603+ {"name" : "name" , "type" : "STRING" , "required" : True },
1604+ ],
1605+ }
1606+ ]
1607+ constraints = [
1608+ {"type" : "UNIQUENESS" , "node_type" : "Person" , "property_name" : "name" }
1609+ ]
1610+
1611+ schema_from_text ._enforce_required_for_constraint_properties (
1612+ node_types , constraints
1613+ )
1614+
1615+ assert node_types [0 ]["properties" ][0 ]["required" ] is True
1616+
1617+
1618+ def test_enforce_required_for_constraint_properties_missing_required_field (
1619+ schema_from_text : SchemaFromTextExtractor ,
1620+ ) -> None :
1621+ node_types : list [dict [str , Any ]] = [
1622+ {
1623+ "label" : "Person" ,
1624+ "properties" : [
1625+ {"name" : "name" , "type" : "STRING" }, # No required field
1626+ ],
1627+ }
1628+ ]
1629+ constraints = [
1630+ {"type" : "UNIQUENESS" , "node_type" : "Person" , "property_name" : "name" }
1631+ ]
1632+
1633+ schema_from_text ._enforce_required_for_constraint_properties (
1634+ node_types , constraints
1635+ )
1636+
1637+ assert node_types [0 ]["properties" ][0 ]["required" ] is True
1638+
1639+
1640+ def test_enforce_required_for_constraint_properties_no_constraints (
1641+ schema_from_text : SchemaFromTextExtractor ,
1642+ ) -> None :
1643+ node_types : list [dict [str , Any ]] = [
1644+ {
1645+ "label" : "Person" ,
1646+ "properties" : [
1647+ {"name" : "name" , "type" : "STRING" , "required" : False },
1648+ ],
1649+ }
1650+ ]
1651+ constraints : list [dict [str , Any ]] = []
1652+
1653+ schema_from_text ._enforce_required_for_constraint_properties (
1654+ node_types , constraints
1655+ )
1656+
1657+ assert node_types [0 ]["properties" ][0 ]["required" ] is False
1658+
1659+
1660+ def test_enforce_required_for_constraint_properties_skips_unconstrained_nodes (
1661+ schema_from_text : SchemaFromTextExtractor ,
1662+ ) -> None :
1663+ node_types : list [dict [str , Any ]] = [
1664+ {
1665+ "label" : "Person" ,
1666+ "properties" : [
1667+ {"name" : "name" , "type" : "STRING" , "required" : False },
1668+ ],
1669+ },
1670+ {
1671+ "label" : "Company" ,
1672+ "properties" : [
1673+ {"name" : "name" , "type" : "STRING" , "required" : False },
1674+ ],
1675+ },
1676+ ]
1677+ constraints = [
1678+ {"type" : "UNIQUENESS" , "node_type" : "Person" , "property_name" : "name" }
1679+ ]
1680+
1681+ schema_from_text ._enforce_required_for_constraint_properties (
1682+ node_types , constraints
1683+ )
1684+
1685+ # Person.name should be required=true
1686+ assert node_types [0 ]["properties" ][0 ]["required" ] is True
1687+ # Company.name should remain required=false (no constraint on Company)
1688+ assert node_types [1 ]["properties" ][0 ]["required" ] is False
1689+
1690+
15701691@pytest .mark .asyncio
15711692async def test_schema_from_text_with_required_properties (
15721693 schema_from_text : SchemaFromTextExtractor ,
@@ -1638,6 +1759,45 @@ async def test_schema_from_text_handles_missing_required_field(
16381759 assert prop .required is False
16391760
16401761
1762+ @pytest .mark .asyncio
1763+ async def test_schema_from_text_enforces_required_for_constrained_properties (
1764+ schema_from_text : SchemaFromTextExtractor ,
1765+ mock_llm : AsyncMock ,
1766+ ) -> None :
1767+ schema_json = """
1768+ {
1769+ "node_types": [
1770+ {
1771+ "label": "Person",
1772+ "properties": [
1773+ {"name": "name", "type": "STRING", "required": false},
1774+ {"name": "email", "type": "STRING", "required": false}
1775+ ]
1776+ }
1777+ ],
1778+ "relationship_types": [],
1779+ "patterns": [],
1780+ "constraints": [
1781+ {"type": "UNIQUENESS", "node_type": "Person", "property_name": "name"}
1782+ ]
1783+ }
1784+ """
1785+ mock_llm .ainvoke .return_value = LLMResponse (content = schema_json )
1786+
1787+ schema = await schema_from_text .run (text = "Sample text" )
1788+
1789+ person = schema .node_type_from_label ("Person" )
1790+ assert person is not None
1791+
1792+ name_prop = next ((p for p in person .properties if p .name == "name" ), None )
1793+ email_prop = next ((p for p in person .properties if p .name == "email" ), None )
1794+
1795+ # name should be auto-fixed to required=true
1796+ assert name_prop is not None and name_prop .required is True
1797+ # email should remain required=false
1798+ assert email_prop is not None and email_prop .required is False
1799+
1800+
16411801@pytest .mark .asyncio
16421802@patch ("neo4j_graphrag.experimental.components.schema.get_structured_schema" )
16431803async def test_schema_from_existing_graph (mock_get_structured_schema : Mock ) -> None :
0 commit comments