]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Make boolean processors consistent between Py/C; coerce to 1/0
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Jun 2016 23:26:28 +0000 (19:26 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 23 Jun 2016 23:58:43 +0000 (19:58 -0400)
The processing performed by the :class:`.Boolean` datatype for backends
that only feature integer types has been made consistent between the
pure Python and C-extension versions, in that the C-extension version
will accept any integer value from the database as a boolean, not just
zero and one; additionally, non-boolean integer values being sent to
the database are coerced to exactly zero or one, instead of being
passed as the original integer value.

Change-Id: I01e647547fd7047bd549dd70e1fa202c51e8328b
Fixes: #3730
doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
lib/sqlalchemy/cextension/processors.c
lib/sqlalchemy/processors.py
test/engine/test_processors.py
test/sql/test_types.py

index 44ae8c2bbfeabd8e49444e912a095d81c12ed11c..9ae17fde090d90ad62191c8b7f44a041b7b80ca1 100644 (file)
 .. changelog::
     :version: 1.1.0b2
 
+    .. change::
+        :tags: bug, sql
+        :tickets: 3730
+
+        The processing performed by the :class:`.Boolean` datatype for backends
+        that only feature integer types has been made consistent between the
+        pure Python and C-extension versions, in that the C-extension version
+        will accept any integer value from the database as a boolean, not just
+        zero and one; additionally, non-boolean integer values being sent to
+        the database are coerced to exactly zero or one, instead of being
+        passed as the original integer value.
+
+        .. seealso::
+
+            :ref:`change_3730`
+
     .. change::
         :tags: bug, sql
         :tickets: 3725
index 4e54b97e1f3cb1841957c95c9fff459f5232cb48..1eea7eaca5bdc0f370563e0f5b312193f45af2ff 100644 (file)
@@ -1507,6 +1507,28 @@ this CHECK constraint can now be disabled using the new
 
 :ticket:`3095`
 
+.. _change_3730:
+
+Non-native boolean integer values coerced to zero/one/None in all cases
+-----------------------------------------------------------------------
+
+The :class:`.Boolean` datatype coerces Python booleans to integer values
+for backends that don't have a native boolean type, such as SQLite and
+MySQL.  On these backends, a CHECK constraint is normally set up which
+ensures the values in the database are in fact one of these two values.
+However, MySQL ignores CHECK constraints, the constraint is optional, and
+an existing database might not have this constraint.  The :class:`.Boolean`
+datatype has been repaired such that an incoming Python-side value that is
+already an integer value is coerced to zero or one, not just passed as-is;
+additionally, the C-extension version of the int-to-boolean processor for
+results now uses the same Python boolean interpretation of the value,
+rather than asserting an exact one or zero value.  This is now consistent
+with the pure-Python int-to-boolean processor and is more forgiving of
+existing data already within the database.   Values of None/NULL are as before
+retained as None/NULL.
+
+:ticket:`3730`
+
 .. _change_2837:
 
 Large parameter and row values are now truncated in logging and exception displays
index 5357e34dc7b4ede5b9c7566330405e61c09ae882..5b7527c2022192fe943928254e88967c1a3686e9 100644 (file)
@@ -22,28 +22,18 @@ typedef int Py_ssize_t;
 static PyObject *
 int_to_boolean(PyObject *self, PyObject *arg)
 {
-    long l = 0;
+    int l = 0;
     PyObject *res;
 
     if (arg == Py_None)
         Py_RETURN_NONE;
 
-
-#if PY_MAJOR_VERSION >= 3
-    l = PyLong_AsLong(arg);
-#else
-    l = PyInt_AsLong(arg);
-#endif
+    l = PyObject_IsTrue(arg);
     if (l == 0) {
         res = Py_False;
     } else if (l == 1) {
         res = Py_True;
-    } else if ((l == -1) && PyErr_Occurred()) {
-        /* -1 can be either the actual value, or an error flag. */
-        return NULL;
     } else {
-        PyErr_SetString(PyExc_ValueError,
-                        "int_to_boolean only accepts None, 0 or 1");
         return NULL;
     }
 
index b57e6740b7571825312a73484e29a8005d5be8b2..98f8a275984467025e512bca608dec6a5130ed47 100644 (file)
@@ -53,7 +53,7 @@ def boolean_to_int(value):
     if value is None:
         return None
     else:
-        return int(value)
+        return int(bool(value))
 
 
 def py_fallback():
@@ -111,7 +111,7 @@ def py_fallback():
         if value is None:
             return None
         else:
-            return value and True or False
+            return bool(value)
 
     DATETIME_RE = re.compile(
         "(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)(?:\.(\d+))?")
index f4df7827c384eaec284bc0eb305980e9afa4d914..47302af979fafa12eccc513f4fcffbdc4cecf7d9 100644 (file)
@@ -2,6 +2,85 @@ from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import assert_raises_message, eq_
 
 
+class _BooleanProcessorTest(fixtures.TestBase):
+    def test_int_to_bool_none(self):
+        eq_(
+            self.module.int_to_boolean(None),
+            None
+        )
+
+    def test_int_to_bool_zero(self):
+        eq_(
+            self.module.int_to_boolean(0),
+            False
+        )
+
+    def test_int_to_bool_one(self):
+        eq_(
+            self.module.int_to_boolean(1),
+            True
+        )
+
+    def test_int_to_bool_positive_int(self):
+        eq_(
+            self.module.int_to_boolean(12),
+            True
+        )
+
+    def test_int_to_bool_negative_int(self):
+        eq_(
+            self.module.int_to_boolean(-4),
+            True
+        )
+
+
+
+class PyBooleanProcessorTest(_BooleanProcessorTest):
+    @classmethod
+    def setup_class(cls):
+        from sqlalchemy import processors
+        cls.module = type(
+            "util", (object,),
+            dict(
+                (k, staticmethod(v))
+                for k, v in list(processors.py_fallback().items())
+            )
+        )
+
+    def test_bool_to_int_false(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(False), 0)
+
+    def test_bool_to_int_true(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(True), 1)
+
+    def test_bool_to_int_positive_int(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(5), 1)
+
+    def test_bool_to_int_negative_int(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(-10), 1)
+
+    def test_bool_to_int_zero(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(0), 0)
+
+    def test_bool_to_int_one(self):
+        from sqlalchemy import processors
+        eq_(processors.boolean_to_int(1), 1)
+
+
+class CBooleanProcessorTest(_BooleanProcessorTest):
+    __requires__ = ('cextensions',)
+
+    @classmethod
+    def setup_class(cls):
+        from sqlalchemy import cprocessors
+        cls.module = cprocessors
+
+
 class _DateProcessorTest(fixtures.TestBase):
     def test_date_no_string(self):
         assert_raises_message(
index e540f92464b0c92ec6e310c09a15eb608eaae5a4..49a1d8f15ffc0183aedf09a618f0f1b8e17a95d9 100644 (file)
@@ -2314,6 +2314,37 @@ class BooleanTest(
             dialect="sqlite"
         )
 
+    @testing.skip_if(lambda: testing.db.dialect.supports_native_boolean)
+    def test_nonnative_processor_coerces_to_onezero(self):
+        boolean_table = self.tables.boolean_table
+        with testing.db.connect() as conn:
+            conn.execute(
+                boolean_table.insert(),
+                {"id": 1, "unconstrained_value": 5}
+            )
+
+            eq_(
+                conn.scalar("select unconstrained_value from boolean_table"),
+                1
+            )
+
+    @testing.skip_if(lambda: testing.db.dialect.supports_native_boolean)
+    def test_nonnative_processor_coerces_integer_to_boolean(self):
+        boolean_table = self.tables.boolean_table
+        with testing.db.connect() as conn:
+            conn.execute(
+                "insert into boolean_table (id, unconstrained_value) values (1, 5)"
+            )
+
+            eq_(
+                conn.scalar("select unconstrained_value from boolean_table"),
+                5
+            )
+
+            eq_(
+                conn.scalar(select([boolean_table.c.unconstrained_value])),
+                True
+            )
 
 class PickleTest(fixtures.TestBase):