: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
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):
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
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):
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
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")
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
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
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)
)
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)
)
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
)
-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)
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")
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"
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={
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):
)
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):