]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The :class:`.CheckConstraint` construct now supports naming
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 Jan 2015 18:38:51 +0000 (13:38 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 30 Jan 2015 18:38:51 +0000 (13:38 -0500)
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

doc/build/changelog/changelog_10.rst
doc/build/changelog/migration_10.rst
doc/build/core/constraints.rst
lib/sqlalchemy/sql/naming.py
lib/sqlalchemy/sql/schema.py
test/sql/test_constraints.py
test/sql/test_metadata.py

index 2c3e26f2e4c63852886e0b965eafeff149529680..89ef868446341bc00a0a7a62cf1188c83aa6763d 100644 (file)
     series as well.  For changes that are specific to 1.0 with an emphasis
     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
index 23ee6f46694e7b4a68ce4ec4eace6f2b722cd6d1..3ba0743f78fa567854e5f5ec651d1a81f6952ca5 100644 (file)
@@ -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:
index 1f855c7248f2021d8b9f7a664254cf3ed7725b92..dfe9e9cdd6a722bf4ca07ad21ef95d75f7c108c9 100644 (file)
@@ -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
index 9e57418b02bf0f7bd156a97acf196e4ca3310706..6508ed620fe81bb9b58a9e328f5ef0be5d96dad9 100644 (file)
@@ -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)
index f3752a7262edb237c8058aded6090bccb13798a6..fa48a16ccfc5a2164f20e99441ca9edd29fda53b 100644 (file)
@@ -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 = [
index 2603f67a3bc2c415d37d74c892f88862124ce008..eb558fc9510595ff5e0aaf84df21af25b7882d53 100644 (file)
@@ -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
         )
index 206f4bd16a7769acdd3cc6e34a686d49dd9f9935..1eec502e7a4cdfbeeb7e4829d603930abb3010c2 100644 (file)
@@ -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"})