{"record":{"id":"d1fc5aea565087cb","repo":"cocoindex-io/cocoindex","slug":"row-count-must-be-positive-d1fc5a","errorCode":null,"errorMessage":"row_count must be positive","messagePattern":"row_count must be positive","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/cocoindex/connectors/snowflake/_target.py","lineNumber":300,"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(\"%s\" for _ in range(row_count))\n        return f'DELETE FROM {qualified_table_name} WHERE \"{pk_cols[0]}\" IN ({markers})'\n\n    row_clauses = []\n    for _ in range(row_count):\n        and_clause = \" AND \".join(f'\"{pk}\" = %s' for pk in pk_cols)\n        row_clauses.append(f\"({and_clause})\")\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:\n        return None\n    if col.use_parse_json:\n        if isinstance(value, str):\n            return value","sourceCodeStart":282,"sourceCodeEnd":318,"githubUrl":"https://github.com/cocoindex-io/cocoindex/blob/e84aa99b3292c5270a4b313b2a7137ad9ce8ab3b/python/cocoindex/connectors/snowflake/_target.py#L282-L318","documentation":"`_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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the caller skips invoking the delete path when the list of keys to delete is empty (guard with `if keys:`).","If you maintain patched code, filter out empty delete batches before calling _apply_actions.","Report upstream if this arises from the library's own action computation, since callers normally never pass row_count <= 0."],"exampleFix":"// before\nawait _apply_actions(conn, actions)  # actions.deletes may be empty\n// after\nif actions.delete_keys:\n    await _apply_actions(conn, actions)","handlingStrategy":"validation","validationCode":"if delete_keys:\n    apply_actions(..., delete_keys=delete_keys)  # skip empty batches","typeGuard":null,"tryCatchPattern":"try:\n    apply_actions(...)\nexcept ValueError as e:\n    if \"row_count must be positive\" in str(e):\n        logging.warning(\"Empty delete batch reached SQL builder; skipping\")\n    else:\n        raise","preventionTips":["Filter empty delete batches before invoking the apply/sync path.","Treat this error as an internal bug: capture the action list and report upstream.","Keep connector code unpatched so action keys are built by the library itself."],"tags":["snowflake","sql","internal-invariant"],"backgroundTag":"internal-invariant-violation","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"}