diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-01-30 13:38:51 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-01-30 13:38:51 -0500 |
commit | 383bb3f708168aedb1832050a84ff054f8211386 (patch) | |
tree | 3bc8837150ab66a2be63c780ac5912f5cb89e5bc | |
parent | 3712e35c329cc3b5106f026be90e04f65412586d (diff) | |
download | sqlalchemy-383bb3f708168aedb1832050a84ff054f8211386.tar.gz |
- The :class:`.CheckConstraint` construct now supports naming
conventions that include the token ``%(column_0_name)s``; the
constraint expression is scanned for columns. Additionally,
naming conventions for check constraints that don't include the
``%(constraint_name)s`` token will now work for :class:`.SchemaType`-
generated constraints, such as those of :class:`.Boolean` and
:class:`.Enum`; this stopped working in 0.9.7 due to :ticket:`3067`.
fixes #3299
-rw-r--r-- | doc/build/changelog/changelog_10.rst | 19 | ||||
-rw-r--r-- | doc/build/changelog/migration_10.rst | 40 | ||||
-rw-r--r-- | doc/build/core/constraints.rst | 136 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/naming.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/schema.py | 61 | ||||
-rw-r--r-- | test/sql/test_constraints.py | 2 | ||||
-rw-r--r-- | test/sql/test_metadata.py | 21 |
7 files changed, 258 insertions, 31 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 2c3e26f2e..89ef86844 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -23,6 +23,25 @@ on compatibility concerns, see :doc:`/changelog/migration_10`. .. change:: + :tags: bug, schema + :tickets: 3299, 3067 + + The :class:`.CheckConstraint` construct now supports naming + conventions that include the token ``%(column_0_name)s``; the + constraint expression is scanned for columns. Additionally, + naming conventions for check constraints that don't include the + ``%(constraint_name)s`` token will now work for :class:`.SchemaType`- + generated constraints, such as those of :class:`.Boolean` and + :class:`.Enum`; this stopped working in 0.9.7 due to :ticket:`3067`. + + .. seealso:: + + :ref:`naming_check_constraints` + + :ref:`naming_schematypes` + + + .. change:: :tags: feature, postgresql, pypy :tickets: 3052 :pullreq: bitbucket:34 diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst index 23ee6f466..3ba0743f7 100644 --- a/doc/build/changelog/migration_10.rst +++ b/doc/build/changelog/migration_10.rst @@ -8,7 +8,7 @@ What's New in SQLAlchemy 1.0? undergoing maintenance releases as of May, 2014, and SQLAlchemy version 1.0, as of yet unreleased. - Document last updated: January 4, 2015 + Document last updated: January 30, 2015 Introduction ============ @@ -598,9 +598,45 @@ required during a CREATE/DROP scenario. :ref:`use_alter` - full description of the new behavior. - :ticket:`3282` + +CHECK Constraints now support the ``%(column_0_name)s`` token in naming conventions +----------------------------------------------------------------------------------- + +The ``%(column_0_name)s`` will derive from the first column found in the +expression of a :class:`.CheckConstraint`:: + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"} + ) + + foo = Table('foo', metadata, + Column('value', Integer), + ) + + CheckConstraint(foo.c.value > 5) + +Will render:: + + CREATE TABLE foo ( + flag BOOL, + CONSTRAINT ck_foo_flag CHECK (flag IN (0, 1)) + ) + +The combination of naming conventions with the constraint produced by a +:class:`.SchemaType` such as :class:`.Boolean` or :class:`.Enum` will also +now make use of all CHECK constraint conventions. + +.. seealso:: + + :ref:`naming_check_constraints` + + :ref:`naming_schematypes` + +:ticket:`3299` + + .. _change_2051: .. _feature_insert_from_select_defaults: diff --git a/doc/build/core/constraints.rst b/doc/build/core/constraints.rst index 1f855c724..dfe9e9cdd 100644 --- a/doc/build/core/constraints.rst +++ b/doc/build/core/constraints.rst @@ -565,6 +565,142 @@ name as follows:: .. versionadded:: 0.9.2 Added the :paramref:`.MetaData.naming_convention` argument. +.. _naming_check_constraints: + +Naming CHECK Constraints +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`.CheckConstraint` object is configured against an arbitrary +SQL expression, which can have any number of columns present, and additionally +is often configured using a raw SQL string. Therefore a common convention +to use with :class:`.CheckConstraint` is one where we expect the object +to have a name already, and we then enhance it with other convention elements. +A typical convention is ``"ck_%(table_name)s_%(constraint_name)s"``:: + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(constraint_name)s"} + ) + + Table('foo', metadata, + Column('value', Integer), + CheckConstraint('value > 5', name='value_gt_5') + ) + +The above table will produce the name ``ck_foo_value_gt_5``:: + + CREATE TABLE foo ( + value INTEGER, + CONSTRAINT ck_foo_value_gt_5 CHECK (value > 5) + ) + +:class:`.CheckConstraint` also supports the ``%(columns_0_name)s`` +token; we can make use of this by ensuring we use a :class:`.Column` or +:func:`.sql.expression.column` element within the constraint's expression, +either by declaring the constraint separate from the table:: + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"} + ) + + foo = Table('foo', metadata, + Column('value', Integer) + ) + + CheckConstraint(foo.c.value > 5) + +or by using a :func:`.sql.expression.column` inline:: + + from sqlalchemy import column + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"} + ) + + foo = Table('foo', metadata, + Column('value', Integer), + CheckConstraint(column('value') > 5) + ) + +Both will produce the name ``ck_foo_value``:: + + CREATE TABLE foo ( + value INTEGER, + CONSTRAINT ck_foo_value CHECK (value > 5) + ) + +The determination of the name of "column zero" is performed by scanning +the given expression for column objects. If the expression has more than +one column present, the scan does use a deterministic search, however the +structure of the expression will determine which column is noted as +"column zero". + +.. versionadded:: 1.0.0 The :class:`.CheckConstraint` object now supports + the ``column_0_name`` naming convention token. + +.. _naming_schematypes: + +Configuring Naming for Boolean, Enum, and other schema types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`.SchemaType` class refers to type objects such as :class:`.Boolean` +and :class:`.Enum` which generate a CHECK constraint accompanying the type. +The name for the constraint here is most directly set up by sending +the "name" parameter, e.g. :paramref:`.Boolean.name`:: + + Table('foo', metadata, + Column('flag', Boolean(name='ck_foo_flag')) + ) + +The naming convention feature may be combined with these types as well, +normally by using a convention which includes ``%(constraint_name)s`` +and then applying a name to the type:: + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(constraint_name)s"} + ) + + Table('foo', metadata, + Column('flag', Boolean(name='flag_bool')) + ) + +The above table will produce the constraint name ``ck_foo_flag_bool``:: + + CREATE TABLE foo ( + flag BOOL, + CONSTRAINT ck_foo_flag_bool CHECK (flag IN (0, 1)) + ) + +The :class:`.SchemaType` classes use special internal symbols so that +the naming convention is only determined at DDL compile time. On Postgresql, +there's a native BOOLEAN type, so the CHECK constraint of :class:`.Boolean` +is not needed; we are safe to set up a :class:`.Boolean` type without a +name, even though a naming convention is in place for check constraints. +This convention will only be consulted for the CHECK constraint if we +run against a database without a native BOOLEAN type like SQLite or +MySQL. + +The CHECK constraint may also make use of the ``column_0_name`` token, +which works nicely with :class:`.SchemaType` since these constraints have +only one column:: + + metadata = MetaData( + naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"} + ) + + Table('foo', metadata, + Column('flag', Boolean()) + ) + +The above schema will produce:: + + CREATE TABLE foo ( + flag BOOL, + CONSTRAINT ck_foo_flag CHECK (flag IN (0, 1)) + ) + +.. versionchanged:: 1.0 Constraint naming conventions that don't include + ``%(constraint_name)s`` again work with :class:`.SchemaType` constraints. + Constraints API --------------- .. autoclass:: Constraint diff --git a/lib/sqlalchemy/sql/naming.py b/lib/sqlalchemy/sql/naming.py index 9e57418b0..6508ed620 100644 --- a/lib/sqlalchemy/sql/naming.py +++ b/lib/sqlalchemy/sql/naming.py @@ -113,10 +113,12 @@ def _constraint_name_for_table(const, table): if isinstance(const.name, conv): return const.name - elif convention is not None and ( - const.name is None or not isinstance(const.name, conv) and - "constraint_name" in convention - ): + elif convention is not None and \ + not isinstance(const.name, conv) and \ + ( + const.name is None or + "constraint_name" in convention or + isinstance(const.name, _defer_name)): return conv( convention % ConventionDict(const, table, metadata.naming_convention) diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index f3752a726..fa48a16cc 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -2381,14 +2381,32 @@ class ColumnCollectionMixin(object): """ - def __init__(self, *columns): + _allow_multiple_tables = False + + def __init__(self, *columns, **kw): + _autoattach = kw.pop('_autoattach', True) self.columns = ColumnCollection() self._pending_colargs = [_to_schema_column_or_string(c) for c in columns] - if self._pending_colargs and \ - isinstance(self._pending_colargs[0], Column) and \ - isinstance(self._pending_colargs[0].table, Table): - self._set_parent_with_dispatch(self._pending_colargs[0].table) + if _autoattach and self._pending_colargs: + columns = [ + c for c in self._pending_colargs + if isinstance(c, Column) and + isinstance(c.table, Table) + ] + + tables = set([c.table for c in columns]) + if len(tables) == 1: + self._set_parent_with_dispatch(tables.pop()) + elif len(tables) > 1 and not self._allow_multiple_tables: + table = columns[0].table + others = [c for c in columns[1:] if c.table is not table] + if others: + raise exc.ArgumentError( + "Column(s) %s are not part of table '%s'." % + (", ".join("'%s'" % c for c in others), + table.description) + ) def _set_parent(self, table): for col in self._pending_colargs: @@ -2420,8 +2438,9 @@ class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): arguments are propagated to the :class:`.Constraint` superclass. """ + _autoattach = kw.pop('_autoattach', True) Constraint.__init__(self, **kw) - ColumnCollectionMixin.__init__(self, *columns) + ColumnCollectionMixin.__init__(self, *columns, _autoattach=_autoattach) def _set_parent(self, table): Constraint._set_parent(self, table) @@ -2449,12 +2468,14 @@ class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint): return len(self.columns._data) -class CheckConstraint(Constraint): +class CheckConstraint(ColumnCollectionConstraint): """A table- or column-level CHECK constraint. Can be included in the definition of a Table or Column. """ + _allow_multiple_tables = True + def __init__(self, sqltext, name=None, deferrable=None, initially=None, table=None, info=None, _create_rule=None, _autoattach=True, _type_bound=False): @@ -2486,20 +2507,19 @@ class CheckConstraint(Constraint): """ + self.sqltext = _literal_as_text(sqltext, warn=False) + + columns = [] + visitors.traverse(self.sqltext, {}, {'column': columns.append}) + super(CheckConstraint, self).\ __init__( - name, deferrable, initially, _create_rule, info=info, - _type_bound=_type_bound) - self.sqltext = _literal_as_text(sqltext, warn=False) + name=name, deferrable=deferrable, + initially=initially, _create_rule=_create_rule, info=info, + _type_bound=_type_bound, _autoattach=_autoattach, + *columns) if table is not None: self._set_parent_with_dispatch(table) - elif _autoattach: - cols = _find_columns(self.sqltext) - tables = set([c.table for c in cols - if isinstance(c.table, Table)]) - if len(tables) == 1: - self._set_parent_with_dispatch( - tables.pop()) def __visit_name__(self): if isinstance(self.parent, Table): @@ -2741,7 +2761,6 @@ class ForeignKeyConstraint(ColumnCollectionConstraint): self._validate_dest_table(table) - def copy(self, schema=None, target_table=None, **kw): fkc = ForeignKeyConstraint( [x.parent.key for x in self.elements], @@ -3064,12 +3083,6 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): ) ) self.table = table - for c in self.columns: - if c.table != self.table: - raise exc.ArgumentError( - "Column '%s' is not part of table '%s'." % - (c, self.table.description) - ) table.indexes.add(self) self.expressions = [ diff --git a/test/sql/test_constraints.py b/test/sql/test_constraints.py index 2603f67a3..eb558fc95 100644 --- a/test/sql/test_constraints.py +++ b/test/sql/test_constraints.py @@ -1063,7 +1063,7 @@ class ConstraintAPITest(fixtures.TestBase): ) assert_raises_message( exc.ArgumentError, - "Column 't2.y' is not part of table 't1'.", + r"Column\(s\) 't2.y' are not part of table 't1'.", Index, "bar", t1.c.x, t2.c.y ) diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 206f4bd16..1eec502e7 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -3441,6 +3441,27 @@ class NamingConventionTest(fixtures.TestBase, AssertsCompiledSQL): ")" ) + def test_schematype_ck_name_boolean_not_on_name(self): + m1 = MetaData(naming_convention={ + "ck": "ck_%(table_name)s_%(column_0_name)s"}) + + u1 = Table('user', m1, + Column('x', Boolean()) + ) + # constraint is not hit + eq_( + [c for c in u1.constraints + if isinstance(c, CheckConstraint)][0].name, "_unnamed_" + ) + # but is hit at compile time + self.assert_compile( + schema.CreateTable(u1), + 'CREATE TABLE "user" (' + "x BOOLEAN, " + "CONSTRAINT ck_user_x CHECK (x IN (0, 1))" + ")" + ) + def test_schematype_ck_name_enum(self): m1 = MetaData(naming_convention={ "ck": "ck_%(table_name)s_%(constraint_name)s"}) |