cocoindex-io/cocoindex · error · ValueError

SQLite primary key column {pk!r} cannot be None

Error message

SQLite primary key column {pk!r} cannot be None

What it means

Primary key values must be non-NULL because they form the target-state identity of a row. `declare_row` raises `ValueError` when any primary key column value is `None`, including non-dict (dataclass/NamedTuple/Pydantic) rows whose pk field was left unset.

Source

Thrown at python/cocoindex/connectors/sqlite/_target.py:1094

    def declare_row(self: "TableTarget[RowT]", *, row: RowT) -> None:
        """
        Declare a row to be upserted to this table.

        Args:
            row: A row object (dict, dataclass, NamedTuple, or Pydantic model).
                 Must include all primary key columns with non-None values.
                 Dict rows may omit nullable non-primary-key columns; omitted values
                 are written as NULL.
        """
        row_dict = self._row_to_dict(row)
        pk_values: list[Any] = []
        for pk in self._table_schema.primary_key:
            if isinstance(row, dict) and pk not in row:
                raise ValueError(f"SQLite row is missing primary key column {pk!r}")
            pk_value = row_dict[pk]
            if pk_value is None:
                raise ValueError(f"SQLite primary key column {pk!r} cannot be None")
            pk_values.append(pk_value)
        coco.declare_target_state(
            self._provider.target_state(tuple(pk_values), row_dict)
        )

    def _row_to_dict(self, row: RowT) -> dict[str, Any]:
        """
        Convert a row (dict or object) into dict[str, Any] using the schema columns,
        and apply column encoders for both dict and object inputs.
        """
        out: dict[str, Any] = {}
        for col_name, col in self._table_schema.columns.items():
            if isinstance(row, dict):
                value = row.get(col_name)
            else:
                value = getattr(row, col_name)

            if value is not None and col.encoder is not None:

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Provide a concrete non-None value for every primary key column before declaring the row
  2. Make the pk field required in the record type so it cannot default to None
  3. Generate ids yourself (uuid/counter) before calling `declare_row`

Example fix

// before
@dataclass
class Row:
    id: int | None = None
target.declare_row(Row())
// after
@dataclass
class Row:
    id: int
target.declare_row(Row(id=uuid4().int))
Defensive patterns

Strategy: validation

Validate before calling

for pk in primary_keys:
    if row[pk] is None:
        raise ValueError(f"{pk} must be non-None before declare_row")

Type guard

def pks_present(row: dict, pks: list[str]) -> bool:
    return all(row.get(pk) is not None for pk in pks)

Try / catch

try:
    target.declare_row(row)
except ValueError as e:
    if "cannot be None" in e.args[0]:
        row[pk_field] = generate_id()
    else:
        raise

Prevention

When it happens

Trigger: Declaring a row where a primary-key field is `None` — e.g. an optional dataclass field defaulting to `None`, or a dict with `"id": None`; also reached for non-dict rows since the check applies to `row_dict[pk]` regardless of row kind.

Common situations: Optional pk fields on dataclasses (`id: int | None = None`) left unset; records built from upstream data where the id failed to populate; auto-increment mental model (SQLite rowid) that does not apply because the engine needs the key up front.

Related errors


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