.. 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
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
============
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:
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
=============================================
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."""
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
'type': coltype,
'nullable': nullable,
'default': default,
- 'autoincrement': default is None,
+ 'autoincrement': 'auto' if default is None else False,
'primary_key': primary_key,
}
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
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(
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(
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 \
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):
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(
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)
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):
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:
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).
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
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()
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:
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.
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
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,
'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):
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',
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(
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'})
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'})
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
"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):
@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(
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)
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
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:
@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)))
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')
@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)
'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):
@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,
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)))
"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'
)
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(
# 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
)
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):
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):