Repository: snowflakedb/snowflake-connector-python
Issue: SNOW-499496: Cursor.executemany() crashes if args is an iterator

To reproduce:
```python
>>> import snowflake.connector
>>> conn = snowflake.connector.connect(...)
>>> conn.cursor().execute('CREATE TABLE "tbl" ("col" int)')
>>> query = 'INSERT INTO "tbl" ("col") VALUES (%s)'
>>> args = ((i,) for i in range(-3, 2))
>>> conn.cursor().executemany(query, args)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../snowflake/connector/cursor.py", line 987, in executemany
    if len(seqparams) == 0:
TypeError: object of type 'generator' has no len()
```
The exception happens on a check (added in 27597cb7) which prevents using `executemany()` with empty args:
https://github.com/snowflakedb/snowflake-connector-python/blob/c37c0713ddf2eb66c7cf15eb38099040c73c0a0a/src/snowflake/connector/cursor.py#L988-L998

```python
>>> conn.cursor().executemany(query, [])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../snowflake/connector/cursor.py", line 988, in executemany
    Error.errorhandler_wrapper(
  File ".../snowflake/connector/errors.py", line 255, in errorhandler_wrapper
    handed_over = Error.hand_to_other_handler(
  File ".../snowflake/connector/errors.py", line 310, in hand_to_other_handler
    cursor.errorhandler(connection, cursor, error_class, error_value)
  File ".../snowflake/connector/errors.py", line 189, in default_errorhandler
    raise error_class(
snowflake.connector.errors.InterfaceError: No parameters are specified for the command: INSERT INTO "tbl" ("col") VALUES (%s)
```
To match the behavior of the other database drivers that Django is tested with, `Cursor.executemany()` should instead simply do nothing if args is empty.

Removing the code block quoted above fixes both issues.

Relevant Django tests:
[test_cursor_executemany_with_iterator](https://github.com/django/django/blob/ba9ced3e9a643a05bc521f0a2e6d02e3569de374/tests/backends/tests.py#L279-L289)
[test_cursor_executemany_with_empty_params_list](https://github.com/django/django/blob/ba9ced3e9a643a05bc521f0a2e6d02e3569de374/tests/backends/tests.py#L273-L277)

Changed files:
src/snowflake/connector/cursor.py, test/integ/test_cursor.py

diff --git a/src/snowflake/connector/cursor.py b/src/snowflake/connector/cursor.py
index f170f5dd..2163cf8b 100644
--- a/src/snowflake/connector/cursor.py
+++ b/src/snowflake/connector/cursor.py
@@ -1002,16 +1002,7 @@ class SnowflakeCursor:
         logger.debug("executing many SQLs/commands")
         command = command.strip(" \\t\\n\\r") if command else None
 
-        if len(seqparams) == 0:
-            Error.errorhandler_wrapper(
-                self.connection,
-                self,
-                InterfaceError,
-                {
-                    "msg": f"No parameters are specified for the command: {command}",
-                    "errno": ER_INVALID_VALUE,
-                },
-            )
+        if not seqparams:
             return self
 
         if self.INSERT_SQL_RE.match(command):
diff --git a/test/integ/test_cursor.py b/test/integ/test_cursor.py
index a9554c5e..a0a7ea88 100644
--- a/test/integ/test_cursor.py
+++ b/test/integ/test_cursor.py
@@ -46,7 +46,6 @@ except ImportError:
 
 from snowflake.connector.errorcode import (
     ER_FAILED_TO_REWRITE_MULTI_ROW_INSERT,
-    ER_INVALID_VALUE,
     ER_NOT_POSITIVE_SIZE,
 )
 from snowflake.connector.sqlstate import SQLSTATE_FEATURE_NOT_SUPPORTED
@@ -679,6 +678,44 @@ def test_executemany_qmark_types(conn, db_parameters):
                 cur.execute(f"drop table if exists {table_name}")
 
 
+@pytest.mark.skipolddriver
+def test_executemany_params_iterator(conn):
+    """Cursor.executemany() works with an interator of params."""
+    table_name = random_string(5, "executemany_params_iterator")
+    with conn() as cnx:
+        c = cnx.cursor()
+        c.execute(f"create temp table {table_name}(bar integer)")
+        fmt = f"insert into {table_name}(bar) values(%(value)s)"
+        c.executemany(fmt, ({"value": x} for x in ("1234", "234", "34", "4")))
+        cnt = 0
+        for rec in c:
+            cnt += int(rec[0])
+        assert cnt == 4, "number of records"
+        assert c.rowcount == 4, "wrong number of records were inserted"
+        c.close()
+
+        c = cnx.cursor()
+        fmt = f"insert into {table_name}(bar) values(%s)"
+        c.executemany(fmt, ((x,) for x in (12345, 1234, 234, 34, 4)))
+        rec = c.fetchone()
+        assert rec[0] == 5, "number of records"
+        assert c.rowcount == 5, "wrong number of records were inserted"
+        c.close()
+
+
+@pytest.mark.skipolddriver
+def test_executemany_empty_params(conn):
+    """Cursor.executemany() does nothing if params is empty."""
+    table_name = random_string(5, "executemany_empty_params")
+    with conn() as cnx:
+        c = cnx.cursor()
+        # The table isn't created, so if this were executed, it would error.
+        fmt = f"insert into {table_name}(aa) values(%(value)s)"
+        c.executemany(fmt, [])
+        assert c.query is None
+        c.close()
+
+
 def test_closed_cursor(conn, db_parameters):
     """Attempts to use the closed cursor. It should raise errors.
 
@@ -1224,18 +1261,6 @@ def test_query_cancellation(conn_cnx):
             cur.abort_query(sf_qid)
 
 
-def test_executemany_error(conn_cnx):
-    """Tests calling executemany without many things."""
-    with conn_cnx() as con:
-        with con.cursor() as cur:
-            with pytest.raises(
-                InterfaceError,
-                match="No parameters are specified for the command: select 1",
-            ) as ie:
-                cur.executemany("select 1", [])
-                assert ie.errno == ER_INVALID_VALUE
-
-
 def test_executemany_insert_rewrite(conn_cnx):
     """Tests calling executemany with a non rewritable pyformat insert query."""
     with conn_cnx() as con: