]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fix bug in naming convention feature where using a check
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 15 Jul 2014 00:26:38 +0000 (20:26 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 15 Jul 2014 00:26:38 +0000 (20:26 -0400)
constraint convention that includes ``constraint_name`` would
then force all :class:`.Boolean` and :class:`.Enum` types to
require names as well, as these implicitly create a
constraint, even if the ultimate target backend were one that does
not require generation of the constraint such as Postgresql.
The mechanics of naming conventions for these particular
constraints has been reorganized such that the naming
determination is done at DDL compile time, rather than at
constraint/table construction time.
fixes #3067

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/naming.py
lib/sqlalchemy/sql/sqltypes.py
test/sql/test_metadata.py

index 8be2346f6eb75db39edf2266ef3801038d3e68c4..24803af35378f21868033c420d0ce4287d88a852 100644 (file)
     :version: 0.9.7
     :released:
 
+    .. change::
+        :tags: bug, sql
+        :tickets: 3067
+        :versions: 1.0.0
+
+        Fix bug in naming convention feature where using a check
+        constraint convention that includes ``constraint_name`` would
+        then force all :class:`.Boolean` and :class:`.Enum` types to
+        require names as well, as these implicitly create a
+        constraint, even if the ultimate target backend were one that does
+        not require generation of the constraint such as Postgresql.
+        The mechanics of naming conventions for these particular
+        constraints has been reorganized such that the naming
+        determination is done at DDL compile time, rather than at
+        constraint/table construction time.
+
     .. change::
         :tags: bug, mssql
         :tickets: 3025
index 7a8b07f8faa5b9f71bb701c1bfb9506a479c4b32..99b74b3c84daa0836ebe9607970b10627fceb514 100644 (file)
@@ -2902,7 +2902,13 @@ class IdentifierPreparer(object):
     def format_savepoint(self, savepoint, name=None):
         return self.quote(name or savepoint.ident)
 
-    def format_constraint(self, constraint):
+    @util.dependencies("sqlalchemy.sql.naming")
+    def format_constraint(self, naming, constraint):
+        if isinstance(constraint.name, elements._defer_name):
+            name = naming._constraint_name_for_table(
+                                        constraint, constraint.table)
+            if name:
+                return self.quote(name)
         return self.quote(constraint.name)
 
     def format_table(self, table, use_schema=True, name=None):
index 3882d9e5b341368d35155048c8bd2ac3e8b2463a..d273d988167ca72f7c0c0942ca01f4d02505fdee 100644 (file)
@@ -3201,6 +3201,22 @@ class _truncated_label(quoted_name):
     def apply_map(self, map_):
         return self
 
+
+class _defer_name(_truncated_label):
+    """mark a name as 'deferred' for the purposes of automated name
+    generation.
+
+    """
+    def __new__(cls, value):
+        if value is None:
+            return _defer_none_name('_unnamed_')
+        else:
+            return super(_defer_name, cls).__new__(cls, value)
+
+
+class _defer_none_name(_defer_name):
+    """indicate a 'deferred' name that was ultimately the value None."""
+
 # for backwards compatibility in case
 # someone is re-implementing the
 # _truncated_identifier() sequence in a custom
index d05054bc6b62321572a524d1a13018d2986c100f..bb838c54231ff449068ddb79d5fe647915ce393c 100644 (file)
@@ -14,7 +14,7 @@ from .schema import Constraint, ForeignKeyConstraint, PrimaryKeyConstraint, \
                 UniqueConstraint, CheckConstraint, Index, Table, Column
 from .. import event, events
 from .. import exc
-from .elements import _truncated_label
+from .elements import _truncated_label, _defer_name, _defer_none_name
 import re
 
 class conv(_truncated_label):
@@ -77,12 +77,12 @@ class ConventionDict(object):
             return list(self.const.columns)[idx]
 
     def _key_constraint_name(self):
-        if not self._const_name:
+        if isinstance(self._const_name, (type(None), _defer_none_name)):
             raise exc.InvalidRequestError(
-                    "Naming convention including "
-                    "%(constraint_name)s token requires that "
-                    "constraint is explicitly named."
-                )
+                "Naming convention including "
+                "%(constraint_name)s token requires that "
+                "constraint is explicitly named."
+            )
         if not isinstance(self._const_name, conv):
             self.const.name = None
         return self._const_name
