cocoindex-io/cocoindex · error · ValueError

row_count must be positive

Error message

row_count must be positive

What it means

`_delete_sql` builds a DELETE statement with one bind placeholder per primary-key row to remove, so it requires `row_count >= 1`. A non-positive count would produce malformed SQL (`IN ()`) and is rejected with this ValueError. It is an internal invariant checked before SQL generation.

Source

Thrown at python/cocoindex/connectors/snowflake/_target.py:300

        f"ON {on_clause}",
    ]

    if non_pk_cols:
        update_list = ", ".join(f'"{c}" = source."{c}"' for c in non_pk_cols)
        sql_parts.append(f"WHEN MATCHED THEN UPDATE SET {update_list}")

    sql_parts.append(
        f"WHEN NOT MATCHED THEN INSERT ({insert_cols}) VALUES ({insert_values})"
    )
    return " ".join(sql_parts)


def _delete_sql(
    qualified_table_name: str, table_schema: TableSchema[Any], *, row_count: int
) -> str:
    pk_cols = table_schema.primary_key
    if row_count <= 0:
        raise ValueError("row_count must be positive")

    if len(pk_cols) == 1:
        markers = ", ".join("%s" for _ in range(row_count))
        return f'DELETE FROM {qualified_table_name} WHERE "{pk_cols[0]}" IN ({markers})'

    row_clauses = []
    for _ in range(row_count):
        and_clause = " AND ".join(f'"{pk}" = %s' for pk in pk_cols)
        row_clauses.append(f"({and_clause})")
    return f"DELETE FROM {qualified_table_name} WHERE {' OR '.join(row_clauses)}"


def _encode_value(col: ColumnDef, value: Any) -> Any:
    if value is None:
        return None
    if col.use_parse_json:
        if isinstance(value, str):
            return value

View on GitHub (pinned to e84aa99b32)

Solutions

  1. Ensure the caller skips invoking the delete path when the list of keys to delete is empty (guard with `if keys:`).
  2. If you maintain patched code, filter out empty delete batches before calling _apply_actions.
  3. Report upstream if this arises from the library's own action computation, since callers normally never pass row_count <= 0.

Example fix

// before
await _apply_actions(conn, actions)  # actions.deletes may be empty
// after
if actions.delete_keys:
    await _apply_actions(conn, actions)
Defensive patterns

Strategy: validation

Validate before calling

if delete_keys:
    apply_actions(..., delete_keys=delete_keys)  # skip empty batches

Try / catch

try:
    apply_actions(...)
except ValueError as e:
    if "row_count must be positive" in str(e):
        logging.warning("Empty delete batch reached SQL builder; skipping")
    else:
        raise

Prevention

When it happens

Trigger: The sync engine calls `_apply_actions` → `_delete_sql` with an empty deletion set (row_count 0 or negative), typically when computing actions without filtering empty delete batches.

Common situations: A run where no rows were deleted but the delete action path was still invoked — usually indicates an internal bug or a custom/patched caller passing an empty key list.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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