]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The system by which a :class:`.Column` considers itself to be an
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 7 Oct 2015 14:02:45 +0000 (10:02 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 7 Oct 2015 14:02:45 +0000 (10:02 -0400)
"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.

22 files changed:
doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/crud.py
lib/sqlalchemy/sql/schema.py
lib/sqlalchemy/testing/schema.py
test/dialect/mysql/test_compiler.py
test/dialect/mysql/test_reflection.py
test/dialect/postgresql/test_query.py
test/dialect/test_sqlite.py
test/engine/test_reflection.py
test/ext/declarative/test_basic.py
test/orm/test_composites.py
test/orm/test_query.py
test/orm/test_relationships.py
test/orm/test_unitofwork.py
test/sql/test_compiler.py
test/sql/test_defaults.py
test/sql/test_insert.py
test/sql/test_metadata.py

index e376fe191ab7f1b9d44d5630f531c1fbddd0335e..dcd43f28d67c7730336aa29442565ebe7d699463 100644 (file)
 .. 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
index 78f77e694677250ca6c42b4817a16165c7d7f6a0..c57c917615b6436c3bb08e91b63fbc678079f9f1 100644 (file)
@@ -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
 =============================================
index 4b3e5bcd16594184015d460d9efa4d244d20c9b3..2c78de2fc685f4245b55fe2aa5e24bc923eba33a 100644 (file)
@@ -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."""
 
index 44a8cf278f5d0a9a9f9ba57549bf5c209057d06a..3f62d76d79c225e2ea62a769a9aad00d52a9e327 100644 (file)
@@ -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,
         }
 
index 691195772c88ffe18664dceb56ae033db8344e81..f1220ce31fc72471925abaa4575c73c6f17af73d 100644 (file)
@@ -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
 
index e6f16b698a8f437a7e3433f3214b002d990cae06..72b66c036c06a4116e2d24dd4c8b5a248287e4dc 100644 (file)
@@ -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)
index 137208584e71ba82a1a94e4c369c31fff777429e..210c6338caaf1e0fe05e39d0d8314c0df6cc7cf1 100644 (file)
@@ -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.
index 93b52ad58b0b1796d7ef075f6e6c9d37ca64bd51..257578668938c95b24296f859554b4db42442725 100644 (file)
@@ -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
 
index 304c310124cd2ce600d0ed566105b52442f48e88..60af82bab76b5d2e9ef177e4a9350f5505e55d4b 100644 (file)
@@ -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):
index 39b39e0066870e8826be14e1745a806ec232ee9d..266fc335eb8f9b1046dbc6e7260ebe5fa42c0a41 100644 (file)
@@ -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',
index 4a33644e05a32c047a2bee29c4db87d80ecea50f..6c10d78cc3b7e1f8e7bc2fa5085364a03def33af 100644 (file)
@@ -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'})
index 68fa72b10a45f53e6cdeb200a7063eef5414e508..33903ff89826f225f35de9d09cc9d6da05a77c4e 100644 (file)
@@ -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(
index 83650609d566863ebe7f1548a425ee0daeb00cc2..b7bf87d63dad4665e07d694d54b030ffd3c9756b 100644 (file)
@@ -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
index ab0de801ce2340e54386738ac07973624b0d7bbd..5165d9cc931ed3e151b1c6ab51a156bc8f1fdbea 100644 (file)
@@ -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:
index 8b777dcdf7e641726e1c2e9ef1a7001c78ce2332..48027ec2d0fea12149c771d8dfcb16294fc97ea4 100644 (file)
@@ -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)))
index 4ae0b010a36657a56cca1c7d20db3b2545ae94aa..833613ec627e9d599b9f8730f33e61d849f4399e 100644 (file)
@@ -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')
index 9e4b38a90fec7ace9404249a5dfc8526f18aa025..061187330321618a6eb0a3202728236ded7c263f 100644 (file)
@@ -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,
index 5a47903f02baf883548ca275867d4de99fe5932f..2f67943f1eef4db3498ff300b9cc8295ea8919ac 100644 (file)
@@ -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)))
index c957b2f8a7fbee8f37e0fa20b7c604bb8cd5aad0..f6f2ec740b589bd9451aa5b77bf94abf55abb443 100644 (file)
@@ -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'
index 673085cf77b2934426c81aad6d8b42bf94ab1fa7..8a0e27ebdf8f4c5bc34344aeaa41a3a3058c66f1 100644 (file)
@@ -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
         )
 
index f66f0b391cd8fbdb9cb70e0a20a91e4200e117d4..bdaf4f38c6378e7aa01408202f5faa1d7717780a 100644 (file)
@@ -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):
index 2e51b9a91a960d3f7e41ffaf2ca4d20bba8cf84f..24f41643915092ce20e3036364278e01e6ecf7ae 100644 (file)
@@ -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):