@@ -144,6 +144,17 @@ def _get_convention(dict_, key):
     else:
         return None
 
+def _constraint_name_for_table(const, table):
+    metadata = table.metadata
+    convention = _get_convention(metadata.naming_convention, type(const))
+    if convention is not None and (
+            const.name is None or not isinstance(const.name, conv) and
+                "constraint_name" in convention
+        ):
+        return conv(
+                convention % ConventionDict(const, table,
+                                metadata.naming_convention)
+                )
 
 @event.listens_for(Constraint, "after_parent_attach")
 @event.listens_for(Index, "after_parent_attach")
@@ -156,12 +167,9 @@ def _constraint_name(const, table):
                     lambda col, table: _constraint_name(const, table)
                 )
     elif isinstance(table, Table):
-        metadata = table.metadata
-        convention = _get_convention(metadata.naming_convention, type(const))
-        if convention is not None:
-            if const.name is None or "constraint_name" in convention:
-                newname = conv(
-                            convention % ConventionDict(const, table, metadata.naming_convention)
-                            )
-                if const.name is None:
-                    const.name = newname
+        if isinstance(const.name, (conv, _defer_name)):
+            return
+
+        newname = _constraint_name_for_table(const, table)
+        if newname is not None:
+            const.name = newname
index b4d2d23901a2cc4588ff0e4ef5815dc0213ebb8e..52e5053ef74d922338289ac68da36fbf5398a0f2 100644 (file)
@@ -13,7 +13,7 @@ import datetime as dt
 import codecs
 
 from .type_api import TypeEngine, TypeDecorator, to_instance
-from .elements import quoted_name, type_coerce
+from .elements import quoted_name, type_coerce, _defer_name
 from .default_comparator import _DefaultColumnComparator
 from .. import exc, util, processors
 from .base import _bind_or_error, SchemaEventTarget
@@ -1132,7 +1132,7 @@ class Enum(String, SchemaType):
 
         e = schema.CheckConstraint(
                         type_coerce(column, self).in_(self.enums),
-                        name=self.name,
+                        name=_defer_name(self.name),
                         _create_rule=util.portable_instancemethod(
                                         self._should_create_constraint)
                     )
@@ -1268,7 +1268,7 @@ class Boolean(TypeEngine, SchemaType):
 
         e = schema.CheckConstraint(
                         type_coerce(column, self).in_([0, 1]),
-                        name=self.name,
+                        name=_defer_name(self.name),
                         _create_rule=util.portable_instancemethod(
                                     self._should_create_constraint)
                     )
index 9542711cb2b4f3fa2f93c2809e01d54f4e42af7e..7aa2059dcf165ba41eb0ca58f920e4ce67399121 100644 (file)
@@ -9,6 +9,7 @@ from sqlalchemy import Integer, String, UniqueConstraint, \
     events, Unicode, types as sqltypes, bindparam, \
     Table, Column, Boolean, Enum, func, text
 from sqlalchemy import schema, exc
+from sqlalchemy.sql import elements, naming
 import sqlalchemy as tsa
 from sqlalchemy.testing import fixtures
 from sqlalchemy import testing
@@ -2811,7 +2812,9 @@ class DialectKWArgTest(fixtures.TestBase):
             )
 
 
-class NamingConventionTest(fixtures.TestBase):
+class NamingConventionTest(fixtures.TestBase, AssertsCompiledSQL):
+    dialect = 'default'
+
     def _fixture(self, naming_convention, table_schema=None):
         m1 = MetaData(naming_convention=naming_convention)
 
@@ -2831,10 +2834,10 @@ class NamingConventionTest(fixtures.TestBase):
         uq = UniqueConstraint(u1.c.data)
         eq_(uq.name, "uq_user_data")
 
