]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The Boolean type, when used on a backend that doesn't
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 6 Dec 2009 22:58:05 +0000 (22:58 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 6 Dec 2009 22:58:05 +0000 (22:58 +0000)
have native boolean support, will generate a CHECK
constraint "col IN (0, 1)" along with the int/smallint-
based column type.  This can be switched off if
desired with create_constraint=False.
Note that MySQL has no native boolean *or* CHECK constraint
support so this feature isn't available on that platform.
[ticket:1589]

14 files changed:
CHANGES
lib/sqlalchemy/dialects/firebird/base.py
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/oracle/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/types.py
test/dialect/test_mssql.py
test/dialect/test_mysql.py
test/dialect/test_sqlite.py
test/orm/test_assorted_eager.py
test/sql/test_types.py

diff --git a/CHANGES b/CHANGES
index 1628f5c70b865403c7284bbd8d6536aca23bc2a3..f5404b0e8a28c6961396793a6de48cfeb11e9897 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -689,6 +689,15 @@ CHANGES
       native enum support will by generate VARCHAR + an inline CHECK
       constraint to enforce the enum.
       [ticket:1109] [ticket:1511]
+    
+    - The Boolean type, when used on a backend that doesn't 
+      have native boolean support, will generate a CHECK 
+      constraint "col IN (0, 1)" along with the int/smallint-
+      based column type.  This can be switched off if 
+      desired with create_constraint=False. 
+      Note that MySQL has no native boolean *or* CHECK constraint
+      support so this feature isn't available on that platform.
+      [ticket:1589]
       
     - PickleType now uses == for comparison of values when
       mutable=True, unless the "comparator" argument with a
index 21fec6b51c80d5d01384b1697f7ad78813f2f403..86d6876a69f5863d4ba15427dddbb5d67b748948 100644 (file)
@@ -123,29 +123,7 @@ RESERVED_WORDS = set([
     ])
 
 
-class _FBBoolean(sqltypes.Boolean):
-    def result_processor(self, dialect, coltype):
-        def process(value):
-            if value is None:
-                return None
-            return value and True or False
-        return process
-
-    def bind_processor(self, dialect):
-        def process(value):
-            if value is True:
-                return 1
-            elif value is False:
-                return 0
-            elif value is None:
-                return None
-            else:
-                return value and True or False
-        return process
-
-
 colspecs = {
-    sqltypes.Boolean: _FBBoolean,
 }
 
 ischema_names = {
@@ -321,10 +299,13 @@ class FBDialect(default.DefaultDialect):
     sequences_optional = False
     supports_default_values = True
     postfetch_lastrowid = False
-
+    
+    supports_native_boolean = False
+    
     requires_name_normalize = True
     supports_empty_insert = False
 
+    
     statement_compiler = FBCompiler
     ddl_compiler = FBDDLCompiler
     preparer = FBIdentifierPreparer
index bd275d75899aadac5cadd5fcf07e4a605076c742..1060446b2729bc5a5ff39d0b88c198837e894dd1 100644 (file)
@@ -604,25 +604,6 @@ class IMAGE(sqltypes.Binary):
 class BIT(sqltypes.TypeEngine):
     __visit_name__ = 'BIT'
     
-class _MSBoolean(sqltypes.Boolean):
-    def result_processor(self, dialect, coltype):
-        def process(value):
-            if value is None:
-                return None
-            return value and True or False
-        return process
-
-    def bind_processor(self, dialect):
-        def process(value):
-            if value is True:
-                return 1
-            elif value is False:
-                return 0
-            elif value is None:
-                return None
-            else:
-                return value and True or False
-        return process
 
 class MONEY(sqltypes.TypeEngine):
     __visit_name__ = 'MONEY'
@@ -640,7 +621,6 @@ class SQL_VARIANT(sqltypes.TypeEngine):
 MSNumeric = _MSNumeric
 MSDateTime = _MSDateTime
 MSDate = _MSDate
-MSBoolean = _MSBoolean
 MSReal = REAL
 MSTinyInteger = TINYINT
 MSTime = TIME
@@ -667,7 +647,6 @@ colspecs = {
     sqltypes.DateTime : _MSDateTime,
     sqltypes.Date : _MSDate,
     sqltypes.Time : TIME,
-    sqltypes.Boolean : _MSBoolean,
 }
 
 ischema_names = {
@@ -1137,6 +1116,7 @@ class MSDialect(default.DefaultDialect):
     colspecs = colspecs
     ischema_names = ischema_names
     
+    supports_native_boolean = False
     supports_unicode_binds = True
     postfetch_lastrowid = True
     
index 3a277389250902e9ebe52062a821be2feb83ce41..01f8b13a7aba98edd5f68bf23c1a1e19896e8b85 100644 (file)
@@ -167,6 +167,13 @@ available.
 
       update(..., mysql_limit=10)
 
+Boolean Types
+-------------
+
+MySQL's BOOL type is a synonym for SMALLINT, so is actually a numeric value,
+and additionally MySQL doesn't support CHECK constraints. Therefore SQLA's
+Boolean type cannot fully constrain values to just "True" and "False" the way it does for most other backends.
+
 Troubleshooting
 ---------------
 
@@ -1080,32 +1087,7 @@ class SET(_StringType):
                 return value
         return process
 
-class _MSBoolean(sqltypes.Boolean):
-    """MySQL BOOLEAN type."""
-
-    __visit_name__ = 'BOOLEAN'
-
-    def result_processor(self, dialect, coltype):
-        def process(value):
-            if value is None:
-                return None
-            return value and True or False
-        return process
-
-    def bind_processor(self, dialect):
-        def process(value):
-            if value is True:
-                return 1
-            elif value is False:
-                return 0
-            elif value is None:
-                return None
-            else:
-                return value and True or False
-        return process
-
 # old names
-MSBoolean = _MSBoolean
 MSTime = _MSTime
 MSSet = SET
 MSEnum = ENUM
@@ -1141,7 +1123,6 @@ colspecs = {
     sqltypes.Numeric: NUMERIC,
     sqltypes.Float: FLOAT,
     sqltypes.Binary: _BinaryType,
-    sqltypes.Boolean: _MSBoolean,
     sqltypes.Time: _MSTime,
     sqltypes.Enum: ENUM,
 }
@@ -1656,7 +1637,6 @@ class MySQLDialect(default.DefaultDialect):
     max_identifier_length = 255
     
     supports_native_enum = True
-    supports_native_boolean = True
     
     supports_sane_rowcount = True
     supports_sane_multi_rowcount = False
index d13e37d60a108be2efafa3bdb997ef7f23e8f1d0..ddcc9f4607e5ab653c2ba56b5babc60d90d1e27d 100644 (file)
@@ -158,25 +158,6 @@ class LONG(sqltypes.Text):
 class _OracleBoolean(sqltypes.Boolean):
     def get_dbapi_type(self, dbapi):
         return dbapi.NUMBER
-    
-    def result_processor(self, dialect, coltype):
-        def process(value):
-            if value is None:
-                return None
-            return value and True or False
-        return process
-
-    def bind_processor(self, dialect):
-        def process(value):
-            if value is True:
-                return 1
-            elif value is False:
-                return 0
-            elif value is None:
-                return None
-            else:
-                return value and True or False
-        return process
 
 colspecs = {
     sqltypes.Boolean : _OracleBoolean,
index d0a87d28232a3b7f05935bf8b8187743899e8657..7a9e2e7109d7f201d4b600c289306ffa7de63111 100644 (file)
@@ -525,6 +525,7 @@ class PGDialect(default.DefaultDialect):
     supports_sane_rowcount = True
     
     supports_native_enum = True
+    supports_native_boolean = True
     
     supports_sequences = True
     sequences_optional = True
index 2b929331a81e2500ea2ccd0542398d1b60b51d1a..d83eb4b8649ac547e5a5ea940e1442f7631a7421 100644 (file)
@@ -146,23 +146,7 @@ class TIME(_DateTimeMixin, sqltypes.Time):
     def result_processor(self, dialect, coltype):
         return self._result_processor(datetime.time)
 
-class _SLBoolean(sqltypes.Boolean):
-    def bind_processor(self, dialect):
-        def process(value):
-            if value is None:
-                return None
-            return value and 1 or 0
-        return process
-
-    def result_processor(self, dialect, coltype):
-        def process(value):
-            if value is None:
-                return None
-            return value == 1
-        return process
-
 colspecs = {
-    sqltypes.Boolean: _SLBoolean,
     sqltypes.Date: DATE,
     sqltypes.DateTime: DATETIME,
     sqltypes.Float: _SLFloat,
index 70087ee739206feb63c083a332efbeed1aefa13f..d3a15dc8b3197907144bfe2af46d61f092a9cee2 100644 (file)
@@ -519,89 +519,97 @@ class Column(SchemaItem, expression.ColumnClause):
             to generate primary key identifiers (i.e. Firebird, Postgresql, 
             Oracle).
 
-        :param default: A scalar, Python callable, or :class:`~sqlalchemy.sql.expression.ClauseElement`
-          representing the *default value* for this column, which will be
-          invoked upon insert if this column is otherwise not specified
-          in the VALUES clause of the insert.  This is a shortcut
-          to using :class:`ColumnDefault` as a positional argument.
+        :param default: A scalar, Python callable, or
+            :class:`~sqlalchemy.sql.expression.ClauseElement` representing the
+            *default value* for this column, which will be invoked upon insert
+            if this column is otherwise not specified in the VALUES clause of
+            the insert. This is a shortcut to using :class:`ColumnDefault` as
+            a positional argument.
           
-          Contrast this argument to ``server_default`` which creates a 
-          default generator on the database side.
+            Contrast this argument to ``server_default`` which creates a 
+            default generator on the database side.
         
-        :param key: An optional string identifier which will identify this ``Column`` 
-            object on the :class:`Table`.  When a key is provided, this is the
-            only identifier referencing the ``Column`` within the application,
-            including ORM attribute mapping; the ``name`` field is used only
-            when rendering SQL.
+        :param key: An optional string identifier which will identify this
+            ``Column`` object on the :class:`Table`. When a key is provided,
+            this is the only identifier referencing the ``Column`` within the
+            application, including ORM attribute mapping; the ``name`` field
+            is used only when rendering SQL.
 
         :param index: When ``True``, indicates that the column is indexed.
-          This is a shortcut for using a :class:`Index` construct on the table.
-          To specify indexes with explicit names or indexes that contain multiple 
-          columns, use the :class:`Index` construct instead.
-
-        :param info: A dictionary which defaults to ``{}``.  A space to store application 
-          specific data. This must be a dictionary.
-
-        :param nullable: If set to the default of ``True``, indicates the column
-            will be rendered as allowing NULL, else it's rendered as NOT NULL.
-            This parameter is only used when issuing CREATE TABLE statements.
-
-        :param onupdate: A scalar, Python callable, or :class:`~sqlalchemy.sql.expression.ClauseElement`
-            representing a default value to be applied to the column within UPDATE
-            statements, which wil be invoked upon update if this column is not present
-            in the SET clause of the update.  This is a shortcut to using 
-            :class:`ColumnDefault` as a positional argument with ``for_update=True``.
+            This is a shortcut for using a :class:`Index` construct on the
+            table. To specify indexes with explicit names or indexes that
+            contain multiple columns, use the :class:`Index` construct
+            instead.
+
+        :param info: A dictionary which defaults to ``{}``. A space to store
+            application specific data. This must be a dictionary.
+
+        :param nullable: If set to the default of ``True``, indicates the 
+            column will be rendered as allowing NULL, else it's rendered as
+            NOT NULL. This parameter is only used when issuing CREATE TABLE
+            statements.
+
+        :param onupdate: A scalar, Python callable, or
+            :class:`~sqlalchemy.sql.expression.ClauseElement` representing a
+            default value to be applied to the column within UPDATE
+            statements, which wil be invoked upon update if this column is not
+            present in the SET clause of the update. This is a shortcut to
+            using :class:`ColumnDefault` as a positional argument with
+            ``for_update=True``.
             
         :param primary_key: If ``True``, marks this column as a primary key
-            column.  Multiple columns can have this flag set to specify composite
-            primary keys.  As an alternative, the primary key of a :class:`Table` can
-            be specified via an explicit :class:`PrimaryKeyConstraint` object.
+            column. Multiple columns can have this flag set to specify
+            composite primary keys. As an alternative, the primary key of a
+            :class:`Table` can be specified via an explicit
+            :class:`PrimaryKeyConstraint` object.
 
-        :param server_default: A :class:`FetchedValue` instance, str, Unicode or
-          :func:`~sqlalchemy.sql.expression.text` construct representing 
-          the DDL DEFAULT value for the column.
+        :param server_default: A :class:`FetchedValue` instance, str, Unicode
+            or :func:`~sqlalchemy.sql.expression.text` construct representing
+            the DDL DEFAULT value for the column.
 
-          String types will be emitted as-is, surrounded by single quotes::
+            String types will be emitted as-is, surrounded by single quotes::
 
-              Column('x', Text, server_default="val")
+                Column('x', Text, server_default="val")
 
-              x TEXT DEFAULT 'val'
+                x TEXT DEFAULT 'val'
 
-          A :func:`~sqlalchemy.sql.expression.text` expression will be 
-          rendered as-is, without quotes::
+            A :func:`~sqlalchemy.sql.expression.text` expression will be
+            rendered as-is, without quotes::
 
-              Column('y', DateTime, server_default=text('NOW()'))0
+                Column('y', DateTime, server_default=text('NOW()'))0
 
-              y DATETIME DEFAULT NOW()
+                y DATETIME DEFAULT NOW()
 
-          Strings and text() will be converted into a :class:`DefaultClause`
-          object upon initialization.
+            Strings and text() will be converted into a :class:`DefaultClause`
+            object upon initialization.
           
-          Use :class:`FetchedValue` to indicate that an already-existing column will generate
-          a default value on the database side which will be available to SQLAlchemy 
-          for post-fetch after inserts.  
-          This construct does not specify any DDL and the implementation is 
-          left to the database, such as via a trigger.
-
-        :param server_onupdate:   A :class:`FetchedValue` instance representing 
-            a database-side default generation function.  This indicates to 
-            SQLAlchemy that a newly generated value will be available after updates.
-            This construct does not specify any DDL and the implementation is 
-            left to the database, such as via a trigger.
-
-        :param quote: Force quoting of this column's name on or off, corresponding
-           to ``True`` or ``False``.  When left at its default of ``None``,
-           the column identifier will be quoted according to whether the name is
-           case sensitive (identifiers with at least one upper case character are 
-           treated as case sensitive), or if it's a reserved word.  This flag 
-           is only needed to force quoting of a reserved word which is not known
-           by the SQLAlchemy dialect.
-
-        :param unique: When ``True``, indicates that this column contains a unique
-            constraint, or if ``index`` is ``True`` as well, indicates that the
-            :class:`Index` should be created with the unique flag.  To specify multiple
-            columns in the constraint/index or to specify an explicit name,
-            use the :class:`UniqueConstraint` or :class:`Index` constructs explicitly.
+            Use :class:`FetchedValue` to indicate that an already-existing
+            column will generate a default value on the database side which
+            will be available to SQLAlchemy for post-fetch after inserts. This
+            construct does not specify any DDL and the implementation is left
+            to the database, such as via a trigger.
+
+        :param server_onupdate:   A :class:`FetchedValue` instance
+             representing a database-side default generation function. This
+             indicates to SQLAlchemy that a newly generated value will be
+             available after updates. This construct does not specify any DDL
+             and the implementation is left to the database, such as via a
+             trigger.
+
+        :param quote: Force quoting of this column's name on or off,
+             corresponding to ``True`` or ``False``. When left at its default
+             of ``None``, the column identifier will be quoted according to
+             whether the name is case sensitive (identifiers with at least one
+             upper case character are treated as case sensitive), or if it's a
+             reserved word. This flag is only needed to force quoting of a
+             reserved word which is not known by the SQLAlchemy dialect.
+
+        :param unique: When ``True``, indicates that this column contains a
+             unique constraint, or if ``index`` is ``True`` as well, indicates
+             that the :class:`Index` should be created with the unique flag.
+             To specify multiple columns in the constraint/index or to specify
+             an explicit name, use the :class:`UniqueConstraint` or
+             :class:`Index` constructs explicitly.
 
         """
 
@@ -640,8 +648,12 @@ class Column(SchemaItem, expression.ColumnClause):
         self.constraints = set()
         self.foreign_keys = util.OrderedSet()
         self._table_events = set()
-        
-        if isinstance(self.type, types.SchemaType):
+
+        # check if this Column is proxying another column
+        if '_proxies' in kwargs:
+            self.proxies = kwargs.pop('_proxies')
+        # otherwise, add DDL-related events
+        elif isinstance(self.type, types.SchemaType):
             self.type._set_parent(self)
             
         if self.default is not None:
@@ -649,6 +661,7 @@ class Column(SchemaItem, expression.ColumnClause):
                 args.append(self.default)
             else:
                 args.append(ColumnDefault(self.default))
+
         if self.server_default is not None:
             if isinstance(self.server_default, FetchedValue):
                 args.append(self.server_default)
@@ -812,9 +825,8 @@ class Column(SchemaItem, expression.ColumnClause):
             key = name or self.key, 
             primary_key = self.primary_key, 
             nullable = self.nullable, 
-            quote=self.quote, *fk)
+            quote=self.quote, _proxies=[self], *fk)
         c.table = selectable
-        c.proxies = [self]
         selectable.columns.add(c)
         if self.primary_key:
             selectable.primary_key.add(c)
index d7dda85e26112896dfe12d46692f2ecfd93b13b9..66b90ce0465b5491b0536dd813833a5ffc8a2fab 100644 (file)
@@ -87,9 +87,11 @@ class AbstractType(Visitable):
         return False
 
     def get_dbapi_type(self, dbapi):
-        """Return the corresponding type object from the underlying DB-API, if any.
+        """Return the corresponding type object from the underlying DB-API, if
+        any.
+        
+         This can be useful for calling ``setinputsizes()``, for example.
 
-        This can be useful for calling ``setinputsizes()``, for example.
         """
         return None
 
@@ -98,6 +100,7 @@ class AbstractType(Visitable):
         translate it to a new operator based on the semantics of this type.
 
         By default, returns the operator unchanged.
+
         """
         return op
 
@@ -1101,7 +1104,7 @@ class PickleType(MutableType, TypeDecorator):
         return self.mutable
 
 
-class Boolean(TypeEngine):
+class Boolean(TypeEngine, SchemaType):
     """A bool datatype.
 
     Boolean typically uses BOOLEAN or SMALLINT on the DDL side, and on
@@ -1111,6 +1114,44 @@ class Boolean(TypeEngine):
 
     __visit_name__ = 'boolean'
 
+    def __init__(self, create_constraint=True, name=None):
+        """Construct a Boolean.
+        
+        :param create_constraint: defaults to True.  If the boolean 
+        is generated as an int/smallint, also create a CHECK constraint
+        on the table that ensures 1 or 0 as a value.
+        
+        :param name: if a CHECK constraint is generated, specify
+        the name of the constraint.
+        
+        """
+        self.create_constraint = create_constraint
+        self.name = name
+        
+    def _set_table(self, table, column):
+        if not self.create_constraint:
+            return
+            
+        def should_create_constraint(compiler):
+            return not compiler.dialect.supports_native_boolean
+
+        e = schema.CheckConstraint(
+                        column.in_([0, 1]),
+                        name=self.name,
+                        _create_rule=should_create_constraint
+                    )
+        table.append_constraint(e)
+    
+    def result_processor(self, dialect, coltype):
+        if dialect.supports_native_boolean:
+            return None
+        else:
+            def process(value):
+                if value is None:
+                    return None
+                return value and True or False
+            return process
+
 class Interval(TypeDecorator):
     """A type for ``datetime.timedelta()`` objects.
 
index b57bc426b81c6057b396d9d192ed815a58e21405..351f44b7636e63bf38f6ee80ad985c448b49a06c 100644 (file)
@@ -921,7 +921,7 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables):
 
         columns = [
             # column type, args, kwargs, expected ddl
-            (mssql.MSBoolean, [], {},
+            (Boolean, [], {},
              'BIT'),
            ]
 
index accc84c2c9a70ab7427534df37cc2b360bef89ff..000bb131bfc8de6a679d56278d521292100fc2dc 100644 (file)
@@ -357,7 +357,7 @@ class TypesTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL):
         meta = MetaData(testing.db)
         bool_table = Table('mysql_bool', meta,
                            Column('b1', BOOLEAN),
-                           Column('b2', mysql.MSBoolean),
+                           Column('b2', Boolean),
                            Column('b3', mysql.MSTinyInteger(1)),
                            Column('b4', mysql.MSTinyInteger))
 
@@ -1068,7 +1068,6 @@ class SQLTest(TestBase, AssertsCompiledSQL):
             # testing
             (Boolean, "t.col"),
             (BOOLEAN, "t.col"),
-            (m.MSBoolean, "t.col"),
 
             (m.MSEnum, "t.col"),
             (m.MSEnum("1", "2"), "t.col"),
index e817d257b51fad23032befd00aa4a633f1e9eb4a..2d5cb1805d7db2b663c0b41a357fb5523ae34dee 100644 (file)
@@ -19,7 +19,7 @@ class TestTypes(TestBase, AssertsExecutionResults):
         meta = MetaData(testing.db)
         t = Table('bool_table', meta,
                   Column('id', Integer, primary_key=True),
-                  Column('boo', Boolean))
+                  Column('boo', Boolean(create_constraint=False)))
 
         try:
             meta.create_all()
@@ -29,12 +29,18 @@ class TestTypes(TestBase, AssertsExecutionResults):
             testing.db.execute("INSERT INTO bool_table (id, boo) VALUES (4, '0');")
             testing.db.execute("INSERT INTO bool_table (id, boo) VALUES (5, 1);")
             testing.db.execute("INSERT INTO bool_table (id, boo) VALUES (6, 0);")
-            assert t.select(t.c.boo).order_by(t.c.id).execute().fetchall() == [(3, True,), (5, True,)]
+            eq_(
+                t.select(t.c.boo).order_by(t.c.id).execute().fetchall(),
+                [(3, True,), (5, True,)]
+            )
         finally:
             meta.drop_all()
 
     def test_string_dates_raise(self):
-        assert_raises(TypeError, testing.db.execute, select([1]).where(bindparam("date", type_=Date)), date=str(datetime.date(2007, 10, 30)))
+        assert_raises(TypeError, 
+                        testing.db.execute, 
+                        select([1]).where(bindparam("date", type_=Date)), 
+                        date=str(datetime.date(2007, 10, 30)))
     
     def test_time_microseconds(self):
         dt = datetime.datetime(2008, 6, 27, 12, 0, 0, 125)  # 125 usec
index 26362168ea50817c7e0f70b25b27f75677ece806..da0883c620b02019e7a3d7c6329496100bd44b06 100644 (file)
@@ -21,18 +21,12 @@ class EagerTest(_base.MappedTest):
     
     @classmethod
     def define_tables(cls, metadata):
-        # determine a literal value for "false" based on the dialect
-        # FIXME: this DefaultClause setup is bogus.
-
-        dialect = testing.db.dialect
-        bp = sa.Boolean().dialect_impl(dialect).bind_processor(dialect)
-
-        if bp:
-            false = str(bp(False))
-        elif testing.against('maxdb'):
-            false = text('FALSE')
+        
+        if testing.db.dialect.supports_native_boolean:
+            false = 'false'
         else:
-            false = str(False)
+            false = "0"
+            
         cls.other_artifacts['false'] = false
 
         Table('owners', metadata ,
index 04c193e01f569ebe9ff6416555fc9295690e6791..bae21091b0e13ac67480db907fdccb91415cf36c 100644 (file)
@@ -844,26 +844,60 @@ class BooleanTest(TestBase, AssertsExecutionResults):
         metadata = MetaData(testing.db)
         bool_table = Table('booltest', metadata,
             Column('id', Integer, primary_key=True),
-            Column('value', Boolean))
+            Column('value', Boolean),
+            Column('unconstrained_value', Boolean(create_constraint=False)),
+            )
         bool_table.create()
+        
     @classmethod
     def teardown_class(cls):
         bool_table.drop()
-    def testbasic(self):
+    
+    def teardown(self):
+        bool_table.delete().execute()
+        
+    def test_boolean(self):
         bool_table.insert().execute(id=1, value=True)
         bool_table.insert().execute(id=2, value=False)
         bool_table.insert().execute(id=3, value=True)
         bool_table.insert().execute(id=4, value=True)
         bool_table.insert().execute(id=5, value=True)
+        bool_table.insert().execute(id=6, value=None)
 
-        res = bool_table.select(
+        res = select([bool_table.c.id, bool_table.c.value]).where(
             bool_table.c.value == True
             ).order_by(bool_table.c.id).execute().fetchall()
         eq_(res, [(1, True), (3, True), (4, True), (5, True)])
 
-        res2 = bool_table.select(bool_table.c.value == False).execute().fetchall()
+        res2 = select([bool_table.c.id, bool_table.c.value]).where(
+                    bool_table.c.value == False).execute().fetchall()
         eq_(res2, [(2, False)])
 
+        res3 = select([bool_table.c.id, bool_table.c.value]).\
+                order_by(bool_table.c.id).\
+                execute().fetchall()
+        eq_(res3, [(1, True), (2, False), 
+                    (3, True), (4, True), 
+                    (5, True), (6, None)])
+        
+        # ensure we're getting True/False, not just ints
+        assert res3[0][1] is True
+        assert res3[1][1] is False
+    
+    @testing.fails_on('mysql', 
+            "The CHECK clause is parsed but ignored by all storage engines.")
+    @testing.skip_if(lambda: testing.db.dialect.supports_native_boolean)
+    def test_constraint(self):
+        assert_raises((exc.IntegrityError, exc.ProgrammingError),
+                        testing.db.execute, 
+                        "insert into booltest (id, value) values(1, 5)")
+
+    @testing.skip_if(lambda: testing.db.dialect.supports_native_boolean)
+    def test_unconstrained(self):
+        testing.db.execute(
+            "insert into booltest (id, unconstrained_value) values (1, 5)")
+    
+        
 class PickleTest(TestBase):
     def test_eq_comparison(self):
         p1 = PickleType()