{"record":{"id":"f43a54745b61f2b9","repo":"cocoindex-io/cocoindex","slug":"row-count-must-be-positive","errorCode":null,"errorMessage":"row_count must be positive","messagePattern":"row_count must be positive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"python/cocoindex/connectors/bigquery/_target.py","lineNumber":314,"sourceCode":"        f\"ON {on_clause}\",\n    ]\n\n    if non_pk_cols:\n        update_list = \", \".join(f\"`{c}` = source.`{c}`\" for c in non_pk_cols)\n        sql_parts.append(f\"WHEN MATCHED THEN UPDATE SET {update_list}\")\n\n    sql_parts.append(\n        f\"WHEN NOT MATCHED THEN INSERT ({insert_cols}) VALUES ({insert_values})\"\n    )\n    return \" \".join(sql_parts)\n\n\ndef _delete_sql(\n    qualified_table_name: str, table_schema: TableSchema[Any], *, row_count: int\n) -> str:\n    pk_cols = table_schema.primary_key\n    if row_count <= 0:\n        raise ValueError(\"row_count must be positive\")\n\n    if len(pk_cols) == 1:\n        markers = \", \".join(f\"@p{i}\" for i in range(row_count))\n        return f\"DELETE FROM {qualified_table_name} WHERE `{pk_cols[0]}` IN ({markers})\"\n\n    row_clauses = []\n    param_idx = 0\n    for _ in range(row_count):\n        and_parts = []\n        for pk in pk_cols:\n            and_parts.append(f\"`{pk}` = @p{param_idx}\")\n            param_idx += 1\n        row_clauses.append(f\"({' AND '.join(and_parts)})\")\n    return f\"DELETE FROM {qualified_table_name} WHERE {' OR '.join(row_clauses)}\"\n\n\ndef _encode_value(col: ColumnDef, value: Any) -> Any:\n    if value is None:","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/cocoindex-io/cocoindex/blob/e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b/python/cocoindex/connectors/bigquery/_target.py#L296-L332","documentation":"Raised by _delete_sql in the BigQuery connector when asked to build a DELETE statement for a non-positive number of rows. The generated SQL uses one positional query parameter (@p0, @p1, ...) per row to delete, so row_count must be at least 1; 0 or negative means the caller computed an empty or invalid batch.","triggerScenarios":"Calling _delete_sql (via _apply_actions during a target sync) with row_count=0 or negative, e.g. when a delete batch contains no primary keys but the delete path is still invoked.","commonSituations":"A sync run where all declared rows were removed and an empty delete batch is generated; bugs in batching logic that pass len(keys)=0; upstream code changes that no longer guard against empty batches.","solutions":["Ensure the caller skips the DELETE entirely when the batch is empty (guard with `if row_count > 0` before calling _delete_sql).","Check what feeds row_count in _apply_actions — inspect why an empty or negative key batch reached the delete path.","Report/fix the batching bug if a non-empty action list yields row_count<=0."],"exampleFix":"// before\nsql = _delete_sql(qualified, schema, row_count=len(keys))\n// after\nif keys:\n    sql = _delete_sql(qualified, schema, row_count=len(keys))","handlingStrategy":"validation","validationCode":"if not keys:\n    return  # or skip building the DELETE\nassert len(keys) > 0, \"delete batch must be non-empty\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Guard delete paths with `if batch:` before generating SQL","Keep batching logic in one place so empty batches are skipped consistently","Add a unit test for the empty-batch case"],"tags":["bigquery","sql","batching"],"backgroundTag":"invalid-argument-value","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"}