Repository: snowflakedb/snowflake-connector-python
Issue: SNOW-499499: Violating NOT NULL constraint should raise IntegrityError instead of ProgrammingError

Trying to save a NULL in a NOT NULL column raises `ProgrammingError`:
```python
>>> import snowflake.connector
>>> conn = snowflake.connector.connect(...)
>>> conn.cursor().execute('CREATE TABLE FOO(BAR char NOT NULL)')
>>> conn.cursor().execute('INSERT INTO FOO VALUES (NULL)')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/tim/.virtualenvs/django39/lib/python3.9/site-packages/snowflake/connector/cursor.py", line 777, in execute
    Error.errorhandler_wrapper(
  File "/home/tim/.virtualenvs/django39/lib/python3.9/site-packages/snowflake/connector/errors.py", line 255, in errorhandler_wrapper
    handed_over = Error.hand_to_other_handler(
  File "/home/tim/.virtualenvs/django39/lib/python3.9/site-packages/snowflake/connector/errors.py", line 310, in hand_to_other_handler
    cursor.errorhandler(connection, cursor, error_class, error_value)
  File "/home/tim/.virtualenvs/django39/lib/python3.9/site-packages/snowflake/connector/errors.py", line 189, in default_errorhandler
    raise error_class(
snowflake.connector.errors.ProgrammingError: NULL result in a non-nullable column
```
It would be consistent with other database drivers to instead raise `IntegrityError`. Some tests in Django's test suite fail because of this difference.

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 021c6f3f..961e13b0 100644
--- a/src/snowflake/connector/cursor.py
+++ b/src/snowflake/connector/cursor.py
@@ -55,6 +55,7 @@ from .errorcode import (
 from .errors import (
     DatabaseError,
     Error,
+    IntegrityError,
     InterfaceError,
     NotSupportedError,
     ProgrammingError,
@@ -787,8 +788,10 @@ class SnowflakeCursor:
                 "sqlstate": self._sqlstate,
                 "sfqid": self._sfqid,
             }
+            is_integrity_error = code == "100072"  # NULL result in a non-nullable column
+            error_class = IntegrityError if is_integrity_error else ProgrammingError
             Error.errorhandler_wrapper(
-                self.connection, self, ProgrammingError, errvalue
+                self.connection, self, error_class, errvalue
             )
         return self
 
diff --git a/test/integ/test_cursor.py b/test/integ/test_cursor.py
index a9554c5e..54744599 100644
--- a/test/integ/test_cursor.py
+++ b/test/integ/test_cursor.py
@@ -994,6 +994,17 @@ def test_fetch_out_of_range_timestamp_value(conn, result_format):
             cur.fetchone()
 
 
+@pytest.mark.skipolddriver
+def test_null_in_non_null(conn):
+    table_name = random_string(5, "null_in_non_null")
+    error_msg = "NULL result in a non-nullable column"
+    with conn() as cnx:
+        cur = cnx.cursor()
+        cur.execute(f"create temp table {table_name}(bar char not null)")
+        with pytest.raises(errors.IntegrityError, match=error_msg):
+            cur.execute(f'insert into {table_name} values (null)')
+
+
 @pytest.mark.parametrize("sql", (None, ""), ids=["None", "empty"])
 def test_empty_execution(conn, sql):
     """Checks whether executing an empty string, or nothing behaves as expected."""