cocoindex-io/cocoindex · error · ValueError

build_relationship_delete requires at least one primary key

Error message

build_relationship_delete requires at least one primary key field

What it means

build_relationship_delete generates MATCH ()-[r:RelType {pk: ...}]->() DELETE r, requiring at least one key property to identify the relationship. An empty pk_fields list would produce invalid Cypher, so it raises ValueError.

Source

Thrown at python/cocoindex/connectors/neo4j/_cypher.py:155

        )
    cypher = (
        f"MERGE (s:{_quote(from_label)} {_key_clause('from_key', from_pk_fields)}) "
        f"MERGE (t:{_quote(to_label)} {_key_clause('to_key', to_pk_fields)}) "
        f"MERGE (s)-[r:{_quote(rel_type)} {_key_clause('rel_key', rel_pk_fields)}]->(t)"
    )
    if has_value_fields:
        cypher += " SET r += $props"
    return cypher


def build_relationship_delete(rel_type: str, pk_fields: Sequence[str]) -> str:
    """``MATCH ()-[r:`RelType` {pk: $key_0, ...}]->() DELETE r``.

    Endpoints are intentionally not deleted — they're tracked by their own
    table handlers and will be deleted by their own reconciler if orphaned.
    """
    if not pk_fields:
        raise ValueError(
            "build_relationship_delete requires at least one primary key field"
        )
    return (
        f"MATCH ()-[r:{_quote(rel_type)} {_key_clause('key', pk_fields)}]->() DELETE r"
    )


def build_node_index_create(
    name: str,
    label: str,
    fields: Sequence[str],
) -> str:
    """``CREATE INDEX <name> IF NOT EXISTS FOR (n:`Label`) ON (n.`f1`, n.`f2`, ...)``.

    Neo4j requires named indexes; ``IF NOT EXISTS`` makes setup idempotent.
    """
    if not fields:
        raise ValueError("build_node_index_create requires at least one field")

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Declare primary key fields on the relationship spec (same ones used in build_relationship_upsert)
  2. Pass the identical rel_pk_fields to both the upsert and delete builders so deletes can find rows the upserts created
  3. Add an early assertion that the relationship PK list is non-empty at schema construction

Example fix

// before
build_relationship_delete(rel_type="MENTIONS", pk_fields=[])
// after
build_relationship_delete(rel_type="MENTIONS", pk_fields=["ordinal"])
Defensive patterns

Strategy: validation

Validate before calling

if not rel_pk_fields:
    raise ValueError("relationship delete needs the same PK fields used for upsert")
cypher = build_relationship_delete(rel_type=rel_type, pk_fields=rel_pk_fields)

Type guard

def pk_ok(fields) -> bool:
    return bool(fields)

Try / catch

try:
    cypher = build_relationship_delete(rel_type, pk_fields)
except ValueError as e:
    if "at least one primary key" in str(e):
        logger.error("cannot reconcile deletes for %r: no relationship PK", rel_type)
        raise
    raise

Prevention

When it happens

Trigger: Calling build_relationship_delete with pk_fields=[] — the relationship spec has no primary key fields when deletions are reconciled.

Common situations: Relationship declared without its own primary key; PK fields list built dynamically and left empty; refactors that removed the relationship's key column.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/452ff3aab9b94fef. Report an issue: GitHub.