-    def test_ck_name(self):
+    def test_ck_name_required(self):
         u1 = self._fixture(naming_convention={
-                        "ck": "ck_%(table_name)s_%(constraint_name)s"
-                    })
+            "ck": "ck_%(table_name)s_%(constraint_name)s"
+        })
         ck = CheckConstraint(u1.c.data == 'x', name='mycheck')
         eq_(ck.name, "ck_user_mycheck")
 
@@ -2845,6 +2848,19 @@ class NamingConventionTest(fixtures.TestBase):
             CheckConstraint, u1.c.data == 'x'
         )
 
+    def test_ck_name_deferred_required(self):
+        u1 = self._fixture(naming_convention={
+            "ck": "ck_%(table_name)s_%(constraint_name)s"
+        })
+        ck = CheckConstraint(u1.c.data == 'x', name=elements._defer_name(None))
+
+        assert_raises_message(
+            exc.InvalidRequestError,
+            r"Naming convention including %\(constraint_name\)s token "
+            "requires that constraint is explicitly named.",
+            schema.AddConstraint(ck).compile
+        )
+
     def test_column_attached_ck_name(self):
         m = MetaData(naming_convention={
                         "ck": "ck_%(table_name)s_%(constraint_name)s"
@@ -2861,6 +2877,17 @@ class NamingConventionTest(fixtures.TestBase):
         Table('t', m, Column('x', Integer), ck)
         eq_(ck.name, "ck_t_x1")
 
+    def test_uq_name_already_conv(self):
+        m = MetaData(naming_convention={
+            "uq": "uq_%(constraint_name)s_%(column_0_name)s"
+        })
+
+        t = Table('mytable', m)
+        uq = UniqueConstraint(name=naming.conv('my_special_key'))
+
+        t.append_constraint(uq)
+        eq_(uq.name, "my_special_key")
+
 
     def test_fk_name_schema(self):
         u1 = self._fixture(naming_convention={
@@ -2922,9 +2949,18 @@ class NamingConventionTest(fixtures.TestBase):
         u1 = Table('user', m1,
             Column('x', Boolean(name='foo'))
             )
+        # constraint is not hit
         eq_(
             [c for c in u1.constraints
-                if isinstance(c, CheckConstraint)][0].name, "ck_user_foo"
+                if isinstance(c, CheckConstraint)][0].name, "foo"
+        )
+        # but is hit at compile time
+        self.assert_compile(
+            schema.CreateTable(u1),
+            "CREATE TABLE user ("
+            "x BOOLEAN, "
+            "CONSTRAINT ck_user_foo CHECK (x IN (0, 1))"
+            ")"
         )
 
     def test_schematype_ck_name_enum(self):
@@ -2936,7 +2972,45 @@ class NamingConventionTest(fixtures.TestBase):
             )
         eq_(
             [c for c in u1.constraints
-                if isinstance(c, CheckConstraint)][0].name, "ck_user_foo"
+                if isinstance(c, CheckConstraint)][0].name, "foo"
+        )
+        # but is hit at compile time
+        self.assert_compile(
+            schema.CreateTable(u1),
+            "CREATE TABLE user ("
+                "x VARCHAR(1), "
+                "CONSTRAINT ck_user_foo CHECK (x IN ('a', 'b'))"
+            ")"
+        )
+
+    def test_schematype_ck_name_boolean_no_name(self):
+        m1 = MetaData(naming_convention={
+            "ck": "ck_%(table_name)s_%(constraint_name)s"
+        })
+
+        u1 = Table(
+            'user', m1,
+            Column('x', Boolean())
+        )
+        # constraint gets special _defer_none_name
+        eq_(
+            [c for c in u1.constraints
+                if isinstance(c, CheckConstraint)][0].name, "_unnamed_"
+        )
+        # no issue with native boolean
+        self.assert_compile(
+            schema.CreateTable(u1),
+            'CREATE TABLE "user" ('
+            "x BOOLEAN"
+            ")",
+            dialect='postgresql'
+        )
+
+        assert_raises_message(
+            exc.InvalidRequestError,
+            "Naming convention including \%\(constraint_name\)s token "
+            "requires that constraint is explicitly named.",
+            schema.CreateTable(u1).compile
         )
 
     def test_ck_constraint_redundant_event(self):