cocoindex-io/cocoindex · error · ValueError

build_node_upsert requires at least one primary key field

Error message

build_node_upsert requires at least one primary key field

What it means

build_node_upsert generates MERGE (n:Label {pk: $key_0, ...}); the MERGE pattern requires at least one key property to match nodes on. An empty pk_fields list would produce invalid Cypher, so it raises ValueError.

Source

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


def vector_index_name(label: str, field: str) -> str:
    """Deterministic vector index name for a (label, field) pair."""
    return f"coco_vec_{label}__{field}"


def build_node_upsert(
    label: str,
    pk_fields: Sequence[str],
    has_value_fields: bool,
) -> str:
    """``MERGE (n:`Label` {pk: $key_0, ...}) [SET n += $props]``.

    Same shape as FalkorDB — Neo4j 5 understands the literal property
    pattern in MERGE just fine.
    """
    if not pk_fields:
        raise ValueError("build_node_upsert requires at least one primary key field")
    cypher = f"MERGE (n:{_quote(label)} {_key_clause('key', pk_fields)})"
    if has_value_fields:
        cypher += " SET n += $props"
    return cypher


def build_node_delete(label: str, pk_fields: Sequence[str]) -> str:
    """``MATCH (n:`Label` {pk: $key_0, ...}) DETACH DELETE n``.

    DETACH DELETE removes any incident edges as a safety measure for nodes
    that are also referenced as relationship endpoints — without it, the
    DELETE would fail on nodes that still have edges.
    """
    if not pk_fields:
        raise ValueError("build_node_delete requires at least one primary key field")
    return f"MATCH (n:{_quote(label)} {_key_clause('key', pk_fields)}) DETACH DELETE n"

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Declare at least one primary key field in the node's table spec
  2. If multiple candidate keys exist, pick a stable one as the PK before building the upsert
  3. Guard the call site: raise your own error early when schema.primary_key is empty

Example fix

// before
build_node_upsert(label="Person", pk_fields=[], has_value_fields=True)
// after
build_node_upsert(label="Person", pk_fields=["id"], has_value_fields=True)
Defensive patterns

Strategy: validation

Validate before calling

pk = list(table_spec.primary_key)
if not pk:
    raise ValueError("node table must declare at least one primary key field before upsert")
build_node_upsert(label=label, pk_fields=pk, has_value_fields=True)

Type guard

def has_pk(schema) -> bool:
    return len(getattr(schema, 'primary_key', ()) ) > 0

Try / catch

try:
    cypher = build_node_upsert(label, pk_fields, has_value_fields)
except ValueError as e:
    if "at least one primary key" in str(e):
        raise SystemExit(f"configure a primary key for node {label!r}") from e
    raise

Prevention

When it happens

Trigger: Calling build_node_upsert (or wiring a node table handler) with pk_fields=[] — i.e. the table spec has no primary key fields declared.

Common situations: Table defined without a primary key; PK fields stripped by a refactor; programmatically assembled field lists that end up empty because the schema's primary_key was empty.

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/43fcfcf6acf2d5b3. Report an issue: GitHub.