From 414af7b61291b3fa77eb6da6a9b123399214089b Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 7 Oct 2015 10:02:45 -0400 Subject: [PATCH] - The system by which a :class:`.Column` considers itself to be an "auto increment" column has been changed, such that autoincrement is no longer implicitly enabled for a :class:`.Table` that has a composite primary key. In order to accommodate being able to enable autoincrement for a composite PK member column while at the same time maintaining SQLAlchemy's long standing behavior of enabling implicit autoincrement for a single integer primary key, a third state has been added to the :paramref:`.Column.autoincrement` parameter ``"auto"``, which is now the default. fixes #3216 - The MySQL dialect no longer generates an extra "KEY" directive when generating CREATE TABLE DDL for a table using InnoDB with a composite primary key with AUTO_INCREMENT on a column that isn't the first column; to overcome InnoDB's limitation here, the PRIMARY KEY constraint is now generated with the AUTO_INCREMENT column placed first in the list of columns. --- doc/build/changelog/changelog_11.rst | 37 +++++++ doc/build/changelog/migration_11.rst | 148 ++++++++++++++++++++++++- lib/sqlalchemy/dialects/mysql/base.py | 31 ------ lib/sqlalchemy/dialects/sqlite/base.py | 22 ++-- lib/sqlalchemy/sql/compiler.py | 2 +- lib/sqlalchemy/sql/crud.py | 91 +++++++++++++-- lib/sqlalchemy/sql/schema.py | 148 +++++++++++++++++++------ lib/sqlalchemy/testing/schema.py | 5 +- test/dialect/mysql/test_compiler.py | 8 +- test/dialect/mysql/test_reflection.py | 34 +++--- test/dialect/postgresql/test_query.py | 65 ++++++----- test/dialect/test_sqlite.py | 40 ++++++- test/engine/test_reflection.py | 19 ++-- test/ext/declarative/test_basic.py | 3 +- test/orm/test_composites.py | 3 +- test/orm/test_query.py | 3 +- test/orm/test_relationships.py | 11 +- test/orm/test_unitofwork.py | 2 +- test/sql/test_compiler.py | 15 +++ test/sql/test_defaults.py | 6 +- test/sql/test_insert.py | 100 +++++++++++++++++ test/sql/test_metadata.py | 122 ++++++++++++++++++++ 22 files changed, 755 insertions(+), 160 deletions(-) diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index e376fe191a..dcd43f28d6 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -21,6 +21,43 @@ .. changelog:: :version: 1.1.0b1 + .. change:: + :tags: change, sql, mysql + :tickets: 3216 + + The system by which a :class:`.Column` considers itself to be an + "auto increment" column has been changed, such that autoincrement + is no longer implicitly enabled for a :class:`.Table` that has a + composite primary key. In order to accommodate being able to enable + autoincrement for a composite PK member column while at the same time + maintaining SQLAlchemy's long standing behavior of enabling + implicit autoincrement for a single integer primary key, a third + state has been added to the :paramref:`.Column.autoincrement` parameter + ``"auto"``, which is now the default. + + .. seealso:: + + :ref:`change_3216` + + :ref:`change_mysql_3216` + + .. change:: + :tags: change, mysql + :tickets: 3216 + + The MySQL dialect no longer generates an extra "KEY" directive when + generating CREATE TABLE DDL for a table using InnoDB with a + composite primary key with AUTO_INCREMENT on a column that isn't the + first column; to overcome InnoDB's limitation here, the PRIMARY KEY + constraint is now generated with the AUTO_INCREMENT column placed + first in the list of columns. + + .. seealso:: + + :ref:`change_mysql_3216` + + :ref:`change_3216` + .. change:: :tags: change, sqlite :pullreq: github:198 diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst index 78f77e6946..c57c917615 100644 --- a/doc/build/changelog/migration_11.rst +++ b/doc/build/changelog/migration_11.rst @@ -16,7 +16,7 @@ What's New in SQLAlchemy 1.1? some issues may be moved to later milestones in order to allow for a timely release. - Document last updated: September 28, 2015 + Document last updated: October 7, 2015 Introduction ============ @@ -256,6 +256,100 @@ configuration of the existing object-level technique of assigning New Features and Improvements - Core ==================================== +.. _change_3216: + +The ``.autoincrement`` directive is no longer implicitly enabled for a composite primary key column +--------------------------------------------------------------------------------------------------- + +SQLAlchemy has always had the convenience feature of enabling the backend database's +"autoincrement" feature for a single-column integer primary key; by "autoincrement" +we mean that the database column will include whatever DDL directives the +database provides in order to indicate an auto-incrementing integer identifier, +such as the SERIAL keyword on Postgresql or AUTO_INCREMENT on MySQL, and additionally +that the dialect will recieve these generated values from the execution +of a :meth:`.Table.insert` construct using techniques appropriate to that +backend. + +What's changed is that this feature no longer turns on automatically for a +*composite* primary key; previously, a table definition such as:: + + Table( + 'some_table', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True) + ) + +Would have "autoincrement" semantics applied to the ``'x'`` column, only +because it's first in the list of primary key columns. In order to +disable this, one would have to turn off ``autoincrement`` on all columns:: + + # old way + Table( + 'some_table', metadata, + Column('x', Integer, primary_key=True, autoincrement=False), + Column('y', Integer, primary_key=True, autoincrement=False) + ) + +With the new behavior, the composite primary key will not have autoincrement +semantics unless a column is marked explcitly with ``autoincrement=True``:: + + # column 'y' will be SERIAL/AUTO_INCREMENT/ auto-generating + Table( + 'some_table', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True, autoincrement=True) + ) + +In order to anticipate some potential backwards-incompatible scenarios, +the :meth:`.Table.insert` construct will perform more thorough checks +for missing primary key values on composite primary key columns that don't +have autoincrement set up; given a table such as:: + + Table( + 'b', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True) + ) + +An INSERT emitted with no values for this table will produce the exception:: + + CompileError: Column 'b.x' is marked as a member of the primary + key for table 'b', but has no Python-side or server-side default + generator indicated, nor does it indicate 'autoincrement=True', + and no explicit value is passed. Primary key columns may not + store NULL. Note that as of SQLAlchemy 1.1, 'autoincrement=True' + must be indicated explicitly for composite (e.g. multicolumn) + primary keys if AUTO_INCREMENT/SERIAL/IDENTITY behavior is + expected for one of the columns in the primary key. CREATE TABLE + statements are impacted by this change as well on most backends. + +For a column that is receiving primary key values from a server-side +default or something less common such as a trigger, the presence of a +value generator can be indicated using :class:`.FetchedValue`:: + + Table( + 'b', metadata, + Column('x', Integer, primary_key=True, server_default=FetchedValue()), + Column('y', Integer, primary_key=True, server_default=FetchedValue()) + ) + +For the very unlikely case where a composite primary key is actually intended +to store NULL in one or more of its columns (only supported on SQLite and MySQL), +specify the column with ``nullable=True``:: + + Table( + 'b', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True, nullable=True) + ) + + + +.. seealso:: + + :ref:`change_mysql_3216` + +:ticket:`3216` .. _change_2528: @@ -787,6 +881,58 @@ emits:: Dialect Improvements and Changes - MySQL ============================================= +.. _change_mysql_3216: + +No more generation of an implicit KEY for composite primary key w/ AUTO_INCREMENT +--------------------------------------------------------------------------------- + +The MySQL dialect had the behavior such that if a composite primary key +on an InnoDB table featured AUTO_INCREMENT on one of its columns which was +not the first column, e.g.:: + + t = Table( + 'some_table', metadata, + Column('x', Integer, primary_key=True, autoincrement=False), + Column('y', Integer, primary_key=True, autoincrement=True), + mysql_engine='InnoDB' + ) + +DDL such as the following would be generated:: + + CREATE TABLE some_table ( + x INTEGER NOT NULL, + y INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (x, y), + KEY idx_autoinc_y (y) + )ENGINE=InnoDB + +Note the above "KEY" with an auto-generated name; this is a change that +found its way into the dialect many years ago in response to the issue that +the AUTO_INCREMENT would otherwise fail on InnoDB without this additional KEY. + +This workaround has been removed and replaced with the much better system +of just stating the AUTO_INCREMENT column *first* within the primary key:: + + CREATE TABLE some_table ( + x INTEGER NOT NULL, + y INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (y, x) + )ENGINE=InnoDB + +Along with the change :ref:`change_3216`, composite primary keys with +or without auto increment are now easier to specify; +:paramref:`.Column.autoincrement` +now defaults to the value ``"auto"`` and the ``autoincrement=False`` +directives are no longer needed:: + + t = Table( + 'some_table', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True, autoincrement=True), + mysql_engine='InnoDB' + ) + + Dialect Improvements and Changes - SQLite ============================================= diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 4b3e5bcd16..2c78de2fc6 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -1916,38 +1916,7 @@ class MySQLCompiler(compiler.SQLCompiler): return None -# ug. "InnoDB needs indexes on foreign keys and referenced keys [...]. -# Starting with MySQL 4.1.2, these indexes are created automatically. -# In older versions, the indexes must be created explicitly or the -# creation of foreign key constraints fails." - class MySQLDDLCompiler(compiler.DDLCompiler): - def create_table_constraints(self, table, **kw): - """Get table constraints.""" - constraint_string = super( - MySQLDDLCompiler, self).create_table_constraints(table, **kw) - - # why self.dialect.name and not 'mysql'? because of drizzle - is_innodb = 'engine' in table.dialect_options[self.dialect.name] and \ - table.dialect_options[self.dialect.name][ - 'engine'].lower() == 'innodb' - - auto_inc_column = table._autoincrement_column - - if is_innodb and \ - auto_inc_column is not None and \ - auto_inc_column is not list(table.primary_key)[0]: - if constraint_string: - constraint_string += ", \n\t" - constraint_string += "KEY %s (%s)" % ( - self.preparer.quote( - "idx_autoinc_%s" % auto_inc_column.name - ), - self.preparer.format_column(auto_inc_column) - ) - - return constraint_string - def get_column_specification(self, column, **kw): """Builds column DDL.""" diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 44a8cf278f..3f62d76d79 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -853,12 +853,20 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): if not column.nullable: colspec += " NOT NULL" - if (column.primary_key and - column.table.dialect_options['sqlite']['autoincrement'] and - len(column.table.primary_key.columns) == 1 and - issubclass(column.type._type_affinity, sqltypes.Integer) and - not column.foreign_keys): - colspec += " PRIMARY KEY AUTOINCREMENT" + if column.primary_key: + if ( + column.autoincrement is True and + len(column.table.primary_key.columns) != 1 + ): + raise exc.CompileError( + "SQLite does not support autoincrement for " + "composite primary keys") + + if (column.table.dialect_options['sqlite']['autoincrement'] and + len(column.table.primary_key.columns) == 1 and + issubclass(column.type._type_affinity, sqltypes.Integer) and + not column.foreign_keys): + colspec += " PRIMARY KEY AUTOINCREMENT" return colspec @@ -1211,7 +1219,7 @@ class SQLiteDialect(default.DefaultDialect): 'type': coltype, 'nullable': nullable, 'default': default, - 'autoincrement': default is None, + 'autoincrement': 'auto' if default is None else False, 'primary_key': primary_key, } diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 691195772c..f1220ce31f 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2381,7 +2381,7 @@ class DDLCompiler(Compiled): text += "CONSTRAINT %s " % formatted_name text += "PRIMARY KEY " text += "(%s)" % ', '.join(self.preparer.quote(c.name) - for c in constraint) + for c in constraint.columns_autoinc_first) text += self.define_constraint_deferrability(constraint) return text diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index e6f16b698a..72b66c036c 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -212,6 +212,7 @@ def _scan_cols( for c in cols: col_key = _getattr_col_key(c) + if col_key in parameters and col_key not in check_columns: _append_param_parameter( @@ -248,6 +249,10 @@ def _scan_cols( elif implicit_return_defaults and \ c in implicit_return_defaults: compiler.returning.append(c) + elif c.primary_key and \ + c is not stmt.table._autoincrement_column and \ + not c.nullable: + _raise_pk_with_no_anticipated_value(c) elif compiler.isupdate: _append_param_update( @@ -285,6 +290,22 @@ def _append_param_parameter( def _append_param_insert_pk_returning(compiler, stmt, c, values, kw): + """Create a primary key expression in the INSERT statement and + possibly a RETURNING clause for it. + + If the column has a Python-side default, we will create a bound + parameter for it and "pre-execute" the Python function. If + the column has a SQL expression default, or is a sequence, + we will add it directly into the INSERT statement and add a + RETURNING element to get the new value. If the column has a + server side default or is marked as the "autoincrement" column, + we will add a RETRUNING element to get at the value. + + If all the above tests fail, that indicates a primary key column with no + noted default generation capabilities that has no parameter passed; + raise an exception. + + """ if c.default is not None: if c.default.is_sequence: if compiler.dialect.supports_sequences and \ @@ -303,9 +324,12 @@ def _append_param_insert_pk_returning(compiler, stmt, c, values, kw): values.append( (c, _create_prefetch_bind_param(compiler, c)) ) - - else: + elif c is stmt.table._autoincrement_column or c.server_default is not None: compiler.returning.append(c) + elif not c.nullable: + # no .default, no .server_default, not autoincrement, we have + # no indication this primary key column will have any value + _raise_pk_with_no_anticipated_value(c) def _create_prefetch_bind_param(compiler, c, process=True, name=None): @@ -342,18 +366,46 @@ def _process_multiparam_default_bind(compiler, c, index, kw): def _append_param_insert_pk(compiler, stmt, c, values, kw): + """Create a bound parameter in the INSERT statement to receive a + 'prefetched' default value. + + The 'prefetched' value indicates that we are to invoke a Python-side + default function or expliclt SQL expression before the INSERT statement + proceeds, so that we have a primary key value available. + + if the column has no noted default generation capabilities, it has + no value passed in either; raise an exception. + + """ if ( - (c.default is not None and - (not c.default.is_sequence or - compiler.dialect.supports_sequences)) or - c is stmt.table._autoincrement_column and - (compiler.dialect.supports_sequences or - compiler.dialect. - preexecute_autoincrement_sequences) + ( + # column has a Python-side default + c.default is not None and + ( + # and it won't be a Sequence + not c.default.is_sequence or + compiler.dialect.supports_sequences + ) + ) + or + ( + # column is the "autoincrement column" + c is stmt.table._autoincrement_column and + ( + # and it's either a "sequence" or a + # pre-executable "autoincrement" sequence + compiler.dialect.supports_sequences or + compiler.dialect.preexecute_autoincrement_sequences + ) + ) ): values.append( (c, _create_prefetch_bind_param(compiler, c)) ) + elif c.default is None and c.server_default is None and not c.nullable: + # no .default, no .server_default, not autoincrement, we have + # no indication this primary key column will have any value + _raise_pk_with_no_anticipated_value(c) def _append_param_insert_hasdefault( @@ -555,3 +607,24 @@ def _get_returning_modifiers(compiler, stmt): return need_pks, implicit_returning, \ implicit_return_defaults, postfetch_lastrowid + + +def _raise_pk_with_no_anticipated_value(c): + msg = ( + "Column '%s.%s' is marked as a member of the " + "primary key for table '%s', " + "but has no Python-side or server-side default generator indicated, " + "nor does it indicate 'autoincrement=True' or 'nullable=True', " + "and no explicit value is passed. " + "Primary key columns typically may not store NULL." + % + (c.table.fullname, c.name, c.table.fullname)) + if len(c.table.primary_key.columns) > 1: + msg += ( + " Note that as of SQLAlchemy 1.1, 'autoincrement=True' must be " + "indicated explicitly for composite (e.g. multicolumn) primary " + "keys if AUTO_INCREMENT/SERIAL/IDENTITY " + "behavior is expected for one of the columns in the primary key. " + "CREATE TABLE statements are impacted by this change as well on " + "most backends.") + raise exc.CompileError(msg) diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 137208584e..210c6338ca 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -572,18 +572,9 @@ class Table(DialectKWArgs, SchemaItem, TableClause): def _init_collections(self): pass - @util.memoized_property + @property def _autoincrement_column(self): - for col in self.primary_key: - if (col.autoincrement and col.type._type_affinity is not None and - issubclass(col.type._type_affinity, - type_api.INTEGERTYPE._type_affinity) and - (not col.foreign_keys or - col.autoincrement == 'ignore_fk') and - isinstance(col.default, (type(None), Sequence)) and - (col.server_default is None or - col.server_default.reflected)): - return col + return self.primary_key._autoincrement_column @property def key(self): @@ -913,17 +904,31 @@ class Column(SchemaItem, ColumnClause): argument is available such as ``server_default``, ``default`` and ``unique``. - :param autoincrement: This flag may be set to ``False`` to - indicate an integer primary key column that should not be - considered to be the "autoincrement" column, that is - the integer primary key column which generates values - implicitly upon INSERT and whose value is usually returned - via the DBAPI cursor.lastrowid attribute. It defaults - to ``True`` to satisfy the common use case of a table - with a single integer primary key column. If the table - has a composite primary key consisting of more than one - integer column, set this flag to True only on the - column that should be considered "autoincrement". + :param autoincrement: Set up "auto increment" semantics for an integer + primary key column. The default value is the string ``"auto"`` + which indicates that a single-column primary key that is of + an INTEGER type should receive auto increment semantics automatically; + all other varieties of primary key columns will not. This + includes that :term:`DDL` such as Postgresql SERIAL or MySQL + AUTO_INCREMENT will be emitted for this column during a table + create, as well as that the column is assumed to generate new + integer primary key values when an INSERT statement invokes which + will be retrieved by the dialect. + + The flag may be set to ``True`` to indicate that a column which + is part of a composite (e.g. multi-column) primary key should + have autoincrement semantics, though note that only one column + within a primary key may have this setting. It can also be + set to ``False`` on a single-column primary key that has a + datatype of INTEGER in order to disable auto increment semantics + for that column. + + .. versionchanged:: 1.1 The autoincrement flag now defaults to + ``"auto"`` which indicates autoincrement semantics by default + for single-column integer primary keys only; for composite + (multi-column) primary keys, autoincrement is never implicitly + enabled; as always, ``autoincrement=True`` will allow for + at most one of those columns to be an "autoincrement" column. The setting *only* has an effect for columns which are: @@ -940,8 +945,8 @@ class Column(SchemaItem, ColumnClause): primary_key=True, autoincrement='ignore_fk') It is typically not desirable to have "autoincrement" enabled - on such a column as its value intends to mirror that of a - primary key column elsewhere. + on a column that refers to another via foreign key, as such a column + is required to refer to a value that originates from elsewhere. * have no server side or client side defaults (with the exception of Postgresql SERIAL). @@ -969,12 +974,6 @@ class Column(SchemaItem, ColumnClause): to generate primary key identifiers (i.e. Firebird, Postgresql, Oracle). - .. versionchanged:: 0.7.4 - ``autoincrement`` accepts a special value ``'ignore_fk'`` - to indicate that autoincrementing status regardless of foreign - key references. This applies to certain composite foreign key - setups, such as the one demonstrated in the ORM documentation - at :ref:`post_update`. :param default: A scalar, Python callable, or :class:`.ColumnElement` expression representing the @@ -1128,7 +1127,7 @@ class Column(SchemaItem, ColumnClause): self.system = kwargs.pop('system', False) self.doc = kwargs.pop('doc', None) self.onupdate = kwargs.pop('onupdate', None) - self.autoincrement = kwargs.pop('autoincrement', True) + self.autoincrement = kwargs.pop('autoincrement', "auto") self.constraints = set() self.foreign_keys = set() @@ -1263,12 +1262,12 @@ class Column(SchemaItem, ColumnClause): if self.primary_key: table.primary_key._replace(self) - Table._autoincrement_column._reset(table) elif self.key in table.primary_key: raise exc.ArgumentError( "Trying to redefine primary-key column '%s' as a " "non-primary-key column on table '%s'" % ( self.key, table.fullname)) + self.table = table if self.index: @@ -3025,11 +3024,96 @@ class PrimaryKeyConstraint(ColumnCollectionConstraint): self.columns.extend(columns) + PrimaryKeyConstraint._autoincrement_column._reset(self) self._set_parent_with_dispatch(self.table) def _replace(self, col): + PrimaryKeyConstraint._autoincrement_column._reset(self) self.columns.replace(col) + @property + def columns_autoinc_first(self): + autoinc = self._autoincrement_column + + if autoinc is not None: + return [autoinc] + [c for c in self.columns if c is not autoinc] + else: + return list(self.columns) + + @util.memoized_property + def _autoincrement_column(self): + + def _validate_autoinc(col, raise_): + if col.type._type_affinity is None or not issubclass( + col.type._type_affinity, + type_api.INTEGERTYPE._type_affinity): + if raise_: + raise exc.ArgumentError( + "Column type %s on column '%s' is not " + "compatible with autoincrement=True" % ( + col.type, + col + )) + else: + return False + elif not isinstance(col.default, (type(None), Sequence)): + if raise_: + raise exc.ArgumentError( + "Column default %s on column %s.%s is not " + "compatible with autoincrement=True" % ( + col.default, + col.table.fullname, col.name + ) + ) + else: + return False + elif ( + col.server_default is not None and + not col.server_default.reflected): + if raise_: + raise exc.ArgumentError( + "Column server default %s on column %s.%s is not " + "compatible with autoincrement=True" % ( + col.server_default, + col.table.fullname, col.name + ) + ) + else: + return False + elif ( + col.foreign_keys and col.autoincrement + not in (True, 'ignore_fk')): + return False + return True + + if len(self.columns) == 1: + col = list(self.columns)[0] + + if col.autoincrement is True: + _validate_autoinc(col, True) + return col + elif ( + col.autoincrement in ('auto', 'ignore_fk') and + _validate_autoinc(col, False) + ): + return col + + else: + autoinc = None + for col in self.columns: + if col.autoincrement is True: + _validate_autoinc(col, True) + if autoinc is not None: + raise exc.ArgumentError( + "Only one Column may be marked " + "autoincrement=True, found both %s and %s." % + (col.name, autoinc.name) + ) + else: + autoinc = col + + return autoinc + class UniqueConstraint(ColumnCollectionConstraint): """A table-level UNIQUE constraint. diff --git a/lib/sqlalchemy/testing/schema.py b/lib/sqlalchemy/testing/schema.py index 93b52ad58b..2575786689 100644 --- a/lib/sqlalchemy/testing/schema.py +++ b/lib/sqlalchemy/testing/schema.py @@ -71,9 +71,12 @@ def Column(*args, **kw): args = [arg for arg in args if not isinstance(arg, schema.ForeignKey)] col = schema.Column(*args, **kw) - if 'test_needs_autoincrement' in test_opts and \ + if test_opts.get('test_needs_autoincrement', False) and \ kw.get('primary_key', False): + if col.default is None and col.server_default is None: + col.autoincrement = True + # allow any test suite to pick up on this col.info['test_needs_autoincrement'] = True diff --git a/test/dialect/mysql/test_compiler.py b/test/dialect/mysql/test_compiler.py index 304c310124..60af82bab7 100644 --- a/test/dialect/mysql/test_compiler.py +++ b/test/dialect/mysql/test_compiler.py @@ -511,9 +511,8 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL): self.assert_compile(schema.CreateTable(t1), 'CREATE TABLE sometable (assigned_id ' 'INTEGER NOT NULL, id INTEGER NOT NULL ' - 'AUTO_INCREMENT, PRIMARY KEY (assigned_id, ' - 'id), KEY idx_autoinc_id (id))ENGINE=Inn' - 'oDB') + 'AUTO_INCREMENT, PRIMARY KEY (id, assigned_id)' + ')ENGINE=InnoDB') t1 = Table('sometable', MetaData(), Column('assigned_id', Integer(), primary_key=True, @@ -537,8 +536,7 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL): 'CREATE TABLE sometable (' 'id INTEGER NOT NULL, ' '`order` INTEGER NOT NULL AUTO_INCREMENT, ' - 'PRIMARY KEY (id, `order`), ' - 'KEY idx_autoinc_order (`order`)' + 'PRIMARY KEY (`order`, id)' ')ENGINE=InnoDB') def test_create_table_with_partition(self): diff --git a/test/dialect/mysql/test_reflection.py b/test/dialect/mysql/test_reflection.py index 39b39e0066..266fc335eb 100644 --- a/test/dialect/mysql/test_reflection.py +++ b/test/dialect/mysql/test_reflection.py @@ -211,49 +211,55 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults): meta = MetaData(testing.db) try: Table('ai_1', meta, - Column('int_y', Integer, primary_key=True), + Column('int_y', Integer, primary_key=True, + autoincrement=True), Column('int_n', Integer, DefaultClause('0'), primary_key=True), - mysql_engine='MyISAM') + mysql_engine='MyISAM') Table('ai_2', meta, - Column('int_y', Integer, primary_key=True), + Column('int_y', Integer, primary_key=True, + autoincrement=True), Column('int_n', Integer, DefaultClause('0'), primary_key=True), - mysql_engine='MyISAM') + mysql_engine='MyISAM') Table('ai_3', meta, Column('int_n', Integer, DefaultClause('0'), primary_key=True, autoincrement=False), - Column('int_y', Integer, primary_key=True), - mysql_engine='MyISAM') + Column('int_y', Integer, primary_key=True, + autoincrement=True), + mysql_engine='MyISAM') Table('ai_4', meta, Column('int_n', Integer, DefaultClause('0'), primary_key=True, autoincrement=False), Column('int_n2', Integer, DefaultClause('0'), primary_key=True, autoincrement=False), - mysql_engine='MyISAM') + mysql_engine='MyISAM') Table('ai_5', meta, - Column('int_y', Integer, primary_key=True), + Column('int_y', Integer, primary_key=True, + autoincrement=True), Column('int_n', Integer, DefaultClause('0'), primary_key=True, autoincrement=False), - mysql_engine='MyISAM') + mysql_engine='MyISAM') Table('ai_6', meta, Column('o1', String(1), DefaultClause('x'), primary_key=True), - Column('int_y', Integer, primary_key=True), - mysql_engine='MyISAM') + Column('int_y', Integer, primary_key=True, + autoincrement=True), + mysql_engine='MyISAM') Table('ai_7', meta, Column('o1', String(1), DefaultClause('x'), primary_key=True), Column('o2', String(1), DefaultClause('x'), primary_key=True), - Column('int_y', Integer, primary_key=True), - mysql_engine='MyISAM') + Column('int_y', Integer, primary_key=True, + autoincrement=True), + mysql_engine='MyISAM') Table('ai_8', meta, Column('o1', String(1), DefaultClause('x'), primary_key=True), Column('o2', String(1), DefaultClause('x'), primary_key=True), - mysql_engine='MyISAM') + mysql_engine='MyISAM') meta.create_all() table_names = ['ai_1', 'ai_2', 'ai_3', 'ai_4', diff --git a/test/dialect/postgresql/test_query.py b/test/dialect/postgresql/test_query.py index 4a33644e05..6c10d78cc3 100644 --- a/test/dialect/postgresql/test_query.py +++ b/test/dialect/postgresql/test_query.py @@ -72,9 +72,12 @@ class InsertTest(fixtures.TestBase, AssertsExecutionResults): engines.testing_engine(options={'implicit_returning': False}), engines.testing_engine(options={'implicit_returning': True}) ]: - assert_raises_message(exc.DBAPIError, - 'violates not-null constraint', - eng.execute, t2.insert()) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + eng.execute, t2.insert() + ) + def test_sequence_insert(self): table = Table( @@ -494,26 +497,26 @@ class InsertTest(fixtures.TestBase, AssertsExecutionResults): engines.testing_engine(options={'implicit_returning': False}) metadata.bind = self.engine table.insert().execute({'id': 30, 'data': 'd1'}) - if self.engine.driver == 'pg8000': - exception_cls = exc.ProgrammingError - elif self.engine.driver == 'pypostgresql': - exception_cls = Exception - else: - exception_cls = exc.IntegrityError - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}) - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}, - {'data': 'd3'}) - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}) - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}, - {'data': 'd3'}) + + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}, + {'data': 'd3'}) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}, + {'data': 'd3'}) + table.insert().execute({'id': 31, 'data': 'd2'}, {'id': 32, 'data': 'd3'}) table.insert(inline=True).execute({'id': 33, 'data': 'd4'}) @@ -530,13 +533,15 @@ class InsertTest(fixtures.TestBase, AssertsExecutionResults): m2 = MetaData(self.engine) table = Table(table.name, m2, autoload=True) table.insert().execute({'id': 30, 'data': 'd1'}) - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}) - assert_raises_message(exception_cls, - 'violates not-null constraint', - table.insert().execute, {'data': 'd2'}, - {'data': 'd3'}) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}) + assert_raises_message( + exc.CompileError, + ".*has no Python-side or server-side default.*", + table.insert().execute, {'data': 'd2'}, + {'data': 'd3'}) table.insert().execute({'id': 31, 'data': 'd2'}, {'id': 32, 'data': 'd3'}) table.insert(inline=True).execute({'id': 33, 'data': 'd4'}) diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index 68fa72b10a..33903ff898 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -20,7 +20,7 @@ from sqlalchemy.engine.url import make_url from sqlalchemy.testing import fixtures, AssertsCompiledSQL, \ AssertsExecutionResults, engines from sqlalchemy import testing -from sqlalchemy.schema import CreateTable +from sqlalchemy.schema import CreateTable, FetchedValue from sqlalchemy.engine.reflection import Inspector from sqlalchemy.testing import mock @@ -751,6 +751,17 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL): "WHERE data > 'a' AND data < 'b''s'", dialect=sqlite.dialect()) + def test_no_autoinc_on_composite_pk(self): + m = MetaData() + t = Table( + 't', m, + Column('x', Integer, primary_key=True, autoincrement=True), + Column('y', Integer, primary_key=True)) + assert_raises_message( + exc.CompileError, + "SQLite does not support autoincrement for composite", + CreateTable(t).compile, dialect=sqlite.dialect() + ) class InsertTest(fixtures.TestBase, AssertsExecutionResults): @@ -781,22 +792,45 @@ class InsertTest(fixtures.TestBase, AssertsExecutionResults): @testing.exclude('sqlite', '<', (3, 3, 8), 'no database support') def test_empty_insert_pk2(self): + # now raises CompileError due to [ticket:3216] assert_raises( - exc.DBAPIError, self._test_empty_insert, + exc.CompileError, self._test_empty_insert, Table( 'b', MetaData(testing.db), Column('x', Integer, primary_key=True), Column('y', Integer, primary_key=True))) @testing.exclude('sqlite', '<', (3, 3, 8), 'no database support') - def test_empty_insert_pk3(self): + def test_empty_insert_pk2_fv(self): assert_raises( exc.DBAPIError, self._test_empty_insert, + Table( + 'b', MetaData(testing.db), + Column('x', Integer, primary_key=True, + server_default=FetchedValue()), + Column('y', Integer, primary_key=True, + server_default=FetchedValue()))) + + @testing.exclude('sqlite', '<', (3, 3, 8), 'no database support') + def test_empty_insert_pk3(self): + # now raises CompileError due to [ticket:3216] + assert_raises( + exc.CompileError, self._test_empty_insert, Table( 'c', MetaData(testing.db), Column('x', Integer, primary_key=True), Column('y', Integer, DefaultClause('123'), primary_key=True))) + @testing.exclude('sqlite', '<', (3, 3, 8), 'no database support') + def test_empty_insert_pk3_fv(self): + assert_raises( + exc.DBAPIError, self._test_empty_insert, + Table( + 'c', MetaData(testing.db), + Column('x', Integer, primary_key=True, + server_default=FetchedValue()), + Column('y', Integer, DefaultClause('123'), primary_key=True))) + @testing.exclude('sqlite', '<', (3, 3, 8), 'no database support') def test_empty_insert_pk4(self): self._test_empty_insert( diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 83650609d5..b7bf87d63d 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -311,22 +311,22 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): Don't mark this test as unsupported for any backend ! - (technically it fails with MySQL InnoDB since "id" comes before "id2") - """ meta = self.metadata - Table('test', meta, + Table( + 'test', meta, Column('id', sa.Integer, primary_key=True), Column('data', sa.String(50)), - mysql_engine='MyISAM' + mysql_engine='InnoDB' ) - Table('test2', meta, - Column('id', sa.Integer, sa.ForeignKey('test.id'), - primary_key=True), + Table( + 'test2', meta, + Column( + 'id', sa.Integer, sa.ForeignKey('test.id'), primary_key=True), Column('id2', sa.Integer, primary_key=True), Column('data', sa.String(50)), - mysql_engine='MyISAM' + mysql_engine='InnoDB' ) meta.create_all() m2 = MetaData(testing.db) @@ -334,7 +334,8 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): assert t1a._autoincrement_column is t1a.c.id t2a = Table('test2', m2, autoload=True) - assert t2a._autoincrement_column is t2a.c.id2 + assert t2a._autoincrement_column is None + @skip('sqlite') @testing.provide_metadata diff --git a/test/ext/declarative/test_basic.py b/test/ext/declarative/test_basic.py index ab0de801ce..5165d9cc93 100644 --- a/test/ext/declarative/test_basic.py +++ b/test/ext/declarative/test_basic.py @@ -1570,8 +1570,7 @@ class DeclarativeTest(DeclarativeTestBase): meta = MetaData(testing.db) t1 = Table( 't1', meta, - Column('id', String(50), - primary_key=True, test_needs_autoincrement=True), + Column('id', String(50), primary_key=True), Column('data', String(50))) meta.create_all() try: diff --git a/test/orm/test_composites.py b/test/orm/test_composites.py index 8b777dcdf7..48027ec2d0 100644 --- a/test/orm/test_composites.py +++ b/test/orm/test_composites.py @@ -313,8 +313,7 @@ class PrimaryKeyTest(fixtures.MappedTest): @classmethod def define_tables(cls, metadata): Table('graphs', metadata, - Column('id', Integer, primary_key=True, - test_needs_autoincrement=True), + Column('id', Integer, primary_key=True), Column('version_id', Integer, primary_key=True, nullable=True), Column('name', String(30))) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index 4ae0b010a3..833613ec62 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -579,8 +579,7 @@ class GetTest(QueryTest): table = Table( 'unicode_data', metadata, Column( - 'id', Unicode(40), primary_key=True, - test_needs_autoincrement=True), + 'id', Unicode(40), primary_key=True), Column('data', Unicode(40))) metadata.create_all() ustring = util.b('petit voix m\xe2\x80\x99a').decode('utf-8') diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index 9e4b38a90f..0611873303 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -931,14 +931,12 @@ class SynonymsAsFKsTest(fixtures.MappedTest): @classmethod def define_tables(cls, metadata): Table("tableA", metadata, - Column("id", Integer, primary_key=True, - test_needs_autoincrement=True), + Column("id", Integer, primary_key=True), Column("foo", Integer,), test_needs_fk=True) Table("tableB", metadata, - Column("id", Integer, primary_key=True, - test_needs_autoincrement=True), + Column("id", Integer, primary_key=True), Column("_a_id", Integer, key='a_id', primary_key=True), test_needs_fk=True) @@ -1093,7 +1091,7 @@ class FKsAsPksTest(fixtures.MappedTest): 'tablec', tableA.metadata, Column('id', Integer, primary_key=True), Column('a_id', Integer, ForeignKey('tableA.id'), - primary_key=True, autoincrement=False, nullable=True)) + primary_key=True, nullable=True)) tableC.create() class C(fixtures.BasicEntity): @@ -2703,8 +2701,7 @@ class ExplicitLocalRemoteTest(fixtures.MappedTest): @classmethod def define_tables(cls, metadata): Table('t1', metadata, - Column('id', String(50), primary_key=True, - test_needs_autoincrement=True), + Column('id', String(50), primary_key=True), Column('data', String(50))) Table('t2', metadata, Column('id', Integer, primary_key=True, diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py index 5a47903f02..2f67943f1e 100644 --- a/test/orm/test_unitofwork.py +++ b/test/orm/test_unitofwork.py @@ -260,7 +260,7 @@ class PKTest(fixtures.MappedTest): def define_tables(cls, metadata): Table('multipk1', metadata, Column('multi_id', Integer, primary_key=True, - test_needs_autoincrement=True), + test_needs_autoincrement=not testing.against('sqlite')), Column('multi_rev', Integer, primary_key=True), Column('name', String(50), nullable=False), Column('value', String(100))) diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py index c957b2f8a7..f6f2ec740b 100644 --- a/test/sql/test_compiler.py +++ b/test/sql/test_compiler.py @@ -2916,6 +2916,21 @@ class DDLTest(fixtures.TestBase, AssertsCompiledSQL): "CREATE TABLE t (x INTEGER, z INTEGER)" ) + def test_composite_pk_constraint_autoinc_first(self): + m = MetaData() + t = Table( + 't', m, + Column('a', Integer, primary_key=True), + Column('b', Integer, primary_key=True, autoincrement=True) + ) + self.assert_compile( + schema.CreateTable(t), + "CREATE TABLE t (" + "a INTEGER NOT NULL, " + "b INTEGER NOT NULL, " + "PRIMARY KEY (b, a))" + ) + class InlineDefaultTest(fixtures.TestBase, AssertsCompiledSQL): __dialect__ = 'default' diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 673085cf77..8a0e27ebdf 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -732,7 +732,6 @@ class AutoIncrementTest(fixtures.TablesTest): ) assert x._autoincrement_column is None - @testing.fails_on('sqlite', 'FIXME: unknown') def test_non_autoincrement(self): # sqlite INT primary keys can be non-unique! (only for ints) nonai = Table( @@ -746,8 +745,9 @@ class AutoIncrementTest(fixtures.TablesTest): # mysql in legacy mode fails on second row nonai.insert().execute(data='row 1') nonai.insert().execute(data='row 2') - assert_raises( - sa.exc.DBAPIError, + assert_raises_message( + sa.exc.CompileError, + ".*has no Python-side or server-side default.*", go ) diff --git a/test/sql/test_insert.py b/test/sql/test_insert.py index f66f0b391c..bdaf4f38c6 100644 --- a/test/sql/test_insert.py +++ b/test/sql/test_insert.py @@ -390,6 +390,106 @@ class InsertTest(_InsertTestBase, fixtures.TablesTest, AssertsCompiledSQL): checkparams={"name_1": "foo"} ) + def test_anticipate_no_pk_composite_pk(self): + t = Table( + 't', MetaData(), Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True) + ) + assert_raises_message( + exc.CompileError, + "Column 't.y' is marked as a member.*" + "Note that as of SQLAlchemy 1.1,", + t.insert().compile, column_keys=['x'] + + ) + + def test_anticipate_no_pk_composite_pk_implicit_returning(self): + t = Table( + 't', MetaData(), Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True) + ) + d = postgresql.dialect() + d.implicit_returning = True + assert_raises_message( + exc.CompileError, + "Column 't.y' is marked as a member.*" + "Note that as of SQLAlchemy 1.1,", + t.insert().compile, dialect=d, column_keys=['x'] + + ) + + def test_anticipate_no_pk_composite_pk_prefetch(self): + t = Table( + 't', MetaData(), Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True) + ) + d = postgresql.dialect() + d.implicit_returning = False + assert_raises_message( + exc.CompileError, + "Column 't.y' is marked as a member.*" + "Note that as of SQLAlchemy 1.1,", + t.insert().compile, dialect=d, column_keys=['x'] + + ) + + def test_anticipate_nullable_composite_pk(self): + t = Table( + 't', MetaData(), Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True, nullable=True) + ) + self.assert_compile( + t.insert(), + "INSERT INTO t (x) VALUES (:x)", + params={'x': 5}, + ) + + def test_anticipate_no_pk_non_composite_pk(self): + t = Table( + 't', MetaData(), + Column('x', Integer, primary_key=True, autoincrement=False), + Column('q', Integer) + ) + assert_raises_message( + exc.CompileError, + "Column 't.x' is marked as a member.*" + "may not store NULL.$", + t.insert().compile, column_keys=['q'] + + ) + + def test_anticipate_no_pk_non_composite_pk_implicit_returning(self): + t = Table( + 't', MetaData(), + Column('x', Integer, primary_key=True, autoincrement=False), + Column('q', Integer) + ) + d = postgresql.dialect() + d.implicit_returning = True + assert_raises_message( + exc.CompileError, + "Column 't.x' is marked as a member.*" + "may not store NULL.$", + t.insert().compile, dialect=d, column_keys=['q'] + + ) + + def test_anticipate_no_pk_non_composite_pk_prefetch(self): + t = Table( + 't', MetaData(), + Column('x', Integer, primary_key=True, autoincrement=False), + Column('q', Integer) + ) + d = postgresql.dialect() + d.implicit_returning = False + assert_raises_message( + exc.CompileError, + "Column 't.x' is marked as a member.*" + "may not store NULL.$", + t.insert().compile, dialect=d, column_keys=['q'] + + ) + class InsertImplicitReturningTest( _InsertTestBase, fixtures.TablesTest, AssertsCompiledSQL): diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 2e51b9a91a..24f4164391 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -1361,6 +1361,128 @@ class TableTest(fixtures.TestBase, AssertsCompiledSQL): assert not t1.c.x.nullable +class PKAutoIncrementTest(fixtures.TestBase): + def test_multi_integer_no_autoinc(self): + pk = PrimaryKeyConstraint( + Column('a', Integer), + Column('b', Integer) + ) + t = Table('t', MetaData()) + t.append_constraint(pk) + + is_(pk._autoincrement_column, None) + + def test_multi_integer_multi_autoinc(self): + pk = PrimaryKeyConstraint( + Column('a', Integer, autoincrement=True), + Column('b', Integer, autoincrement=True) + ) + t = Table('t', MetaData()) + t.append_constraint(pk) + + assert_raises_message( + exc.ArgumentError, + "Only one Column may be marked", + lambda: pk._autoincrement_column + ) + + def test_single_integer_no_autoinc(self): + pk = PrimaryKeyConstraint( + Column('a', Integer), + ) + t = Table('t', MetaData()) + t.append_constraint(pk) + + is_(pk._autoincrement_column, pk.columns['a']) + + def test_single_string_no_autoinc(self): + pk = PrimaryKeyConstraint( + Column('a', String), + ) + t = Table('t', MetaData()) + t.append_constraint(pk) + + is_(pk._autoincrement_column, None) + + def test_single_string_illegal_autoinc(self): + t = Table('t', MetaData(), Column('a', String, autoincrement=True)) + pk = PrimaryKeyConstraint( + t.c.a + ) + t.append_constraint(pk) + + assert_raises_message( + exc.ArgumentError, + "Column type VARCHAR on column 't.a'", + lambda: pk._autoincrement_column + ) + + def test_single_integer_illegal_default(self): + t = Table( + 't', MetaData(), + Column('a', Integer, autoincrement=True, default=lambda: 1)) + pk = PrimaryKeyConstraint( + t.c.a + ) + t.append_constraint(pk) + + assert_raises_message( + exc.ArgumentError, + "Column default.*on column t.a is not compatible", + lambda: pk._autoincrement_column + ) + + def test_single_integer_illegal_server_default(self): + t = Table( + 't', MetaData(), + Column('a', Integer, + autoincrement=True, server_default=func.magic())) + pk = PrimaryKeyConstraint( + t.c.a + ) + t.append_constraint(pk) + + assert_raises_message( + exc.ArgumentError, + "Column server default.*on column t.a is not compatible", + lambda: pk._autoincrement_column + ) + + def test_implicit_autoinc_but_fks(self): + m = MetaData() + Table('t1', m, Column('id', Integer, primary_key=True)) + t2 = Table( + 't2', MetaData(), + Column('a', Integer, ForeignKey('t1.id'))) + pk = PrimaryKeyConstraint( + t2.c.a + ) + t2.append_constraint(pk) + is_(pk._autoincrement_column, None) + + def test_explicit_autoinc_but_fks(self): + m = MetaData() + Table('t1', m, Column('id', Integer, primary_key=True)) + t2 = Table( + 't2', MetaData(), + Column('a', Integer, ForeignKey('t1.id'), autoincrement=True)) + pk = PrimaryKeyConstraint( + t2.c.a + ) + t2.append_constraint(pk) + is_(pk._autoincrement_column, t2.c.a) + + t3 = Table( + 't3', MetaData(), + Column('a', Integer, + ForeignKey('t1.id'), autoincrement='ignore_fk')) + pk = PrimaryKeyConstraint( + t3.c.a + ) + t3.append_constraint(pk) + is_(pk._autoincrement_column, t3.c.a) + + class SchemaTypeTest(fixtures.TestBase): class MyType(sqltypes.SchemaType, sqltypes.TypeEngine): -- 2.47.2