From 7dc4df8a68eafd406e7378eedbb9c26188611a5c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 6 Dec 2009 22:58:05 +0000 Subject: [PATCH] - 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] --- CHANGES | 9 ++ lib/sqlalchemy/dialects/firebird/base.py | 27 +--- lib/sqlalchemy/dialects/mssql/base.py | 22 +-- lib/sqlalchemy/dialects/mysql/base.py | 34 +---- lib/sqlalchemy/dialects/oracle/base.py | 19 --- lib/sqlalchemy/dialects/postgresql/base.py | 1 + lib/sqlalchemy/dialects/sqlite/base.py | 16 --- lib/sqlalchemy/schema.py | 156 +++++++++++---------- lib/sqlalchemy/types.py | 47 ++++++- test/dialect/test_mssql.py | 2 +- test/dialect/test_mysql.py | 3 +- test/dialect/test_sqlite.py | 12 +- test/orm/test_assorted_eager.py | 16 +-- test/sql/test_types.py | 42 +++++- 14 files changed, 204 insertions(+), 202 deletions(-) diff --git a/CHANGES b/CHANGES index 1628f5c70b..f5404b0e8a 100644 --- 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 diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index 21fec6b51c..86d6876a69 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -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 diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index bd275d7589..1060446b27 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -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 diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 3a27738925..01f8b13a7a 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -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 diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index d13e37d60a..ddcc9f4607 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -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, diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index d0a87d2823..7a9e2e7109 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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 diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 2b929331a8..d83eb4b864 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -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, diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 70087ee739..d3a15dc8b3 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -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) diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index d7dda85e26..66b90ce046 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -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. diff --git a/test/dialect/test_mssql.py b/test/dialect/test_mssql.py index b57bc426b8..351f44b763 100644 --- a/test/dialect/test_mssql.py +++ b/test/dialect/test_mssql.py @@ -921,7 +921,7 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables): columns = [ # column type, args, kwargs, expected ddl - (mssql.MSBoolean, [], {}, + (Boolean, [], {}, 'BIT'), ] diff --git a/test/dialect/test_mysql.py b/test/dialect/test_mysql.py index accc84c2c9..000bb131bf 100644 --- a/test/dialect/test_mysql.py +++ b/test/dialect/test_mysql.py @@ -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"), diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index e817d257b5..2d5cb1805d 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -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 diff --git a/test/orm/test_assorted_eager.py b/test/orm/test_assorted_eager.py index 26362168ea..da0883c620 100644 --- a/test/orm/test_assorted_eager.py +++ b/test/orm/test_assorted_eager.py @@ -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 , diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 04c193e01f..bae21091b0 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -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() -- 2.47.3