{"record":{"id":"8707f34b395da902","repo":"cocoindex-io/cocoindex","slug":"build-relationship-upsert-requires-pk-fields-for-f-8707f3","errorCode":null,"errorMessage":"build_relationship_upsert requires PK fields for from, to, and the relationship","messagePattern":"build_relationship_upsert requires PK fields for from, to, and the relationship","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/cocoindex/connectors/neo4j/_cypher.py","lineNumber":135,"sourceCode":"    return f\"MATCH (n:{_quote(label)} {_key_clause('key', pk_fields)}) DETACH DELETE n\"\n\n\ndef build_relationship_upsert(\n    rel_type: str,\n    from_label: str,\n    from_pk_fields: Sequence[str],\n    to_label: str,\n    to_pk_fields: Sequence[str],\n    rel_pk_fields: Sequence[str],\n    has_value_fields: bool,\n) -> str:\n    \"\"\"Three MERGEs: source endpoint, target endpoint, then the relationship.\n\n    Endpoint properties are NOT touched — they are owned by their table's own\n    record handler. We only ``SET r += $props`` on the relationship itself.\n    \"\"\"\n    if not from_pk_fields or not to_pk_fields or not rel_pk_fields:\n        raise ValueError(\n            \"build_relationship_upsert requires PK fields for from, to, and the relationship\"\n        )\n    cypher = (\n        f\"MERGE (s:{_quote(from_label)} {_key_clause('from_key', from_pk_fields)}) \"\n        f\"MERGE (t:{_quote(to_label)} {_key_clause('to_key', to_pk_fields)}) \"\n        f\"MERGE (s)-[r:{_quote(rel_type)} {_key_clause('rel_key', rel_pk_fields)}]->(t)\"\n    )\n    if has_value_fields:\n        cypher += \" SET r += $props\"\n    return cypher\n\n\ndef build_relationship_delete(rel_type: str, pk_fields: Sequence[str]) -> str:\n    \"\"\"``MATCH ()-[r:`RelType` {pk: $key_0, ...}]->() DELETE r``.\n\n    Endpoints are intentionally not deleted — they're tracked by their own\n    table handlers and will be deleted by their own reconciler if orphaned.\n    \"\"\"","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/cocoindex-io/cocoindex/blob/e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b/python/cocoindex/connectors/neo4j/_cypher.py#L117-L153","documentation":"build_relationship_upsert performs three MERGEs: source node, target node, then the relationship. Each MERGE needs its own key clause, so all three PK lists (from_pk_fields, to_pk_fields, rel_pk_fields) must be non-empty; any empty one makes the pattern invalid and raises ValueError.","triggerScenarios":"Calling build_relationship_upsert with an empty from_pk_fields, to_pk_fields, or rel_pk_fields — e.g. the relationship record, one of its endpoint tables, or the relationship itself has no primary key fields declared.","commonSituations":"Relationship rows defined without a PK (assuming endpoints alone identify them); endpoint node tables defined without primary keys; dynamic schemas where one endpoint's PK list was computed as empty.","solutions":["Declare primary key fields for the relationship record itself in addition to both endpoint tables","Verify both from/to node specs have non-empty primary keys","Fail fast in your schema-loading code if any of the three PK lists is empty"],"exampleFix":"// before\nbuild_relationship_upsert(rel_type=\"MENTIONS\", from_label=\"Doc\", to_label=\"Doc\", from_pk_fields=[\"id\"], to_pk_fields=[\"id\"], rel_pk_fields=[])\n// after\nbuild_relationship_upsert(rel_type=\"MENTIONS\", from_label=\"Doc\", to_label=\"Doc\", from_pk_fields=[\"id\"], to_pk_fields=[\"id\"], rel_pk_fields=[\"ordinal\"])","handlingStrategy":"validation","validationCode":"assert from_pk_fields and to_pk_fields and rel_pk_fields, (\n    \"relationship upsert needs PK fields for both endpoints and the relationship\"\n)\nbuild_relationship_upsert(rel_type=rel_type, from_label=from_label, to_label=to_label,\n    from_pk_fields=from_pk_fields, to_pk_fields=to_pk_fields, rel_pk_fields=rel_pk_fields)","typeGuard":"def all_have_pks(from_pk, to_pk, rel_pk) -> bool:\n    return bool(from_pk) and bool(to_pk) and bool(rel_pk)","tryCatchPattern":"try:\n    cypher = build_relationship_upsert(...)\nexcept ValueError as e:\n    if \"requires PK fields\" in str(e):\n        logger.error(\"relationship %r or an endpoint lacks a primary key\", rel_type)\n        raise\n    raise","preventionTips":["Model the relationship with its own primary key fields, not just endpoint keys","Ensure both endpoint node tables declare primary keys before defining relationships","Validate relationship specs at load: all three PK lists must be non-empty"],"tags":["neo4j","cypher","primary-key","relationship"],"backgroundTag":"missing-required-argument","analyzedSha":"e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b","analyzedAt":"2026-09-08T15:59:19.997Z","contentChangedAt":"2026-09-08T15:59:19.997Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}